| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| use std::path; | ||
| use xmltree; | ||
|
|
||
| use crate::anchor::Anchor; | ||
| use crate::component::GlifComponent; | ||
| use crate::error::GlifParserError; | ||
| use crate::point::{PointData}; | ||
| use crate::outline::{Outline, OutlineType}; | ||
|
|
||
| mod read; | ||
| mod write; | ||
|
|
||
| pub use read::read_ufo_glif as read; | ||
| pub use write::write_ufo_glif as write; | ||
|
|
||
| #[derive(Clone, Debug, PartialEq)] | ||
| pub struct Glif<PD: PointData> { | ||
| pub outline: Option<Outline<PD>>, | ||
| pub order: OutlineType, | ||
| pub anchors: Vec<Anchor>, | ||
| /// Note that these components are not yet parsed or checked for infinite loops. You need to | ||
| /// call either ``GlifComponent::to_component_of`` on each of these, or ``Glif::flatten``. | ||
| pub components: Vec<GlifComponent>, | ||
| pub width: Option<u64>, | ||
| pub unicode: Vec<char>, | ||
| pub name: String, | ||
| pub format: u8, // we only understand 2 | ||
| /// It's up to the API consumer to set this. | ||
| pub filename: Option<path::PathBuf>, | ||
| /// We give you the <lib> as an XML Element. Note, however, that in the UFO spec it is a plist | ||
| /// dictionary. You're going to need to parse this with a plist parser, such as plist.rs. You | ||
| /// may want to tell xmltree to write it back to a string first; however, it may be possible to | ||
| /// parse plist from xmltree::Element. Might change some day to a ``plist::Dictionary``. | ||
| pub lib: Option<xmltree::Element>, | ||
| /// This is an XML structure that will be written into a comment in the .glif file. | ||
| pub private_lib: Option<xmltree::Element>, | ||
| /// By default <MFEK>. Allows you to choose another root for your private lib. | ||
| pub private_lib_root: &'static str | ||
| } | ||
|
|
||
| impl<PD: PointData> Glif<PD> { | ||
| pub fn new() -> Self { | ||
| Glif { | ||
| outline: None, | ||
| order: OutlineType::Cubic, // default when only corners | ||
| anchors: vec![], | ||
| components: vec![], | ||
| width: None, | ||
| unicode: vec![], | ||
| name: String::new(), | ||
| format: 2, | ||
| filename: None, | ||
| lib: None, | ||
| private_lib: None, | ||
| private_lib_root: "MFEK" | ||
| } | ||
| } | ||
|
|
||
| pub fn name_to_filename(&self) -> String { | ||
| let mut ret = String::new(); | ||
| let chars: Vec<char> = self.name.chars().collect(); | ||
| for c in chars { | ||
| ret.push(c); | ||
| if ('A'..'Z').contains(&c) { | ||
| ret.push('_'); | ||
| } | ||
| } | ||
| ret.push_str(".glif"); | ||
| ret | ||
| } | ||
|
|
||
| pub fn filename_is_sane(&self) -> Result<bool, GlifParserError> { | ||
| match &self.filename { | ||
| Some(gfn) => { | ||
| let gfn_fn = match gfn.file_name() { | ||
| Some(gfn_fn) => gfn_fn, | ||
| None => { return Err(GlifParserError::GlifFilenameInsane("Glif file name is directory".to_string())) } | ||
| }; | ||
|
|
||
| Ok(self.name_to_filename() == gfn_fn.to_str().ok_or(GlifParserError::GlifFilenameInsane("Glif file name has unknown encoding".to_string()))?) | ||
| } | ||
| None => Err(GlifParserError::GlifFilenameInsane("Glif file name is not set".to_string())) | ||
| } | ||
| } | ||
|
|
||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| use std::convert::TryInto; | ||
|
|
||
| use log::warn; | ||
|
|
||
| use super::Glif; | ||
| use crate::error::{GlifParserError::{self, GlifInputError}}; | ||
| use crate::component::GlifComponent; | ||
| use crate::outline::{self, get_outline_type, GlifContour, GlifOutline, OutlineType}; | ||
| use crate::point::{GlifPoint, PointData, parse_point_type}; | ||
| use crate::anchor::Anchor; | ||
|
|
||
| macro_rules! input_error { | ||
| ($str:expr) => { | ||
| GlifInputError($str.to_string()) | ||
| } | ||
| } | ||
|
|
||
| // From .glif XML, return a parse tree | ||
| /// Read UFO .glif XML to Glif struct | ||
| pub fn read_ufo_glif<PD: PointData>(glif: &str) -> Result<Glif<PD>, GlifParserError> { | ||
| let mut glif = xmltree::Element::parse(glif.as_bytes())?; | ||
|
|
||
| let mut ret = Glif::new(); | ||
|
|
||
| if glif.name != "glyph" { | ||
| return Err(input_error!("Root element not <glyph>")) | ||
| } | ||
|
|
||
| if glif.attributes.get("format").ok_or(input_error!("no format in <glyph>"))? != "2" { | ||
| return Err(input_error!("<glyph> format not 2")) | ||
| } | ||
|
|
||
| ret.name = glif | ||
| .attributes | ||
| .get("name") | ||
| .ok_or(input_error!("<glyph> has no name"))? | ||
| .clone(); | ||
| let advance = glif | ||
| .take_child("advance"); | ||
|
|
||
| ret.width = if let Some(a) = advance { | ||
| Some(a.attributes | ||
| .get("width") | ||
| .ok_or(input_error!("<advance> has no width"))? | ||
| .parse() | ||
| .or(Err(input_error!("<advance> width not int")))?) | ||
| } else { | ||
| None | ||
| }; | ||
|
|
||
| let mut unicodes = vec![]; | ||
| while let Some(u) = glif.take_child("unicode") { | ||
| let unicodehex = u | ||
| .attributes | ||
| .get("hex") | ||
| .ok_or(input_error!("<unicode> has no hex"))?; | ||
| unicodes.push( | ||
| char::from_u32( | ||
| u32::from_str_radix(unicodehex, 16) | ||
| .or(Err(input_error!("<unicode> hex not int")))? | ||
| ) | ||
| .ok_or(input_error!("<unicode> char conversion failed"))?, | ||
| ); | ||
| } | ||
|
|
||
| ret.unicode = unicodes; | ||
|
|
||
| let mut anchors: Vec<Anchor> = Vec::new(); | ||
|
|
||
| while let Some(anchor_el) = glif.take_child("anchor") { | ||
| let mut anchor = Anchor::new(); | ||
|
|
||
| anchor.x = anchor_el | ||
| .attributes | ||
| .get("x") | ||
| .ok_or(input_error!("<anchor> missing x"))? | ||
| .parse() | ||
| .or(Err(input_error!("<anchor> x not float")))?; | ||
| anchor.y = anchor_el | ||
| .attributes | ||
| .get("y") | ||
| .ok_or(input_error!("<anchor> missing y"))? | ||
| .parse() | ||
| .or(Err(input_error!("<anchor> y not float")))?; | ||
| anchor.class = anchor_el | ||
| .attributes | ||
| .get("name") | ||
| .ok_or(input_error!("<anchor> missing class"))? | ||
| .clone(); | ||
| anchors.push(anchor); | ||
| } | ||
|
|
||
| ret.anchors = anchors; | ||
|
|
||
| let mut goutline: GlifOutline = Vec::new(); | ||
|
|
||
| let outline_el = glif.take_child("outline"); | ||
|
|
||
| if outline_el.is_some() { | ||
| let mut outline_elu = outline_el.unwrap(); | ||
| while let Some(mut contour_el) = outline_elu.take_child("contour") { | ||
| let mut gcontour: GlifContour = Vec::new(); | ||
| while let Some(point_el) = contour_el.take_child("point") { | ||
| let mut gpoint = GlifPoint::new(); | ||
|
|
||
| gpoint.x = point_el | ||
| .attributes | ||
| .get("x") | ||
| .ok_or(input_error!("<point> missing x"))? | ||
| .parse() | ||
| .or(Err(input_error!("<point> x not float")))?; | ||
| gpoint.y = point_el | ||
| .attributes | ||
| .get("y") | ||
| .ok_or(input_error!("<point> missing y"))? | ||
| .parse() | ||
| .or(Err(input_error!("<point> y not float")))?; | ||
|
|
||
| match point_el.attributes.get("name") { | ||
| Some(p) => gpoint.name = Some(p.clone()), | ||
| None => {} | ||
| } | ||
|
|
||
| gpoint.ptype = | ||
| parse_point_type(point_el.attributes.get("type").as_ref().map(|s| s.as_str())); | ||
|
|
||
| gcontour.push(gpoint); | ||
| } | ||
| if gcontour.len() > 0 { | ||
| goutline.push(gcontour); | ||
| } | ||
| } | ||
|
|
||
| while let Some(component_el) = outline_elu.take_child("component") { | ||
| let mut gcomponent = GlifComponent::new(); | ||
| component_el.attributes.get("xScale").map(|e|{ gcomponent.xScale = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("xyScale").map(|e|{ gcomponent.xyScale = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("yxScale").map(|e|{ gcomponent.yxScale = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("yScale").map(|e|{ gcomponent.yScale = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("xOffset").map(|e|{ gcomponent.xOffset = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("yOffset").map(|e|{ gcomponent.yOffset = e.as_str().try_into().unwrap(); }); | ||
| component_el.attributes.get("identifier").map(|e|{ gcomponent.identifier = Some(e.clone()); }); | ||
| gcomponent.base = component_el.attributes.get("base").ok_or(input_error!("<component> missing base"))?.clone(); | ||
| ret.components.push(gcomponent); | ||
| } | ||
| } | ||
|
|
||
| if let Some(lib) = glif.take_child("lib") { | ||
| ret.lib = Some(lib); | ||
| } | ||
|
|
||
| // This will read the first XML comment understandable as itself containing XML. | ||
| for child in &glif.children { | ||
| if let xmltree::XMLNode::Comment(c) = child { | ||
| let tree = xmltree::Element::parse(c.as_bytes()); | ||
| match tree { | ||
| Ok(plib) => { | ||
| ret.private_lib = Some(plib); | ||
| break | ||
| }, | ||
| Err(_) => { | ||
| warn!("Private dictionary found but unreadable"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ret.order = get_outline_type(&goutline); | ||
|
|
||
| let outline = match ret.order { | ||
| OutlineType::Cubic => outline::create::cubic_outline(&goutline), | ||
| OutlineType::Quadratic => outline::create::quadratic_outline(&goutline), | ||
| OutlineType::Spiro => Err(input_error!("Spiro as yet unimplemented"))?, | ||
| }; | ||
|
|
||
| if outline.len() > 0 { | ||
| ret.outline = Some(outline); | ||
| } | ||
|
|
||
| Ok(ret) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| use xmltree; | ||
|
|
||
| use super::Glif; | ||
| use crate::error::GlifParserError; | ||
| use crate::point::{Handle, PointData, PointType, point_type_to_string}; | ||
| use crate::codepoint::Codepoint; | ||
|
|
||
| fn build_ufo_point_from_handle(handle: Handle) -> Option<xmltree::Element> | ||
| { | ||
| match handle { | ||
| Handle::At(x, y) => { | ||
| let mut glyph = xmltree::Element::new("point"); | ||
| glyph.attributes.insert("x".to_owned(), x.to_string()); | ||
| glyph.attributes.insert("y".to_owned(), y.to_string()); | ||
| return Some(glyph); | ||
| }, | ||
| _ => {} | ||
| } | ||
|
|
||
| None | ||
| } | ||
|
|
||
| /// Write Glif struct to UFO .glif XML | ||
| pub fn write_ufo_glif<PD: PointData>(glif: &Glif<PD>) -> Result<String, GlifParserError> | ||
| { | ||
| let mut glyph = xmltree::Element::new("glyph"); | ||
| glyph.attributes.insert("name".to_owned(), glif.name.to_string()); | ||
| glyph.attributes.insert("format".to_owned(), glif.format.to_string()); | ||
|
|
||
| match glif.width { | ||
| Some(w) => { | ||
| let mut advanceel = xmltree::Element::new("advance"); | ||
| advanceel.attributes.insert("width".to_owned(), w.to_string()); | ||
| glyph.children.push(xmltree::XMLNode::Element(advanceel)); | ||
| }, | ||
| None => {} | ||
| }; | ||
|
|
||
| for hex in glif.unicode.iter() { | ||
| let mut unicode = xmltree::Element::new("unicode"); | ||
| unicode.attributes.insert("hex".to_owned(), (hex as &dyn Codepoint).display()); | ||
| glyph.children.push(xmltree::XMLNode::Element(unicode)); | ||
| } | ||
|
|
||
| for anchor in glif.anchors.iter() { | ||
| let mut anchor_node = xmltree::Element::new("anchor"); | ||
| anchor_node.attributes.insert("x".to_owned(), anchor.x.to_string()); | ||
| anchor_node.attributes.insert("y".to_owned(), anchor.y.to_string()); | ||
| anchor_node.attributes.insert("name".to_owned(), anchor.class.to_string()); | ||
| glyph.children.push(xmltree::XMLNode::Element(anchor_node)); | ||
| } | ||
|
|
||
| let mut outline_node = xmltree::Element::new("outline"); | ||
| match &glif.outline | ||
| { | ||
| Some(outline) => { | ||
| for contour in outline { | ||
| // if we find a move point at the start of things we set this to false | ||
| let open_contour = contour.first().unwrap().ptype == PointType::Move; | ||
| let mut contour_node = xmltree::Element::new("contour"); | ||
|
|
||
| let mut last_point = None; | ||
| for point in contour { | ||
| if let Some(_lp) = last_point { | ||
| // if there was a point prior to this one we emit our b handle | ||
| if let Some(handle_node) = build_ufo_point_from_handle(point.b) { | ||
| contour_node.children.push(xmltree::XMLNode::Element(handle_node)); | ||
| } | ||
| } | ||
|
|
||
| let mut point_node = xmltree::Element::new("point"); | ||
| point_node.attributes.insert("x".to_owned(), point.x.to_string()); | ||
| point_node.attributes.insert("y".to_owned(), point.y.to_string()); | ||
|
|
||
| match point_type_to_string(point.ptype) { | ||
| Some(ptype_string) => {point_node.attributes.insert("type".to_owned(), ptype_string);}, | ||
| None => {} | ||
| } | ||
|
|
||
| match &point.name { | ||
| Some(name) => {point_node.attributes.insert("name".to_owned(), name.to_string());}, | ||
| None => {} | ||
| } | ||
|
|
||
| // Point>T> does not contain fields for smooth, or identifier. | ||
| contour_node.children.push(xmltree::XMLNode::Element(point_node)); | ||
| match point.ptype { | ||
| PointType::Line | PointType::Curve | PointType::Move => { | ||
| if let Some(handle_node) = build_ufo_point_from_handle(point.a) { | ||
| contour_node.children.push(xmltree::XMLNode::Element(handle_node)); | ||
| } | ||
| }, | ||
| PointType::QCurve => { | ||
| //QCurve currently unhandled. This needs to be implemented. | ||
| }, | ||
| _ => { } // I don't think this should be reachable in a well formed Glif object? | ||
| } | ||
|
|
||
| last_point = Some(point); | ||
| } | ||
|
|
||
| // if a move wasn't our first point then we gotta close the shape by emitting the first point's b handle | ||
| if !open_contour { | ||
| if let Some(handle_node) = build_ufo_point_from_handle(contour.first().unwrap().b) { | ||
| contour_node.children.push(xmltree::XMLNode::Element(handle_node)); | ||
| } | ||
| } | ||
|
|
||
| outline_node.children.push(xmltree::XMLNode::Element(contour_node)); | ||
| } | ||
|
|
||
| }, | ||
| None => {} | ||
| } | ||
|
|
||
| for component in &glif.components { | ||
| let mut component_node = xmltree::Element::new("component"); | ||
| component_node.attributes.insert("base".to_string(), component.base.clone()); | ||
| match component.identifier { | ||
| Some(ref id) => {component_node.attributes.insert("identifier".to_string(), id.clone());}, | ||
| None => {} | ||
| } | ||
| component_node.attributes.insert("xScale".to_string(), component.xScale.to_string()); | ||
| component_node.attributes.insert("xyScale".to_string(), component.xyScale.to_string()); | ||
| component_node.attributes.insert("yxScale".to_string(), component.yxScale.to_string()); | ||
| component_node.attributes.insert("yScale".to_string(), component.yScale.to_string()); | ||
| component_node.attributes.insert("xOffset".to_string(), component.xOffset.to_string()); | ||
| component_node.attributes.insert("yOffset".to_string(), component.yOffset.to_string()); | ||
| outline_node.children.push(xmltree::XMLNode::Element(component_node)); | ||
| } | ||
|
|
||
| glyph.children.push(xmltree::XMLNode::Element(outline_node)); | ||
|
|
||
| match &glif.lib { | ||
| Some(lib_node) => { | ||
| glyph.children.push(xmltree::XMLNode::Element(lib_node.clone())); | ||
| } | ||
| None => {} | ||
| } | ||
|
|
||
| let config = xmltree::EmitterConfig::new().perform_indent(false).write_document_declaration(false); | ||
|
|
||
| match &glif.private_lib { | ||
| Some(lib_node) => { | ||
| let mut private_xml = Vec::new(); | ||
| lib_node.write_with_config(&mut private_xml, config)?; | ||
| let private_xml = String::from_utf8(private_xml)?; | ||
| glyph.children.push(xmltree::XMLNode::Comment(private_xml)); | ||
| }, | ||
| None => {} | ||
| } | ||
|
|
||
| let config = xmltree::EmitterConfig::new().perform_indent(true); | ||
|
|
||
| let mut ret_string: Vec<u8> = Vec::new(); | ||
| glyph.write_with_config(&mut ret_string, config)?; | ||
|
|
||
| return Ok(String::from_utf8(ret_string)?); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| pub mod create; | ||
|
|
||
| use log::info; | ||
|
|
||
| use crate::point::{GlifPoint, Point, PointType}; | ||
|
|
||
| pub type Contour<PD> = Vec<Point<PD>>; | ||
| pub type Outline<PD> = Vec<Contour<PD>>; | ||
|
|
||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||
| pub enum OutlineType { | ||
| Cubic, | ||
| Quadratic, | ||
| // As yet unimplemented. | ||
| // Will be in <lib> with cubic Bezier equivalents in <outline>. | ||
| Spiro, | ||
| } | ||
|
|
||
| pub type GlifContour = Vec<GlifPoint>; | ||
| pub type GlifOutline = Vec<GlifContour>; | ||
|
|
||
| pub fn get_outline_type(goutline: &GlifOutline) -> OutlineType { | ||
| for gc in goutline.iter() { | ||
| for gp in gc.iter() { | ||
| match gp.ptype { | ||
| PointType::Curve => return OutlineType::Cubic, | ||
| PointType::QCurve => return OutlineType::Quadratic, | ||
| _ => {} | ||
| } | ||
| } | ||
| } | ||
| info!("Defaulting outline with only lines or unrecognized points to cubic"); | ||
| OutlineType::Cubic // path has no off-curve point, only lines | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| use std::collections::VecDeque; | ||
|
|
||
| use super::{Outline, GlifOutline, Contour}; | ||
| use crate::point::{Point, Handle, PointData, PointType, GlifPoint}; | ||
|
|
||
| use log::warn; | ||
|
|
||
| fn midpoint(x1: f32, x2: f32, y1: f32, y2: f32) -> (f32, f32) { | ||
| ((x1 + x2) / 2., (y1 + y2) / 2.) | ||
| } | ||
|
|
||
| // UFO uses the same compact format as TTF, so we need to expand it. | ||
| pub fn quadratic_outline<PD: PointData>(goutline: &GlifOutline) -> Outline<PD> { | ||
| let mut outline: Outline<PD> = Vec::new(); | ||
|
|
||
| let mut temp_outline: VecDeque<VecDeque<GlifPoint>> = VecDeque::new(); | ||
|
|
||
| let mut stack: VecDeque<&GlifPoint> = VecDeque::new(); | ||
|
|
||
| for gc in goutline.iter() { | ||
| let mut temp_contour = VecDeque::new(); | ||
|
|
||
| for gp in gc.iter() { | ||
| match gp.ptype { | ||
| PointType::OffCurve => { | ||
| stack.push_back(&gp); | ||
| } | ||
| _ => {} | ||
| } | ||
|
|
||
| if stack.len() == 2 { | ||
| let h1 = stack.pop_front().unwrap(); | ||
| let h2 = stack.pop_front().unwrap(); | ||
| let mp = midpoint(h1.x, h2.x, h1.y, h2.y); | ||
|
|
||
| temp_contour.push_back(h1.clone()); | ||
| temp_contour.push_back(GlifPoint { | ||
| x: mp.0, | ||
| y: mp.1, | ||
| ptype: PointType::QCurve, | ||
| smooth: true, | ||
| name: gp.name.clone(), | ||
| }); | ||
| stack.push_back(h2); | ||
| } else if gp.ptype != PointType::OffCurve { | ||
| let h1 = stack.pop_front(); | ||
| match h1 { | ||
| Some(h) => temp_contour.push_back(h.clone()), | ||
| _ => {} | ||
| } | ||
| temp_contour.push_back(gp.clone()); | ||
| } | ||
| } | ||
| if let (Some(h1), Some(h2)) = (stack.pop_front(), temp_contour.get(0)) { | ||
| let mp = midpoint(h1.x, h2.x, h1.y, h2.y); | ||
| let (t, tx, ty) = (h2.ptype, h2.x, h2.y); | ||
| temp_contour.push_back(h1.clone()); | ||
| if t == PointType::OffCurve { | ||
| temp_contour.push_back(GlifPoint { | ||
| x: mp.0, | ||
| y: mp.1, | ||
| ptype: PointType::QCurve, | ||
| smooth: true, | ||
| name: None, | ||
| }); | ||
| } else { | ||
| temp_contour.push_back(GlifPoint { | ||
| x: tx, | ||
| y: ty, | ||
| ptype: PointType::QClose, | ||
| smooth: true, | ||
| name: None, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| temp_outline.push_back(temp_contour); | ||
| assert_eq!(stack.len(), 0); | ||
| } | ||
|
|
||
| for gc in temp_outline.iter() { | ||
| let mut contour: Contour<PD> = Vec::new(); | ||
|
|
||
| for gp in gc.iter() { | ||
| match gp.ptype { | ||
| PointType::OffCurve => { | ||
| stack.push_back(&gp); | ||
| } | ||
| _ => { | ||
| assert!(stack.len() < 2); | ||
| let h1 = stack.pop_front(); | ||
|
|
||
| if let Some(_) = h1 { | ||
| contour.last_mut().map(|p| p.a = Handle::from(h1)); | ||
| } | ||
|
|
||
| contour.push(Point { | ||
| x: gp.x, | ||
| y: gp.y, | ||
| a: Handle::Colocated, | ||
| b: Handle::Colocated, | ||
| name: gp.name.clone(), | ||
| ptype: gp.ptype, | ||
| data: None, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if contour.len() > 0 { | ||
| outline.push(contour); | ||
| } else { | ||
| warn!("Dropped empty contour. Lone `move` point in .glif? GlifContour: {:?}", &gc); | ||
| } | ||
| } | ||
|
|
||
| outline | ||
| } | ||
|
|
||
| // Stack based outline builder. Push all offcurve points onto the stack, pop them when we see an on | ||
| // curve point. For each point, we add one handle to the current point, and one to the last. We | ||
| // then connect the last point to the first to make the loop, (if path is closed). | ||
| pub fn cubic_outline<PD: PointData>(goutline: &GlifOutline) -> Outline<PD> { | ||
| let mut outline: Outline<PD> = Vec::new(); | ||
|
|
||
| let mut stack: VecDeque<&GlifPoint> = VecDeque::new(); | ||
|
|
||
| for gc in goutline.iter() { | ||
| let mut contour: Contour<PD> = Vec::new(); | ||
|
|
||
| for gp in gc.iter() { | ||
| match gp.ptype { | ||
| PointType::OffCurve => { | ||
| stack.push_back(&gp); | ||
| } | ||
| PointType::Line | PointType::Move | PointType::Curve => { | ||
| let h1 = stack.pop_front(); | ||
| let h2 = stack.pop_front(); | ||
|
|
||
| contour.last_mut().map(|p| p.a = Handle::from(h1)); | ||
|
|
||
| contour.push(Point { | ||
| x: gp.x, | ||
| y: gp.y, | ||
| a: Handle::Colocated, | ||
| b: Handle::from(h2), | ||
| name: gp.name.clone(), | ||
| ptype: gp.ptype, | ||
| data: None, | ||
| }); | ||
| } | ||
| PointType::QCurve => { | ||
| unreachable!("Quadratic point in cubic glyph! File is corrupt.") | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
||
| let h1 = stack.pop_front(); | ||
| let h2 = stack.pop_front(); | ||
|
|
||
| contour.last_mut().map(|p| p.a = Handle::from(h1)); | ||
|
|
||
| if contour.len() > 0 && contour[0].ptype != PointType::Move { | ||
| contour.first_mut().map(|p| p.b = Handle::from(h2)); | ||
| } | ||
|
|
||
| if contour.len() == 1 && contour.first().unwrap().ptype == PointType::Move { | ||
| warn!("Dropped empty contour. Lone `move` point in .glif? GlifContour: {:?}", &gc); | ||
| } | ||
| else if contour.len() > 0 { | ||
| outline.push(contour); | ||
| } | ||
| } | ||
|
|
||
| outline | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| use std::fmt::Debug; | ||
|
|
||
| /// A "close to the source" .glif `<point>` | ||
| #[derive(Clone, Debug, PartialEq)] | ||
| pub struct GlifPoint { | ||
| pub x: f32, | ||
| pub y: f32, | ||
| pub smooth: bool, | ||
| pub name: Option<String>, | ||
| pub ptype: PointType, | ||
| } | ||
|
|
||
| impl GlifPoint { | ||
| pub fn new() -> GlifPoint { | ||
| GlifPoint { | ||
| x: 0., | ||
| y: 0., | ||
| ptype: PointType::Undefined, | ||
| smooth: false, | ||
| name: None, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[derive(Debug, Copy, Clone, PartialEq)] | ||
| pub enum PointType { | ||
| Undefined, | ||
| Move, | ||
| Curve, | ||
| QCurve, | ||
| QClose, | ||
| Line, | ||
| OffCurve, | ||
| } // Undefined used by new(), shouldn't appear in Point<PointData> structs | ||
|
|
||
| /// A handle on a point | ||
| #[derive(Debug, Copy, Clone, PartialEq)] | ||
| pub enum Handle { | ||
| Colocated, | ||
| At(f32, f32), | ||
| } | ||
|
|
||
| impl From<Option<&GlifPoint>> for Handle { | ||
| fn from(point: Option<&GlifPoint>) -> Handle { | ||
| match point { | ||
| Some(p) => Handle::At(p.x, p.y), | ||
| None => Handle::Colocated, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // The nightly feature is superior because it means people don't need to write e.g. | ||
| // `impl PointData for TheirPointDataType {}` | ||
| /// API consumers may put any clonable type as an associated type to Glif, which will appear along | ||
| /// with each Point. You could use this to implement, e.g., hyperbeziers. The Glif Point's would | ||
| /// still represent a Bézier curve, but you could put hyperbezier info along with the Point. | ||
| pub trait PointData = Clone + Debug; | ||
|
|
||
| /// A Skia-friendly point | ||
| #[derive(Debug, Clone, PartialEq)] | ||
| pub struct Point<PD> { | ||
| pub x: f32, | ||
| pub y: f32, | ||
| pub a: Handle, | ||
| pub b: Handle, | ||
| pub name: Option<String>, | ||
| pub ptype: PointType, | ||
| pub data: Option<PD>, | ||
| } | ||
|
|
||
| /// For use by ``Point::handle_or_colocated`` | ||
| #[derive(Debug, Copy, Clone, PartialEq)] | ||
| pub enum WhichHandle { | ||
| Neither, | ||
| A, | ||
| B, | ||
| } | ||
|
|
||
| impl<PD: PointData> Point<PD> { | ||
| pub fn new() -> Point<PD> { | ||
| Point { | ||
| x: 0., | ||
| y: 0., | ||
| a: Handle::Colocated, | ||
| b: Handle::Colocated, | ||
| ptype: PointType::Undefined, | ||
| name: None, | ||
| data: None, | ||
| } | ||
| } | ||
|
|
||
| /// Make a point from its x and y position and type | ||
| pub fn from_x_y_type(at: (f32, f32), ptype: PointType) -> Point<PD> { | ||
| Point { | ||
| x: at.0, | ||
| y: at.1, | ||
| a: Handle::Colocated, | ||
| b: Handle::Colocated, | ||
| ptype: ptype, | ||
| name: None, | ||
| data: None, | ||
| } | ||
| } | ||
|
|
||
| /// Return an x, y position for a point, or one of its handles. If called with | ||
| /// WhichHandle::Neither, return position for point. | ||
| pub fn handle_or_colocated( | ||
| &self, | ||
| which: WhichHandle, | ||
| transform_x: fn(f32) -> f32, | ||
| transform_y: fn(f32) -> f32, | ||
| ) -> (f32, f32) { | ||
| let handle = match which { | ||
| WhichHandle::A => self.a, | ||
| WhichHandle::B => self.b, | ||
| WhichHandle::Neither => Handle::Colocated, | ||
| }; | ||
| match handle { | ||
| Handle::At(x, y) => (transform_x(x), transform_y(y)), | ||
| Handle::Colocated => (transform_x(self.x), transform_y(self.y)), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn parse_point_type(pt: Option<&str>) -> PointType { | ||
| match pt { | ||
| Some("move") => PointType::Move, | ||
| Some("line") => PointType::Line, | ||
| Some("qcurve") => PointType::QCurve, | ||
| Some("curve") => PointType::Curve, | ||
| _ => PointType::OffCurve, | ||
| } | ||
| } | ||
|
|
||
| pub fn point_type_to_string(ptype: PointType) -> Option<String> | ||
| { | ||
| return match ptype{ | ||
| PointType::Undefined => None, | ||
| PointType::OffCurve => None, | ||
| PointType::QClose => None, // should probably be removed from PointType | ||
| PointType::Move => Some(String::from("move")), | ||
| PointType::Curve => Some(String::from("curve")), | ||
| PointType::QCurve => Some(String::from("qcurve")), | ||
| PointType::Line => Some(String::from("line")), | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <glyph name="acute" format="2"> | ||
| <advance width="547" /> | ||
| <unicode hex="b4" /> | ||
| <outline> | ||
| <component yxScale="0" xyScale="0" yScale="1" base="grave" xScale="-1" yOffset="0" xOffset="496.285" /> | ||
| </outline> | ||
| </glyph> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <glyph format="2" name="gershayim"> | ||
| <advance width="547" /> | ||
| <unicode hex="5f4" /> | ||
| <outline> | ||
| <component yScale="1" xOffset="95" base="acute" xyScale="0" xScale="1" yOffset="-145" yxScale="0" /> | ||
| <component yScale="1" yOffset="-145" xyScale="0" xScale="1" base="acute" yxScale="0" xOffset="-75" /> | ||
| </outline> | ||
| </glyph> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <glyph format="2" name="grave"> | ||
| <advance width="547" /> | ||
| <unicode hex="60" /> | ||
| <outline> | ||
| <contour> | ||
| <point x="330.54276" type="curve" y="488.85776" /> | ||
| <point x="295.44174" y="531.4338" /> | ||
| <point x="260.34073" y="574.00977" /> | ||
| <point x="225.23975" y="616.58575" type="curve" /> | ||
| <point y="623.6999" x="219.37463" /> | ||
| <point x="199.74713" y="625.1888" /> | ||
| <point y="619.04083" x="192.87599" type="curve" /> | ||
| <point x="187.11832" y="613.88916" /> | ||
| <point x="181.36066" y="608.7375" /> | ||
| <point y="603.5858" x="175.603" type="curve" /> | ||
| <point x="168.25041" y="597.0071" /> | ||
| <point y="576.89" x="168.13324" /> | ||
| <point x="175.40869" type="curve" y="570.2261" /> | ||
| <point y="532.80176" x="216.26736" /> | ||
| <point y="495.3774" x="257.12604" /> | ||
| <point type="curve" y="457.9531" x="297.9847" /> | ||
| <point x="318.56238" y="439.10504" /> | ||
| <point x="348.29373" y="467.32657" /> | ||
| </contour> | ||
| <contour> | ||
| <point type="curve" x="328.3793" y="491.13693" /> | ||
| <point y="528.5613" x="287.52063" /> | ||
| <point y="565.9856" x="246.66197" /> | ||
| <point x="205.80331" y="603.4099" type="curve" /> | ||
| <point y="610.07385" x="198.52785" /> | ||
| <point x="198.25642" y="563.47144" /> | ||
| <point y="570.0502" type="curve" x="205.60901" /> | ||
| <point x="211.36668" y="575.20184" /> | ||
| <point y="580.3535" x="217.12434" /> | ||
| <point y="585.5052" type="curve" x="222.882" /> | ||
| <point x="229.75316" y="591.65314" /> | ||
| <point y="595.0744" x="184.65315" /> | ||
| <point x="190.51825" y="587.96027" type="curve" /> | ||
| <point x="225.61926" y="545.3843" /> | ||
| <point y="502.80826" x="260.72028" /> | ||
| <point y="460.23227" type="curve" x="295.82126" /> | ||
| <point y="460.23227" x="295.82126" /> | ||
| <point x="328.3793" y="491.13693" /> | ||
| </contour> | ||
| </outline> | ||
| </glyph> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| use std::fs; | ||
| use glifparser; | ||
| //use trees; | ||
|
|
||
| #[test] | ||
| fn test_components() { | ||
| let gliffn = "test_data/TT2020Base.ufo/glyphs/gershayim.glif"; | ||
| let glifxml = fs::read_to_string(gliffn).unwrap(); | ||
| let mut glif: glifparser::Glif<()> = glifparser::glif::read(&glifxml).unwrap(); | ||
| glif.filename = Some(gliffn.into()); | ||
| let sanity = glif.filename_is_sane(); | ||
| assert!(sanity.is_ok() && sanity.unwrap()); | ||
| assert!(glif.components.len() == 2); | ||
| //assert!(&glif.components[0].base == "acute"); | ||
| /*let forest: Result<trees::Forest<glifparser::Component<()>>, _> = (&glif).into(); | ||
| match forest { | ||
| Ok(f) => eprintln!("{:?}", f), | ||
| Err(e) => eprintln!("{}", e) | ||
| }*/ | ||
| let flattened = glif.flatten().unwrap(); | ||
| let flatxml = glifparser::glif::write(&flattened).unwrap(); | ||
| assert!(flatxml.len() > 0); | ||
| //fs::write("/tmp/out.glif", flatxml); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| use xmltree; | ||
| use glifparser; | ||
|
|
||
| #[test] | ||
| fn test_roundtrip_mfek_private_lib() { | ||
| let mut glif: glifparser::Glif<()> = glifparser::Glif::new(); | ||
| let mut root = xmltree::Element::new("MFEK"); | ||
| let mut el = xmltree::Element::new("test"); | ||
| el.attributes.insert("equals".to_string(), "3<>-<>".to_string()); | ||
| root.children.push(xmltree::XMLNode::Element(el)); | ||
| glif.private_lib = Some(root); | ||
| let xml = glifparser::glif::write(&glif).unwrap(); | ||
| eprintln!("{}",&xml); | ||
| let glif2: glifparser::Glif<()> = glifparser::glif::read(&xml).unwrap(); | ||
| let xml2 = glifparser::glif::write(&glif2).unwrap(); | ||
| assert_eq!(glif, glif2); | ||
| assert_eq!(xmltree::Element::parse(xml.as_bytes()).unwrap(), xmltree::Element::parse(xml2.as_bytes()).unwrap()); | ||
| } |