#[cfg(test)]
mod tests;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::{Error, ErrorKind};
use std::path::Path;
use std::sync::Arc;
use crate::GenerationHooks;
use tokio::task::JoinSet;
use super::options::resolve_codepoints;
use crate::types::MissingGlyphBehavior;
#[derive(Clone)]
pub(crate) struct LoadedSvgFile {
pub contents: Arc<str>,
pub glyph_name: String,
pub path: String,
}
#[allow(
dead_code,
reason = "variant sources are consumed by later generation phases"
)]
pub(crate) struct VariantFamilySources<Files = Vec<LoadedSvgFile>> {
pub variants: Vec<Files>,
pub glyphs: Vec<LogicalGlyphSources>,
}
#[allow(
dead_code,
reason = "logical glyphs are consumed by later generation phases"
)]
pub(crate) struct LogicalGlyphSources {
pub name: String,
pub codepoint: u32,
pub sources: Box<[Option<VariantGlyphSource>]>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum VariantGlyphSource {
Source {
variant_index: usize,
source_index: usize,
},
Blank,
}
async fn load_svg_contents(paths: &[String]) -> std::io::Result<Vec<(String, String)>> {
let mut tasks = JoinSet::new();
for (index, path) in paths.iter().cloned().enumerate() {
tasks.spawn(async move { (index, path.clone(), tokio::fs::read_to_string(path).await) });
}
let mut results = Vec::with_capacity(paths.len());
while let Some(result) = tasks.join_next().await {
results.push(
result.map_err(|error| {
std::io::Error::other(format!("SVG loading task failed: {error}"))
})?,
);
}
results.sort_by_key(|(index, _, _)| *index);
results
.into_iter()
.map(|(_, path, contents)| match contents {
Ok(contents) => Ok((path, contents)),
Err(error) => Err(std::io::Error::other(format!(
"Failed to read source SVG file '{path}': {error}"
))),
})
.collect()
}
#[cfg(test)]
pub(crate) async fn load_variant_svg_files(
variant_paths: &[Vec<String>],
rename: Option<&(dyn Fn(&str) -> String + Send + Sync)>,
) -> std::io::Result<Vec<Vec<LoadedSvgFile>>> {
load_variant_svg_files_with_hooks(variant_paths, &crate::hooks::RenameHooks(rename)).await
}
pub(crate) async fn load_svg_files_with_hooks<H: GenerationHooks>(
paths: &[String],
hooks: &H,
validate_names: bool,
) -> Result<Vec<LoadedSvgFile>, H::Error> {
let raw = load_svg_contents(paths).await?;
let glyph_names = if let Some(glyph_names) = hooks.rename(paths).await? {
if glyph_names.len() != raw.len() {
return Err(Error::new(
ErrorKind::InvalidInput,
"rename callback returned an unexpected number of glyph names".to_owned(),
)
.into());
}
glyph_names
} else {
raw.iter()
.map(|(path, _)| default_glyph_name_from_path(path))
.collect::<std::io::Result<_>>()?
};
let source_files = raw
.into_iter()
.zip(glyph_names)
.map(|((path, contents), glyph_name)| LoadedSvgFile {
contents: contents.into(),
glyph_name,
path,
})
.collect::<Vec<_>>();
if validate_names {
validate_glyph_names(&source_files)?;
}
Ok(source_files)
}
pub(crate) async fn load_variant_svg_files_with_hooks<H: GenerationHooks>(
variant_paths: &[Vec<String>],
hooks: &H,
) -> Result<Vec<Vec<LoadedSvgFile>>, H::Error> {
let lengths = variant_paths.iter().map(Vec::len).collect::<Vec<_>>();
let paths = variant_paths.iter().flatten().cloned().collect::<Vec<_>>();
let source_files = load_svg_files_with_hooks(&paths, hooks, false).await?;
Ok(split_loaded_variant_files(source_files, &lengths)?)
}
fn split_loaded_variant_files(
source_files: Vec<LoadedSvgFile>,
lengths: &[usize],
) -> std::io::Result<Vec<Vec<LoadedSvgFile>>> {
let mut source_files = source_files.into_iter();
let mut variants = Vec::with_capacity(lengths.len());
for length in lengths {
let variant = source_files.by_ref().take(*length).collect::<Vec<_>>();
validate_glyph_names(&variant)?;
variants.push(variant);
}
Ok(variants)
}
pub(crate) fn build_variant_family_sources<Files: AsRef<[LoadedSvgFile]>>(
variants: Vec<Files>,
explicit_codepoints: &BTreeMap<String, u32>,
start_codepoint: u32,
) -> std::io::Result<(VariantFamilySources<Files>, BTreeMap<String, u32>)> {
let mut name_to_index = HashMap::new();
let mut glyphs = Vec::<(String, Box<[Option<VariantGlyphSource>]>)>::new();
for (variant_index, files) in variants.iter().enumerate() {
for (source_index, file) in files.as_ref().iter().enumerate() {
let glyph_index = *name_to_index
.entry(file.glyph_name.clone())
.or_insert_with(|| {
let index = glyphs.len();
glyphs.push((
file.glyph_name.clone(),
vec![None; variants.len()].into_boxed_slice(),
));
index
});
glyphs[glyph_index].1[variant_index] = Some(VariantGlyphSource::Source {
variant_index,
source_index,
});
}
}
let codepoints = resolve_codepoints(
glyphs.iter().map(|(name, _)| name.as_str()),
explicit_codepoints,
start_codepoint,
)?;
let glyphs = glyphs
.into_iter()
.map(|(name, sources)| LogicalGlyphSources {
codepoint: codepoints[&name],
name,
sources,
})
.collect();
Ok((VariantFamilySources { variants, glyphs }, codepoints))
}
pub(crate) fn resolve_missing_glyphs<Files>(
family: &mut VariantFamilySources<Files>,
behavior: MissingGlyphBehavior,
fallback_index: Option<usize>,
variant_names: &[&str],
) -> std::io::Result<()> {
if behavior == MissingGlyphBehavior::Error {
let missing = family
.glyphs
.iter()
.flat_map(|glyph| {
glyph
.sources
.iter()
.enumerate()
.filter(|(_, source)| source.is_none())
.map(|(variant_index, _)| {
format!(
"\"{}\" in variant \"{}\"",
glyph.name, variant_names[variant_index]
)
})
})
.collect::<Vec<_>>();
if !missing.is_empty() {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Missing glyphs: {}.", missing.join(", ")),
));
}
return Ok(());
}
if behavior == MissingGlyphBehavior::Fallback {
let fallback_index = fallback_index.expect("validated fallback must resolve");
if let Some(glyph) = family
.glyphs
.iter()
.find(|glyph| glyph.sources[fallback_index].is_none())
{
return Err(Error::new(
ErrorKind::InvalidInput,
format!(
"Fallback variant \"{}\" is missing glyph \"{}\".",
variant_names[fallback_index], glyph.name
),
));
}
}
for glyph in &mut family.glyphs {
let replacement = if behavior == MissingGlyphBehavior::Blank {
VariantGlyphSource::Blank
} else {
let fallback_index = fallback_index.expect("validated fallback must resolve");
glyph.sources[fallback_index].expect("fallback completeness checked above")
};
for source in &mut glyph.sources {
source.get_or_insert(replacement);
}
}
Ok(())
}
pub(crate) fn validate_glyph_names(source_files: &[LoadedSvgFile]) -> std::io::Result<()> {
let mut seen_names = HashSet::with_capacity(source_files.len());
for source_file in source_files {
if !seen_names.insert(source_file.glyph_name.clone()) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
format!(
"The glyph name \"{}\" must be unique.",
source_file.glyph_name
),
));
}
}
Ok(())
}
fn default_glyph_name_from_path(path: &str) -> Result<String, Error> {
Path::new(path)
.file_stem()
.and_then(|stem| stem.to_str())
.map(str::to_owned)
.ok_or_else(|| {
Error::new(
ErrorKind::InvalidInput,
format!("Unable to derive glyph name from '{path}'."),
)
})
}
#[cfg(test)]
fn glyph_name_from_path(
path: &str,
rename: Option<&(dyn Fn(&str) -> String + Send + Sync)>,
) -> Result<String, Error> {
match rename {
Some(rename) => Ok(rename(path)),
None => default_glyph_name_from_path(path),
}
}