//! CFF hinting. use read_fonts::{ ps::{cs::CommandSink, hinting::Blues}, types::Fixed, }; // "Default values for OS/2 typoAscender/Descender.." // See const ICF_TOP: Fixed = Fixed::from_i32(880); const ICF_BOTTOM: Fixed = Fixed::from_i32(-120); // const MAX_BLUES: usize = 7; const MAX_OTHER_BLUES: usize = 5; const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES; // const MAX_HINTS: usize = 96; // One bit per stem hint // const HINT_MASK_SIZE: usize = MAX_HINTS.div_ceil(8); // Constant for hint adjustment and em box hint placement. // const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000); // const EPSILON: Fixed = Fixed::from_bits(1); /// Parameters used to generate the stem and counter zones for the hinting /// algorithm. #[derive(Clone)] pub(crate) struct HintParams { pub blues: Blues, pub family_blues: Blues, pub other_blues: Blues, pub family_other_blues: Blues, pub blue_scale: Fixed, pub blue_shift: Fixed, pub blue_fuzz: Fixed, pub language_group: i32, } impl Default for HintParams { fn default() -> Self { Self { blues: Blues::default(), other_blues: Blues::default(), family_blues: Blues::default(), family_other_blues: Blues::default(), // See blue_scale: Fixed::from_f64(0.039625), blue_shift: Fixed::from_i32(7), blue_fuzz: Fixed::ONE, language_group: 0, } } } /// See #[derive(Copy, Clone, PartialEq, Default, Debug)] struct BlueZone { is_bottom: bool, cs_bottom_edge: Fixed, cs_top_edge: Fixed, cs_flat_edge: Fixed, ds_flat_edge: Fixed, } /// Hinting state for a PostScript subfont. /// /// Note that hinter states depend on the scale, subfont index and /// variation coordinates of a glyph. They can be retained and reused /// if those values remain the same. #[derive(Copy, Clone, PartialEq, Default)] pub(crate) struct HintState { scale: Fixed, blue_scale: Fixed, blue_shift: Fixed, blue_fuzz: Fixed, language_group: i32, suppress_overshoot: bool, do_em_box_hints: bool, boost: Fixed, darken_y: Fixed, zones: [BlueZone; MAX_BLUE_ZONES], zone_count: usize, } impl HintState { pub fn new(params: &HintParams, scale: Fixed) -> Self { let mut state = Self { scale, blue_scale: params.blue_scale, blue_shift: params.blue_shift, blue_fuzz: params.blue_fuzz, language_group: params.language_group, suppress_overshoot: false, do_em_box_hints: false, boost: Fixed::ZERO, darken_y: Fixed::ZERO, zones: [BlueZone::default(); MAX_BLUE_ZONES], zone_count: 0, }; state.build_zones(params); state } fn zones(&self) -> &[BlueZone] { &self.zones[..self.zone_count] } /// Initialize zones from the set of blues values. /// /// See fn build_zones(&mut self, params: &HintParams) { self.do_em_box_hints = false; // match (self.language_group, params.blues.values().len()) { (1, 2) => { let blues = params.blues.values(); if blues[0].0 < ICF_BOTTOM && blues[0].1 < ICF_BOTTOM && blues[1].0 > ICF_TOP && blues[1].1 > ICF_TOP { // FreeType generates synthetic hints here. We'll do it // later when building the hint map. self.do_em_box_hints = true; return; } } (1, 0) => { self.do_em_box_hints = true; return; } _ => {} } let mut zones = [BlueZone::default(); MAX_BLUE_ZONES]; let mut max_zone_height = Fixed::ZERO; let mut zone_ix = 0usize; // Copy blues and other blues to a combined array of top and bottom zones. for blue in params.blues.values().iter().take(MAX_BLUES) { let (bottom, top) = *blue; let zone_height = top - bottom; if zone_height < Fixed::ZERO { // Reject zones with negative height continue; } max_zone_height = max_zone_height.max(zone_height); let zone = &mut zones[zone_ix]; zone.cs_bottom_edge = bottom; zone.cs_top_edge = top; if zone_ix == 0 { // First blue value is bottom zone zone.is_bottom = true; zone.cs_flat_edge = top; } else { // Remaining blue values are top zones zone.is_bottom = false; // Adjust both edges of top zone upward by twice darkening amount zone.cs_top_edge += twice(self.darken_y); zone.cs_bottom_edge += twice(self.darken_y); zone.cs_flat_edge = zone.cs_bottom_edge; } zone_ix += 1; } for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) { let (bottom, top) = *blue; let zone_height = top - bottom; if zone_height < Fixed::ZERO { // Reject zones with negative height continue; } max_zone_height = max_zone_height.max(zone_height); let zone = &mut zones[zone_ix]; // All "other" blues are bottom zone zone.is_bottom = true; zone.cs_bottom_edge = bottom; zone.cs_top_edge = top; zone.cs_flat_edge = top; zone_ix += 1; } // Adjust for family blues let units_per_pixel = Fixed::ONE / self.scale; for zone in &mut zones[..zone_ix] { let flat = zone.cs_flat_edge; let mut min_diff = Fixed::MAX; if zone.is_bottom { // In a bottom zone, the top edge is the flat edge. // Search family other blues for bottom zones. Look for the // closest edge that is within the one pixel threshold. for blue in params.family_other_blues.values() { let family_flat = blue.1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { zone.cs_flat_edge = family_flat; min_diff = diff; if diff == Fixed::ZERO { break; } } } // Check the first member of family blues, which is a bottom // zone if !params.family_blues.values().is_empty() { let family_flat = params.family_blues.values()[0].1; let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { zone.cs_flat_edge = family_flat; } } } else { // In a top zone, the bottom edge is the flat edge. // Search family blues for top zones, skipping the first, which // is a bottom zone. Look for closest family edge that is // within the one pixel threshold. for blue in params.family_blues.values().iter().skip(1) { let family_flat = blue.0 + twice(self.darken_y); let diff = (flat - family_flat).abs(); if diff < min_diff && diff < units_per_pixel { zone.cs_flat_edge = family_flat; min_diff = diff; if diff == Fixed::ZERO { break; } } } } } if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) { // Clamp at maximum scale self.blue_scale = Fixed::ONE / max_zone_height; } // Suppress overshoot and boost blue zones at small sizes if self.scale < self.blue_scale { self.suppress_overshoot = true; self.boost = Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale); // boost must remain less than 0.5, or baseline could go negative self.boost = self.boost.min(Fixed::from_bits(0x7FFF)); } if self.darken_y != Fixed::ZERO { self.boost = Fixed::ZERO; } // Set device space alignment for each zone; apply boost amount before // rounding flat edge let scale = self.scale; let boost = self.boost; for zone in &mut zones[..zone_ix] { let boost = if zone.is_bottom { -boost } else { boost }; zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round(); } self.zones = zones; self.zone_count = zone_ix; } /// Check whether a hint is captured by one of the blue zones. /// /// See fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool { // We use some wrapping arithmetic on this value below to avoid panics // on overflow and match FreeType's behavior // See let fuzz = self.blue_fuzz; let mut captured = false; let mut adjustment = Fixed::ZERO; for zone in self.zones() { if zone.is_bottom && bottom_edge.is_bottom() && zone.cs_bottom_edge.wrapping_sub(fuzz) <= bottom_edge.cs_coord && bottom_edge.cs_coord <= zone.cs_top_edge.wrapping_add(fuzz) { // Bottom edge captured by bottom zone. adjustment = if self.suppress_overshoot { zone.ds_flat_edge } else if zone.cs_top_edge.wrapping_sub(bottom_edge.cs_coord) >= self.blue_shift { // Guarantee minimum of 1 pixel overshoot bottom_edge .ds_coord .round() .min(zone.ds_flat_edge - Fixed::ONE) } else { bottom_edge.ds_coord.round() }; adjustment -= bottom_edge.ds_coord; captured = true; break; } if !zone.is_bottom && top_edge.is_top() && zone.cs_bottom_edge.wrapping_sub(fuzz) <= top_edge.cs_coord && top_edge.cs_coord <= zone.cs_top_edge.wrapping_add(fuzz) { // Top edge captured by top zone. adjustment = if self.suppress_overshoot { zone.ds_flat_edge } else if top_edge.cs_coord.wrapping_sub(zone.cs_bottom_edge) >= self.blue_shift { // Guarantee minimum of 1 pixel overshoot top_edge .ds_coord .round() .max(zone.ds_flat_edge + Fixed::ONE) } else { top_edge.ds_coord.round() }; adjustment -= top_edge.ds_coord; captured = true; break; } } if captured { // Move both edges and mark them as "locked" if bottom_edge.is_valid() { bottom_edge.ds_coord += adjustment; bottom_edge.lock(); } if top_edge.is_valid() { top_edge.ds_coord += adjustment; top_edge.lock(); } } captured } } /// #[derive(Copy, Clone, Default)] struct StemHint { /// If true, device space position is valid is_used: bool, // Character space position min: Fixed, max: Fixed, // Device space position after first use ds_min: Fixed, ds_max: Fixed, } // Hint flags const GHOST_BOTTOM: u8 = 0x1; const GHOST_TOP: u8 = 0x2; const PAIR_BOTTOM: u8 = 0x4; const PAIR_TOP: u8 = 0x8; const LOCKED: u8 = 0x10; const SYNTHETIC: u8 = 0x20; /// #[derive(Copy, Clone, PartialEq, Default, Debug)] struct Hint { flags: u8, /// Index in original stem hint array (if not synthetic) index: u8, cs_coord: Fixed, ds_coord: Fixed, scale: Fixed, } impl Hint { fn is_valid(&self) -> bool { self.flags != 0 } fn is_bottom(&self) -> bool { self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0 } fn is_top(&self) -> bool { self.flags & (GHOST_TOP | PAIR_TOP) != 0 } fn is_pair(&self) -> bool { self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0 } fn is_pair_top(&self) -> bool { self.flags & PAIR_TOP != 0 } fn is_locked(&self) -> bool { self.flags & LOCKED != 0 } fn is_synthetic(&self) -> bool { self.flags & SYNTHETIC != 0 } fn lock(&mut self) { self.flags |= LOCKED } /// Hint initialization from an incoming stem hint. /// /// See fn setup( &mut self, stem: &StemHint, index: u8, origin: Fixed, scale: Fixed, darken_y: Fixed, is_bottom: bool, ) { // "Ghost hints" are used to align a single edge rather than a // stem-- think the top and bottom edges of an uppercase // sans-serif I. // These are encoded internally with stem hints of width -21 // and -20 for bottom and top hints, respectively. const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21); const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20); let width = stem.max - stem.min; if width == GHOST_BOTTOM_WIDTH { if is_bottom { self.cs_coord = stem.max; self.flags = GHOST_BOTTOM; } else { self.flags = 0; } } else if width == GHOST_TOP_WIDTH { if !is_bottom { self.cs_coord = stem.min; self.flags = GHOST_TOP; } else { self.flags = 0; } } else if width < Fixed::ZERO { // If width < 0, this is an inverted pair. We follow FreeType and // swap the coordinates if is_bottom { self.cs_coord = stem.max; self.flags = PAIR_BOTTOM; } else { self.cs_coord = stem.min; self.flags = PAIR_TOP; } } else { // This is a normal pair if is_bottom { self.cs_coord = stem.min; self.flags = PAIR_BOTTOM; } else { self.cs_coord = stem.max; self.flags = PAIR_TOP; } } if self.is_top() { // For top hints, adjust character space position up by twice the // darkening amount self.cs_coord += twice(darken_y); } self.cs_coord += origin; self.scale = scale; self.index = index; // If original stem hint was used, copy the position if self.flags != 0 && stem.is_used { if self.is_top() { self.ds_coord = stem.ds_max; } else { self.ds_coord = stem.ds_min; } self.lock(); } else { self.ds_coord = self.cs_coord * scale; } } } /// Collection of adjusted hint edges. /// /// #[derive(Copy, Clone)] struct HintMap { edges: [Hint; MAX_HINTS], len: usize, is_valid: bool, scale: Fixed, } impl HintMap { fn new(scale: Fixed) -> Self { Self { edges: [Hint::default(); MAX_HINTS], len: 0, is_valid: false, scale, } } fn clear(&mut self) { self.len = 0; self.is_valid = false; } /// Transform character space coordinate to device space. /// /// Based on fn transform(&self, coord: Fixed) -> Fixed { if self.len == 0 { return coord * self.scale; } let limit = self.len - 1; let mut i = 0; while i < limit && coord >= self.edges[i + 1].cs_coord { i += 1; } while i > 0 && coord < self.edges[i].cs_coord { i -= 1; } let first_edge = &self.edges[0]; if i == 0 && coord < first_edge.cs_coord { // Special case for points below first edge: use uniform scale ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord } else { // Use highest edge where cs_coord >= edge.cs_coord let edge = &self.edges[i]; ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord } } /// Insert hint edges into map, sorted by character space coordinate. /// /// Based on fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) { let (is_pair, mut first_edge) = if !bottom.is_valid() { // Bottom is invalid: insert only top edge (false, *top) } else if !top.is_valid() { // Top is invalid: insert only bottom edge (false, *bottom) } else { // We have a valid pair! (true, *bottom) }; let mut second_edge = *top; if is_pair && top.cs_coord < bottom.cs_coord { // Paired edges must be in proper order. FT just ignores the hint. return; } let edge_count = if is_pair { 2 } else { 1 }; if self.len + edge_count > MAX_HINTS { // Won't fit. Again, ignore. return; } // Find insertion index that keeps the edge list sorted let mut insert_ix = 0; while insert_ix < self.len { if self.edges[insert_ix].cs_coord >= first_edge.cs_coord { break; } insert_ix += 1; } // Discard hints that overlap in character space if insert_ix < self.len { let current = &self.edges[insert_ix]; // Existing edge is the same if (current.cs_coord == first_edge.cs_coord) // Pair straddles the next edge || (is_pair && current.cs_coord <= second_edge.cs_coord) // Inserting between paired edges || current.is_pair_top() { return; } } // Recompute device space locations using initial hint map if !first_edge.is_locked() { if let Some(initial) = initial { if is_pair { // Preserve stem width: position center of stem with // initial hint map and two edges with nominal scale // let mid = initial.transform(midpoint(first_edge.cs_coord, second_edge.cs_coord)); let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale; first_edge.ds_coord = mid - half_width; second_edge.ds_coord = mid + half_width; } else { first_edge.ds_coord = initial.transform(first_edge.cs_coord); } } } // Now discard hints that overlap in device space: if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord { // Inserting after an existing edge return; } if insert_ix < self.len && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord) || first_edge.ds_coord > self.edges[insert_ix].ds_coord) { // Inserting before an existing edge return; } // If we're inserting in the middle, make room in the edge array if insert_ix != self.len { let mut src_index = self.len - 1; let mut dst_index = self.len + edge_count - 1; loop { self.edges[dst_index] = self.edges[src_index]; if src_index == insert_ix { break; } src_index -= 1; dst_index -= 1; } } self.edges[insert_ix] = first_edge; if is_pair { self.edges[insert_ix + 1] = second_edge; } self.len += edge_count; } /// Adjust hint pairs so that one of the two edges is on a pixel boundary. /// /// Based on fn adjust(&mut self) { let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS]; let mut saved_count = 0usize; let mut i = 0; // From FT with adjustments for variable names: // "First pass is bottom-up (font hint order) without look-ahead. // Locked edges are already adjusted. // Unlocked edges begin with ds_coord from `initial_map'. // Save edges that are not optimally adjusted in `saved' array, // and process them in second pass." let limit = self.len; while i < limit { let is_pair = self.edges[i].is_pair(); let j = if is_pair { i + 1 } else { i }; if !self.edges[i].is_locked() { // We can adjust hint edges that are not locked let frac_down = self.edges[i].ds_coord.fract(); let frac_up = self.edges[j].ds_coord.fract(); // There are four possibilities. We compute them all. // (moves down are negative) let down_move_down = Fixed::ZERO - frac_down; let up_move_down = Fixed::ZERO - frac_up; let down_move_up = if frac_down == Fixed::ZERO { Fixed::ZERO } else { Fixed::ONE - frac_down }; let up_move_up = if frac_up == Fixed::ZERO { Fixed::ZERO } else { Fixed::ONE - frac_up }; // Smallest move up let move_up = down_move_up.min(up_move_up); // Smallest move down let move_down = down_move_down.max(up_move_down); let mut save_edge = false; let adjustment; // Check for room to move up: // 1. We're at the top of the array, or // 2. The next edge is at or above the proposed move up if j >= self.len - 1 || self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + move_up + MIN_COUNTER) { // Also check for room to move down... if i == 0 || self.edges[i - 1].ds_coord <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { // .. and move the smallest distance adjustment = if -move_down < move_up { move_down } else { move_up }; } else { adjustment = move_up; } } else if i == 0 || self.edges[i - 1].ds_coord <= (self.edges[i].ds_coord + move_down - MIN_COUNTER) { // We can move down adjustment = move_down; // True if the move is not optimum save_edge = move_up < -move_down; } else { // We can't move either way without overlapping adjustment = Fixed::ZERO; save_edge = true; } // Capture non-optimal adjustments and save them for a second // pass. This is only possible if the edge above is unlocked // and can be moved. if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() { // (index, desired adjustment) saved[saved_count] = (j, move_up - adjustment); saved_count += 1; } // Apply the adjustment self.edges[i].ds_coord += adjustment; if is_pair { self.edges[j].ds_coord += adjustment; } } // Compute the new edge scale if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord { let a = self.edges[i]; let b = self.edges[i - 1]; self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } if is_pair { if self.edges[j].cs_coord != self.edges[j - 1].cs_coord { let a = self.edges[j]; let b = self.edges[j - 1]; self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord); } i += 1; } i += 1; } // Second pass tries to move non-optimal edges up if the first // pass created room for (j, adjustment) in saved[..saved_count].iter().copied().rev() { if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) { self.edges[j].ds_coord += adjustment; if self.edges[j].is_pair() { self.edges[j - 1].ds_coord += adjustment; } } } } /// Builds a hintmap from hints and mask. /// /// If `initial_map` is invalid, this recurses one level to initialize /// it. If `is_initial` is true, simply build the initial map. /// /// Based on fn build( &mut self, state: &HintState, mask: Option, mut initial_map: Option<&mut HintMap>, stems: &mut [StemHint], origin: Fixed, is_initial: bool, ) { let scale = state.scale; let darken_y = Fixed::ZERO; if !is_initial { if let Some(initial_map) = &mut initial_map { if !initial_map.is_valid { // Note: recursive call here to build the initial map if it // is provided and invalid initial_map.build(state, Some(HintMask::all()), None, stems, origin, true); } } } let initial_map = initial_map.map(|x| x as &HintMap); self.clear(); // If the mask is missing or invalid, assume all hints are active let mut mask = mask.unwrap_or_else(HintMask::all); if !mask.is_valid { mask = HintMask::all(); } if state.do_em_box_hints { // FreeType generates these during blues initialization. Do // it here just to avoid carrying the extra state in the // already large HintState struct. // let mut bottom = Hint::default(); bottom.cs_coord = ICF_BOTTOM - EPSILON; bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER; bottom.scale = scale; bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC; let mut top = Hint::default(); top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y); top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER; top.scale = scale; top.flags = GHOST_TOP | LOCKED | SYNTHETIC; let invalid = Hint::default(); self.insert(&bottom, &invalid, initial_map); self.insert(&invalid, &top, initial_map); } let mut tmp_mask = mask; // FreeType iterates over the hint mask with some fancy bit logic. We // do the simpler thing and loop over the stems. // for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); bottom.setup(stem, hint_ix, origin, scale, darken_y, true); top.setup(stem, hint_ix, origin, scale, darken_y, false); // Insert hints that are locked or captured by a blue zone if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) { if is_initial { self.insert(&bottom, &top, None); } else { self.insert(&bottom, &top, initial_map); } // Avoid processing this hint in the second pass tmp_mask.clear(i); } } if is_initial { // Heuristic: insert a point at (0, 0) if it's not covered by a // mapping. Ensures a lock at baseline for glyphs missing a // baseline hint. if self.len == 0 || self.edges[0].cs_coord > Fixed::ZERO || self.edges[self.len - 1].cs_coord < Fixed::ZERO { let edge = Hint { flags: GHOST_BOTTOM | LOCKED | SYNTHETIC, scale, ..Default::default() }; let invalid = Hint::default(); self.insert(&edge, &invalid, None); } } else { // Insert hints that were skipped in the first pass for (i, stem) in stems.iter().enumerate() { if !tmp_mask.get(i) { continue; } let hint_ix = i as u8; let mut bottom = Hint::default(); let mut top = Hint::default(); bottom.setup(stem, hint_ix, origin, scale, darken_y, true); top.setup(stem, hint_ix, origin, scale, darken_y, false); self.insert(&bottom, &top, initial_map); } } // Adjust edges that are not locked to blue zones self.adjust(); if !is_initial { // Save position of edges that were used by the hint map. for edge in &self.edges[..self.len] { if edge.is_synthetic() { continue; } let stem = &mut stems[edge.index as usize]; if edge.is_top() { stem.ds_max = edge.ds_coord; } else { stem.ds_min = edge.ds_coord; } stem.is_used = true; } } self.is_valid = true; } } /// Bitmask that specifies which hints are currently active. /// /// "Each bit of the mask, starting with the most-significant bit of /// the first byte, represents the corresponding hint zone in the /// order in which the hints were declared at the beginning of /// the charstring." /// /// See /// Also #[derive(Copy, Clone, PartialEq, Default)] struct HintMask { mask: [u8; HINT_MASK_SIZE], is_valid: bool, } impl HintMask { fn new(bytes: &[u8]) -> Option { let len = bytes.len(); if len > HINT_MASK_SIZE { return None; } let mut mask = Self::default(); mask.mask[..len].copy_from_slice(&bytes[..len]); mask.is_valid = true; Some(mask) } fn all() -> Self { Self { mask: [0xFF; HINT_MASK_SIZE], is_valid: true, } } fn clear(&mut self, bit: usize) { self.mask[bit >> 3] &= !msb_mask(bit); } fn get(&self, bit: usize) -> bool { self.mask[bit >> 3] & msb_mask(bit) != 0 } } /// Returns a bit mask for the selected bit with the /// most significant bit at index 0. fn msb_mask(bit: usize) -> u8 { 1 << (7 - (bit & 0x7)) } pub(super) struct HintingSink<'a, S> { state: &'a HintState, sink: &'a mut S, stem_hints: [StemHint; MAX_HINTS], stem_count: u8, mask: HintMask, initial_map: HintMap, map: HintMap, /// Most recent move_to in character space. start_point: Option<[Fixed; 2]>, /// Most recent line_to. First two elements are coords in character /// space and the last two are in device space. pending_line: Option<[Fixed; 4]>, } impl<'a, S: CommandSink> HintingSink<'a, S> { pub fn new(state: &'a HintState, sink: &'a mut S) -> Self { let scale = state.scale; Self { state, sink, stem_hints: [StemHint::default(); MAX_HINTS], stem_count: 0, mask: HintMask::all(), initial_map: HintMap::new(scale), map: HintMap::new(scale), start_point: None, pending_line: None, } } fn maybe_close_subpath(&mut self) { // This requires some explanation. The hint mask can be modified // during charstring evaluation which changes the set of hints that // are applied. FreeType ensures that the closing line for any subpath // is transformed with the same hint map as the starting point for the // subpath. This is done by stashing a copy of the hint map that is // active when a new subpath is started. Unlike FreeType, we make use // of close elements, so we can cheat a bit here and avoid the // extra hintmap. If we're closing an open subpath and have a pending // line and the line is not equal to the start point in character // space, then we emit the saved device space coordinates for the // line. If the coordinates do match in character space, we omit // that line. The unconditional close command ensures that the // start and end points coincide. // Note: this doesn't apply to subpaths that end in cubics. match (self.start_point.take(), self.pending_line.take()) { (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => { if start != [cs_x, cs_y] { self.sink.line_to(ds_x, ds_y); } self.sink.close(); } (Some(_), _) => self.sink.close(), _ => {} } } fn flush_pending_line(&mut self) { if let Some([_, _, x, y]) = self.pending_line.take() { self.sink.line_to(x, y); } } fn hint(&mut self, coord: Fixed) -> Fixed { if !self.map.is_valid { self.build_hint_map(Some(self.mask), Fixed::ZERO); } trunc(self.map.transform(coord)) } fn scale(&self, coord: Fixed) -> Fixed { trunc(coord * self.state.scale) } fn add_stem(&mut self, min: Fixed, max: Fixed) { let index = self.stem_count as usize; if index >= MAX_HINTS || self.map.is_valid { return; } let stem = &mut self.stem_hints[index]; stem.min = min; stem.max = max; stem.is_used = false; stem.ds_min = Fixed::ZERO; stem.ds_max = Fixed::ZERO; self.stem_count = index as u8 + 1; } fn build_hint_map(&mut self, mask: Option, origin: Fixed) { self.map.build( self.state, mask, Some(&mut self.initial_map), &mut self.stem_hints[..self.stem_count as usize], origin, false, ); } } impl CommandSink for HintingSink<'_, S> { fn hstem(&mut self, min: Fixed, max: Fixed) { self.add_stem(min, max); } fn hint_mask(&mut self, mask: &[u8]) { // For invalid hint masks, FreeType assumes all hints are active. // See let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); if mask != self.mask { self.mask = mask; self.map.is_valid = false; } } fn counter_mask(&mut self, mask: &[u8]) { // For counter masks, we build a temporary hint map "just to // place and lock those stems participating in the counter // mask." Building the map modifies the stem hint array as a // side effect. // See let mask = HintMask::new(mask).unwrap_or_else(HintMask::all); let mut map = HintMap::new(self.state.scale); map.build( self.state, Some(mask), Some(&mut self.initial_map), &mut self.stem_hints[..self.stem_count as usize], Fixed::ZERO, false, ); } fn clear_hints(&mut self) { // This resets all hinting state that is derived from accumulated // stem hints. // This is used when evaluating the implied SEAC operator which // processes a base char followed by an accent char and expects // hinting state to be fresh for each. self.stem_count = 0; self.map = HintMap::new(self.state.scale); self.initial_map = HintMap::new(self.state.scale); self.mask = HintMask::all(); } fn move_to(&mut self, x: Fixed, y: Fixed) { self.maybe_close_subpath(); self.start_point = Some([x, y]); let x = self.scale(x); let y = self.hint(y); self.sink.move_to(x, y); } fn line_to(&mut self, x: Fixed, y: Fixed) { self.flush_pending_line(); let ds_x = self.scale(x); let ds_y = self.hint(y); self.pending_line = Some([x, y, ds_x, ds_y]); } fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) { self.flush_pending_line(); let cx1 = self.scale(cx1); let cy1 = self.hint(cy1); let cx2 = self.scale(cx2); let cy2 = self.hint(cy2); let x = self.scale(x); let y = self.hint(y); self.sink.curve_to(cx1, cy1, cx2, cy2, x, y); } fn close(&mut self) { // We emit close commands based on the sequence of moves. // See `maybe_close_subpath` } fn finish(&mut self) { self.maybe_close_subpath(); self.sink.finish(); } } /// FreeType converts from 16.16 to 26.6 by truncation. We keep our /// values in 16.16 so simply zero the low 10 bits to match the /// precision when converting to f32. fn trunc(value: Fixed) -> Fixed { Fixed::from_bits(value.to_bits() & !0x3FF) } fn half(value: Fixed) -> Fixed { Fixed::from_bits(value.to_bits() / 2) } fn twice(value: Fixed) -> Fixed { Fixed::from_bits(value.to_bits().wrapping_mul(2)) } /// Computes midpoint between `a` and `b`, avoiding overflow if the sum /// of the high 16 bits exceeds `i16::MAX`. fn midpoint(a: Fixed, b: Fixed) -> Fixed { a + half(b - a) } #[cfg(test)] mod tests { use super::*; use raw::{types::F2Dot14, FontRef}; fn make_hint_state() -> HintState { fn make_blues(values: &[f64]) -> Blues { Blues::new(values.iter().copied().map(Fixed::from_f64)) } // // // // // let params = HintParams { blues: make_blues(&[ -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0, ]), other_blues: make_blues(&[-255.0, -240.0]), blue_scale: Fixed::from_f64(0.05), blue_shift: Fixed::from_i32(7), blue_fuzz: Fixed::ZERO, ..Default::default() }; HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64)) } #[test] fn scaled_blue_zones() { let state = make_hint_state(); assert!(!state.do_em_box_hints); assert_eq!(state.zone_count, 6); assert_eq!(state.boost, Fixed::from_bits(27035)); assert!(state.suppress_overshoot); // FreeType generates the following zones: let expected_zones = &[ // csBottomEdge -983040 int // csTopEdge 0 int // csFlatEdge 0 int // dsFlatEdge 0 int // bottomZone 1 '\x1' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(-983040), is_bottom: true, ..Default::default() }, // csBottomEdge 35127296 int // csTopEdge 35848192 int // csFlatEdge 35127296 int // dsFlatEdge 589824 int // bottomZone 0 '\0' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(35127296), cs_top_edge: Fixed::from_bits(35848192), cs_flat_edge: Fixed::from_bits(35127296), ds_flat_edge: Fixed::from_bits(589824), is_bottom: false, }, // csBottomEdge 37421056 int // csTopEdge 38141952 int // csFlatEdge 37421056 int // dsFlatEdge 589824 int // bottomZone 0 '\0' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(37421056), cs_top_edge: Fixed::from_bits(38141952), cs_flat_edge: Fixed::from_bits(37421056), ds_flat_edge: Fixed::from_bits(589824), is_bottom: false, }, // csBottomEdge 46792704 int // csTopEdge 47579136 int // csFlatEdge 46792704 int // dsFlatEdge 786432 int // bottomZone 0 '\0' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(46792704), cs_top_edge: Fixed::from_bits(47579136), cs_flat_edge: Fixed::from_bits(46792704), ds_flat_edge: Fixed::from_bits(786432), is_bottom: false, }, // csBottomEdge 49807360 int // csTopEdge 50593792 int // csFlatEdge 49807360 int // dsFlatEdge 786432 int // bottomZone 0 '\0' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(49807360), cs_top_edge: Fixed::from_bits(50593792), cs_flat_edge: Fixed::from_bits(49807360), ds_flat_edge: Fixed::from_bits(786432), is_bottom: false, }, // csBottomEdge -16711680 int // csTopEdge -15728640 int // csFlatEdge -15728640 int // dsFlatEdge -262144 int // bottomZone 1 '\x1' unsigned char BlueZone { cs_bottom_edge: Fixed::from_bits(-16711680), cs_top_edge: Fixed::from_bits(-15728640), cs_flat_edge: Fixed::from_bits(-15728640), ds_flat_edge: Fixed::from_bits(-262144), is_bottom: true, }, ]; assert_eq!(state.zones(), expected_zones); } #[test] fn blue_zone_capture() { let state = make_hint_state(); let bottom_edge = Hint { flags: PAIR_BOTTOM, ds_coord: Fixed::from_f64(2.3), ..Default::default() }; let top_edge = Hint { flags: PAIR_TOP, // This value chosen to fit within the first "top" blue zone cs_coord: Fixed::from_bits(35127297), ds_coord: Fixed::from_f64(2.3), ..Default::default() }; // Capture both { let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge); assert!(state.capture(&mut bottom_edge, &mut top_edge)); assert!(bottom_edge.is_locked()); assert!(top_edge.is_locked()); } // Capture none { // Used to guarantee the edges are below all blue zones and will // not be captured let min_cs_coord = Fixed::MIN; let mut bottom_edge = Hint { cs_coord: min_cs_coord, ..bottom_edge }; let mut top_edge = Hint { cs_coord: min_cs_coord, ..top_edge }; assert!(!state.capture(&mut bottom_edge, &mut top_edge)); assert!(!bottom_edge.is_locked()); assert!(!top_edge.is_locked()); } // Capture bottom, ignore invalid top { let mut bottom_edge = bottom_edge; let mut top_edge = Hint { // Empty flags == invalid hint flags: 0, ..top_edge }; assert!(state.capture(&mut bottom_edge, &mut top_edge)); assert!(bottom_edge.is_locked()); assert!(!top_edge.is_locked()); } // Capture top, ignore invalid bottom { let mut bottom_edge = Hint { // Empty flags == invalid hint flags: 0, ..bottom_edge }; let mut top_edge = top_edge; assert!(state.capture(&mut bottom_edge, &mut top_edge)); assert!(!bottom_edge.is_locked()); assert!(top_edge.is_locked()); } } #[test] fn hint_mask_ops() { const MAX_BITS: usize = HINT_MASK_SIZE * 8; let all_bits = HintMask::all(); for i in 0..MAX_BITS { assert!(all_bits.get(i)); } let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap(); for i in 0..MAX_BITS { assert_eq!(i & 1 != 0, odd_bits.get(i)); } let mut cleared_bits = odd_bits; for i in 0..MAX_BITS { if i & 1 != 0 { cleared_bits.clear(i); } } assert_eq!(cleared_bits.mask, HintMask::default().mask); } #[test] fn hint_mapping() { let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap(); let cff_font = super::super::Outlines::new(&font).unwrap(); let state = cff_font .subfont(0, Some(8.0), &[F2Dot14::from_f32(-1.0); 2]) .unwrap() .hint_state; let mut initial_map = HintMap::new(state.scale); let mut map = HintMap::new(state.scale); // Stem hints from Cantarell-VF.otf glyph id 2 let mut stems = [ StemHint { min: Fixed::from_bits(1376256), max: Fixed::ZERO, ..Default::default() }, StemHint { min: Fixed::from_bits(16318464), max: Fixed::from_bits(17563648), ..Default::default() }, StemHint { min: Fixed::from_bits(45481984), max: Fixed::from_bits(44171264), ..Default::default() }, ]; map.build( &state, Some(HintMask::all()), Some(&mut initial_map), &mut stems, Fixed::ZERO, false, ); // FT generates the following hint map: // // index csCoord dsCoord scale flags // 0 0.00 0.00 526 gbL // 1 249.00 250.14 524 pb // 1 268.00 238.22 592 pt // 2 694.00 750.41 524 gtL let expected_edges = [ Hint { index: 0, cs_coord: Fixed::from_f64(0.0), ds_coord: Fixed::from_f64(0.0), scale: Fixed::from_bits(526), flags: GHOST_BOTTOM | LOCKED, }, Hint { index: 1, cs_coord: Fixed::from_bits(16318464), ds_coord: Fixed::from_bits(131072), scale: Fixed::from_bits(524), flags: PAIR_BOTTOM, }, Hint { index: 1, cs_coord: Fixed::from_bits(17563648), ds_coord: Fixed::from_bits(141028), scale: Fixed::from_bits(592), flags: PAIR_TOP, }, Hint { index: 2, cs_coord: Fixed::from_bits(45481984), ds_coord: Fixed::from_bits(393216), scale: Fixed::from_bits(524), flags: GHOST_TOP | LOCKED, }, ]; assert_eq!(expected_edges, &map.edges[..map.len]); // And FT generates the following mappings let mappings = [ // (coord in font units, expected hinted coord in device space) in 16.16 (0, 0), // 0 -> 0 (44302336, 382564), // 676 -> 5.828125 (45481984, 393216), // 694 -> 6 (16318464, 131072), // 249 -> 2 (17563648, 141028), // 268 -> 2.140625 (49676288, 426752), // 758 -> 6.5 (56754176, 483344), // 866 -> 7.375 (57868288, 492252), // 883 -> 7.5 (50069504, 429896), // 764 -> 6.546875 ]; for (coord, expected) in mappings { assert_eq!( map.transform(Fixed::from_bits(coord)), Fixed::from_bits(expected) ); } } #[test] fn midpoint_avoids_overflow() { // We encountered an overflow in the HintMap::insert midpoint // calculation for glyph id 950 at size 74 in // KawkabMono-Bold v0.501 . // Test that our midpoint function doesn't overflow when the sum of // the high 16 bits of the two values exceeds i16::MAX. let a = i16::MAX as i32; let b = a - 1; assert!(a + b > i16::MAX as i32); let mid = super::midpoint(Fixed::from_i32(a), Fixed::from_i32(b)); assert_eq!((a + b) / 2, mid.to_bits() >> 16); } /// HintingSink is mostly pass-through. This test captures the logic /// around omission of pending lines that match subpath start. /// See HintingSink::maybe_close_subpath for details. #[test] fn hinting_sink_omits_closing_line_that_matches_start() { let state = HintState { scale: Fixed::ONE, ..Default::default() }; let mut path = Path::default(); let mut sink = HintingSink::new(&state, &mut path); let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)]; let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)]; let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)]; let curve = [ Fixed::from_f64(3.0), Fixed::from_f64(4.0), Fixed::from_f64(5.0), Fixed::from_f64(6.0), Fixed::from_f64(1.0), Fixed::from_f64(2.0), ]; // First subpath, closing line matches start sink.move_to(move1_2[0], move1_2[1]); sink.line_to(line2_3[0], line2_3[1]); sink.line_to(line1_2[0], line1_2[1]); // Second subpath, closing line does not match start sink.move_to(move1_2[0], move1_2[1]); sink.line_to(line2_3[0], line2_3[1]); sink.line_to(line3_4[0], line3_4[1]); // Third subpath, ends with cubic. Still emits a close command // even though end point matches start. sink.move_to(move1_2[0], move1_2[1]); sink.line_to(line2_3[0], line2_3[1]); sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]); sink.finish(); // Subpaths always end with a close command. If a final line coincides // with the start of a subpath, it is omitted. assert_eq!( &path.0, &[ // First subpath MoveTo(move1_2), LineTo(line2_3), // line1_2 is omitted Close, // Second subpath MoveTo(move1_2), LineTo(line2_3), LineTo(line3_4), Close, // Third subpath MoveTo(move1_2), LineTo(line2_3), CurveTo(curve), Close, ] ); } #[derive(Copy, Clone, PartialEq, Debug)] enum Command { MoveTo([Fixed; 2]), LineTo([Fixed; 2]), CurveTo([Fixed; 6]), Close, } use Command::*; #[derive(Default)] struct Path(Vec); impl CommandSink for Path { fn move_to(&mut self, x: Fixed, y: Fixed) { self.0.push(MoveTo([x, y])); } fn line_to(&mut self, x: Fixed, y: Fixed) { self.0.push(LineTo([x, y])); } fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) { self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y])); } fn close(&mut self) { self.0.push(Close); } } }