1use super::{
4 super::{
5 super::{unscaled::UnscaledOutlineBuf, OutlineGlyphCollection},
6 shape::{ShapedCluster, Shaper},
7 style::{ScriptGroup, StyleClass},
8 },
9 ScaledWidth,
10};
11use crate::{collections::SmallVec, FontRef, MetadataProvider};
12use raw::types::F2Dot14;
13use raw::TableProvider;
14
15const MAX_BLUES: usize = 8;
19
20const MAX_INLINE_POINTS: usize = 256;
23
24const BLUE_STRING_MAX_LEN: usize = 51;
26
27#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
29#[repr(transparent)]
30pub struct BlueZones(u16);
31
32impl BlueZones {
33 pub const NONE: Self = Self(0);
40 pub const TOP: Self = Self(1 << 1);
41 pub const SUB_TOP: Self = Self(1 << 2);
42 pub const NEUTRAL: Self = Self(1 << 3);
43 pub const ADJUSTMENT: Self = Self(1 << 4);
44 pub const X_HEIGHT: Self = Self(1 << 5);
45 pub const LONG: Self = Self(1 << 6);
46 pub const HORIZONTAL: Self = Self(1 << 2);
47 pub const RIGHT: Self = Self::TOP;
48
49 pub const fn contains(self, other: Self) -> bool {
50 self.0 & other.0 == other.0
51 }
52
53 #[must_use]
56 pub(crate) const fn union(self, other: Self) -> Self {
57 Self(self.0 | other.0)
58 }
59
60 pub(crate) fn is_top_like(self) -> bool {
61 self & (Self::TOP | Self::SUB_TOP) != Self::NONE
62 }
63
64 pub fn is_top(self) -> bool {
65 self.contains(Self::TOP)
66 }
67
68 pub fn is_sub_top(self) -> bool {
69 self.contains(Self::SUB_TOP)
70 }
71
72 pub fn is_neutral(self) -> bool {
73 self.contains(Self::NEUTRAL)
74 }
75
76 pub fn is_x_height(self) -> bool {
77 self.contains(Self::X_HEIGHT)
78 }
79
80 pub(crate) fn is_long(self) -> bool {
81 self.contains(Self::LONG)
82 }
83
84 pub(crate) fn is_horizontal(self) -> bool {
85 self.contains(Self::HORIZONTAL)
86 }
87
88 pub(crate) fn is_right(self) -> bool {
89 self.contains(Self::RIGHT)
90 }
91
92 #[must_use]
93 pub fn retain_top_like_or_neutral(self) -> Self {
94 self & (Self::TOP | Self::SUB_TOP | Self::NEUTRAL)
95 }
96}
97
98impl core::ops::Not for BlueZones {
99 type Output = Self;
100
101 fn not(self) -> Self::Output {
102 Self(!self.0)
103 }
104}
105
106impl core::ops::BitOr for BlueZones {
107 type Output = Self;
108
109 fn bitor(self, rhs: Self) -> Self::Output {
110 Self(self.0 | rhs.0)
111 }
112}
113
114impl core::ops::BitOrAssign for BlueZones {
115 fn bitor_assign(&mut self, rhs: Self) {
116 self.0 |= rhs.0;
117 }
118}
119
120impl core::ops::BitAnd for BlueZones {
121 type Output = Self;
122
123 fn bitand(self, rhs: Self) -> Self::Output {
124 Self(self.0 & rhs.0)
125 }
126}
127
128impl core::ops::BitAndAssign for BlueZones {
129 fn bitand_assign(&mut self, rhs: Self) {
130 self.0 &= rhs.0;
131 }
132}
133
134#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
141pub struct UnscaledBlue {
142 pub position: i32,
144 pub overshoot: i32,
146 pub ascender: i32,
148 pub descender: i32,
150 pub zones: BlueZones,
152}
153
154pub(crate) type UnscaledBlues = SmallVec<UnscaledBlue, MAX_BLUES>;
155
156#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
158pub struct ScaledBlue {
159 pub position: ScaledWidth,
161 pub overshoot: ScaledWidth,
163 pub zones: BlueZones,
165 pub is_active: bool,
167}
168
169pub(crate) type ScaledBlues = SmallVec<ScaledBlue, MAX_BLUES>;
170
171pub(crate) fn compute_unscaled_blues(
173 shaper: &Shaper,
174 coords: &[F2Dot14],
175 style: &StyleClass,
176) -> [UnscaledBlues; 2] {
177 match style.script.group {
178 ScriptGroup::Default => [
179 Default::default(),
181 compute_default_blues(shaper, coords, style),
182 ],
183 ScriptGroup::Cjk => compute_cjk_blues(shaper, coords, style),
184 ScriptGroup::Indic => Default::default(),
186 }
187}
188
189fn compute_default_blues(shaper: &Shaper, coords: &[F2Dot14], style: &StyleClass) -> UnscaledBlues {
193 let mut blues = UnscaledBlues::new();
194 let (mut outline_buf, mut flats, mut rounds) = buffers();
195 let (glyphs, units_per_em) = things_all_blues_need(shaper.font());
196 let flat_threshold = units_per_em / 14;
197 let mut cluster_shaper = shaper.cluster_shaper(style);
198 let mut shaped_cluster = ShapedCluster::default();
199 for (blue_str, blue_zones) in style.script.blues {
201 let mut ascender = i32::MIN;
202 let mut descender = i32::MAX;
203 let mut n_flats = 0;
204 let mut n_rounds = 0;
205 if blues.len() >= MAX_BLUES {
206 continue;
207 }
208 for cluster in blue_str.split(' ') {
209 let mut best_y_extremum = if blue_zones.is_top() {
210 i32::MIN
211 } else {
212 i32::MAX
213 };
214 let mut best_is_round = false;
215 cluster_shaper.shape(cluster, &mut shaped_cluster);
216 for (glyph, y_offset) in shaped_cluster
217 .iter()
218 .filter(|g| g.id.to_u32() != 0)
219 .filter_map(|g| Some((glyphs.get(g.id)?, g.y_offset)))
220 {
221 outline_buf.clear();
222 if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
223 continue;
224 }
225 let outline = outline_buf.as_ref();
226 if outline.points.len() <= 2 {
228 continue;
229 }
230 let mut best_y: Option<i16> = None;
231 let best_contour_and_point = if blue_zones.is_top_like() {
234 outline.find_last_contour(|point| {
235 if best_y.is_none() || Some(point.y) > best_y {
236 best_y = Some(point.y);
237 ascender = ascender.max(point.y as i32 + y_offset);
238 true
239 } else {
240 descender = descender.min(point.y as i32 + y_offset);
241 false
242 }
243 })
244 } else {
245 outline.find_last_contour(|point| {
246 if best_y.is_none() || Some(point.y) < best_y {
247 best_y = Some(point.y);
248 descender = descender.min(point.y as i32 + y_offset);
249 true
250 } else {
251 ascender = ascender.max(point.y as i32 + y_offset);
252 false
253 }
254 })
255 };
256 let Some((best_contour_range, best_point_ix)) = best_contour_and_point else {
257 continue;
258 };
259 let best_contour = &outline.points[best_contour_range];
260 let Some(mut best_y) = best_y.map(i32::from) else {
261 continue;
262 };
263 let best_x = best_contour[best_point_ix].x as i32;
264 let [mut on_point_first, mut on_point_last] =
267 if best_contour[best_point_ix].is_on_curve() {
268 [Some(best_point_ix); 2]
269 } else {
270 [None; 2]
271 };
272 let mut segment_first = best_point_ix;
273 let mut segment_last = best_point_ix;
274 for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
278 let dist = (prev.y as i32 - best_y).abs();
279 if dist > 5 && ((prev.x as i32 - best_x).abs() <= (20 * dist)) {
281 break;
282 }
283 segment_first = ix;
284 if prev.is_on_curve() {
285 on_point_first = Some(ix);
286 if on_point_last.is_none() {
287 on_point_last = Some(ix);
288 }
289 }
290 }
291 let mut next_ix = 0;
292 for (ix, next) in cycle_forward(best_contour, best_point_ix) {
293 next_ix = ix;
296 let dist = (next.y as i32 - best_y).abs();
297 if dist > 5 && ((next.x as i32 - best_x).abs() <= (20 * dist)) {
299 break;
300 }
301 segment_last = ix;
302 if next.is_on_curve() {
303 on_point_last = Some(ix);
304 if on_point_first.is_none() {
305 on_point_first = Some(ix);
306 }
307 }
308 }
309 if blue_zones.is_long() {
310 let length_threshold = units_per_em / 25;
329 let dist = (best_contour[segment_last].x as i32
330 - best_contour[segment_first].x as i32)
331 .abs();
332 if dist < length_threshold
333 && satisfies_min_long_segment_len(
334 segment_first,
335 segment_last,
336 best_contour.len() - 1,
337 )
338 {
339 let height_threshold = units_per_em / 4;
341 let mut prev_ix = best_point_ix;
343 for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
344 if prev.x as i32 != best_x {
345 prev_ix = ix;
346 break;
347 }
348 }
349 if prev_ix == best_point_ix {
351 continue;
352 }
353 let is_ltr = (best_contour[prev_ix].x as i32) < best_x;
354 let mut first = segment_last;
355 let mut last = first;
356 let mut p_first = None;
357 let mut p_last = None;
358 let mut hit = false;
359 loop {
360 if !hit {
361 first = last;
363 if best_contour[first].is_on_curve() {
365 p_first = Some(first);
366 p_last = Some(first);
367 } else {
368 p_first = None;
369 p_last = None;
370 }
371 hit = true;
372 }
373 if last < best_contour.len() - 1 {
374 last += 1;
375 } else {
376 last = 0;
377 }
378 if (best_y - best_contour[first].y as i32).abs() > height_threshold {
379 hit = false;
381 if last == segment_first {
382 break;
383 }
384 continue;
385 }
386 let dist =
387 (best_contour[last].y as i32 - best_contour[first].y as i32).abs();
388 if dist > 5
389 && (best_contour[last].x as i32 - best_contour[first].x as i32)
390 .abs()
391 <= 20 * dist
392 {
393 hit = false;
394 if last == segment_first {
395 break;
396 }
397 continue;
398 }
399 if best_contour[last].is_on_curve() {
400 p_last = Some(last);
401 if p_first.is_none() {
402 p_first = Some(last);
403 }
404 }
405 let first_x = best_contour[first].x as i32;
406 let last_x = best_contour[last].x as i32;
407 let is_cur_ltr = first_x < last_x;
408 let dx = (last_x - first_x).abs();
409 if is_cur_ltr == is_ltr && dx >= length_threshold {
410 loop {
411 if last < best_contour.len() - 1 {
412 last += 1;
413 } else {
414 last = 0;
415 }
416 let dy = (best_contour[last].y as i32
417 - best_contour[first].y as i32)
418 .abs();
419 if dy > 5
420 && (best_contour[next_ix].x as i32
421 - best_contour[first].x as i32)
422 .abs()
423 <= 20 * dist
424 {
425 if last > 0 {
426 last -= 1;
427 } else {
428 last = best_contour.len() - 1;
429 }
430 break;
431 }
432 p_last = Some(last);
433 if best_contour[last].is_on_curve() {
434 p_last = Some(last);
435 if p_first.is_none() {
436 p_first = Some(last);
437 }
438 }
439 if last == segment_first {
440 break;
441 }
442 }
443 best_y = best_contour[first].y as i32;
444 segment_first = first;
445 segment_last = last;
446 on_point_first = p_first;
447 on_point_last = p_last;
448 break;
449 }
450 if last == segment_first {
451 break;
452 }
453 }
454 }
455 }
456 best_y += y_offset;
457 let is_round = match (on_point_first, on_point_last) {
463 (Some(first), Some(last))
464 if (best_contour[last].x as i32 - best_contour[first].x as i32).abs()
465 > flat_threshold =>
466 {
467 false
468 }
469 _ => {
470 !best_contour[segment_first].is_on_curve()
471 || !best_contour[segment_last].is_on_curve()
472 }
473 };
474 if is_round && blue_zones.is_neutral() {
475 continue;
477 }
478 if blue_zones.is_top() {
480 if best_y > best_y_extremum {
481 best_y_extremum = best_y;
482 best_is_round = is_round;
483 }
484 } else if best_y < best_y_extremum {
485 best_y_extremum = best_y;
486 best_is_round = is_round;
487 }
488 }
489 if best_y_extremum != i32::MIN && best_y_extremum != i32::MAX {
490 if best_is_round {
491 if let Some(round) = rounds.get_mut(n_rounds) {
492 *round = best_y_extremum;
493 n_rounds += 1;
494 }
495 } else if let Some(flat) = flats.get_mut(n_flats) {
496 *flat = best_y_extremum;
497 n_flats += 1;
498 }
499 }
500 }
501 if n_flats == 0 && n_rounds == 0 {
502 continue;
503 }
504 rounds[..n_rounds].sort_unstable();
505 flats[..n_flats].sort_unstable();
506 let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
507 let val = rounds[n_rounds / 2];
508 (val, val)
509 } else if n_rounds == 0 {
510 let val = flats[n_flats / 2];
511 (val, val)
512 } else {
513 (flats[n_flats / 2], rounds[n_rounds / 2])
514 };
515 if blue_shoot != blue_ref {
516 let over_ref = blue_shoot > blue_ref;
517 if blue_zones.is_top_like() ^ over_ref {
518 let val = (blue_shoot + blue_ref) / 2;
519 blue_ref = val;
520 blue_shoot = val;
521 }
522 }
523 let mut blue = UnscaledBlue {
524 position: blue_ref,
525 overshoot: blue_shoot,
526 ascender,
527 descender,
528 zones: blue_zones.retain_top_like_or_neutral(),
529 };
530 if blue_zones.is_x_height() {
531 blue.zones |= BlueZones::ADJUSTMENT;
532 }
533 blues.push(blue);
534 }
535 let mut sorted_indices: [usize; MAX_BLUES] = core::array::from_fn(|ix| ix);
537 let blue_values = blues.as_mut_slice();
538 let len = blue_values.len();
539 if len == 0 {
540 return blues;
541 }
542 for i in 1..len {
544 for j in (1..=i).rev() {
545 let first = &blue_values[sorted_indices[j - 1]];
546 let second = &blue_values[sorted_indices[j]];
547 let a = if first.zones.is_top_like() {
548 first.position
549 } else {
550 first.overshoot
551 };
552 let b = if second.zones.is_top_like() {
553 second.position
554 } else {
555 second.overshoot
556 };
557 if b >= a {
558 break;
559 }
560 sorted_indices.swap(j, j - 1);
561 }
562 }
563 for i in 0..len - 1 {
565 let index1 = sorted_indices[i];
566 let index2 = sorted_indices[i + 1];
567 let first = &blue_values[index1];
568 let second = &blue_values[index2];
569 let a = if first.zones.is_top_like() {
570 first.overshoot
571 } else {
572 first.position
573 };
574 let b = if second.zones.is_top_like() {
575 second.overshoot
576 } else {
577 second.position
578 };
579 if a > b {
580 if first.zones.is_top_like() {
581 blue_values[index1].overshoot = b;
582 } else {
583 blue_values[index1].position = b;
584 }
585 }
586 }
587 blues
588}
589
590fn satisfies_min_long_segment_len(first_ix: usize, last_ix: usize, contour_last: usize) -> bool {
594 let inclusive_diff = if first_ix <= last_ix {
595 last_ix - first_ix
596 } else {
597 contour_last - first_ix + 1 + last_ix
603 };
604 inclusive_diff + 2 <= contour_last
608}
609
610fn compute_cjk_blues(
620 shaper: &Shaper,
621 coords: &[F2Dot14],
622 style: &StyleClass,
623) -> [UnscaledBlues; 2] {
624 let mut blues = [UnscaledBlues::new(), UnscaledBlues::new()];
625 let (mut outline_buf, mut flats, mut fills) = buffers();
626 let (glyphs, _) = things_all_blues_need(shaper.font());
627 let mut cluster_shaper = shaper.cluster_shaper(style);
628 let mut shaped_cluster = ShapedCluster::default();
629 for (blue_str, blue_zones) in style.script.blues {
631 let is_horizontal = blue_zones.is_horizontal();
632 if is_horizontal {
637 continue;
638 }
639 let is_right = blue_zones.is_right();
640 let is_top = blue_zones.is_top();
641 let blues = &mut blues[!is_horizontal as usize];
642 if blues.len() >= MAX_BLUES {
643 continue;
644 }
645 let mut n_flats = 0;
646 let mut n_fills = 0;
647 let mut is_fill = true;
648 for cluster in blue_str.split(' ') {
649 if cluster == "|" {
653 is_fill = false;
654 continue;
655 }
656 cluster_shaper.shape(cluster, &mut shaped_cluster);
657 for glyph in shaped_cluster
658 .iter()
659 .filter(|g| g.id.to_u32() != 0)
660 .filter_map(|g| glyphs.get(g.id))
661 {
662 outline_buf.clear();
663 if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
664 continue;
665 }
666 let outline = outline_buf.as_ref();
667 if outline.points.len() <= 2 {
669 continue;
670 }
671 let best_pos = outline
673 .points
674 .iter()
675 .map(|p| if is_horizontal { p.x } else { p.y })
676 .reduce(
677 if (is_horizontal && is_right) || (!is_horizontal && is_top) {
678 |a: i16, c: i16| a.max(c)
679 } else {
680 |a: i16, c: i16| a.min(c)
681 },
682 );
683 let Some(best_pos) = best_pos else {
684 continue;
685 };
686 if is_fill {
687 if let Some(fill) = fills.get_mut(n_fills) {
688 *fill = best_pos;
689 n_fills += 1;
690 }
691 } else if let Some(flat) = flats.get_mut(n_flats) {
692 *flat = best_pos;
693 n_flats += 1;
694 }
695 }
696 }
697 if n_flats == 0 && n_fills == 0 {
698 continue;
699 }
700 fills[..n_fills].sort_unstable();
703 flats[..n_flats].sort_unstable();
704 let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
705 let value = fills[n_fills / 2] as i32;
706 (value, value)
707 } else if n_fills == 0 {
708 let value = flats[n_flats / 2] as i32;
709 (value, value)
710 } else {
711 (fills[n_fills / 2] as i32, flats[n_flats / 2] as i32)
712 };
713 if blue_shoot != blue_ref {
716 let under_ref = blue_shoot < blue_ref;
717 if blue_zones.is_top() ^ under_ref {
718 blue_ref = (blue_shoot + blue_ref) / 2;
719 blue_shoot = blue_ref;
720 }
721 }
722 blues.push(UnscaledBlue {
723 position: blue_ref,
724 overshoot: blue_shoot,
725 ascender: 0,
726 descender: 0,
727 zones: *blue_zones & BlueZones::TOP,
728 });
729 }
730 blues
731}
732
733#[inline(always)]
734fn buffers<T: Copy + Default>() -> (
735 UnscaledOutlineBuf<MAX_INLINE_POINTS>,
736 [T; BLUE_STRING_MAX_LEN],
737 [T; BLUE_STRING_MAX_LEN],
738) {
739 (
740 UnscaledOutlineBuf::<MAX_INLINE_POINTS>::new(),
741 [T::default(); BLUE_STRING_MAX_LEN],
742 [T::default(); BLUE_STRING_MAX_LEN],
743 )
744}
745
746#[inline(always)]
748fn things_all_blues_need<'a>(font: &FontRef<'a>) -> (OutlineGlyphCollection<'a>, i32) {
749 (
750 font.outline_glyphs(),
751 font.head()
752 .map(|head| head.units_per_em())
753 .unwrap_or_default() as i32,
754 )
755}
756
757pub(super) fn cycle_forward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
760 let len = items.len();
761 let start = start + 1;
762 (0..len).map(move |ix| {
763 let real_ix = (ix + start) % len;
764 (real_ix, &items[real_ix])
765 })
766}
767
768pub(super) fn cycle_backward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
771 let len = items.len();
772 (0..len).rev().map(move |ix| {
773 let real_ix = (ix + start) % len;
774 (real_ix, &items[real_ix])
775 })
776}
777
778#[cfg(test)]
779mod tests {
780 use crate::outline::autohint::metrics::BlueZones;
781
782 use super::{
783 super::super::{
784 shape::{Shaper, ShaperMode},
785 style,
786 },
787 satisfies_min_long_segment_len, UnscaledBlue,
788 };
789 use raw::{types::Tag, FontRef};
790
791 #[test]
792 fn latin_blues() {
793 let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
794 let shaper = Shaper::new(&font, ShaperMode::Nominal);
795 let style = &style::STYLE_CLASSES[super::StyleClass::LATN];
796 let blues = super::compute_default_blues(&shaper, &[], style);
797 let values = blues.as_slice();
798 let expected = [
799 UnscaledBlue {
800 position: 714,
801 overshoot: 725,
802 ascender: 725,
803 descender: -230,
804 zones: BlueZones::TOP,
805 },
806 UnscaledBlue {
807 position: 0,
808 overshoot: -10,
809 ascender: 725,
810 descender: -10,
811 zones: BlueZones::default(),
812 },
813 UnscaledBlue {
814 position: 760,
815 overshoot: 760,
816 ascender: 770,
817 descender: -240,
818 zones: BlueZones::TOP,
819 },
820 UnscaledBlue {
821 position: 536,
822 overshoot: 546,
823 ascender: 546,
824 descender: -10,
825 zones: BlueZones::TOP | BlueZones::ADJUSTMENT,
826 },
827 UnscaledBlue {
828 position: 0,
829 overshoot: -10,
830 ascender: 546,
831 descender: -10,
832 zones: BlueZones::default(),
833 },
834 UnscaledBlue {
835 position: -240,
836 overshoot: -240,
837 ascender: 760,
838 descender: -240,
839 zones: BlueZones::default(),
840 },
841 ];
842 assert_eq!(values, &expected);
843 }
844
845 #[test]
846 fn hebrew_long_blues() {
847 let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
848 let shaper = Shaper::new(&font, ShaperMode::Nominal);
849 let style = &style::STYLE_CLASSES[super::StyleClass::HEBR];
851 let blues = super::compute_default_blues(&shaper, &[], style);
852 let values = blues.as_slice();
853 assert_eq!(values.len(), 3);
854 let expected = [
855 UnscaledBlue {
856 position: 592,
857 overshoot: 592,
858 ascender: 647,
859 descender: -240,
860 zones: BlueZones::TOP,
861 },
862 UnscaledBlue {
863 position: 0,
864 overshoot: -9,
865 ascender: 647,
866 descender: -9,
867 zones: BlueZones::default(),
868 },
869 UnscaledBlue {
870 position: -240,
871 overshoot: -240,
872 ascender: 647,
873 descender: -240,
874 zones: BlueZones::default(),
875 },
876 ];
877 assert_eq!(values, &expected);
878 }
879
880 #[test]
881 fn cjk_blues() {
882 let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
883 let shaper = Shaper::new(&font, ShaperMode::Nominal);
884 let style = &style::STYLE_CLASSES[super::StyleClass::HANI];
885 let blues = super::compute_cjk_blues(&shaper, &[], style);
886 let values = blues[1].as_slice();
887 let expected = [
888 UnscaledBlue {
889 position: 837,
890 overshoot: 824,
891 ascender: 0,
892 descender: 0,
893 zones: BlueZones::TOP,
894 },
895 UnscaledBlue {
896 position: -78,
897 overshoot: -66,
898 ascender: 0,
899 descender: 0,
900 zones: BlueZones::default(),
901 },
902 ];
903 assert_eq!(values, &expected);
904 }
905
906 #[test]
907 fn c2sc_shaped_blues() {
908 let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
909 let shaper = Shaper::new(&font, ShaperMode::BestEffort);
910 let style = &style::STYLE_CLASSES[super::StyleClass::LATN_C2SC];
911 let blues = super::compute_default_blues(&shaper, &[], style);
912 let values = blues.as_slice();
913 let expected = [
915 UnscaledBlue {
916 position: 571,
917 overshoot: 571,
918 ascender: 571,
919 descender: 0,
920 zones: BlueZones::TOP,
921 },
922 UnscaledBlue {
923 position: 0,
924 overshoot: 0,
925 ascender: 571,
926 descender: 0,
927 zones: BlueZones::default(),
928 },
929 ];
930 assert_eq!(values, &expected);
931 }
932
933 #[test]
936 fn long_segment_len_avoid_overflow() {
937 assert!(satisfies_min_long_segment_len(22, 0, 22));
943 }
944
945 #[test]
946 fn cycle_iter_forward() {
947 let items = [0, 1, 2, 3, 4, 5, 6, 7];
948 let from_5 = super::cycle_forward(&items, 5)
949 .map(|(_, val)| *val)
950 .collect::<Vec<_>>();
951 assert_eq!(from_5, &[6, 7, 0, 1, 2, 3, 4, 5]);
952 let from_last = super::cycle_forward(&items, 7)
953 .map(|(_, val)| *val)
954 .collect::<Vec<_>>();
955 assert_eq!(from_last, &items);
956 let _ = super::cycle_forward::<i32>(&[], 5).count();
958 }
959
960 #[test]
961 fn cycle_iter_backward() {
962 let items = [0, 1, 2, 3, 4, 5, 6, 7];
963 let from_5 = super::cycle_backward(&items, 5)
964 .map(|(_, val)| *val)
965 .collect::<Vec<_>>();
966 assert_eq!(from_5, &[4, 3, 2, 1, 0, 7, 6, 5]);
967 let from_0 = super::cycle_backward(&items, 0)
968 .map(|(_, val)| *val)
969 .collect::<Vec<_>>();
970 assert_eq!(from_0, &[7, 6, 5, 4, 3, 2, 1, 0]);
971 let _ = super::cycle_backward::<i32>(&[], 5).count();
973 }
974
975 #[test]
976 fn blue_string_cluster_count_saturates() {
977 let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
978 let shaper = Shaper::new(&font, ShaperMode::Nominal);
979 let style_51 = synthetic_default_style(1, super::BLUE_STRING_MAX_LEN);
980 let style_70 = synthetic_default_style(1, super::BLUE_STRING_MAX_LEN + 19);
981 let blues_51 = super::compute_default_blues(&shaper, &[], &style_51);
982 let blues_70 = super::compute_default_blues(&shaper, &[], &style_70);
983 assert_eq!(blues_70, blues_51);
984 }
985
986 #[test]
987 fn blue_count_saturates_at_max_blues() {
988 let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
989 let shaper = Shaper::new(&font, ShaperMode::Nominal);
990 let known_good_blues = style::STYLE_CLASSES[super::StyleClass::LATN].script.blues;
991 let mut repeated = Vec::with_capacity(super::MAX_BLUES + 4);
992 for i in 0..(super::MAX_BLUES + 4) {
993 repeated.push(known_good_blues[i % known_good_blues.len()]);
994 }
995 let style = synthetic_style_with_blues(repeated);
996 let blues = super::compute_default_blues(&shaper, &[], &style);
997 assert_eq!(blues.len(), super::MAX_BLUES);
998 }
999
1000 fn synthetic_default_style(num_blues: usize, clusters_per_blue: usize) -> style::StyleClass {
1001 let cluster = (0..clusters_per_blue)
1002 .map(|_| "o")
1003 .collect::<Vec<_>>()
1004 .join(" ");
1005 let cluster: &'static str = Box::leak(cluster.into_boxed_str());
1006 let mut blues = Vec::with_capacity(num_blues);
1007 for _ in 0..num_blues {
1008 blues.push((cluster, BlueZones::TOP));
1009 }
1010 synthetic_style_with_blues(blues)
1011 }
1012
1013 fn synthetic_style_with_blues(blues: Vec<(&'static str, BlueZones)>) -> style::StyleClass {
1014 let blues: &'static [(&'static str, BlueZones)] = Box::leak(blues.into_boxed_slice());
1015 let script = Box::leak(Box::new(style::ScriptClass {
1016 name: "synthetic",
1017 group: style::ScriptGroup::Default,
1018 tag: Tag::new(b"DFLT"),
1019 hint_top_to_bottom: true,
1020 std_chars: "o",
1021 blues,
1022 }));
1023 style::StyleClass {
1024 name: "synthetic",
1025 index: 0,
1026 script,
1027 feature: None,
1028 }
1029 }
1030}