Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deduplicate codepoints on read and write #268

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions src/glyph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod serialize;
#[cfg(test)]
mod tests;

use std::collections::HashSet;
use std::path::{Path, PathBuf};

#[cfg(feature = "kurbo")]
Expand Down Expand Up @@ -38,7 +39,7 @@ pub struct Glyph {
/// A collection of glyph Unicode code points.
///
/// The first entry defines the primary Unicode value for this glyph.
pub codepoints: Vec<char>,
pub codepoints: Codepoints,
/// Arbitrary glyph note.
pub note: Option<String>,
/// A collection of glyph guidelines.
Expand Down Expand Up @@ -115,7 +116,7 @@ impl Glyph {
name,
height: 0.0,
width: 0.0,
codepoints: Vec::new(),
codepoints: Codepoints::new(),
note: None,
guidelines: Vec::new(),
anchors: Vec::new(),
Expand Down Expand Up @@ -256,6 +257,72 @@ impl Data for Glyph {
}
}

/// A collection of codepoints assigned to a glyph.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Codepoints {
codepoints: Vec<char>,
}

impl Codepoints {
pub fn new() -> Self {
Self::default()
}

pub fn add(&mut self, codepoint: char) {
if !self.codepoints.contains(&codepoint) {
self.codepoints.push(codepoint);
}
}

pub fn remove(&mut self, codepoint: char) {
self.codepoints.retain(|&c| c != codepoint);
}

pub fn clear(&mut self) {
self.codepoints.clear()
}

pub fn len(&self) -> usize {
self.codepoints.len()
}

pub fn as_slice(&self) -> &[char] {
self.codepoints.as_slice()
}
}

impl IntoIterator for Codepoints {
type Item = char;
type IntoIter = std::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
self.codepoints.into_iter()
}
}

impl<'a> IntoIterator for &'a Codepoints {
type Item = char;
type IntoIter = std::iter::Cloned<std::slice::Iter<'a, char>>;

fn into_iter(self) -> Self::IntoIter {
self.codepoints.iter().cloned()
}
}

impl From<Vec<char>> for Codepoints {
fn from(chars: Vec<char>) -> Self {
// Deduplicate code points while preserving order.
let mut codepoints = Vec::new();
let mut seen_codepoints = HashSet::new();
for codepoint in chars {
if seen_codepoints.insert(codepoint) {
codepoints.push(codepoint);
}
}
Self { codepoints }
}
}

/// A reference position in a glyph, such as for attaching accents.
///
/// See the [Anchor section] of the UFO spec for more information.
Expand Down
3 changes: 2 additions & 1 deletion src/glyph/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ impl<'names> GlifParser<'names> {
}

self.glyph.load_object_libs()?;

Ok(self.glyph)
}

Expand Down Expand Up @@ -409,7 +410,7 @@ impl<'names> GlifParser<'names> {
.map_err(|_| value.to_string())
.and_then(|n| char::try_from(n).map_err(|_| value.to_string()))
.map_err(|_| ErrorKind::BadHexValue)?;
self.glyph.codepoints.push(chr);
self.glyph.codepoints.add(chr);
}
_other => return Err(ErrorKind::UnexpectedAttribute.into()),
}
Expand Down
2 changes: 1 addition & 1 deletion src/glyph/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ impl Glyph {
writer.write_event(Event::Start(start)).map_err(GlifWriteError::Xml)?;

for codepoint in &self.codepoints {
writer.write_event(char_to_event(*codepoint)).map_err(GlifWriteError::Xml)?;
writer.write_event(char_to_event(codepoint)).map_err(GlifWriteError::Xml)?;
}

// Skip serializing advance if both values are zero, infinite, subnormal, or NaN.
Expand Down
27 changes: 27 additions & 0 deletions src/glyph/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -868,3 +868,30 @@ fn has_component_with_base() {
assert!(glyph.has_component_with_base("dieresis"));
assert!(!glyph.has_component_with_base("Z"));
}

#[test]
fn deduplicate_unicodes() {
let data = r#"
<?xml version="1.0" encoding="UTF-8"?>
<glyph name="period" format="2">
<unicode hex="0065"/>
<unicode hex="0066"/>
<unicode hex="0065"/>
<unicode hex="0067"/>
</glyph>
"#;
let mut glyph = parse_glyph(data.as_bytes()).unwrap();
assert_eq!(glyph.codepoints, vec!['e', 'f', 'g'].into());

glyph.codepoints = vec!['e', 'f', 'e', 'g'].into();
let data2 = glyph.encode_xml().unwrap();
let data2 = std::str::from_utf8(&data2).unwrap();
let data2_expected = r#"<?xml version="1.0" encoding="UTF-8"?>
<glyph name="period" format="2">
<unicode hex="0065"/>
<unicode hex="0066"/>
<unicode hex="0067"/>
</glyph>
"#;
assert_eq!(data2, data2_expected);
}
2 changes: 1 addition & 1 deletion src/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ mod tests {
let glyph = layer.get_glyph("A").expect("failed to load glyph 'A'");
assert_eq!(glyph.height, 0.);
assert_eq!(glyph.width, 1190.);
assert_eq!(glyph.codepoints, vec!['A']);
assert_eq!(glyph.codepoints, vec!['A'].into());
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions tests/save.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn save_default() {
fn save_new_file() {
let mut my_ufo = Font::new();
let mut my_glyph = Glyph::new("A");
my_glyph.codepoints = vec!['A'];
my_glyph.codepoints = vec!['A'].into();
my_glyph.note = Some("I did a glyph!".into());
let mut plist = Plist::new();
plist.insert("my-cool-key".into(), plist::Value::Integer(420_u32.into()));
Expand All @@ -42,7 +42,7 @@ fn save_new_file() {
let loaded = Font::load(dir).unwrap();
assert!(loaded.default_layer().get_glyph("A").is_some());
let glyph = loaded.default_layer().get_glyph("A").unwrap();
assert_eq!(glyph.codepoints, vec!['A']);
assert_eq!(glyph.codepoints, vec!['A'].into());
let lib_val = glyph.lib.get("my-cool-key").and_then(|val| val.as_unsigned_integer());
assert_eq!(lib_val, Some(420));
}
Expand Down