//! Apply edge hints to an outline. //! //! This happens in three passes: //! 1. Align points that are directly attached to edges. These are the points //! which originally generated the edge and are coincident with the edge //! coordinate (within a threshold) for a given axis. This may include //! points that were originally classified as weak. //! 2. Interpolate non-weak points that were not touched by the previous pass. //! This searches for the edges that enclose the point and interpolates the //! coordinate based on the adjustment applied to those edges. //! 3. Interpolate remaining untouched points. These are generally the weak //! points: those that are very near other points or lacking a dominant //! inward or outward direction. //! //! The final result is a fully hinted outline. use super::super::{ metrics::{fixed_div, fixed_mul, Scale}, outline::{Outline, Point}, recorder::HintsRecorder, style::ScriptGroup, topo::{Axis, Dimension}, ScaleFlags, }; use core::cmp::Ordering; use raw::tables::glyf::PointMarker; /// Align all points of an edge to the same coordinate value. /// /// See pub(crate) fn align_edge_points( outline: &mut Outline, axis: &Axis, group: ScriptGroup, scale: &Scale, ) -> Option<()> { let edges = axis.edges.as_slice(); let segments = axis.segments.as_slice(); let points = outline.points.as_mut_slice(); // Snapping is configurable for CJK // See let snap = group == ScriptGroup::Default || (axis.dim == Dimension::Horizontal && scale.flags.contains(ScaleFlags::HORIZONTAL_SNAP)) || (axis.dim == Dimension::Vertical && scale.flags.contains(ScaleFlags::VERTICAL_SNAP)); for segment in segments { let Some(edge) = segment.edge(edges) else { continue; }; let delta = edge.pos - edge.opos; let mut point_ix = segment.first(); let last_ix = segment.last(); loop { let point = points.get_mut(point_ix)?; if axis.dim == Dimension::Horizontal { if snap { point.x = edge.pos; } else { point.x += delta; } point.flags.set_marker(PointMarker::TOUCHED_X); } else { if snap { point.y = edge.pos; } else { point.y += delta; } point.flags.set_marker(PointMarker::TOUCHED_Y); } if point_ix == last_ix { break; } point_ix = point.next(); } } Some(()) } /// Align the strong points; equivalent to the TrueType `IP` instruction. /// /// See pub(crate) fn align_strong_points( outline: &mut Outline, axis: &mut Axis, mut recorder: Option<&mut HintsRecorder>, ) -> Option<()> { if axis.edges.is_empty() { return Some(()); } let dim = axis.dim; let touch_flag = if dim == Dimension::Horizontal { PointMarker::TOUCHED_X } else { PointMarker::TOUCHED_Y }; let points = outline.points.as_mut_slice(); 'points: for (point_ix, point) in points.iter_mut().enumerate() { // Skip points that are already touched; do weak interpolation in the // next pass if point .flags .has_marker(touch_flag | PointMarker::WEAK_INTERPOLATION) { continue; } let (u, ou) = if dim == Dimension::Vertical { (point.fy, point.oy) } else { (point.fx, point.ox) }; let edges = axis.edges.as_mut_slice(); // Is the point before the first edge? let edge = edges.first()?; let delta = edge.fpos as i32 - u; if delta >= 0 { if let Some(recorder) = recorder.as_mut() { recorder.record_ip_before(dim, point_ix); } store_point(point, dim, edge.pos - (edge.opos - ou)); continue; } // Is the point after the last edge? let edge = edges.last()?; let delta = u - edge.fpos as i32; if delta >= 0 { if let Some(recorder) = recorder.as_mut() { recorder.record_ip_after(dim, point_ix); } store_point(point, dim, edge.pos + (ou - edge.opos)); continue; } // Find enclosing edges; for a small number of edges, use a linear // search. // Note: this is actually critical for matching FreeType in cases where // we have more than one edge with the same fpos. When this happens, // linear and binary searches can produce different results. // See let min_ix = if edges.len() <= 8 { if let Some((min_ix, edge)) = edges .iter() .enumerate() .find(|(_ix, edge)| edge.fpos as i32 >= u) { if edge.fpos as i32 == u { if let Some(recorder) = recorder.as_mut() { recorder.record_ip_on(dim, point_ix, min_ix); } store_point(point, dim, edge.pos); continue 'points; } min_ix } else { 0 } } else { let mut min_ix = 0; let mut max_ix = edges.len(); while min_ix < max_ix { let mid_ix = (min_ix + max_ix) >> 1; let edge = &edges[mid_ix]; let fpos = edge.fpos as i32; match u.cmp(&fpos) { Ordering::Less => max_ix = mid_ix, Ordering::Greater => min_ix = mid_ix + 1, Ordering::Equal => { // We are on an edge if let Some(recorder) = recorder.as_mut() { recorder.record_ip_on(dim, point_ix, mid_ix); } store_point(point, dim, edge.pos); continue 'points; } } } min_ix }; // Point is not on an edge if let Some(before_ix) = min_ix.checked_sub(1) { let edge_before = edges.get(before_ix)?; let before_pos = edge_before.pos; let before_fpos = edge_before.fpos as i32; let scale = if edge_before.scale == 0 { let edge_after = edges.get(min_ix)?; let scale = fixed_div( edge_after.pos - edge_before.pos, edge_after.fpos as i32 - before_fpos, ); edges[before_ix].scale = scale; scale } else { edge_before.scale }; if let Some(recorder) = recorder.as_mut() { recorder.record_ip_between(dim, point_ix, before_ix, min_ix); } store_point(point, dim, before_pos + fixed_mul(u - before_fpos, scale)); } } Some(()) } /// Align the weak points; equivalent to the TrueType `IUP` instruction. /// /// See pub(crate) fn align_weak_points(outline: &mut Outline, dim: Dimension) -> Option<()> { let touch_marker = if dim == Dimension::Horizontal { for point in &mut outline.points { point.u = point.x; point.v = point.ox; } PointMarker::TOUCHED_X } else { for point in &mut outline.points { point.u = point.y; point.v = point.oy; } PointMarker::TOUCHED_Y }; for contour in &outline.contours { let points = outline.points.get_mut(contour.range())?; // Find first touched point let Some(first_touched_ix) = points .iter() .position(|point| point.flags.has_marker(touch_marker)) else { continue; }; let last_ix = points.len() - 1; let mut point_ix = first_touched_ix; let mut last_touched_ix; 'outer: loop { // Skip any touched neighbors while point_ix < last_ix && points.get(point_ix + 1)?.flags.has_marker(touch_marker) { point_ix += 1; } last_touched_ix = point_ix; // Find the next touched point point_ix += 1; loop { if point_ix > last_ix { break 'outer; } if points[point_ix].flags.has_marker(touch_marker) { break; } point_ix += 1; } iup_interpolate( points, last_touched_ix + 1, point_ix - 1, last_touched_ix, point_ix, ); } if last_touched_ix == first_touched_ix { // Special case: only one point was touched iup_shift(points, 0, last_ix, first_touched_ix); } else { // Interpolate the remainder if last_touched_ix < last_ix { iup_interpolate( points, last_touched_ix + 1, last_ix, last_touched_ix, first_touched_ix, ); } if first_touched_ix > 0 { iup_interpolate( points, 0, first_touched_ix - 1, last_touched_ix, first_touched_ix, ); } } } // Save interpolated values if dim == Dimension::Horizontal { for point in &mut outline.points { point.x = point.u; } } else { for point in &mut outline.points { point.y = point.u; } } Some(()) } #[inline(always)] fn store_point(point: &mut Point, dim: Dimension, u: i32) { if dim == Dimension::Horizontal { point.x = u; point.flags.set_marker(PointMarker::TOUCHED_X); } else { point.y = u; point.flags.set_marker(PointMarker::TOUCHED_Y); } } /// Shift original coordinates of all points between `p1_ix` and `p2_ix` /// (inclusive) to get hinted coordinates using the same difference as /// given by the point at `ref_ix`. /// /// The `u` and `v` members are the current and original coordinate values, /// respectively. /// /// See fn iup_shift(points: &mut [Point], p1_ix: usize, p2_ix: usize, ref_ix: usize) -> Option<()> { let ref_point = points.get(ref_ix)?; let delta = ref_point.u - ref_point.v; if delta == 0 { return Some(()); } for point in points.get_mut(p1_ix..ref_ix)? { point.u = point.v + delta; } for point in points.get_mut(ref_ix + 1..=p2_ix)? { point.u = point.v + delta; } Some(()) } /// Interpolate the original coordinates all of points between `p1_ix` and /// `p2_ix` (inclusive) to get hinted coordinates, using the points at /// `ref1_ix` and `ref2_ix` as the reference points. /// /// The `u` and `v` members are the current and original coordinate values, /// respectively. /// /// See fn iup_interpolate( points: &mut [Point], p1_ix: usize, p2_ix: usize, ref1_ix: usize, ref2_ix: usize, ) -> Option<()> { if p1_ix > p2_ix { return Some(()); } let mut ref_point1 = points.get(ref1_ix)?; let mut ref_point2 = points.get(ref2_ix)?; if ref_point1.v > ref_point2.v { core::mem::swap(&mut ref_point1, &mut ref_point2); } let (u1, v1) = (ref_point1.u, ref_point1.v); let (u2, v2) = (ref_point2.u, ref_point2.v); let d1 = u1 - v1; let d2 = u2 - v2; if u1 == u2 || v1 == v2 { for point in points.get_mut(p1_ix..=p2_ix)? { point.u = if point.v <= v1 { point.v + d1 } else if point.v >= v2 { point.v + d2 } else { u1 }; } } else { let scale = fixed_div(u2 - u1, v2 - v1); for point in points.get_mut(p1_ix..=p2_ix)? { point.u = if point.v <= v1 { point.v + d1 } else if point.v >= v2 { point.v + d2 } else { u1 + fixed_mul(point.v - v1, scale) }; } } Some(()) } #[cfg(test)] mod tests { use super::{ super::super::{ metrics::{compute_unscaled_style_metrics, Scale}, recorder::{EdgeAction, HintAction, HintsRecorder, PointAction}, shape::{Shaper, ShaperMode}, style, }, super::{EdgeMetrics, HintedMetrics}, *, }; use crate::{attribute::Style, MetadataProvider}; use raw::{ types::{F2Dot14, GlyphId}, FontRef, TableProvider, }; #[test] fn hinted_coords_and_metrics_default() { let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap(); let (outline, metrics) = hint_outline( &font, 16.0, Default::default(), GlyphId::new(9), &style::STYLE_CLASSES[style::StyleClass::HEBR], ); // Expected values were painfully extracted from FreeType with some // printf debugging #[rustfmt::skip] let expected_coords = [ (133, -256), (133, 282), (133, 343), (146, 431), (158, 463), (158, 463), (57, 463), (30, 463), (0, 495), (0, 534), (0, 548), (2, 570), (11, 604), (17, 633), (50, 633), (50, 629), (50, 604), (77, 576), (101, 576), (163, 576), (180, 576), (192, 562), (192, 542), (192, 475), (190, 457), (187, 423), (187, 366), (187, 315), (187, -220), (178, -231), (159, -248), (146, -256), ]; let coords = outline .points .iter() .map(|point| (point.x, point.y)) .collect::>(); assert_eq!(coords, expected_coords); let expected_metrics = HintedMetrics { x_scale: 67109, edge_metrics: Some(EdgeMetrics { left_opos: 15, left_pos: 0, right_opos: 210, right_pos: 192, }), }; assert_eq!(metrics, expected_metrics); } #[test] fn recorder_collects_edge_and_point_actions() { let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap(); let shaper = Shaper::new(&font, ShaperMode::Nominal); let glyph = font.outline_glyphs().get(GlyphId::new(9)).unwrap(); let mut outline = Outline::default(); outline.fill(&glyph, &[], Default::default()).unwrap(); let metrics = compute_unscaled_style_metrics( &shaper, &[], &style::STYLE_CLASSES[style::StyleClass::HEBR], Default::default(), ); let scale = Scale::new( 16.0, font.head().unwrap().units_per_em() as i32, Style::Normal, Default::default(), metrics.style_class().script.group, ); let mut recorder = HintsRecorder::default(); super::super::hint_outline_with_recorder( &mut outline, &metrics, &scale, None, &mut recorder, ); assert!(recorder .actions .iter() .any(|record| matches!(record, HintAction::Edge(edge) if edge.blue.is_some()))); assert!(recorder.actions.iter().any(|record| { matches!( record, HintAction::Edge(edge) if matches!(edge.action, EdgeAction::Blue | EdgeAction::BlueAnchor | EdgeAction::Anchor | EdgeAction::Stem | EdgeAction::Adjust) ) })); assert!(recorder.actions.iter().any(|record| { matches!( record, HintAction::Point(point) if matches!(point.action, PointAction::IpBefore | PointAction::IpAfter | PointAction::IpOn | PointAction::IpBetween) ) })); } #[test] fn hinted_coords_and_metrics_cjk() { let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap(); let (outline, metrics) = hint_outline( &font, 16.0, Default::default(), GlyphId::new(9), &style::STYLE_CLASSES[style::StyleClass::HANI], ); // Expected values were painfully extracted from FreeType with some // printf debugging let expected_coords = [ (279, 768), (568, 768), (618, 829), (618, 829), (634, 812), (657, 788), (685, 758), (695, 746), (692, 720), (667, 720), (288, 720), (704, 704), (786, 694), (785, 685), (777, 672), (767, 670), (767, 163), (767, 159), (750, 148), (728, 142), (716, 142), (704, 142), (402, 767), (473, 767), (473, 740), (450, 598), (338, 357), (236, 258), (220, 270), (274, 340), (345, 499), (390, 675), (344, 440), (398, 425), (464, 384), (496, 343), (501, 307), (486, 284), (458, 281), (441, 291), (434, 314), (398, 366), (354, 416), (334, 433), (832, 841), (934, 830), (932, 819), (914, 804), (896, 802), (896, 30), (896, 5), (885, -35), (848, -60), (809, -65), (807, -51), (794, -27), (781, -19), (767, -11), (715, 0), (673, 5), (673, 21), (673, 21), (707, 18), (756, 15), (799, 13), (807, 13), (821, 13), (832, 23), (832, 35), (407, 624), (594, 624), (594, 546), (396, 546), (569, 576), (558, 576), (599, 614), (677, 559), (671, 552), (654, 547), (636, 545), (622, 458), (572, 288), (488, 130), (357, -5), (259, -60), (246, -45), (327, 9), (440, 150), (516, 311), (558, 486), (128, 542), (158, 581), (226, 576), (223, 562), (207, 543), (193, 539), (193, -44), (193, -46), (175, -56), (152, -64), (141, -64), (128, -64), (195, 850), (300, 820), (295, 799), (259, 799), (234, 712), (163, 543), (80, 395), (33, 338), (19, 347), (54, 410), (120, 575), (176, 759), ]; let coords = outline .points .iter() .map(|point| (point.x, point.y)) .collect::>(); assert_eq!(coords, expected_coords); let expected_metrics = HintedMetrics { x_scale: 67109, edge_metrics: Some(EdgeMetrics { left_opos: 141, left_pos: 128, right_opos: 933, right_pos: 896, }), }; assert_eq!(metrics, expected_metrics); } /// Empty glyphs (like spaces) have no edges and therefore no edge /// metrics #[test] fn missing_edge_metrics() { let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap(); let (_outline, metrics) = hint_outline( &font, 16.0, Default::default(), GlyphId::new(1), &style::STYLE_CLASSES[style::StyleClass::LATN], ); let expected_metrics = HintedMetrics { x_scale: 65536, edge_metrics: None, }; assert_eq!(metrics, expected_metrics); } // Specific test case for which // uses the Ahem // font #[test] fn skia_ahem_test_case() { let font = FontRef::new(font_test_data::AHEM).unwrap(); let outline = hint_outline( &font, 24.0, Default::default(), // This glyph is the typical Ahem block square; the link to the // font description above more detail. GlyphId::new(5), &style::STYLE_CLASSES[style::StyleClass::LATN], ) .0; let expected_coords = [(0, 1216), (1536, 1216), (1536, -320), (0, -320)]; // See // Note that Skia inverts y coords let expected_float_coords = [(0.0, 19.0), (24.0, 19.0), (24.0, -5.0), (0.0, -5.0)]; let coords = outline .points .iter() .map(|point| (point.x, point.y)) .collect::>(); let float_coords = coords .iter() .map(|(x, y)| (*x as f32 / 64.0, *y as f32 / 64.0)) .collect::>(); assert_eq!(coords, expected_coords); assert_eq!(float_coords, expected_float_coords); } fn hint_outline( font: &FontRef, size: f32, coords: &[F2Dot14], gid: GlyphId, style: &style::StyleClass, ) -> (Outline, HintedMetrics) { let shaper = Shaper::new(font, ShaperMode::Nominal); let glyphs = font.outline_glyphs(); let glyph = glyphs.get(gid).unwrap(); let mut outline = Outline::default(); outline.fill(&glyph, coords, Default::default()).unwrap(); let metrics = compute_unscaled_style_metrics(&shaper, coords, style, Default::default()); let scale = Scale::new( size, font.head().unwrap().units_per_em() as i32, Style::Normal, Default::default(), metrics.style_class().script.group, ); let hinted_metrics = super::super::hint_outline(&mut outline, &metrics, &scale, None); (outline, hinted_metrics) } }