use crate::args::ExistingGlyphHandling;
use babelfont::{Font, SmolStr};
use indexmap::{IndexMap, IndexSet};
use std::collections::HashMap;
pub struct GlyphsetFilter {
pub incoming_glyphset: IndexSet<SmolStr>,
pub existing_glyphs: IndexSet<SmolStr>,
mappings_to_delete: IndexMap<SmolStr, Vec<char>>,
pub existing_glyph_handling: ExistingGlyphHandling,
}
impl GlyphsetFilter {
pub fn new(
include_glyphs: Vec<SmolStr>,
exclude_glyphs: Vec<SmolStr>,
include_codepoints: Vec<char>,
font1: &mut babelfont::Font,
font2: &babelfont::Font,
existing_glyph_handling: ExistingGlyphHandling,
) -> Self {
let mut blacklist: IndexSet<SmolStr> = exclude_glyphs.iter().cloned().collect();
let mut existing_map = IndexMap::new();
let existing_glyphs = font1
.glyphs
.iter()
.map(|g| g.name.clone())
.collect::<IndexSet<SmolStr>>();
let mut mappings_to_delete: IndexMap<SmolStr, Vec<char>> = IndexMap::new();
for glyph in font1.glyphs.iter() {
for cp in glyph.codepoints.iter().flat_map(|cp| char::from_u32(*cp)) {
existing_map.insert(cp, glyph.name.clone());
}
}
let include_glyphs = if include_glyphs.is_empty() && include_codepoints.is_empty() {
log::info!(
"No include glyphs or codepoints specified, including all glyphs from font 2"
);
font2.glyphs.iter().map(|g| g.name.clone()).collect()
} else {
include_glyphs
};
let mut incoming_glyphset = IndexSet::new();
for glyph_name in include_glyphs.iter() {
incoming_glyphset.insert(glyph_name.clone());
}
for glyph in font2.glyphs.iter() {
if blacklist.contains(&glyph.name) {
continue;
}
for cp in glyph.codepoints.iter().flat_map(|cp| char::from_u32(*cp)) {
if include_codepoints.contains(&cp) {
incoming_glyphset.insert(glyph.name.clone());
}
if !incoming_glyphset.contains(&glyph.name) {
continue;
}
if existing_map.contains_key(&cp) {
if existing_glyph_handling == ExistingGlyphHandling::Skip {
log::info!(
"Skipping glyph '{}' for codepoint U+{:04X} as it exists in font 1",
glyph.name,
cp as u32
);
blacklist.insert(glyph.name.clone());
continue;
} else if existing_glyph_handling == ExistingGlyphHandling::Replace {
mappings_to_delete
.entry(glyph.name.clone())
.or_default()
.push(cp);
}
}
}
}
for glyph_name in blacklist.iter() {
incoming_glyphset.shift_remove(glyph_name);
}
GlyphsetFilter {
incoming_glyphset,
existing_glyphs,
mappings_to_delete,
existing_glyph_handling,
}
}
pub fn new_from_glyphs(
include_glyphs: Vec<SmolStr>,
font1: &mut babelfont::Font,
font2: &babelfont::Font,
existing_glyph_handling: ExistingGlyphHandling,
) -> Self {
GlyphsetFilter::new(
include_glyphs,
vec![],
vec![],
font1,
font2,
existing_glyph_handling,
)
}
pub fn new_from_codepoints(
include_codepoints: Vec<char>,
font1: &mut babelfont::Font,
font2: &babelfont::Font,
existing_glyph_handling: ExistingGlyphHandling,
) -> Self {
GlyphsetFilter::new(
vec![],
vec![],
include_codepoints,
font1,
font2,
existing_glyph_handling,
)
}
pub(crate) fn de_encode(&self, font_1: &mut Font, font_2: &mut Font) {
let mut existing_codepoints: IndexSet<u32> = IndexSet::new();
for glyph in font_1.glyphs.iter_mut() {
if let Some(codepoints) = self.mappings_to_delete.get(&glyph.name) {
let codepoints_u32 = codepoints
.iter()
.map(|c| *c as u32)
.collect::<IndexSet<u32>>();
glyph.codepoints.retain(|cp| !codepoints_u32.contains(cp));
log::info!(
"De-encoded codepoints {:?} from glyph '{}'",
codepoints_u32,
glyph.name
);
}
existing_codepoints.extend(glyph.codepoints.iter());
}
for glyph in self.incoming_glyphset.iter() {
if let Some(glyph_in_font2) = font_2.glyphs.get_mut(glyph) {
glyph_in_font2
.codepoints
.retain(|cp| !existing_codepoints.contains(cp));
}
}
}
pub(crate) fn check_for_presence(&mut self, font_2: &Font) {
let font2_glyphs = font_2
.glyphs
.iter()
.map(|g| g.name.clone())
.collect::<IndexSet<SmolStr>>();
let not_there: IndexSet<SmolStr> = self
.incoming_glyphset
.difference(&font2_glyphs)
.cloned()
.collect();
if !not_there.is_empty() {
log::warn!(
"The following glyphs were selected for inclusion but are not present in font 2: {:?}",
not_there
);
for glyph_name in not_there {
self.incoming_glyphset.shift_remove(&glyph_name);
}
}
}
#[allow(dead_code)] fn policy(&self, _glyph_name: &str) -> ExistingGlyphHandling {
self.existing_glyph_handling
}
pub(crate) fn close_components(&mut self, font_2: &Font) {
for glyph in self.incoming_glyphset.clone().iter() {
self._close_components(glyph, font_2);
}
}
fn _close_components(&mut self, glyph_name: &str, font_2: &Font) {
let Some(glyph) = font_2.glyphs.get(glyph_name) else {
return;
};
let component_set = glyph
.layers
.iter()
.flat_map(|layer| layer.shapes.iter())
.filter_map(|shape| match shape {
babelfont::Shape::Component(comp) => Some(comp.reference.clone()),
_ => None,
})
.collect::<IndexSet<SmolStr>>();
if component_set.is_empty() {
return;
}
for component_name in component_set.iter() {
if self.incoming_glyphset.contains(component_name) {
continue;
}
if self.existing_glyphs.contains(component_name) {
if self.existing_glyph_handling == ExistingGlyphHandling::Replace {
log::info!(
"Replacing component glyph '{}' used in glyph '{}' already present in font 1",
component_name,
glyph_name
);
self.incoming_glyphset.insert(component_name.clone());
self._close_components(component_name, font_2);
} else {
log::warn!(
"Component glyph '{}' used in glyph '{}' is already present in font 1, not replacing it",
component_name,
glyph_name
);
}
} else {
log::info!(
"Adding component glyph '{}' used in glyph '{}' to incoming glyphset",
component_name,
glyph_name
);
self.incoming_glyphset.insert(component_name.clone());
self._close_components(component_name, font_2);
}
}
}
pub(crate) fn sort_glyphset(&mut self, font_2: &Font) {
let font2_glyphorder: HashMap<&str, usize> = font_2
.glyphs
.iter()
.enumerate()
.map(|(ix, g)| (g.name.as_str(), ix))
.collect();
self.incoming_glyphset.sort_by_key(|g| {
font2_glyphorder
.get(g.as_str())
.copied()
.unwrap_or(usize::MAX)
});
}
pub(crate) fn final_glyphset(&self) -> Vec<SmolStr> {
self.existing_glyphs
.union(&self.incoming_glyphset)
.cloned()
.collect()
}
}
#[allow(clippy::unwrap_used, clippy::expect_used)]
#[cfg(test)]
mod tests {
use crate::args::GlyphSelection;
use super::*;
#[test]
fn test_glyphset() {
let gs: GlyphSelection = GlyphSelection {
glyphs: vec![],
glyphs_file: None,
codepoints: None,
codepoints_file: Some("../babelfont/resources/codepoints.txt".to_string()),
exclude_glyphs: vec![],
exclude_glyphs_file: None,
};
let mut font1 = babelfont::load("../babelfont/resources/NotoSansLimbu.glyphs").unwrap();
let font2 = babelfont::load("../babelfont/resources/NotoSans.glyphspackage").unwrap();
let filter = GlyphsetFilter::new(
gs.get_include_glyphs()
.expect("Failed to get include glyphs"),
vec![],
gs.get_codepoints().expect("Failed to get codepoints"),
&mut font1,
&font2,
ExistingGlyphHandling::Skip,
);
let final_glyphs = filter.final_glyphset();
assert!(final_glyphs.contains(&"a".into()));
assert!(final_glyphs.contains(&"uni1904".into()));
assert!(!final_glyphs.contains(&"softsign-cy".into()));
assert!(!final_glyphs.contains(&"softsign-cy.loclBGR".into()));
}
}