1072 lines
39 KiB
Rust
1072 lines
39 KiB
Rust
//! Segment computation and linking.
|
|
//!
|
|
//! A segment is a series of at least two consecutive points that are
|
|
//! appropriately aligned along a coordinate axis.
|
|
//!
|
|
//! The linking stage associates pairs of segments to form stems and
|
|
//! identifies serifs with a post-process pass.
|
|
|
|
use super::super::{
|
|
derived_constant,
|
|
metrics::fixed_div,
|
|
outline::Outline,
|
|
style::ScriptGroup,
|
|
topo::{Axis, Dimension, Segment},
|
|
};
|
|
use raw::tables::glyf::PointFlags;
|
|
|
|
// Bounds for score, position and coordinate values.
|
|
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1598>
|
|
const MAX_SCORE: i32 = 32000;
|
|
const MIN_SCORE: i32 = -32000;
|
|
|
|
/// Computes segments for the Latin writing system.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1537>
|
|
pub(crate) fn compute_segments(
|
|
outline: &mut Outline,
|
|
axis: &mut Axis,
|
|
_group: ScriptGroup,
|
|
) -> bool {
|
|
assign_point_uvs(outline, axis.dim);
|
|
if !build_segments(outline, axis) {
|
|
return false;
|
|
}
|
|
adjust_segment_heights(outline, axis);
|
|
// This is never actually executed due to a bug in FreeType
|
|
// See point 2 at <https://github.com/googlefonts/fontations/issues/1129>
|
|
// if group != ScriptGroup::Default {
|
|
// _detect_round_segments_cjk(outline, axis);
|
|
// }
|
|
true
|
|
}
|
|
|
|
/// Link segments to form stems and serifs.
|
|
///
|
|
/// If `max_width` is provided, use it to refine the scoring function.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1990>
|
|
pub(crate) fn link_segments(
|
|
outline: &Outline,
|
|
axis: &mut Axis,
|
|
scale: i32,
|
|
group: ScriptGroup,
|
|
max_width: Option<i32>,
|
|
) {
|
|
if group == ScriptGroup::Default {
|
|
link_segments_default(outline, axis, max_width);
|
|
} else {
|
|
link_segments_cjk(outline, axis, scale)
|
|
}
|
|
}
|
|
|
|
/// Link segments to form stems and serifs.
|
|
///
|
|
/// If `max_width` is provided, use it to refine the scoring function.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1990>
|
|
fn link_segments_default(outline: &Outline, axis: &mut Axis, max_width: Option<i32>) {
|
|
let max_width = max_width.unwrap_or_default();
|
|
// Heuristic value to set up a minimum for overlapping
|
|
let len_threshold = derived_constant(outline.units_per_em, 8).max(1);
|
|
// Heuristic value to weight lengths
|
|
let len_score = derived_constant(outline.units_per_em, 6000);
|
|
// Heuristic value to weight distances (not a latin constant since
|
|
// it works on multiples of stem width)
|
|
let dist_score = 3000;
|
|
// Compare each segment to the others.. O(n^2)
|
|
let segments = axis.segments.as_mut_slice();
|
|
for ix1 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
if seg1.dir != axis.major_dir {
|
|
continue;
|
|
}
|
|
let pos1 = seg1.pos as i32;
|
|
// Search for stems having opposite directions with seg1 to the
|
|
// "left" of seg2
|
|
for ix2 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
let seg2 = segments[ix2];
|
|
let pos2 = seg2.pos as i32;
|
|
if seg1.dir.is_opposite(seg2.dir) && pos2 > pos1 {
|
|
// Compute distance between the segments
|
|
// Note: the min/max functions chosen here are intentional
|
|
let min = seg1.min_coord.max(seg2.min_coord) as i32;
|
|
let max = seg1.max_coord.min(seg2.max_coord) as i32;
|
|
// Compute maximum coordinate difference or how much they
|
|
// overlap
|
|
let len = max - min;
|
|
if len >= len_threshold {
|
|
// verbatim from FreeType:
|
|
// "The score is the sum of two demerits indicating the
|
|
// `badness' of a fit, measured along the segments' main axis
|
|
// and orthogonal to it, respectively.
|
|
//
|
|
// - The less overlapping along the main axis, the worse it
|
|
// is, causing a larger demerit.
|
|
//
|
|
// - The nearer the orthogonal distance to a stem width, the
|
|
// better it is, causing a smaller demerit. For simplicity,
|
|
// however, we only increase the demerit for values that
|
|
// exceed the largest stem width."
|
|
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2054>
|
|
let dist = pos2 - pos1;
|
|
let dist_demerit = if max_width != 0 {
|
|
// Distance demerits are based on multiples of max_width
|
|
let delta = (dist << 10) / max_width - (1 << 10);
|
|
if delta > 10_000 {
|
|
MAX_SCORE
|
|
} else if delta > 0 {
|
|
delta * delta / dist_score
|
|
} else {
|
|
0
|
|
}
|
|
} else {
|
|
dist
|
|
};
|
|
let score = dist_demerit + len_score / len;
|
|
if score < seg1.score {
|
|
let seg1 = &mut segments[ix1];
|
|
seg1.score = score;
|
|
seg1.link_ix = Some(ix2 as u16);
|
|
}
|
|
if score < seg2.score {
|
|
let seg2 = &mut segments[ix2];
|
|
seg2.score = score;
|
|
seg2.link_ix = Some(ix1 as u16);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Now compute "serif" segments
|
|
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2109>
|
|
for ix1 in 0..segments.len() {
|
|
let Some(ix2) = segments[ix1].link_ix else {
|
|
continue;
|
|
};
|
|
let seg2_link = segments[ix2 as usize].link_ix;
|
|
if seg2_link != Some(ix1 as u16) {
|
|
let seg1 = &mut segments[ix1];
|
|
seg1.link_ix = None;
|
|
seg1.serif_ix = seg2_link;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Link segments to form stems and serifs for the CJK script group.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L848>
|
|
fn link_segments_cjk(outline: &Outline, axis: &mut Axis, scale: i32) {
|
|
// Heuristic value to set up a minimum for overlapping
|
|
let len_threshold = derived_constant(outline.units_per_em, 8);
|
|
let dist_threshold = fixed_div(64 * 3, scale);
|
|
// Compare each segment to the others.. O(n^2)
|
|
let segments = axis.segments.as_mut_slice();
|
|
for ix1 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
if seg1.dir != axis.major_dir {
|
|
continue;
|
|
}
|
|
let pos1 = seg1.pos as i32;
|
|
// Search for stems having opposite directions with seg1 to the
|
|
// "left" of seg2
|
|
for ix2 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
let seg2 = segments[ix2];
|
|
if ix1 == ix2 || !seg1.dir.is_opposite(seg2.dir) {
|
|
continue;
|
|
}
|
|
let pos2 = seg2.pos as i32;
|
|
let dist = pos2 - pos1;
|
|
if dist < 0 {
|
|
continue;
|
|
}
|
|
// Compute distance between the segments
|
|
// Note: the min/max functions chosen here are intentional
|
|
let min = seg1.min_coord.max(seg2.min_coord) as i32;
|
|
let max = seg1.max_coord.min(seg2.max_coord) as i32;
|
|
// Compute maximum coordinate difference or how much they
|
|
// overlap
|
|
let len = max - min;
|
|
if len >= len_threshold {
|
|
let check_seg = |seg: &Segment| {
|
|
// Some more magic heuristics...
|
|
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L896>
|
|
(dist * 8 < seg.score * 9) && (dist * 8 < seg.score * 7 || seg.len < len)
|
|
};
|
|
if check_seg(&seg1) {
|
|
let seg = &mut segments[ix1];
|
|
seg.score = dist;
|
|
seg.len = len;
|
|
seg.link_ix = Some(ix2 as _);
|
|
}
|
|
if check_seg(&seg2) {
|
|
let seg = &mut segments[ix2];
|
|
seg.score = dist;
|
|
seg.len = len;
|
|
seg.link_ix = Some(ix1 as _);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Now compute "serif" segments
|
|
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L917>
|
|
for ix1 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
if seg1.score >= dist_threshold {
|
|
continue;
|
|
}
|
|
let Some(link1) = seg1.link(segments).copied() else {
|
|
continue;
|
|
};
|
|
// Unwrap is fine because we checked for existence above
|
|
let link1_ix = seg1.link_ix.unwrap() as usize;
|
|
if link1.link_ix != Some(ix1 as u16) || link1.pos <= seg1.pos {
|
|
continue;
|
|
}
|
|
for ix2 in 0..segments.len() {
|
|
let seg2 = segments[ix2];
|
|
if seg2.pos > seg1.pos || ix1 == ix2 {
|
|
continue;
|
|
}
|
|
let Some(link2) = seg2.link(segments).copied() else {
|
|
continue;
|
|
};
|
|
if link2.link_ix != Some(ix2 as u16) || link2.pos < link1.pos {
|
|
continue;
|
|
}
|
|
if seg1.pos == seg2.pos && link1.pos == link2.pos {
|
|
continue;
|
|
}
|
|
if seg2.score <= seg1.score || seg1.score * 4 <= seg2.score {
|
|
continue;
|
|
}
|
|
if seg1.len >= seg2.len * 3 {
|
|
// Again, we definitely have a valid link2
|
|
let link2_ix = seg2.link_ix.unwrap() as usize;
|
|
for seg in segments.iter_mut() {
|
|
let link_ix = seg.link_ix;
|
|
if link_ix == Some(ix2 as u16) {
|
|
seg.link_ix = None;
|
|
seg.serif_ix = Some(link1_ix as u16);
|
|
} else if link_ix == Some(link2_ix as u16) {
|
|
seg.link_ix = None;
|
|
seg.serif_ix = Some(ix1 as u16);
|
|
}
|
|
}
|
|
} else {
|
|
segments[ix1].link_ix = None;
|
|
segments[link1_ix].link_ix = None;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
for ix1 in 0..segments.len() {
|
|
let seg1 = segments[ix1];
|
|
let Some(seg2) = seg1.link(segments).copied() else {
|
|
continue;
|
|
};
|
|
if seg2.link_ix != Some(ix1 as u16) {
|
|
segments[ix1].link_ix = None;
|
|
if seg2.score < dist_threshold || seg1.score < seg2.score * 4 {
|
|
segments[ix1].serif_ix = seg2.link_ix;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set the (u, v) values to font unit coords for each point depending
|
|
/// on the axis dimension.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1562>
|
|
fn assign_point_uvs(outline: &mut Outline, dim: Dimension) {
|
|
if dim == Axis::HORIZONTAL {
|
|
for point in &mut outline.points {
|
|
point.u = point.fx;
|
|
point.v = point.fy;
|
|
}
|
|
} else {
|
|
for point in &mut outline.points {
|
|
point.u = point.fy;
|
|
point.v = point.fx;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Build the set of segments for each contour.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1588>
|
|
fn build_segments(outline: &mut Outline, axis: &mut Axis) -> bool {
|
|
let flat_threshold = outline.units_per_em / 14;
|
|
axis.segments.clear();
|
|
let major_dir = axis.major_dir.normalize();
|
|
let mut segment_dir = major_dir;
|
|
let points = outline.points.as_mut_slice();
|
|
for contour in &outline.contours {
|
|
let is_single_point_contour = contour.range().len() == 1;
|
|
let mut point_ix = contour.first();
|
|
let mut last_ix = contour.prev(point_ix);
|
|
let mut state = State::default();
|
|
let mut prev_state = state;
|
|
let mut prev_segment_ix: Option<usize> = None;
|
|
let mut segment_ix = 0;
|
|
// Check if we're starting on an edge and if so, find
|
|
// the starting point
|
|
if points[point_ix].out_dir.is_same_axis(major_dir)
|
|
&& points[last_ix].out_dir.is_same_axis(major_dir)
|
|
{
|
|
last_ix = point_ix;
|
|
loop {
|
|
point_ix = contour.prev(point_ix);
|
|
if !points[point_ix].out_dir.is_same_axis(major_dir) {
|
|
point_ix = contour.next(point_ix);
|
|
break;
|
|
}
|
|
if point_ix == last_ix {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
last_ix = point_ix;
|
|
let mut on_edge = false;
|
|
let mut passed = false;
|
|
loop {
|
|
if on_edge {
|
|
// Get min and max position
|
|
let point = points[point_ix];
|
|
state.min_pos = state.min_pos.min(point.u);
|
|
state.max_pos = state.max_pos.max(point.u);
|
|
// Get min and max coordinate and flags
|
|
let v = point.v;
|
|
if v < state.min_coord {
|
|
state.min_coord = v;
|
|
state.min_flags = point.flags;
|
|
}
|
|
if v > state.max_coord {
|
|
state.max_coord = v;
|
|
state.max_flags = point.flags;
|
|
}
|
|
// Get min and max coord of on curve points
|
|
if point.is_on_curve() {
|
|
state.min_on_coord = state.min_on_coord.min(point.v);
|
|
state.max_on_coord = state.max_on_coord.max(point.v);
|
|
}
|
|
if point.out_dir != segment_dir || point_ix == last_ix {
|
|
if prev_segment_ix.is_none()
|
|
|| axis.segments[segment_ix].first_ix
|
|
!= axis.segments[prev_segment_ix.unwrap()].last_ix
|
|
{
|
|
// The points are different signifying that we are
|
|
// leaving an edge, so create a new segment
|
|
let segment = &mut axis.segments[segment_ix];
|
|
segment.last_ix = point_ix as u16;
|
|
state.apply_to_segment(segment, flat_threshold);
|
|
prev_segment_ix = Some(segment_ix);
|
|
prev_state = state;
|
|
} else {
|
|
// The points are the same, so merge the segments
|
|
let prev_segment = &mut axis.segments[prev_segment_ix.unwrap()];
|
|
if prev_segment.last_point(points).in_dir == point.in_dir {
|
|
// We have identical directions; unify segments
|
|
// and update constraints
|
|
state.min_pos = prev_state.min_pos.min(state.min_pos);
|
|
state.max_pos = prev_state.max_pos.max(state.max_pos);
|
|
if prev_state.min_coord < state.min_coord {
|
|
state.min_coord = prev_state.min_coord;
|
|
state.min_flags = prev_state.min_flags;
|
|
}
|
|
if prev_state.max_coord > state.max_coord {
|
|
state.max_coord = prev_state.max_coord;
|
|
state.max_flags = prev_state.max_flags;
|
|
}
|
|
state.min_on_coord = prev_state.min_on_coord.min(state.min_on_coord);
|
|
state.max_on_coord = prev_state.max_on_coord.max(state.max_on_coord);
|
|
prev_segment.last_ix = point_ix as u16;
|
|
state.apply_to_segment(prev_segment, flat_threshold);
|
|
} else {
|
|
// We have different directions; use the
|
|
// properties of the longer segment
|
|
if (prev_state.max_coord - prev_state.min_coord).abs()
|
|
> (state.max_coord - state.min_coord).abs()
|
|
{
|
|
// Discard current segment
|
|
prev_state.min_pos = prev_state.min_pos.min(state.min_pos);
|
|
prev_state.max_pos = prev_state.max_pos.max(state.max_pos);
|
|
prev_segment.last_ix = point_ix as u16;
|
|
prev_segment.pos =
|
|
((prev_state.min_pos + prev_state.max_pos) >> 1) as i16;
|
|
prev_segment.delta =
|
|
((prev_state.max_pos - prev_state.min_pos) >> 1) as i16;
|
|
} else {
|
|
// Discard previous segment
|
|
state.min_pos = state.min_pos.min(prev_state.min_pos);
|
|
state.max_pos = state.max_pos.max(prev_state.max_pos);
|
|
let mut segment = axis.segments[segment_ix];
|
|
segment.last_ix = point_ix as u16;
|
|
state.apply_to_segment(&mut segment, flat_threshold);
|
|
axis.segments[prev_segment_ix.unwrap()] = segment;
|
|
prev_state = state;
|
|
}
|
|
}
|
|
axis.segments.pop();
|
|
}
|
|
on_edge = false;
|
|
}
|
|
}
|
|
if point_ix == last_ix {
|
|
if passed {
|
|
break;
|
|
}
|
|
passed = true;
|
|
}
|
|
let point = points[point_ix];
|
|
if !on_edge && (point.out_dir.is_same_axis(major_dir) || is_single_point_contour) {
|
|
if axis.segments.len() > 1000 {
|
|
axis.segments.clear();
|
|
return false;
|
|
}
|
|
segment_ix = axis.segments.len();
|
|
segment_dir = point.out_dir;
|
|
let mut segment = Segment {
|
|
dir: segment_dir,
|
|
first_ix: point_ix as u16,
|
|
last_ix: point_ix as u16,
|
|
score: MAX_SCORE,
|
|
..Default::default()
|
|
};
|
|
state.min_pos = point.u;
|
|
state.max_pos = point.u;
|
|
state.min_coord = point.v;
|
|
state.max_coord = point.v;
|
|
state.min_flags = point.flags;
|
|
state.max_flags = point.flags;
|
|
if !point.is_on_curve() {
|
|
state.min_on_coord = MAX_SCORE;
|
|
state.max_on_coord = MIN_SCORE;
|
|
} else {
|
|
state.min_on_coord = point.v;
|
|
state.max_on_coord = point.v;
|
|
}
|
|
on_edge = true;
|
|
if is_single_point_contour {
|
|
segment.pos = state.min_pos as i16;
|
|
if !point.is_on_curve() {
|
|
segment.flags |= Segment::ROUND;
|
|
}
|
|
segment.min_coord = point.v as i16;
|
|
segment.max_coord = point.v as i16;
|
|
segment.height = 0;
|
|
on_edge = false;
|
|
}
|
|
axis.segments.push(segment);
|
|
}
|
|
point_ix = contour.next(point_ix);
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Slightly increase the height of segments when it makes sense to better
|
|
/// detect and ignore serifs.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1933>
|
|
fn adjust_segment_heights(outline: &mut Outline, axis: &mut Axis) {
|
|
let points = outline.points.as_slice();
|
|
for segment in &mut axis.segments {
|
|
let first = segment.first_point(points);
|
|
let last = segment.last_point(points);
|
|
fn adjust_height(segment: &mut Segment, v1: i32, v2: i32) {
|
|
segment.height = (segment.height as i32 + ((v1 - v2) >> 1)) as i16;
|
|
}
|
|
let prev = &points[first.prev()];
|
|
let next = &points[last.next()];
|
|
if first.v < last.v {
|
|
if prev.v < first.v {
|
|
adjust_height(segment, first.v, prev.v);
|
|
}
|
|
if next.v > last.v {
|
|
adjust_height(segment, next.v, last.v);
|
|
}
|
|
} else {
|
|
if prev.v > first.v {
|
|
adjust_height(segment, prev.v, first.v);
|
|
}
|
|
if next.v < last.v {
|
|
adjust_height(segment, last.v, next.v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Performs the additional step of detecting round segments for the CJK script
|
|
/// group.
|
|
///
|
|
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L818>
|
|
fn _detect_round_segments_cjk(outline: &mut Outline, axis: &mut Axis) {
|
|
let points = outline.points.as_slice();
|
|
// A segment is considered round if it doesn't have successive on-curve
|
|
// points
|
|
for segment in &mut axis.segments {
|
|
segment.flags &= !Segment::ROUND;
|
|
let mut point_ix = segment.first();
|
|
let last_ix = segment.last();
|
|
let first_point = &points[point_ix];
|
|
let mut is_prev_on_curve = first_point.is_on_curve();
|
|
point_ix = first_point.next();
|
|
loop {
|
|
let point = &points[point_ix];
|
|
let is_on_curve = point.is_on_curve();
|
|
if is_prev_on_curve && is_on_curve {
|
|
// Two on-curves in a row means we're not a round segment
|
|
break;
|
|
}
|
|
is_prev_on_curve = is_on_curve;
|
|
point_ix = point.next();
|
|
if point_ix == last_ix {
|
|
// We've reached the last point without two successive
|
|
// on-curves so we're round
|
|
segment.flags |= Segment::ROUND;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Capture current and previous state while computing segments.
|
|
///
|
|
/// Values measured along a segment (point.v) are called "coordinates" and
|
|
/// values orthogonal to it (point.u) are called "positions"
|
|
#[derive(Copy, Clone)]
|
|
struct State {
|
|
min_pos: i32,
|
|
max_pos: i32,
|
|
min_coord: i32,
|
|
max_coord: i32,
|
|
min_flags: PointFlags,
|
|
max_flags: PointFlags,
|
|
min_on_coord: i32,
|
|
max_on_coord: i32,
|
|
}
|
|
|
|
impl Default for State {
|
|
fn default() -> Self {
|
|
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1598>
|
|
Self {
|
|
min_pos: MAX_SCORE,
|
|
max_pos: MIN_SCORE,
|
|
min_coord: MAX_SCORE,
|
|
max_coord: MIN_SCORE,
|
|
min_flags: PointFlags::default(),
|
|
max_flags: PointFlags::default(),
|
|
min_on_coord: MAX_SCORE,
|
|
max_on_coord: MIN_SCORE,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl State {
|
|
fn apply_to_segment(&self, segment: &mut Segment, flat_threshold: i32) {
|
|
segment.pos = ((self.min_pos + self.max_pos) >> 1) as i16;
|
|
segment.delta = ((self.max_pos - self.min_pos) >> 1) as i16;
|
|
// A segment is round if either end point is a
|
|
// control and the length of the on points in
|
|
// between fits within a heuristic limit.
|
|
if (!self.min_flags.is_on_curve() || !self.max_flags.is_on_curve())
|
|
&& (self.max_on_coord - self.min_on_coord) < flat_threshold
|
|
{
|
|
segment.flags |= Segment::ROUND;
|
|
}
|
|
segment.min_coord = self.min_coord as i16;
|
|
segment.max_coord = self.max_coord as i16;
|
|
segment.height = segment.max_coord - segment.min_coord;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{super::super::outline::Direction, *};
|
|
use crate::MetadataProvider;
|
|
use raw::{types::GlyphId, FontRef};
|
|
|
|
#[test]
|
|
fn horizontal_segments() {
|
|
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
|
|
let glyphs = font.outline_glyphs();
|
|
let glyph = glyphs.get(GlyphId::new(8)).unwrap();
|
|
let mut outline = Outline::default();
|
|
outline.fill(&glyph, Default::default()).unwrap();
|
|
let mut axis = Axis::new(Axis::HORIZONTAL, outline.orientation);
|
|
compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
|
|
link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
|
|
let segments = retain_segment_test_fields(&axis.segments);
|
|
let expected = [
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 55,
|
|
delta: 0,
|
|
min_coord: 26,
|
|
max_coord: 360,
|
|
height: 372,
|
|
link_ix: Some(3),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 112,
|
|
delta: 0,
|
|
min_coord: 481,
|
|
max_coord: 504,
|
|
height: 34,
|
|
link_ix: Some(2),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 168,
|
|
delta: 0,
|
|
min_coord: 483,
|
|
max_coord: 504,
|
|
height: 26,
|
|
link_ix: Some(1),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 109,
|
|
delta: 0,
|
|
min_coord: 109,
|
|
max_coord: 366,
|
|
height: 288,
|
|
link_ix: Some(0),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 453,
|
|
delta: 0,
|
|
min_coord: 169,
|
|
max_coord: 432,
|
|
height: 304,
|
|
link_ix: Some(7),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Up,
|
|
pos: 62,
|
|
delta: 0,
|
|
min_coord: 517,
|
|
max_coord: 566,
|
|
height: 76,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Down,
|
|
pos: 103,
|
|
delta: 0,
|
|
min_coord: 619,
|
|
max_coord: 647,
|
|
height: 41,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 507,
|
|
delta: 0,
|
|
min_coord: 40,
|
|
max_coord: 485,
|
|
height: 498,
|
|
link_ix: Some(4),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
];
|
|
assert_eq!(segments, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_segments() {
|
|
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
|
|
let glyphs = font.outline_glyphs();
|
|
let glyph = glyphs.get(GlyphId::new(8)).unwrap();
|
|
let mut outline = Outline::default();
|
|
outline.fill(&glyph, Default::default()).unwrap();
|
|
let mut axis = Axis::new(Axis::VERTICAL, outline.orientation);
|
|
compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
|
|
link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
|
|
let segments = retain_segment_test_fields(&axis.segments);
|
|
let expected = [
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Left,
|
|
pos: 0,
|
|
delta: 0,
|
|
min_coord: 85,
|
|
max_coord: 470,
|
|
height: 418,
|
|
link_ix: Some(2),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 504,
|
|
delta: 0,
|
|
min_coord: 112,
|
|
max_coord: 168,
|
|
height: 56,
|
|
link_ix: Some(3),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 109,
|
|
delta: 0,
|
|
min_coord: 109,
|
|
max_coord: 427,
|
|
height: 327,
|
|
link_ix: Some(0),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Left,
|
|
pos: 483,
|
|
delta: 0,
|
|
min_coord: 86,
|
|
max_coord: 400,
|
|
height: 352,
|
|
link_ix: Some(1),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 647,
|
|
delta: 0,
|
|
min_coord: 76,
|
|
max_coord: 103,
|
|
height: 29,
|
|
link_ix: None,
|
|
serif_ix: Some(1),
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 592,
|
|
delta: 0,
|
|
min_coord: 131,
|
|
max_coord: 437,
|
|
height: 346,
|
|
link_ix: None,
|
|
serif_ix: Some(1),
|
|
..Default::default()
|
|
},
|
|
];
|
|
assert_eq!(segments, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn cjk_horizontal_segments() {
|
|
let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
|
|
let glyphs = font.outline_glyphs();
|
|
let glyph = glyphs.get(GlyphId::new(9)).unwrap();
|
|
let mut outline = Outline::default();
|
|
outline.fill(&glyph, Default::default()).unwrap();
|
|
let mut axis = Axis::new(Axis::HORIZONTAL, outline.orientation);
|
|
compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
|
|
link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
|
|
let segments = retain_segment_test_fields(&axis.segments);
|
|
let expected = [
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 731,
|
|
delta: 0,
|
|
min_coord: 155,
|
|
max_coord: 676,
|
|
height: 524,
|
|
link_ix: Some(1),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 670,
|
|
delta: 0,
|
|
min_coord: 133,
|
|
max_coord: 712,
|
|
height: 579,
|
|
link_ix: Some(0),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 458,
|
|
delta: 0,
|
|
min_coord: 741,
|
|
max_coord: 757,
|
|
height: 88,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 911,
|
|
delta: 0,
|
|
min_coord: -9,
|
|
max_coord: 791,
|
|
height: 821,
|
|
link_ix: Some(5),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 693,
|
|
delta: 0,
|
|
min_coord: -7,
|
|
max_coord: 9,
|
|
height: 18,
|
|
link_ix: None,
|
|
serif_ix: Some(5),
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 849,
|
|
delta: 0,
|
|
min_coord: 11,
|
|
max_coord: 829,
|
|
height: 823,
|
|
link_ix: Some(3),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 569,
|
|
delta: 0,
|
|
min_coord: 547,
|
|
max_coord: 576,
|
|
height: 29,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Down,
|
|
pos: 201,
|
|
delta: 0,
|
|
min_coord: -57,
|
|
max_coord: 540,
|
|
height: 599,
|
|
link_ix: Some(8),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Up,
|
|
pos: 138,
|
|
delta: 0,
|
|
min_coord: -78,
|
|
max_coord: 543,
|
|
height: 640,
|
|
link_ix: Some(7),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
];
|
|
assert_eq!(segments, &expected);
|
|
}
|
|
|
|
#[test]
|
|
fn cjk_vertical_segments() {
|
|
let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
|
|
let glyphs = font.outline_glyphs();
|
|
let glyph = glyphs.get(GlyphId::new(9)).unwrap();
|
|
let mut outline = Outline::default();
|
|
outline.fill(&glyph, Default::default()).unwrap();
|
|
let mut axis = Axis::new(Axis::VERTICAL, outline.orientation);
|
|
compute_segments(&mut outline, &mut axis, ScriptGroup::Cjk);
|
|
link_segments(&outline, &mut axis, 67109, ScriptGroup::Cjk, None);
|
|
let segments = retain_segment_test_fields(&axis.segments);
|
|
let expected = [
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 758,
|
|
delta: 0,
|
|
min_coord: 280,
|
|
max_coord: 545,
|
|
height: 288,
|
|
link_ix: Some(1),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Left,
|
|
pos: 729,
|
|
delta: 0,
|
|
min_coord: 288,
|
|
max_coord: 674,
|
|
height: 391,
|
|
link_ix: Some(0),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Left,
|
|
pos: 133,
|
|
delta: 0,
|
|
min_coord: 670,
|
|
max_coord: 693,
|
|
height: 34,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 757,
|
|
delta: 0,
|
|
min_coord: 393,
|
|
max_coord: 458,
|
|
height: 70,
|
|
link_ix: None,
|
|
serif_ix: Some(0),
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Right,
|
|
pos: 3,
|
|
delta: 2,
|
|
min_coord: 727,
|
|
max_coord: 838,
|
|
height: 133,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Right,
|
|
pos: 576,
|
|
delta: 0,
|
|
min_coord: 397,
|
|
max_coord: 569,
|
|
height: 177,
|
|
link_ix: Some(7),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Left,
|
|
pos: 547,
|
|
delta: 0,
|
|
min_coord: 387,
|
|
max_coord: 569,
|
|
height: 182,
|
|
link_ix: None,
|
|
serif_ix: Some(7),
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 0,
|
|
dir: Direction::Left,
|
|
pos: 576,
|
|
delta: 0,
|
|
min_coord: 536,
|
|
max_coord: 546,
|
|
height: 10,
|
|
link_ix: Some(5),
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Left,
|
|
pos: -78,
|
|
delta: 0,
|
|
min_coord: 138,
|
|
max_coord: 161,
|
|
height: 34,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
Segment {
|
|
flags: 1,
|
|
dir: Direction::Left,
|
|
pos: 788,
|
|
delta: 0,
|
|
min_coord: 262,
|
|
max_coord: 294,
|
|
height: 46,
|
|
link_ix: None,
|
|
serif_ix: None,
|
|
..Default::default()
|
|
},
|
|
];
|
|
assert_eq!(segments, &expected);
|
|
}
|
|
|
|
// Retain the fields that are valid and comparable after
|
|
// the segment pass.
|
|
fn retain_segment_test_fields(segments: &[Segment]) -> Vec<Segment> {
|
|
segments
|
|
.iter()
|
|
.map(|segment| Segment {
|
|
flags: segment.flags,
|
|
dir: segment.dir,
|
|
pos: segment.pos,
|
|
delta: segment.delta,
|
|
min_coord: segment.min_coord,
|
|
max_coord: segment.max_coord,
|
|
height: segment.height,
|
|
link_ix: segment.link_ix,
|
|
serif_ix: segment.serif_ix,
|
|
..Default::default()
|
|
})
|
|
.collect()
|
|
}
|
|
}
|