use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind};
use std::sync::Arc;
use rayon::prelude::*;
use super::parse::parse_svg_glyph;
use super::types::{
CachedGlyph, CachedProcessedGlyph, GlyphCache, GlyphWorkItem, ParsedGlyph, PreparedSvgFont,
ProcessedGlyph, SvgOptions,
};
use super::{finalize_plan, process_glyph_with_plan};
use crate::input::LoadedSvgSource;
pub(super) enum IncrementalGlyph {
Fresh(ParsedGlyph),
Cached {
codepoint: u32,
index: usize,
name: String,
glyph: Arc<CachedGlyph>,
},
}
impl IncrementalGlyph {
pub(super) fn dimensions(&self) -> (f64, f64) {
match self {
Self::Fresh(glyph) => (glyph.height, glyph.width),
Self::Cached { glyph, .. } => (glyph.height, glyph.width),
}
}
fn index(&self) -> usize {
match self {
Self::Fresh(glyph) => glyph.index,
Self::Cached { index, .. } => *index,
}
}
pub(super) fn into_parsed(self) -> ParsedGlyph {
match self {
Self::Fresh(glyph) => glyph,
Self::Cached {
codepoint,
index,
name,
glyph,
} => ParsedGlyph {
color_layers: glyph.color_layers.clone(),
codepoint,
height: glyph.height,
index,
name,
paths: glyph.paths.clone(),
width: glyph.width,
},
}
}
pub(super) fn to_parsed(&self, codepoint: u32, index: usize, name: &str) -> ParsedGlyph {
let (height, width, paths, color_layers) = match self {
Self::Fresh(glyph) => (glyph.height, glyph.width, &glyph.paths, &glyph.color_layers),
Self::Cached { glyph, .. } => {
(glyph.height, glyph.width, &glyph.paths, &glyph.color_layers)
}
};
ParsedGlyph {
color_layers: color_layers.clone(),
codepoint,
height,
index,
name: name.to_owned(),
paths: paths.clone(),
width,
}
}
}
pub(crate) fn prepare_svg_font_incremental(
options: &SvgOptions,
source_files: &[LoadedSvgSource],
cache: &mut GlyphCache,
) -> Result<PreparedSvgFont, Error> {
let glyphs = parse_glyphs_incremental(options, source_files, cache)?;
finalize_glyphs_incremental(options, glyphs, source_files, cache)
}
pub(crate) fn source_content_hash(contents: &str) -> [u8; 16] {
md5::compute(contents.as_bytes()).0
}
pub(super) fn parse_glyphs_incremental(
options: &SvgOptions,
source_files: &[LoadedSvgSource],
cache: &mut GlyphCache,
) -> Result<Vec<IncrementalGlyph>, Error> {
if source_files.is_empty() {
return Err(Error::new(
ErrorKind::InvalidInput,
"Expected at least one SVG file for native generation.",
));
}
let preserve_aspect_ratio = options.preserve_aspect_ratio.unwrap_or(false);
let current: HashSet<&str> = source_files.iter().map(|file| file.id.as_str()).collect();
cache.entries.retain(|id, _| current.contains(id.as_str()));
cache
.content_hashes
.retain(|id, _| current.contains(id.as_str()));
cache
.processed_entries
.retain(|id, _| current.contains(id.as_str()));
for source_file in source_files {
let selected = options.selects_color(&source_file.glyph_name);
if cache
.entries
.get(&source_file.id)
.is_some_and(|cached| cached.color_layers.is_some() != selected)
{
cache.entries.remove(&source_file.id);
cache.processed_entries.remove(&source_file.id);
}
if cache.entries.contains_key(&source_file.id) {
continue;
}
let hash = source_content_hash(&source_file.contents);
if let Some(cached) = cache.by_content_hash.get(&hash)
&& cached.color_layers.is_some() == selected
{
cache.entries.insert(source_file.id.clone(), cached.clone());
cache.content_hashes.insert(source_file.id.clone(), hash);
}
}
let mut codepoints = Vec::with_capacity(source_files.len());
for source_file in source_files {
let codepoint = options
.codepoints
.get(source_file.glyph_name.as_str())
.copied()
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
format!(
"Missing resolved codepoint for glyph '{}'.",
source_file.glyph_name
),
)
})?;
codepoints.push(codepoint);
}
let parser_options = options.parser_options();
let parsed: Vec<(usize, ParsedGlyph)> = source_files
.par_iter()
.enumerate()
.filter(|(_, source_file)| !cache.entries.contains_key(&source_file.id))
.map(|(index, source_file)| {
let work = GlyphWorkItem {
codepoint: codepoints[index],
index,
name: &source_file.glyph_name,
source_file,
};
parse_svg_glyph(
&work,
preserve_aspect_ratio,
&parser_options,
options.selects_color(work.name),
)
.map_err(|error| options.source_error(&source_file.id, error))
.map(|glyph| (index, glyph))
})
.collect::<Result<Vec<_>, Error>>()
.map_err(|error| Error::new(ErrorKind::InvalidData, error.to_string()))?;
#[cfg(test)]
{
cache.parse_count += parsed.len();
}
for (index, glyph) in &parsed {
let source_file = &source_files[*index];
let hash = source_content_hash(&source_file.contents);
let cached = Arc::new(CachedGlyph {
color_layers: glyph.color_layers.clone(),
height: glyph.height,
paths: glyph.paths.clone(),
width: glyph.width,
});
cache.by_content_hash.insert(hash, cached.clone());
cache.content_hashes.insert(source_file.id.clone(), hash);
cache.entries.insert(source_file.id.clone(), cached);
}
let active_hashes: HashSet<[u8; 16]> = cache.content_hashes.values().copied().collect();
cache
.by_content_hash
.retain(|hash, _| active_hashes.contains(hash));
let mut freshly_parsed: HashMap<usize, ParsedGlyph> = parsed.into_iter().collect();
let mut glyphs = Vec::with_capacity(source_files.len());
for (index, source_file) in source_files.iter().enumerate() {
let glyph = match freshly_parsed.remove(&index) {
Some(glyph) => IncrementalGlyph::Fresh(glyph),
None => {
let cached = cache
.entries
.get(&source_file.id)
.expect("an unchanged file must have a cache entry");
IncrementalGlyph::Cached {
codepoint: codepoints[index],
index,
name: source_file.glyph_name.clone(),
glyph: Arc::clone(cached),
}
}
};
glyphs.push(glyph);
}
glyphs.sort_by_key(IncrementalGlyph::index);
Ok(glyphs)
}
pub(super) fn processed_glyph_cache_signature(plan: &super::FinalizePlan) -> [u8; 16] {
let mut bytes = Vec::with_capacity(8 * 5 + 7);
bytes.extend_from_slice(&[
plan.normalize as u8,
plan.fixed_width as u8,
plan.center_horizontally as u8,
plan.center_vertically as u8,
plan.optimize_output as u8,
plan.serialize_path as u8,
plan.structure_path as u8,
]);
for value in [
plan.round,
plan.max_glyph_height,
plan.font_height,
plan.font_width,
plan.descent,
] {
bytes.extend_from_slice(&value.to_bits().to_le_bytes());
}
md5::compute(bytes).0
}
fn finalize_glyphs_incremental(
options: &SvgOptions,
glyphs: Vec<IncrementalGlyph>,
source_files: &[LoadedSvgSource],
cache: &mut GlyphCache,
) -> Result<PreparedSvgFont, Error> {
let plan = finalize_plan(
options,
&glyphs,
|glyph| glyph.dimensions().0,
|glyph| glyph.dimensions().1,
);
let signature = processed_glyph_cache_signature(&plan);
if cache.processed_signature != Some(signature) {
cache.processed_entries.clear();
cache.processed_signature = Some(signature);
}
let processed: Vec<(usize, ProcessedGlyph, CachedProcessedGlyph)> = glyphs
.into_par_iter()
.enumerate()
.filter(|(_, glyph)| {
!cache
.processed_entries
.contains_key(&source_files[glyph.index()].id)
})
.map(|(_, glyph)| {
let path_index = glyph.index();
process_glyph_with_plan(glyph.into_parsed(), &plan).map(|glyph| {
let cached = CachedProcessedGlyph {
color_layers: glyph.color_layers.clone(),
height: glyph.height,
path_data: glyph.path_data.clone(),
ttf_path: glyph.ttf_path.clone(),
ttf_path_hash: glyph.ttf_path_hash,
width: glyph.width,
};
(path_index, glyph, cached)
})
})
.collect::<Result<Vec<_>, Error>>()
.map_err(|error| Error::new(ErrorKind::InvalidData, error.to_string()))?;
#[cfg(test)]
{
cache.process_count += processed.len();
}
let mut freshly_processed = HashMap::with_capacity(processed.len());
for (index, glyph, cached) in processed {
cache
.processed_entries
.insert(source_files[index].id.clone(), cached);
freshly_processed.insert(index, glyph);
}
let mut processed_glyphs = Vec::with_capacity(source_files.len());
for (index, source_file) in source_files.iter().enumerate() {
let glyph = match freshly_processed.remove(&index) {
Some(glyph) => glyph,
None => {
let cached = cache
.processed_entries
.get(&source_file.id)
.expect("an unchanged file must have a processed cache entry");
let codepoint = glyphs_codepoint(options, source_file)?;
ProcessedGlyph {
color_layers: cached.color_layers.clone(),
codepoint,
height: cached.height,
index,
name: source_file.glyph_name.clone(),
path_data: cached.path_data.clone(),
ttf_path: cached.ttf_path.clone(),
ttf_path_hash: cached.ttf_path_hash,
width: cached.width,
}
}
};
processed_glyphs.push(glyph);
}
processed_glyphs.sort_by_key(|glyph| glyph.index);
Ok(PreparedSvgFont {
ascent: plan.ascent,
descent: plan.descent,
font_height: plan.font_height,
font_id: plan.font_id,
font_width: plan.font_width,
metadata: plan.metadata,
processed_glyphs,
})
}
fn glyphs_codepoint(options: &SvgOptions, source_file: &LoadedSvgSource) -> Result<u32, Error> {
options
.codepoints
.get(source_file.glyph_name.as_str())
.copied()
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
format!(
"Missing resolved codepoint for glyph '{}'.",
source_file.glyph_name
),
)
})
}