diff --git a/Cargo.toml b/Cargo.toml index fa82c60..0e69d91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,7 @@ xmltree = "0.10.3" mustache = { git = "https://git.shadamethyst.xyz/adri326/rust-mustache.git" } clap = { version = "3.2", features = ["derive"] } mkdirp = "1.0.0" +resvg = "0.23" +usvg = "0.23" +tiny-skia = "0.6" +png = "0.17" diff --git a/src/export.rs b/src/export.rs new file mode 100644 index 0000000..715db2a --- /dev/null +++ b/src/export.rs @@ -0,0 +1,128 @@ +use usvg::{ + Tree, + NodeExt, + Options, +}; +use xmltree::{Element}; +use std::path::{PathBuf}; +use std::collections::HashSet; + +#[derive(Debug)] +pub enum ExportError { + Xml(xmltree::Error), + XmlParse(xmltree::ParseError), + Usvg(usvg::Error), + Io(PathBuf, std::io::Error), + NoBBox, + Utf8(std::string::FromUtf8Error), + Encode(png::EncodingError), +} + +impl From for ExportError { + fn from(err: xmltree::ParseError) -> Self { + Self::XmlParse(err) + } +} + +impl From for ExportError { + fn from(err: xmltree::Error) -> Self { + Self::Xml(err) + } +} + +impl From for ExportError { + fn from(err: usvg::Error) -> Self { + Self::Usvg(err) + } +} + +impl From for ExportError { + fn from(err: std::string::FromUtf8Error) -> Self { + Self::Utf8(err) + } +} + +impl From for ExportError { + fn from(err: png::EncodingError) -> Self { + Self::Encode(err) + } +} + +pub fn get_new_bbox(svg: &Tree) -> Option<(f64, f64, f64, f64)> { + let bbox = svg.root().calculate_bbox()?; + if bbox.width() > bbox.height() { + let y = bbox.y() - (bbox.width() - bbox.height()) / 2.0; + + Some((bbox.x(), y, bbox.width(), bbox.width())) + } else { + let x = bbox.x() - (bbox.height() - bbox.width()) / 2.0; + + Some((x, bbox.y(), bbox.height(), bbox.height())) + } +} + +fn get_usvg(svg_str: &str) -> Result { + let usvg_options = Options::default(); + Tree::from_str(svg_str, &usvg_options.to_ref()) +} + +fn get_xml(svg_str: &str) -> Result { + Element::parse(svg_str.as_bytes()) +} + +fn xml_to_str(svg_xml: &Element) -> Result { + let mut s: Vec = Vec::new(); + + svg_xml.write(&mut s)?; + + Ok(String::from_utf8(s)?) +} + +pub fn resize(svg_str: String) -> Result { + if let Some(new_bbox) = get_new_bbox(&get_usvg(&svg_str)?) { + let mut svg_xml = get_xml(&svg_str)?; + svg_xml.attributes.insert( + "viewBox".to_string(), + format!("{} {} {} {}", new_bbox.0, new_bbox.1, new_bbox.2, new_bbox.3), + ); + + xml_to_str(&svg_xml) + } else { + Err(ExportError::NoBBox) + } +} + +pub fn export( + mut svg_str: String, + output_dir: &PathBuf, + output_name: String, + args: &super::Args, +) -> Result<(), ExportError> { + if args.resize { + svg_str = resize(svg_str)?; + } + + mkdirp::mkdirp(output_dir.join("vector")).unwrap(); + + let output = output_dir.join(&format!("vector/{}.svg", output_name)); + std::fs::write(output.clone(), svg_str.clone()).map_err(|err| ExportError::Io(output, err))?; + + let svg_usvg = get_usvg(&svg_str)?; + for resolution in args.dim.iter().copied().filter(|r| *r != 0).collect::>() { + mkdirp::mkdirp(output_dir.join(&format!("{}", resolution))).unwrap(); + let output = output_dir.join(&format!("{}/{}.png", resolution, output_name)); + + let mut image = tiny_skia::Pixmap::new(resolution, resolution).unwrap(); + + resvg::render( + &svg_usvg, + usvg::FitTo::Width(resolution), + tiny_skia::Transform::identity(), + image.as_mut() + ).unwrap(); + + image.save_png(output)?; + } + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4fc5398..e0d3097 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,35 +8,45 @@ use parse::*; pub mod template; use template::*; +pub mod export; +use export::*; + fn main() { let args = Args::parse(); - let species = Arc::new(load_species(args.decl).unwrap()); + let species = Arc::new(load_species(args.decl.clone()).unwrap()); let context = RenderingContext::new(species); - let output_dir = args.output_dir.unwrap_or(PathBuf::from("output/vector/")); - - mkdirp::mkdirp(output_dir.clone()).unwrap(); + let output_dir = args.output_dir.clone().unwrap_or(PathBuf::from("output/")); if args.names.is_empty() { for name in context.species().variants.keys() { - generate_variant(&context, name, &output_dir); + generate_variant(&context, name, &output_dir, &args); } } else { for name in args.names.iter() { - generate_variant(&context, name, &output_dir); + generate_variant(&context, name, &output_dir, &args); } } } -fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf) { +fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf, args: &Args) { if let Some(path) = context.species().variants.get(name) { match context.compile(path).and_then(|template| { template.render_data_to_string(&context.get_data()) }) { - Ok(rendered) => { - let output = output_dir.join(&format!("{}_{}.svg", context.species().name, name)); - std::fs::write(output, rendered).unwrap(); + Ok(svg) => { + match export( + svg, + output_dir, + format!("{}_{}", context.species().name, name), + args + ) { + Ok(_) => {} + Err(err) => { + eprintln!("Error while rendering {}: {:?}", name, err); + } + } } Err(err) => { eprintln!("Error while rendering {}: {}", name, err); @@ -49,7 +59,7 @@ fn generate_variant(context: &RenderingContext, name: &str, output_dir: &PathBuf #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] -struct Args { +pub struct Args { /// A folder containing the declaration from which the emotes should be generated #[clap(short, long, value_parser)] decl: PathBuf, @@ -58,6 +68,14 @@ struct Args { #[clap(value_parser)] names: Vec, + /// Automatically resize the SVG's viewBox, defaults to true + #[clap(short, long, value_parser, default_value = "true")] + resize: bool, + + /// Dimension to export the images as; can be specified multiple times + #[clap(long, value_parser)] + dim: Vec, + /// Output directory #[clap(short, long, value_parser)] output_dir: Option,