//! Helpers for unit testing
use super::Pen;
use crate::{
tables::glyf::PointFlags,
types::{F26Dot6, F2Dot14, GlyphId, Point},
};
use core::str::FromStr;
#[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<PathElement>,
last_end: Option<[f32; 2]>,
}
impl Pen 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<F2Dot14>,
pub points: Vec<Point<F26Dot6>>,
pub contours: Vec<u16>,
pub flags: Vec<PointFlags>,
pub path: Vec<PathElement>,
}
pub fn parse_glyph_outlines(source: &str) -> Vec<GlyphOutline> {
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::<Vec<_>>();
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<F>(source: &str) -> Vec<[F; 2]>
where
F: FromStr + Copy + Default,
<F as FromStr>::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
}