use skia_safe as skia; use skia::{Point as SkPoint, Path as SkPath}; use skia::path::{Verb as SkVerb}; use crate::point::{Handle, Point, PointData, PointType}; use crate::{Contour, Outline}; use crate::outline::QuadToCubic; /// Get an outline from a Skia path. Outline is guaranteed to contain only Curve's, no QCurve's. pub trait FromSkiaPath { fn from_skia_path(skp: &skia::Path) -> Self; } // These types should only be used outside this file with care and are shorthand for FromSkiaPath pub type SkPointTuple = (PointType, Vec, Option); pub type SkContour = Vec; pub type SkOutline = Vec; pub trait TryIntoPointType { fn try_into_pointtype(&self) -> Option; } impl TryIntoPointType for SkVerb { fn try_into_pointtype(&self) -> Option { match self { SkVerb::Move => Some(PointType::Move), SkVerb::Cubic => Some(PointType::Curve), SkVerb::Quad | SkVerb::Conic => Some(PointType::QCurve), SkVerb::Line => Some(PointType::Line), SkVerb::Close | SkVerb::Done => None, } } } pub trait SplitSkiaPath { fn split_skia_path(&self) -> SkOutline; } impl SplitSkiaPath for skia::Path { fn split_skia_path(&self) -> SkOutline { // These are iterators over (Verb, Vec) // We need two of them because the for loop consumes `iter`, meaning we can't get the conic // weight from it. This is ugly, I know, but converting C API's to Rust often are. let iter = skia::path::Iter::new(self, false); let mut iter2 = skia::path::Iter::new(self, false); let mut skoutline: SkOutline = vec![]; let mut skcontour: SkContour = vec![]; // First we're going to split a path into its constituent contours for p in iter { iter2.next(); let (verb, point) = p; let conic_weight = iter2.conic_weight(); // This lets us know if the QCurve needs conversion let mut holding_conic = false; if verb == SkVerb::Done { break } else if verb == SkVerb::Conic { holding_conic = true; } let ptype = verb.try_into_pointtype(); if ptype == Some(PointType::Move) { if skcontour.len() > 0 { skoutline.push(skcontour.clone()); } skcontour = vec![]; } //eprintln!("Verb: {:?}, Point: {:?}, Conic Weight: {:?}", verb, &point, conic_weight); if let Some(pty) = ptype { skcontour.push((pty, point, if holding_conic { Some(conic_weight.unwrap()) } else { None })); } else { debug_assert!(skcontour[0].0 == PointType::Move); skcontour.remove(0); // drop "off curve" point } } if skcontour.len() > 0 { skoutline.push(skcontour); } skoutline } } pub trait ConicsToCubics { fn conics_to_cubics(self) -> Self; } impl ConicsToCubics for SkOutline { fn conics_to_cubics(mut self) -> Self { // The path could contain conics, so our next task is to resolve conics to quads for skc in self.iter_mut() { for skp in skc.iter_mut() { let skp: &mut SkPointTuple = skp; let (_verbs, points, conic_weight) = skp; if let Some(cw) = conic_weight { debug_assert_eq!(points.len(), 3); // magic number 5 for pow2 1 from https://fiddle.skia.org/c/@Path_ConvertConicToQuads let mut new_points = vec![SkPoint::default(); 5]; // convert_conic_to_quads(p0: impl Into, p1: impl Into, p2: impl Into, // w: scalar, pts: &mut [Point], pow2: usize) -> Option let ok = SkPath::convert_conic_to_quads(points[0], points[1], points[2], *cw, &mut new_points, 1); // number of quad bezier's == 2 debug_assert!(ok.is_some() && ok.unwrap() == 2); // We already changed the verb above by resolving skia::PointType::Conic as our PointType::QCurve // So, we only need to update the points, a quad is already expected. *points = new_points; // We do this to mark the points as changed, so we can panic if it's ever not // infinity *conic_weight = Some(f32::INFINITY); } } } // Now we have to change the converted conics to two quads, to match other curves that // might be the result of quad_to as opposed to conic_to. let mut final_skoutline: SkOutline = vec![]; for skc in self { let mut skcontour: SkContour = vec![]; for skp in skc.into_iter() { let (ptype, points, conic_weight) = skp; if ptype == PointType::QCurve { // same magic number as above. we always use a pow2 of 1 to convert conics to // quads, so we get 5 points assert!(points.len() == 3 || points.len() == 5); let new_points_n = if points.len() == 3 { 1 } else { 2 }; // We don't want any quads, despite all the work we've done to get them. Skia // doesn't have a function like convert_conic_to_cubics, only to_quads, so now // that we have the quads we can make cubics. skcontour.push((PointType::Curve, [points[0], points[1], points[2]].quad_to_cubic().to_vec(), None)); if new_points_n == 2 { skcontour.push((PointType::Curve, [points[2], points[3], points[4]].quad_to_cubic().to_vec(), None)); } } else { skcontour.push((ptype, points, conic_weight)); } } final_skoutline.push(skcontour); } //eprintln!("{:#?}", &final_skoutline); final_skoutline } } // This is a complex conversion. I documented it as best I could, and left around old debug // eprintln's in case you find a broken case. It's quite complicated and takes multiple passes // because Skia path's can contain conics and quads, both of which we want to upconvert to cubics. impl FromSkiaPath for Outline { fn from_skia_path(skp: &skia::Path) -> Outline { Outline::from_skoutline(skp.split_skia_path().conics_to_cubics()) } } pub trait FromSkOutline { fn from_skoutline(skoutline: SkOutline) -> Self; } impl FromSkOutline for Outline { fn from_skoutline(mut skoutline: SkOutline) -> Self { // Now we know that we have no conics, and no quads. In Skia terms, we only have the verbs // Move, Line, Cubic, Close, and Done. // // We can now convert to a cubic Bézier-backed glifparser Outline. let mut ret: Outline = Outline::new(); for skc in skoutline.iter_mut() { let skc_len = skc.len(); let mut contour: Contour = Contour::new(); let first_points: &SkPointTuple = &skc[0]; let mut prev_points; for (i, skp) in skc.iter().enumerate() { let (ptype, points, conic_weights) = skp; debug_assert!(conic_weights.iter().all(|c|!c.is_finite())); if i != 0 { prev_points = &skc[i-1]; } else { prev_points = &skc[skc_len - 1]; } let mut point = Point:: { name: None, data: None, x: points[0].x, y: points[0].y, smooth: false, // These will be fixed below, if needed a: Handle::Colocated, b: Handle::Colocated, ptype: *ptype, }; match ptype { PointType::Move => {}, PointType::Curve => { point.a = Handle::At(points[1].x, points[1].y); if prev_points.0 == PointType::Curve { point.b = Handle::At(prev_points.1[2].x, prev_points.1[2].y); } if i == skc_len-1 { if first_points.0 == PointType::Curve { contour[0].a = Handle::At(first_points.1[1].x, first_points.1[1].y); } contour[0].b = Handle::At(points[2].x, points[2].y); } }, PointType::Line => { if i != 0 && prev_points.0 == PointType::Curve { // Lines aren't allowed to have off-curve points in glif format point.ptype = PointType::Curve; point.b = Handle::At(prev_points.1[2].x, prev_points.1[2].y); } }, _ => unreachable!("") } contour.push(point); } if contour.first().map(|p|p.ptype == PointType::Move).unwrap_or(false) { fixup_skia_open_contour(&mut contour, &skc); } ret.push(contour); } ret } } fn fixup_skia_open_contour(contour: &mut Contour, skc: &SkContour) { // Skia doesn't put a last point like we expect for open contours let skc_len = skc.len(); let first = contour.first().unwrap().clone(); let last = contour.last().unwrap(); if last.ptype == PointType::Curve || last.ptype == PointType::Line { let ptype = match skc[skc_len-1].1.len() { 2 => PointType::Line, 4 => PointType::Curve, _ => {unreachable!()} }; let p = match ptype { PointType::Line => { skc[skc_len-1].1[1] }, PointType::Curve => { skc[skc_len-1].1[3] }, _ => {unreachable!()} }; let mut glifl: Point = Point::from_x_y_type((p.x, p.y), ptype); if ptype == PointType::Curve { let h_prev = skc[skc_len-1].1[2]; if p != h_prev { glifl.b = Handle::At(h_prev.x, h_prev.y); } } contour.push(glifl); } // Skia adds Move followed by Line at same spot, .glif format only needs the Move if contour.len() >= 2 && contour[1].x == first.x && contour[1].y == first.y { contour[0].a = contour[1].a; contour[0].b = contour[1].b; contour.remove(1); } }