From 007d5589cfcd01ef96e171681d50090b8f8ca2cd Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 6 Sep 2024 20:53:48 -0400 Subject: [PATCH] [skrifa] autohint: CJK edge hinting (#1138) Completes the CJK hinting algorithm --- skrifa/src/outline/autohint/hint.rs | 187 ++++- skrifa/src/outline/autohint/latin/hint.rs | 771 +++++++++++++------ skrifa/src/outline/autohint/latin/metrics.rs | 17 +- skrifa/src/outline/autohint/latin/mod.rs | 3 +- skrifa/src/outline/autohint/metrics.rs | 9 +- skrifa/src/outline/autohint/outline.rs | 2 +- skrifa/src/outline/autohint/style.rs | 3 +- 7 files changed, 736 insertions(+), 256 deletions(-) diff --git a/skrifa/src/outline/autohint/hint.rs b/skrifa/src/outline/autohint/hint.rs index ed558d8f7..189c49c06 100644 --- a/skrifa/src/outline/autohint/hint.rs +++ b/skrifa/src/outline/autohint/hint.rs @@ -20,29 +20,49 @@ use super::{ axis::{Axis, Dimension}, metrics::{fixed_div, fixed_mul, Scale}, outline::{Outline, Point}, + style::ScriptGroup, }; use core::cmp::Ordering; /// Align all points of an edge to the same coordinate value. /// /// See -pub(crate) fn align_edge_points(outline: &mut Outline, axis: &Axis) -> Option<()> { +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 == Axis::HORIZONTAL && scale.flags & Scale::HORIZONTAL_SNAP != 0) + || (axis.dim == Axis::VERTICAL && scale.flags & Scale::VERTICAL_SNAP != 0)); 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 == Axis::HORIZONTAL { - point.x = edge.pos; + if snap { + point.x = edge.pos; + } else { + point.x += delta; + } point.flags.set_marker(PointMarker::TOUCHED_X); } else { - point.y = edge.pos; + if snap { + point.y = edge.pos; + } else { + point.y += delta; + } point.flags.set_marker(PointMarker::TOUCHED_Y); } if point_ix == last_ix { @@ -67,7 +87,8 @@ pub(crate) fn align_strong_points(outline: &mut Outline, axis: &mut Axis) -> Opt } else { PointMarker::TOUCHED_Y }; - 'points: for point in &mut outline.points { + let points = outline.points.as_mut_slice(); + 'points: for point in points { // Skip points that are already touched; do weak interpolation in the // next pass if point @@ -326,15 +347,17 @@ mod tests { }; #[test] - fn hinted_coords_and_metrics() { + fn hinted_coords_and_metrics_default() { let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap(); - let (outline, metrics) = hint_latin_outline( + 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), @@ -388,12 +411,158 @@ mod tests { assert_eq!(metrics, expected_metrics); } + #[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 = latin::HintedMetrics { + x_scale: 67109, + edge_metrics: Some(latin::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_latin_outline( + let (_outline, metrics) = hint_outline( &font, 16.0, Default::default(), @@ -413,7 +582,7 @@ mod tests { #[test] fn skia_ahem_test_case() { let font = FontRef::new(font_test_data::AHEM).unwrap(); - let outline = hint_latin_outline( + let outline = hint_outline( &font, 24.0, Default::default(), @@ -440,7 +609,7 @@ mod tests { assert_eq!(float_coords, expected_float_coords); } - fn hint_latin_outline( + fn hint_outline( font: &FontRef, size: f32, coords: &[F2Dot14], diff --git a/skrifa/src/outline/autohint/latin/hint.rs b/skrifa/src/outline/autohint/latin/hint.rs index 29c2b676c..720ff6b24 100644 --- a/skrifa/src/outline/autohint/latin/hint.rs +++ b/skrifa/src/outline/autohint/latin/hint.rs @@ -6,7 +6,8 @@ use super::super::{ axis::{Axis, Dimension, Edge}, - metrics::{fixed_mul_div, pix_round, Scale, ScaledAxisMetrics, ScaledWidth}, + metrics::{fixed_mul_div, pix_floor, pix_round, Scale, ScaledAxisMetrics, ScaledWidth}, + style::ScriptGroup, }; /// Main Latin grid-fitting routine. @@ -17,6 +18,7 @@ use super::super::{ pub(crate) fn hint_edges( axis: &mut Axis, metrics: &ScaledAxisMetrics, + group: ScriptGroup, scale: &Scale, mut top_to_bottom_hinting: bool, ) { @@ -24,10 +26,16 @@ pub(crate) fn hint_edges( top_to_bottom_hinting = false; } // First align horizontal edges to blue zones if needed - let anchor_ix = align_edges_to_blues(axis, metrics, scale); + let anchor_ix = align_edges_to_blues(axis, metrics, group, scale); // Now align the stem edges - let (serif_count, anchor_ix) = - align_stem_edges(axis, metrics, scale, top_to_bottom_hinting, anchor_ix); + let (serif_count, anchor_ix) = align_stem_edges( + axis, + metrics, + group, + scale, + top_to_bottom_hinting, + anchor_ix, + ); let edges = axis.edges.as_mut_slice(); // Special case for lowercase m if axis.dim == Axis::HORIZONTAL && (edges.len() == 6 || edges.len() == 12) { @@ -35,7 +43,7 @@ pub(crate) fn hint_edges( } // Handle serifs and single segment edges if serif_count > 0 || anchor_ix.is_none() { - align_remaining_edges(axis, top_to_bottom_hinting, anchor_ix); + align_remaining_edges(axis, group, top_to_bottom_hinting, serif_count, anchor_ix); } } @@ -45,62 +53,64 @@ pub(crate) fn hint_edges( fn align_edges_to_blues( axis: &mut Axis, metrics: &ScaledAxisMetrics, + group: ScriptGroup, scale: &Scale, ) -> Option { let mut anchor_ix = None; - // For a vertical axis, begin by aligning stems to blue zones - if axis.dim == Axis::VERTICAL { - for edge_ix in 0..axis.edges.len() { - let edges = axis.edges.as_mut_slice(); - let edge = &edges[edge_ix]; - if edge.flags & Edge::DONE != 0 { - continue; - } - let edge2_ix = edge.link_ix.map(|x| x as usize); - let edge2 = edge2_ix.map(|ix| &edges[ix]); - // If we have two neutral zones, skip one of them - if let (true, Some(edge2)) = (edge.blue_edge.is_some(), edge2) { - if edge2.blue_edge.is_some() { - let skip_ix = if edge2.flags & Edge::NEUTRAL != 0 { - edge2_ix - } else if edge.flags & Edge::NEUTRAL != 0 { - Some(edge_ix) - } else { - None - }; - if let Some(skip_ix) = skip_ix { - let skip_edge = &mut edges[skip_ix]; - skip_edge.blue_edge = None; - skip_edge.flags &= !Edge::NEUTRAL; - } - } - } - // Flip edges if the other is aligned to a blue zone - let blue = edges[edge_ix].blue_edge; - let (blue, edge1_ix, edge2_ix) = if let Some(blue) = blue { - (blue, Some(edge_ix), edge2_ix) - } else if let Some(edge2_blue) = edge2_ix.and_then(|ix| edges[ix].blue_edge) { - (edge2_blue, edge2_ix, Some(edge_ix)) - } else { - (Default::default(), None, None) - }; - let Some(edge1_ix) = edge1_ix else { - continue; - }; - let edge1 = &mut edges[edge1_ix]; - edge1.pos = blue.fitted; - edge1.flags |= Edge::DONE; - if let Some(edge2_ix) = edge2_ix { - let edge2 = &mut edges[edge2_ix]; - if edge2.blue_edge.is_none() { - edge2.flags |= Edge::DONE; - align_linked_edge(axis, metrics, scale, edge1_ix, edge2_ix); + // For default script group, only do vertical blues + if group == ScriptGroup::Default && axis.dim != Axis::VERTICAL { + return anchor_ix; + } + for edge_ix in 0..axis.edges.len() { + let edges = axis.edges.as_mut_slice(); + let edge = &edges[edge_ix]; + if edge.flags & Edge::DONE != 0 { + continue; + } + let edge2_ix = edge.link_ix.map(|x| x as usize); + let edge2 = edge2_ix.map(|ix| &edges[ix]); + // If we have two neutral zones, skip one of them. + if let (true, Some(edge2)) = (edge.blue_edge.is_some(), edge2) { + if edge2.blue_edge.is_some() { + let skip_ix = if edge2.flags & Edge::NEUTRAL != 0 { + edge2_ix + } else if edge.flags & Edge::NEUTRAL != 0 { + Some(edge_ix) + } else { + None + }; + if let Some(skip_ix) = skip_ix { + let skip_edge = &mut edges[skip_ix]; + skip_edge.blue_edge = None; + skip_edge.flags &= !Edge::NEUTRAL; } } - if anchor_ix.is_none() { - anchor_ix = Some(edge_ix); + } + // Flip edges if the other is aligned to a blue zone + let blue = edges[edge_ix].blue_edge; + let (blue, edge1_ix, edge2_ix) = if let Some(blue) = blue { + (blue, Some(edge_ix), edge2_ix) + } else if let Some(edge2_blue) = edge2_ix.and_then(|ix| edges[ix].blue_edge) { + (edge2_blue, edge2_ix, Some(edge_ix)) + } else { + (Default::default(), None, None) + }; + let Some(edge1_ix) = edge1_ix else { + continue; + }; + let edge1 = &mut edges[edge1_ix]; + edge1.pos = blue.fitted; + edge1.flags |= Edge::DONE; + if let Some(edge2_ix) = edge2_ix { + let edge2 = &mut edges[edge2_ix]; + if edge2.blue_edge.is_none() { + edge2.flags |= Edge::DONE; + align_linked_edge(axis, metrics, group, scale, edge1_ix, edge2_ix); } } + if anchor_ix.is_none() { + anchor_ix = Some(edge_ix); + } } anchor_ix } @@ -111,11 +121,14 @@ fn align_edges_to_blues( fn align_stem_edges( axis: &mut Axis, metrics: &ScaledAxisMetrics, + group: ScriptGroup, scale: &Scale, top_to_bottom_hinting: bool, mut anchor_ix: Option, ) -> (usize, Option) { let mut serif_count = 0; + let mut last_stem_pos = None; + let mut delta = 0; // Now align all other stem edges // This code starts at: for edge_ix in 0..axis.edges.len() { @@ -129,102 +142,133 @@ fn align_stem_edges( serif_count += 1; continue; }; + // For CJK, skip stems that are too close. We'll deal with them later + // See + if group != ScriptGroup::Default { + if let Some(last_pos) = last_stem_pos { + if edge.pos < last_pos + 64 || edges[edge2_ix].pos < last_pos + 64 { + serif_count += 1; + continue; + } + } + } // This shouldn't happen? if edges[edge2_ix].blue_edge.is_some() { edges[edge2_ix].flags |= Edge::DONE; - align_linked_edge(axis, metrics, scale, edge2_ix, edge_ix); + align_linked_edge(axis, metrics, group, scale, edge2_ix, edge_ix); continue; } - // Now align the stem - // Note: the branches here are reversed from the FreeType code - // See - if let Some(anchor_ix) = anchor_ix { - let anchor = &edges[anchor_ix]; - let edge = edges[edge_ix]; - let edge2 = edges[edge2_ix]; - let original_pos = anchor.pos + (edge.opos - anchor.opos); - let original_len = edge2.opos - edge.opos; - let original_center = original_pos + (original_len >> 1); - let cur_len = stem_width( - axis.dim, - metrics, - scale, - original_len, - 0, - edge.flags, - edge2.flags, - ); - if edge2.flags & Edge::DONE != 0 { - let new_pos = edge2.pos - cur_len; - edges[edge_ix].pos = new_pos; - } else if cur_len < 96 { - let cur_pos1 = pix_round(original_center); - let (u_off, d_off) = if cur_len <= 64 { (32, 32) } else { (38, 26) }; - let delta1 = (original_center - (cur_pos1 - u_off)).abs(); - let delta2 = (original_center - (cur_pos1 + d_off)).abs(); - let cur_pos1 = if delta1 < delta2 { - cur_pos1 - u_off + if group == ScriptGroup::Default { + // Now align the stem + // Note: the branches here are reversed from the FreeType code + // See + if let Some(anchor_ix) = anchor_ix { + let anchor = &edges[anchor_ix]; + let edge = edges[edge_ix]; + let edge2 = edges[edge2_ix]; + let original_pos = anchor.pos + (edge.opos - anchor.opos); + let original_len = edge2.opos - edge.opos; + let original_center = original_pos + (original_len >> 1); + let cur_len = stem_width( + metrics, + group, + scale, + original_len, + 0, + edge.flags, + edge2.flags, + ); + if edge2.flags & Edge::DONE != 0 { + let new_pos = edge2.pos - cur_len; + edges[edge_ix].pos = new_pos; + } else if cur_len < 96 { + let cur_pos1 = pix_round(original_center); + let (u_off, d_off) = if cur_len <= 64 { (32, 32) } else { (38, 26) }; + let delta1 = (original_center - (cur_pos1 - u_off)).abs(); + let delta2 = (original_center - (cur_pos1 + d_off)).abs(); + let cur_pos1 = if delta1 < delta2 { + cur_pos1 - u_off + } else { + cur_pos1 + d_off + }; + edges[edge_ix].pos = cur_pos1 - cur_len / 2; + edges[edge2_ix].pos = cur_pos1 + cur_len / 2; } else { - cur_pos1 + d_off - }; - edges[edge_ix].pos = cur_pos1 - cur_len / 2; - edges[edge2_ix].pos = cur_pos1 + cur_len / 2; - } else { - let cur_pos1 = pix_round(original_pos); - let delta1 = (cur_pos1 + (cur_len >> 1) - original_center).abs(); - let cur_pos2 = pix_round(original_pos + original_len) - cur_len; - let delta2 = (cur_pos2 + (cur_len >> 1) - original_center).abs(); - let new_pos = if delta1 < delta2 { cur_pos1 } else { cur_pos2 }; - let new_pos2 = new_pos + cur_len; - edges[edge_ix].pos = new_pos; - edges[edge2_ix].pos = new_pos2; - } - edges[edge_ix].flags |= Edge::DONE; - edges[edge2_ix].flags |= Edge::DONE; - if edge_ix > 0 { - adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting); - } - } else { - // No stem has been aligned yet - let edge = edges[edge_ix]; - let edge2 = edges[edge2_ix]; - let original_len = edge2.opos - edge.opos; - let cur_len = stem_width( - axis.dim, - metrics, - scale, - original_len, - 0, - edge.flags, - edge2.flags, - ); - // Some "voodoo" to specially round edges for small stem widths - let (u_off, d_off) = if cur_len <= 64 { - // width <= 1px - (32, 32) + let cur_pos1 = pix_round(original_pos); + let delta1 = (cur_pos1 + (cur_len >> 1) - original_center).abs(); + let cur_pos2 = pix_round(original_pos + original_len) - cur_len; + let delta2 = (cur_pos2 + (cur_len >> 1) - original_center).abs(); + let new_pos = if delta1 < delta2 { cur_pos1 } else { cur_pos2 }; + let new_pos2 = new_pos + cur_len; + edges[edge_ix].pos = new_pos; + edges[edge2_ix].pos = new_pos2; + } + edges[edge_ix].flags |= Edge::DONE; + edges[edge2_ix].flags |= Edge::DONE; + if edge_ix > 0 { + adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting); + } } else { - // 1px < width < 1.5px - (38, 26) - }; - if cur_len < 96 { - let original_center = edge.opos + (original_len >> 1); - let mut cur_pos1 = pix_round(original_center); - let error1 = (original_center - (cur_pos1 - u_off)).abs(); - let error2 = (original_center - (cur_pos1 + d_off)).abs(); - if error1 < error2 { - cur_pos1 -= u_off; + // No stem has been aligned yet + let edge = edges[edge_ix]; + let edge2 = edges[edge2_ix]; + let original_len = edge2.opos - edge.opos; + let cur_len = stem_width( + metrics, + group, + scale, + original_len, + 0, + edge.flags, + edge2.flags, + ); + // Some "voodoo" to specially round edges for small stem widths + let (u_off, d_off) = if cur_len <= 64 { + // width <= 1px + (32, 32) + } else { + // 1px < width < 1.5px + (38, 26) + }; + if cur_len < 96 { + let original_center = edge.opos + (original_len >> 1); + let mut cur_pos1 = pix_round(original_center); + let error1 = (original_center - (cur_pos1 - u_off)).abs(); + let error2 = (original_center - (cur_pos1 + d_off)).abs(); + if error1 < error2 { + cur_pos1 -= u_off; + } else { + cur_pos1 += d_off; + } + let edge_pos = cur_pos1 - cur_len / 2; + edges[edge_ix].pos = edge_pos; + edges[edge2_ix].pos = edge_pos + cur_len; } else { - cur_pos1 += d_off; + edges[edge_ix].pos = pix_round(edge.opos); } - let edge_pos = cur_pos1 - cur_len / 2; - edges[edge_ix].pos = edge_pos; - edges[edge2_ix].pos = edge_pos + cur_len; + edges[edge_ix].flags |= Edge::DONE; + align_linked_edge(axis, metrics, group, scale, edge_ix, edge2_ix); + anchor_ix = Some(edge_ix); + } + } else { + // More CJK divergence + // See + if edge2_ix < edge_ix { + last_stem_pos = Some(edge.pos); + edges[edge_ix].flags |= Edge::DONE; + align_linked_edge(axis, metrics, group, scale, edge2_ix, edge_ix); + continue; + } + if axis.dim != Axis::VERTICAL && anchor_ix.is_none() { + delta = hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta); } else { - edges[edge_ix].pos = pix_round(edge.opos); + hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta); } - edges[edge_ix].flags |= Edge::DONE; - align_linked_edge(axis, metrics, scale, edge_ix, edge2_ix); anchor_ix = Some(edge_ix); + axis.edges[edge_ix].flags |= Edge::DONE; + let edge2 = &mut axis.edges[edge2_ix]; + edge2.flags |= Edge::DONE; + last_stem_pos = Some(edge2.pos); } } (serif_count, anchor_ix) @@ -265,70 +309,107 @@ fn hint_lowercase_m(edges: &mut [Edge]) { } /// Align serif and single segment edges. -/// -/// See fn align_remaining_edges( axis: &mut Axis, + group: ScriptGroup, top_to_bottom_hinting: bool, + mut serif_count: usize, mut anchor_ix: Option, ) { - for edge_ix in 0..axis.edges.len() { - let edges = &mut axis.edges; - let edge = &edges[edge_ix]; - if edge.flags & Edge::DONE != 0 { - continue; - } - let mut delta = 1000; - if let Some(serif) = edge.serif(edges) { - delta = (serif.opos - edge.opos).abs(); - } - if delta < 64 + 16 { - // delta is only < 1000 if edge.serif_ix is Some(_) - let serif_ix = edge.serif_ix.unwrap() as usize; - align_serif_edge(axis, serif_ix, edge_ix) - } else if let Some(anchor_ix) = anchor_ix { - let mut before_ix = None; - for ix in (0..=edge_ix.saturating_sub(1)).rev() { - if edges[ix].flags & Edge::DONE != 0 { - before_ix = Some(ix); - break; - } + if group == ScriptGroup::Default { + /// See + for edge_ix in 0..axis.edges.len() { + let edges = &mut axis.edges; + let edge = &edges[edge_ix]; + if edge.flags & Edge::DONE != 0 { + continue; } - let mut after_ix = None; - for ix in edge_ix + 1..edges.len() { - if edges[ix].flags & Edge::DONE != 0 { - after_ix = Some(ix); - break; - } + let mut delta = 1000; + if let Some(serif) = edge.serif(edges) { + delta = (serif.opos - edge.opos).abs(); } - if let Some((before_ix, after_ix)) = before_ix.zip(after_ix) { - let before = &edges[before_ix]; - let after = &edges[after_ix]; - let new_pos = if after.opos == before.opos { - before.pos + if delta < 64 + 16 { + // delta is only < 1000 if edge.serif_ix is Some(_) + let serif_ix = edge.serif_ix.unwrap() as usize; + align_serif_edge(axis, serif_ix, edge_ix) + } else if let Some(anchor_ix) = anchor_ix { + let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix); + if let Some((before_ix, after_ix)) = before_ix.zip(after_ix) { + let before = &edges[before_ix]; + let after = &edges[after_ix]; + let new_pos = if after.opos == before.opos { + before.pos + } else { + before.pos + + fixed_mul_div( + edge.opos - before.opos, + after.pos - before.pos, + after.opos - before.opos, + ) + }; + edges[edge_ix].pos = new_pos; } else { - before.pos - + fixed_mul_div( - edge.opos - before.opos, - after.pos - before.pos, - after.opos - before.opos, - ) - }; - edges[edge_ix].pos = new_pos; + let anchor = &edges[anchor_ix]; + let new_pos = anchor.pos + ((edge.opos - anchor.opos + 16) & !31); + edges[edge_ix].pos = new_pos; + } } else { - let anchor = &edges[anchor_ix]; - let new_pos = anchor.pos + ((edge.opos - anchor.opos + 16) & !31); + anchor_ix = Some(edge_ix); + let new_pos = pix_round(edge.opos); edges[edge_ix].pos = new_pos; } - } else { - anchor_ix = Some(edge_ix); - let new_pos = pix_round(edge.opos); - edges[edge_ix].pos = new_pos; + let edges = &mut axis.edges; + edges[edge_ix].flags |= Edge::DONE; + adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting); + adjust_link(edges, edge_ix, LinkDir::Next, top_to_bottom_hinting); + } + } else { + // See + for edge_ix in 0..axis.edges.len() { + let edge = &mut axis.edges[edge_ix]; + if edge.flags & Edge::DONE != 0 { + continue; + } + if let Some(serif_ix) = edge.serif_ix.map(|ix| ix as usize) { + edge.flags |= Edge::DONE; + align_serif_edge(axis, serif_ix, edge_ix); + serif_count = serif_count.saturating_sub(1); + } + } + if serif_count == 0 { + return; + } + for edge_ix in 0..axis.edges.len() { + let edges = axis.edges.as_mut_slice(); + let edge = &edges[edge_ix]; + if edge.flags & Edge::DONE != 0 { + continue; + } + let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix); + match (before_ix, after_ix) { + (Some(before_ix), None) => { + align_serif_edge(axis, before_ix, edge_ix); + } + (None, Some(after_ix)) => { + align_serif_edge(axis, after_ix, edge_ix); + } + (Some(before_ix), Some(after_ix)) => { + let before = edges[before_ix]; + let after = edges[after_ix]; + if after.fpos == before.fpos { + edges[edge_ix].pos = before.pos; + } else { + edges[edge_ix].pos = before.pos + + fixed_mul_div( + edge.fpos as i32 - before.fpos as i32, + after.pos - before.pos, + after.fpos as i32 - before.fpos as i32, + ); + } + } + _ => {} + } } - let edges = &mut axis.edges; - edges[edge_ix].flags |= Edge::DONE; - adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting); - adjust_link(edges, edge_ix, LinkDir::Next, top_to_bottom_hinting); } } @@ -376,6 +457,26 @@ fn adjust_link( Some(()) } +/// Returns the indices of the "completed" edges before and after the given +/// edge index. +fn find_bounding_completed_edges(edges: &[Edge], ix: usize) -> [Option; 2] { + let before_ix = edges + .get(..ix) + .unwrap_or_default() + .iter() + .enumerate() + .rev() + .filter_map(|(ix, edge)| (edge.flags & Edge::DONE != 0).then_some(ix)) + .next(); + let after_ix = edges + .iter() + .enumerate() + .skip(ix + 1) + .filter_map(|(ix, edge)| (edge.flags & Edge::DONE != 0).then_some(ix)) + .next(); + [before_ix, after_ix] +} + /// Snap a scaled width to one of the standard widths. /// /// See @@ -409,33 +510,37 @@ fn snap_width(widths: &[ScaledWidth], width: i32) -> i32 { /// /// See fn stem_width( - dim: Dimension, metrics: &ScaledAxisMetrics, + group: ScriptGroup, scale: &Scale, width: i32, base_delta: i32, base_flags: u8, stem_flags: u8, ) -> i32 { - if scale.flags & Scale::STEM_ADJUST == 0 || metrics.width_metrics.is_extra_light { + if scale.flags & Scale::STEM_ADJUST == 0 + || (group == ScriptGroup::Default && metrics.width_metrics.is_extra_light) + { return width; } - let is_vertical = dim == Axis::VERTICAL; + let is_vertical = metrics.dim == Axis::VERTICAL; let sign = if width < 0 { -1 } else { 1 }; let mut dist = width.abs(); if (is_vertical && scale.flags & Scale::VERTICAL_SNAP == 0) || (!is_vertical && scale.flags & Scale::HORIZONTAL_SNAP == 0) { // Do smooth hinting - if (stem_flags & Edge::SERIF != 0) && is_vertical && (dist < 3 * 64) { - // Don't touch widths of serifs - return dist * sign; - } else if base_flags & Edge::ROUND != 0 { - if dist < 80 { - dist = 64; + if group == ScriptGroup::Default { + if (stem_flags & Edge::SERIF != 0) && is_vertical && (dist < 3 * 64) { + // Don't touch widths of serifs + return dist * sign; + } else if base_flags & Edge::ROUND != 0 { + if dist < 80 { + dist = 64; + } + } else if dist < 56 { + dist = 56; } - } else if dist < 56 { - dist = 56; } if !metrics.widths.is_empty() { // Compare to standard width @@ -445,28 +550,52 @@ fn stem_width( dist = min_width.max(48); return dist * sign; } - if dist < 3 * 64 { - let delta = dist & 63; - dist &= -64; - if delta < 10 { - dist += delta; - } else if delta < 32 { - dist += 10; - } else if delta < 54 { - dist += 54; + if group == ScriptGroup::Default { + // Default/Latin behavior + // See + if dist < 3 * 64 { + let delta = dist & 63; + dist &= -64; + if delta < 10 { + dist += delta; + } else if delta < 32 { + dist += 10; + } else if delta < 54 { + dist += 54; + } else { + dist += delta; + } } else { - dist += delta; + let mut new_base_delta = 0; + if (width > 0 && base_delta > 0) || (width < 0 && base_delta < 0) { + if scale.size < 10.0 { + new_base_delta = base_delta; + } else if scale.size < 30.0 { + new_base_delta = (base_delta * (30.0 - scale.size) as i32) / 20; + } + } + dist = (dist - new_base_delta.abs() + 32) & !63; } } else { - let mut new_base_delta = 0; - if (width > 0 && base_delta > 0) || (width < 0 && base_delta < 0) { - if scale.size < 10.0 { - new_base_delta = base_delta; - } else if scale.size < 30.0 { - new_base_delta = (base_delta * (30.0 - scale.size) as i32) / 20; + // Divergent CJK behavior + // See + if dist < 54 { + dist += (54 - dist) / 2; + } else if dist < 3 * 64 { + let delta = dist & 63; + dist &= -64; + if delta < 10 { + dist += delta; + } else if delta < 22 { + dist += 10; + } else if delta < 42 { + dist += delta; + } else if delta < 54 { + dist += 54; + } else { + dist += delta; } } - dist = (dist - new_base_delta.abs() + 32) & !63; } } } else { @@ -497,11 +626,14 @@ fn stem_width( // Only round to integer if distortion is less than // 1/4 pixel dist = (dist + 22) & !63; - let delta = (dist - original_dist).abs(); - if delta >= 16 { - dist = original_dist; - if dist < 48 { - dist = (dist + 64) >> 1; + if group == ScriptGroup::Default { + // See + let delta = (dist - original_dist).abs(); + if delta >= 16 { + dist = original_dist; + if dist < 48 { + dist = (dist + 64) >> 1; + } } } } else { @@ -519,6 +651,7 @@ fn stem_width( fn align_linked_edge( axis: &mut Axis, metrics: &ScaledAxisMetrics, + group: ScriptGroup, scale: &Scale, base_edge_ix: usize, stem_edge_ix: usize, @@ -529,8 +662,8 @@ fn align_linked_edge( let width = stem_edge.opos - base_edge.opos; let base_delta = base_edge.pos - base_edge.opos; let fitted_width = stem_width( - axis.dim, metrics, + group, scale, width, base_delta, @@ -550,6 +683,114 @@ fn align_serif_edge(axis: &mut Axis, base_edge_ix: usize, serif_edge_ix: usize) edges[serif_edge_ix].pos = base_edge.pos + (serif_edge.opos - base_edge.opos); } +/// Adjusts both edges of a stem and returns the delta. +/// +/// See +fn hint_normal_stem_cjk( + axis: &mut Axis, + metrics: &ScaledAxisMetrics, + group: ScriptGroup, + scale: &Scale, + edge_ix: usize, + edge2_ix: usize, + anchor: i32, +) -> i32 { + const MAX_HORIZONTAL_GAP: i32 = 9; + const MAX_VERTICAL_GAP: i32 = 15; + const MAX_DELTA_ABS: i32 = 14; + let edge = axis.edges[edge_ix]; + let edge2 = axis.edges[edge2_ix]; + let do_stem_adjust = scale.flags & Scale::STEM_ADJUST != 0; + let threshold_delta = if do_stem_adjust { + 0 + } else { + let delta = if axis.dim == Axis::VERTICAL { + MAX_HORIZONTAL_GAP + } else { + MAX_VERTICAL_GAP + }; + if edge.flags & Edge::ROUND != 0 && edge2.flags & Edge::ROUND != 0 { + delta + } else { + delta / 3 + } + }; + let threshold = 64 - threshold_delta; + let original_len = edge2.opos - edge.opos; + let cur_len = stem_width( + metrics, + group, + scale, + original_len, + 0, + edge.flags, + edge2.flags, + ); + let original_center = (edge.opos + edge2.opos) / 2 + anchor; + let cur_pos1 = original_center - cur_len / 2; + let cur_pos2 = cur_pos1 + cur_len; + let mut finish = |mut delta: i32| { + if !do_stem_adjust { + delta = delta.clamp(-MAX_DELTA_ABS, MAX_DELTA_ABS); + } + let adjustment = cur_pos1 + delta; + if edge.opos < edge2.opos { + axis.edges[edge_ix].pos = adjustment; + axis.edges[edge2_ix].pos = adjustment + cur_len; + } else { + axis.edges[edge2_ix].pos = adjustment; + axis.edges[edge_ix].pos = adjustment + cur_len; + } + delta + }; + let mut d_off1 = cur_pos1 - pix_floor(cur_pos1); + let mut d_off2 = cur_pos2 - pix_floor(cur_pos2); + let mut delta = 0; + if d_off1 == 0 || d_off2 == 0 { + return finish(delta); + } + let mut u_off1 = 64 - d_off1; + let mut u_off2 = 64 - d_off2; + if cur_len <= threshold { + if d_off2 < cur_len { + delta = if u_off1 <= d_off2 { u_off1 } else { -d_off2 }; + } + return finish(delta); + } + if threshold < 64 + && (d_off1 >= threshold + || u_off1 >= threshold + || d_off2 >= threshold + || u_off2 >= threshold) + { + return finish(delta); + } + let mut offset = cur_len & 63; + if offset < 32 { + if u_off1 <= offset || d_off2 <= offset { + return finish(delta); + } + } else { + offset = 64 - threshold; + } + d_off1 = threshold - u_off1; + u_off1 -= offset; + u_off2 = threshold - d_off2; + d_off2 -= offset; + if d_off1 <= u_off1 { + u_off1 = -d_off1; + } + if d_off2 <= u_off2 { + u_off2 = -d_off2; + } + if u_off1.abs() <= u_off2.abs() { + delta = u_off1; + } else { + delta = u_off2; + } + finish(delta) +} + #[cfg(test)] mod tests { use super::{ @@ -565,9 +806,70 @@ mod tests { use raw::{types::GlyphId, FontRef, TableProvider}; #[test] - fn edge_hinting() { - let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap(); - let class = &style::STYLE_CLASSES[style::StyleClass::HEBR]; + fn edge_hinting_default() { + let expected_h_edges = [ + (0, Edge::DONE | Edge::ROUND), + (133, Edge::DONE), + (187, Edge::DONE), + (192, Edge::DONE | Edge::ROUND), + ]; + let expected_v_edges = [ + (-256, Edge::DONE), + (463, Edge::DONE), + (576, Edge::DONE | Edge::ROUND | Edge::SERIF), + (633, Edge::DONE), + ]; + check_edges( + font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, + GlyphId::new(9), + style::StyleClass::HEBR, + &expected_h_edges, + &expected_v_edges, + ); + } + + #[test] + fn edge_hinting_cjk() { + let expected_h_edges = [ + (128, Edge::DONE), + (193, Edge::DONE), + (473, 0), + (594, 0), + (704, Edge::DONE), + (673, Edge::DONE), + (767, Edge::DONE), + (832, Edge::DONE), + (896, Edge::DONE), + ]; + let expected_v_edges = [ + (-64, Edge::DONE | Edge::ROUND), + (15, Edge::ROUND), + (142, Edge::ROUND), + (546, Edge::DONE), + (624, Edge::DONE), + (576, Edge::DONE), + (720, Edge::DONE), + (768, Edge::DONE), + (799, Edge::ROUND), + ]; + check_edges( + font_test_data::NOTOSERIFTC_AUTOHINT_METRICS, + GlyphId::new(9), + style::StyleClass::HANI, + &expected_h_edges, + &expected_v_edges, + ); + } + + fn check_edges( + font_data: &[u8], + glyph_id: GlyphId, + class: usize, + expected_h_edges: &[(i32, u8)], + expected_v_edges: &[(i32, u8)], + ) { + let font = FontRef::new(font_data).unwrap(); + let class = &style::STYLE_CLASSES[class]; let unscaled_metrics = latin::metrics::compute_unscaled_style_metrics(&font, Default::default(), class); let scale = metrics::Scale::new( @@ -578,7 +880,7 @@ mod tests { ); let scaled_metrics = latin::metrics::scale_style_metrics(&unscaled_metrics, scale); let glyphs = font.outline_glyphs(); - let glyph = glyphs.get(GlyphId::new(9)).unwrap(); + let glyph = glyphs.get(glyph_id).unwrap(); let mut outline = Outline::default(); outline.fill(&glyph, Default::default()).unwrap(); let mut axes = [ @@ -613,23 +915,12 @@ mod tests { hint_edges( axis, &scaled_metrics.axes[dim], + class.script.group, &scale, class.script.hint_top_to_bottom, ); } // Only pos and flags fields are modified by edge hinting - let expected_h_edges = [ - (0, Edge::DONE | Edge::ROUND), - (133, Edge::DONE), - (187, Edge::DONE), - (192, Edge::DONE | Edge::ROUND), - ]; - let expected_v_edges = [ - (-256, Edge::DONE), - (463, Edge::DONE), - (576, Edge::DONE | Edge::ROUND | Edge::SERIF), - (633, Edge::DONE), - ]; let h_edges = axes[Axis::HORIZONTAL] .edges .iter() diff --git a/skrifa/src/outline/autohint/latin/metrics.rs b/skrifa/src/outline/autohint/latin/metrics.rs index 01d2713c9..a3ceb9299 100644 --- a/skrifa/src/outline/autohint/latin/metrics.rs +++ b/skrifa/src/outline/autohint/latin/metrics.rs @@ -89,7 +89,11 @@ pub(crate) fn scale_style_metrics( scale_axis(&unscaled_metrics.axes[0]), scale_axis(&unscaled_metrics.axes[1]), ]; - ScaledStyleMetrics { scale, axes } + ScaledStyleMetrics { + scale, + group: unscaled_metrics.style_class().script.group, + axes, + } } /// Computes scaled metrics for a single axis. @@ -102,7 +106,10 @@ fn scale_default_axis_metrics( blues: &[UnscaledBlue], scale: &mut Scale, ) -> ScaledAxisMetrics { - let mut axis = ScaledAxisMetrics::default(); + let mut axis = ScaledAxisMetrics { + dim, + ..Default::default() + }; if dim == Axis::HORIZONTAL { axis.scale = scale.x_scale; axis.delta = scale.x_delta; @@ -220,7 +227,11 @@ fn scale_cjk_axis_metrics( blues: &[UnscaledBlue], scale: &mut Scale, ) -> ScaledAxisMetrics { - let mut axis = ScaledAxisMetrics::default(); + let mut axis = ScaledAxisMetrics { + dim, + ..Default::default() + }; + axis.dim = dim; if dim == Axis::HORIZONTAL { axis.scale = scale.x_scale; axis.delta = scale.x_delta; diff --git a/skrifa/src/outline/autohint/latin/mod.rs b/skrifa/src/outline/autohint/latin/mod.rs index 5048f897b..dd587c87a 100644 --- a/skrifa/src/outline/autohint/latin/mod.rs +++ b/skrifa/src/outline/autohint/latin/mod.rs @@ -88,10 +88,11 @@ pub(crate) fn hint_outline( hint::hint_edges( &mut axis, &scaled_metrics.axes[dim], + group, scale, hint_top_to_bottom, ); - super::hint::align_edge_points(outline, &axis); + super::hint::align_edge_points(outline, &axis, group, scale); super::hint::align_strong_points(outline, &mut axis); super::hint::align_weak_points(outline, dim); if dim == 0 && axis.edges.len() > 1 { diff --git a/skrifa/src/outline/autohint/metrics.rs b/skrifa/src/outline/autohint/metrics.rs index 95bc03a31..f4698d9f2 100644 --- a/skrifa/src/outline/autohint/metrics.rs +++ b/skrifa/src/outline/autohint/metrics.rs @@ -3,7 +3,7 @@ use super::{ super::Target, axis::Dimension, - style::{GlyphStyleMap, StyleClass}, + style::{GlyphStyleMap, ScriptGroup, StyleClass}, }; use crate::{attribute::Style, collections::SmallVec, FontRef}; use alloc::vec::Vec; @@ -45,6 +45,7 @@ impl UnscaledAxisMetrics { /// Scaled metrics for a single axis. #[derive(Clone, Default, Debug)] pub(crate) struct ScaledAxisMetrics { + pub dim: Dimension, /// Font unit to 26.6 scale in the axis direction. pub scale: i32, /// 1/64 pixel delta in the axis direction. @@ -152,6 +153,8 @@ impl UnscaledStyleMetricsSet { pub(crate) struct ScaledStyleMetrics { /// Multidimensional scaling factors and deltas. pub scale: Scale, + /// Script set for the associated style. + pub group: ScriptGroup, /// Per-dimension scaled metrics. pub axes: [ScaledAxisMetrics; 2], } @@ -352,6 +355,10 @@ pub(crate) fn pix_round(a: i32) -> i32 { (a + 32) & !63 } +pub(crate) fn pix_floor(a: i32) -> i32 { + a & !63 +} + #[cfg(test)] mod tests { use super::{super::style::STYLE_CLASSES, *}; diff --git a/skrifa/src/outline/autohint/outline.rs b/skrifa/src/outline/autohint/outline.rs index 64ed660c6..3b2182739 100644 --- a/skrifa/src/outline/autohint/outline.rs +++ b/skrifa/src/outline/autohint/outline.rs @@ -350,7 +350,7 @@ impl Outline { let in_y = point.fy - prev_v.fy; let out_x = next_u.fx - point.fx; let out_y = next_u.fy - point.fy; - if (in_x ^ out_x) >= 0 || (in_y ^ out_y) >= 0 { + if (in_x ^ out_x) >= 0 && (in_y ^ out_y) >= 0 { // Both vectors point into the same quadrant points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION); points[v_index].u = u_index as _; diff --git a/skrifa/src/outline/autohint/style.rs b/skrifa/src/outline/autohint/style.rs index 78c614d08..7ff8ffbad 100644 --- a/skrifa/src/outline/autohint/style.rs +++ b/skrifa/src/outline/autohint/style.rs @@ -200,11 +200,12 @@ impl Default for GlyphStyleMap { /// Determines which algorithms the autohinter will use while generating /// metrics and processing a glyph outline. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)] pub(crate) enum ScriptGroup { /// All scripts that are not CJK or Indic. /// /// FreeType calls this Latin. + #[default] Default, Cjk, Indic,