87 changes: 87 additions & 0 deletions src/glif.rs
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()))
}
}

}

181 changes: 181 additions & 0 deletions src/glif/read.rs
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)
}
159 changes: 159 additions & 0 deletions src/glif/write.rs
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)?);
}
693 changes: 17 additions & 676 deletions src/lib.rs

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions src/outline.rs
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
}
177 changes: 177 additions & 0 deletions src/outline/create.rs
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
}
146 changes: 146 additions & 0 deletions src/point.rs
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")),
}
}
8 changes: 8 additions & 0 deletions test_data/TT2020Base.ufo/glyphs/acute.glif
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>
9 changes: 9 additions & 0 deletions test_data/TT2020Base.ufo/glyphs/gershayim.glif
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>
47 changes: 47 additions & 0 deletions test_data/TT2020Base.ufo/glyphs/grave.glif
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>
24 changes: 24 additions & 0 deletions tests/components.rs
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);
}
18 changes: 18 additions & 0 deletions tests/private_lib.rs
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());
}