//! Helpers for unit testing use super::OutlinePen; use core::str::FromStr; use raw::{ tables::glyf::PointFlags, types::{F26Dot6, F2Dot14, GlyphId, Point}, }; #[derive(Copy, Clone, PartialEq, Debug)] // clippy doesn't like the common To suffix #[allow(clippy::enum_variant_names)] pub enum PathElement { MoveTo([f32; 2]), LineTo([f32; 2]), QuadTo([f32; 4]), CurveTo([f32; 6]), } #[derive(Default)] pub struct Path { pub elements: Vec, last_end: Option<[f32; 2]>, } impl OutlinePen for Path { fn move_to(&mut self, x: f32, y: f32) { self.elements.push(PathElement::MoveTo([x, y])); self.last_end = Some([x, y]); } fn line_to(&mut self, x: f32, y: f32) { self.elements.push(PathElement::LineTo([x, y])); self.last_end = Some([x, y]); } fn quad_to(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) { self.elements.push(PathElement::QuadTo([x0, y0, x1, y1])); self.last_end = Some([x1, y1]); } fn curve_to(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32) { self.elements .push(PathElement::CurveTo([x0, y0, x1, y1, x2, y2])); self.last_end = Some([x2, y2]); } fn close(&mut self) { // FT_Outline_Decompose does not generate close commands, so for // testing purposes, we insert a line to same point as the most // recent move_to (if the last command didn't end at the same point) // which copies FreeType's behavior. let last_move = self .elements .iter() .rev() .find(|element| matches!(*element, PathElement::MoveTo(_))) .copied(); if let Some(PathElement::MoveTo(point)) = last_move { if Some(point) != self.last_end { self.elements.push(PathElement::LineTo(point)); } } } } #[derive(Clone, Default, Debug)] pub struct GlyphOutline { pub glyph_id: GlyphId, pub size: f32, pub coords: Vec, pub points: Vec>, pub contours: Vec, pub flags: Vec, pub path: Vec, } pub fn parse_glyph_outlines(source: &str) -> Vec { let mut outlines = vec![]; let mut cur_outline = GlyphOutline::default(); for line in source.lines() { let line = line.trim(); if line == "-" { outlines.push(cur_outline.clone()); } else if line.starts_with("glyph") { cur_outline = GlyphOutline::default(); let parts = line.split(' ').collect::>(); cur_outline.glyph_id = GlyphId::new(parts[1].parse().unwrap()); cur_outline.size = parts[2].parse().unwrap(); } else if line.starts_with("coords") { for coord in line.split(' ').skip(1) { cur_outline .coords .push(F2Dot14::from_f32(coord.parse().unwrap())); } } else if line.starts_with("contours") { for contour in line.split(' ').skip(1) { cur_outline.contours.push(contour.parse().unwrap()); } } else if line.starts_with("points") { let is_scaled = cur_outline.size != 0.0; for mut point in parse_points(line.strip_prefix("points").unwrap().trim()) { if !is_scaled { point[0] <<= 6; point[1] <<= 6; } cur_outline.points.push(Point { x: F26Dot6::from_bits(point[0]), y: F26Dot6::from_bits(point[1]), }); } } else if line.starts_with("tags") { for tag in line.split(' ').skip(1) { cur_outline .flags .push(PointFlags::from_bits(tag.parse().unwrap())); } } else { match line.as_bytes()[0] { b'm' => { let points = parse_points(line.strip_prefix("m ").unwrap().trim()); cur_outline.path.push(PathElement::MoveTo(points[0])); } b'l' => { let points = parse_points(line.strip_prefix("l ").unwrap().trim()); cur_outline.path.push(PathElement::LineTo(points[0])); } b'q' => { let points = parse_points(line.strip_prefix("q ").unwrap().trim()); cur_outline.path.push(PathElement::QuadTo([ points[0][0], points[0][1], points[1][0], points[1][1], ])); } b'c' => { let points = parse_points(line.strip_prefix("c ").unwrap().trim()); cur_outline.path.push(PathElement::CurveTo([ points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1], ])); } _ => panic!("unexpected path element"), } } } outlines } fn parse_points(source: &str) -> Vec<[F; 2]> where F: FromStr + Copy + Default, ::Err: core::fmt::Debug, { let mut points = vec![]; for point in source.split(' ') { let point = point.trim(); if point.is_empty() { continue; } let mut components = [F::default(); 2]; for (i, component) in point.trim().split(',').take(2).enumerate() { components[i] = F::from_str(component).unwrap(); } points.push(components); } points }