mod parse;
mod process;
mod serialize;
#[cfg(test)]
mod tests;
pub(crate) mod types;
mod winding;
use rayon::prelude::*;
use std::collections::{HashMap, HashSet};
use std::io::{Error, ErrorKind};
use parse::parse_svg_glyph;
use process::{build_unicode_values, process_glyph};
pub(crate) use serialize::build_svg_font;
use types::{
CachedGlyph, CachedProcessedGlyph, GlyphCache, GlyphWorkItem, ParsedGlyph, PreparedSvgFont,
ProcessedGlyph, SvgOptions,
};
use crate::types::{LoadedSvgFile, ResolvedGenerateWebfontsOptions};
struct FinalizePlan {
normalize: bool,
fixed_width: bool,
center_horizontally: bool,
center_vertically: bool,
ligature: bool,
round: f64,
max_glyph_height: f64,
font_height: f64,
font_width: f64,
ascent: f64,
descent: f64,
font_id: String,
metadata: String,
optimize_output: bool,
}
pub(crate) fn svg_options_from_options(
options: &ResolvedGenerateWebfontsOptions,
) -> SvgOptions<'_> {
let svg_format = options
.format_options
.as_ref()
.and_then(|value| value.svg.as_ref());
SvgOptions {
ascent: options.ascent,
center_horizontally: options.center_horizontally,
center_vertically: options.center_vertically,
codepoints: &options.codepoints,
descent: options.descent,
fixed_width: options.fixed_width,
font_height: options.font_height,
font_id: svg_format.and_then(|v| v.font_id.as_deref()),
font_name: &options.font_name,
font_style: options.font_style.as_deref(),
font_weight: options.font_weight.as_deref(),
ligature: options.ligature,
metadata: svg_format.and_then(|v| v.metadata.as_deref()),
normalize: options.normalize,
optimize_output: options.optimize_output,
preserve_aspect_ratio: options.preserve_aspect_ratio,
round: options.round,
}
}
pub(crate) fn prepare_svg_font(
options: &SvgOptions,
source_files: &[LoadedSvgFile],
) -> Result<PreparedSvgFont, Error> {
let glyphs = parse_glyphs(options, source_files)?;
finalize_glyphs(options, glyphs)
}
pub(crate) fn parse_glyphs(
options: &SvgOptions,
source_files: &[LoadedSvgFile],
) -> Result<Vec<ParsedGlyph>, 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 mut work_items = Vec::with_capacity(source_files.len());
for (index, source_file) in source_files.iter().enumerate() {
let name = &source_file.glyph_name;
let codepoint = options
.codepoints
.get(name.as_str())
.copied()
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
format!("Missing resolved codepoint for glyph '{name}'."),
)
})?;
work_items.push(GlyphWorkItem {
codepoint,
index,
name,
source_file,
});
}
let mut glyphs = work_items
.par_iter()
.map(|item| parse_svg_glyph(item, preserve_aspect_ratio))
.collect::<Result<Vec<_>, Error>>()
.map_err(|error| Error::new(ErrorKind::InvalidData, error.to_string()))?;
glyphs.sort_by_key(|glyph| glyph.index);
Ok(glyphs)
}
pub(crate) fn finalize_glyphs(
options: &SvgOptions,
glyphs: Vec<ParsedGlyph>,
) -> Result<PreparedSvgFont, Error> {
let plan = finalize_plan(options, &glyphs);
let mut processed_glyphs = glyphs
.into_par_iter()
.map(|glyph| process_glyph_with_plan(glyph, &plan))
.collect::<Result<Vec<_>, Error>>()
.map_err(|error| Error::new(ErrorKind::InvalidData, error.to_string()))?;
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 finalize_plan(options: &SvgOptions, glyphs: &[ParsedGlyph]) -> FinalizePlan {
let normalize = options.normalize;
let max_glyph_height = glyphs
.iter()
.fold(0.0_f64, |current, glyph| current.max(glyph.height));
let max_glyph_width = glyphs
.iter()
.fold(0.0_f64, |current, glyph| current.max(glyph.width));
let font_height = options.font_height.unwrap_or(max_glyph_height.max(1.0));
let descent = options.descent.unwrap_or(0.0);
let mut font_width = if max_glyph_height > 0.0 {
max_glyph_width
} else {
max_glyph_width.max(1.0)
};
if normalize {
font_width = glyphs
.iter()
.map(|glyph| {
if glyph.height > 0.0 {
(font_height / glyph.height) * glyph.width
} else {
glyph.width
}
})
.fold(0.0_f64, f64::max);
} else if options.font_height.is_some() && max_glyph_height > 0.0 {
font_width *= font_height / max_glyph_height;
}
FinalizePlan {
normalize,
fixed_width: options.fixed_width.unwrap_or(false),
center_horizontally: options.center_horizontally.unwrap_or(false),
center_vertically: options.center_vertically.unwrap_or(false),
ligature: options.ligature,
round: options.round.unwrap_or(10e12),
max_glyph_height,
font_height,
font_width,
ascent: options.ascent.unwrap_or(font_height - descent),
descent,
font_id: options.font_id.unwrap_or(options.font_name).to_owned(),
metadata: options.metadata.unwrap_or_default().to_owned(),
optimize_output: options.optimize_output.unwrap_or(false),
}
}
fn process_glyph_with_plan(
glyph: ParsedGlyph,
plan: &FinalizePlan,
) -> Result<ProcessedGlyph, Error> {
process_glyph(
glyph,
plan.normalize,
plan.fixed_width,
plan.center_horizontally,
plan.center_vertically,
plan.ligature,
plan.round,
plan.max_glyph_height,
plan.font_height,
plan.font_width,
plan.descent,
plan.optimize_output,
)
}
pub(crate) fn prepare_svg_font_incremental(
options: &SvgOptions,
source_files: &[LoadedSvgFile],
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
}
fn parse_glyphs_incremental(
options: &SvgOptions,
source_files: &[LoadedSvgFile],
cache: &mut GlyphCache,
) -> Result<Vec<ParsedGlyph>, 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.path.as_str()).collect();
cache
.entries
.retain(|path, _| current.contains(path.as_str()));
cache
.content_hashes
.retain(|path, _| current.contains(path.as_str()));
cache
.processed_entries
.retain(|path, _| current.contains(path.as_str()));
for source_file in source_files {
if cache.entries.contains_key(&source_file.path) {
continue;
}
let hash = source_content_hash(&source_file.contents);
if let Some(cached) = cache.by_content_hash.get(&hash) {
cache
.entries
.insert(source_file.path.clone(), cached.clone());
cache.content_hashes.insert(source_file.path.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 parsed: Vec<(usize, ParsedGlyph)> = source_files
.par_iter()
.enumerate()
.filter(|(_, source_file)| !cache.entries.contains_key(&source_file.path))
.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).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 = CachedGlyph {
height: glyph.height,
paths: glyph.paths.clone(),
width: glyph.width,
};
cache.by_content_hash.insert(hash, cached.clone());
cache.content_hashes.insert(source_file.path.clone(), hash);
cache.entries.insert(source_file.path.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) => glyph,
None => {
let cached = cache
.entries
.get(&source_file.path)
.expect("an unchanged file must have a cache entry");
ParsedGlyph {
codepoint: codepoints[index],
height: cached.height,
index,
name: source_file.glyph_name.clone(),
paths: cached.paths.clone(),
width: cached.width,
}
}
};
glyphs.push(glyph);
}
glyphs.sort_by_key(|glyph| glyph.index);
Ok(glyphs)
}
fn processed_glyph_cache_signature(plan: &FinalizePlan) -> [u8; 16] {
let mut bytes = Vec::with_capacity(8 * 5 + 5);
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,
]);
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<ParsedGlyph>,
source_files: &[LoadedSvgFile],
cache: &mut GlyphCache,
) -> Result<PreparedSvgFont, Error> {
let plan = finalize_plan(options, &glyphs);
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].path)
})
.map(|(_, glyph)| {
let path_index = glyph.index;
process_glyph_with_plan(glyph, &plan).map(|glyph| {
let cached = CachedProcessedGlyph {
height: glyph.height,
path_data: glyph.path_data.clone(),
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].path.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.path)
.expect("an unchanged file must have a processed cache entry");
let codepoint = glyphs_codepoint(options, source_file)?;
ProcessedGlyph {
codepoint,
height: cached.height,
index,
name: source_file.glyph_name.clone(),
path_data: cached.path_data.clone(),
unicode_values: build_unicode_values(
&source_file.glyph_name,
codepoint,
plan.ligature,
),
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: &LoadedSvgFile) -> 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
),
)
})
}