use std::fs::File;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::process;
use std::sync::Arc;
use pico_args::Arguments;
const HELP: &str = "\
usvg (micro SVG) is an SVG simplification tool.
USAGE:
usvg [OPTIONS] <in-svg> <out-svg> # from file to file
usvg [OPTIONS] <in-svg> -c # from file to stdout
usvg [OPTIONS] - <out-svg> # from stdin to file
usvg [OPTIONS] - -c # from stdin to stdout
OPTIONS:
-h, --help Prints help information
-V, --version Prints version information
-c Prints the output SVG to the stdout
--dpi DPI Sets the resolution
[default: 96] [possible values: 10..4000 (inclusive)]
--stylesheet PATH Inject a stylesheet that should be used when resolving
CSS attributes.
--languages LANG Sets a comma-separated list of languages that
will be used during the 'systemLanguage'
attribute resolving
Examples: 'en-US', 'en-US, ru-RU', 'en, ru'
[default: en]
--shape-rendering HINT Selects the default shape rendering method
[default: geometricPrecision]
[possible values: optimizeSpeed, crispEdges,
geometricPrecision]
--text-rendering HINT Selects the default text rendering method
[default: optimizeLegibility]
[possible values: optimizeSpeed, optimizeLegibility,
geometricPrecision]
--image-rendering HINT Selects the default image rendering method
[default: optimizeQuality]
[possible values: optimizeQuality, optimizeSpeed, smooth, high-quality, crisp-edges, pixelated]
--resources-dir DIR Sets a directory that will be used during
relative paths resolving.
Expected to be the same as the directory that
contains the SVG file, but can be set to any.
[default: input file directory
or none when reading from stdin]
--font-family FAMILY Sets the default font family that will be
used when no 'font-family' is present
[default: Times New Roman]
--font-size SIZE Sets the default font size that will be
used when no 'font-size' is present
[default: 12] [possible values: 1..192 (inclusive)]
--serif-family FAMILY Sets the 'serif' font family.
Will be used when no 'font-family' is present
[default: Times New Roman]
--sans-serif-family FAMILY Sets the 'sans-serif' font family
[default: Arial]
--cursive-family FAMILY Sets the 'cursive' font family
[default: Comic Sans MS]
--fantasy-family FAMILY Sets the 'fantasy' font family
[default: Impact]
--monospace-family FAMILY Sets the 'monospace' font family
[default: Courier New]
--use-font-file PATH Load a specified font file into the fonts database.
Will be used during text to path conversion.
This option can be set multiple times
--use-fonts-dir PATH Loads all fonts from the specified directory
into the fonts database.
Will be used during text to path conversion.
This option can be set multiple times
--skip-system-fonts Disables system fonts loading.
You should add some fonts manually using
--use-font-file and/or --use-fonts-dir
Otherwise, text elements will not be processes
--list-fonts Lists successfully loaded font faces.
Useful for debugging
--default-width LENGTH Sets the default width of the SVG viewport. Like
the '--default-height' option, this option
controls what size relative units in the document
will use as a base if there is no viewBox and
document width or height are relative.
[values: 1..4294967295 (inclusive)] [default: 100]
--default-height LENGTH Sets the default height of the SVG viewport.
Refer to the explanation of the '--default-width'
option. [values: 1..4294967295 (inclusive)] [default: 100]
--preserve-text Do not convert text into paths.
--id-prefix Adds a prefix to each ID attribute
--indent INDENT Sets the XML nodes indent
[values: none, 0, 1, 2, 3, 4, tabs] [default: 4]
--attrs-indent INDENT Sets the XML attributes indent
[values: none, 0, 1, 2, 3, 4, tabs] [default: none]
--coordinates-precision NUM Set the coordinates numeric precision.
Smaller precision can lead to a malformed output in some cases
[values: 2..8 (inclusive)] [default: 8]
--transforms-precision NUM Set the transform values numeric precision.
Smaller precision can lead to a malformed output in some cases
[values: 2..8 (inclusive)] [default: 8]
--quiet Disables warnings
ARGS:
<in-svg> Input file
<out-svg> Output file
";
#[derive(Debug)]
struct Args {
dpi: u32,
languages: Vec<String>,
shape_rendering: usvg::ShapeRendering,
text_rendering: usvg::TextRendering,
image_rendering: usvg::ImageRendering,
resources_dir: Option<PathBuf>,
font_family: Option<String>,
font_size: u32,
serif_family: Option<String>,
sans_serif_family: Option<String>,
cursive_family: Option<String>,
fantasy_family: Option<String>,
monospace_family: Option<String>,
font_files: Vec<PathBuf>,
font_dirs: Vec<PathBuf>,
skip_system_fonts: bool,
preserve_text: bool,
list_fonts: bool,
default_width: u32,
default_height: u32,
id_prefix: Option<String>,
indent: xmlwriter::Indent,
attrs_indent: xmlwriter::Indent,
coordinates_precision: Option<u8>,
transforms_precision: Option<u8>,
style_sheet: Option<PathBuf>,
quiet: bool,
input: String,
output: String,
}
fn collect_args() -> Result<Args, pico_args::Error> {
let mut input = Arguments::from_env();
if input.contains(["-h", "--help"]) {
print!("{}", HELP);
process::exit(0);
}
if input.contains(["-V", "--version"]) {
println!("{}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
Ok(Args {
dpi: input.opt_value_from_fn("--dpi", parse_dpi)?.unwrap_or(96),
languages: input
.opt_value_from_fn("--languages", parse_languages)?
.unwrap_or(vec!["en".to_string()]), shape_rendering: input
.opt_value_from_str("--shape-rendering")?
.unwrap_or_default(),
text_rendering: input
.opt_value_from_str("--text-rendering")?
.unwrap_or_default(),
image_rendering: input
.opt_value_from_str("--image-rendering")?
.unwrap_or_default(),
resources_dir: input
.opt_value_from_str("--resources-dir")
.unwrap_or_default(),
font_family: input.opt_value_from_str("--font-family")?,
font_size: input
.opt_value_from_fn("--font-size", parse_font_size)?
.unwrap_or(12),
serif_family: input.opt_value_from_str("--serif-family")?,
sans_serif_family: input.opt_value_from_str("--sans-serif-family")?,
cursive_family: input.opt_value_from_str("--cursive-family")?,
fantasy_family: input.opt_value_from_str("--fantasy-family")?,
monospace_family: input.opt_value_from_str("--monospace-family")?,
font_files: input.values_from_str("--use-font-file")?,
font_dirs: input.values_from_str("--use-fonts-dir")?,
skip_system_fonts: input.contains("--skip-system-fonts"),
preserve_text: input.contains("--preserve-text"),
list_fonts: input.contains("--list-fonts"),
default_width: input
.opt_value_from_fn("--default-width", parse_length)?
.unwrap_or(100),
default_height: input
.opt_value_from_fn("--default-height", parse_length)?
.unwrap_or(100),
id_prefix: input.opt_value_from_str("--id-prefix")?,
indent: input
.opt_value_from_fn("--indent", parse_indent)?
.unwrap_or(xmlwriter::Indent::Spaces(4)),
attrs_indent: input
.opt_value_from_fn("--attrs-indent", parse_indent)?
.unwrap_or(xmlwriter::Indent::None),
coordinates_precision: input
.opt_value_from_fn("--coordinates-precision", parse_precision)?,
transforms_precision: input.opt_value_from_fn("--transforms-precision", parse_precision)?,
style_sheet: input.opt_value_from_str("--stylesheet").unwrap_or_default(),
quiet: input.contains("--quiet"),
input: input.free_from_str()?,
output: input.free_from_str()?,
})
}
fn parse_dpi(s: &str) -> Result<u32, String> {
let n: u32 = s.parse().map_err(|_| "invalid number")?;
if n >= 10 && n <= 4000 {
Ok(n)
} else {
Err("DPI out of bounds".to_string())
}
}
fn parse_font_size(s: &str) -> Result<u32, String> {
let n: u32 = s.parse().map_err(|_| "invalid number")?;
if n > 0 && n <= 192 {
Ok(n)
} else {
Err("font size out of bounds".to_string())
}
}
fn parse_languages(s: &str) -> Result<Vec<String>, String> {
let mut langs = Vec::new();
for lang in s.split(',') {
langs.push(lang.trim().to_string());
}
if langs.is_empty() {
return Err("languages list cannot be empty".to_string());
}
Ok(langs)
}
fn parse_indent(s: &str) -> Result<xmlwriter::Indent, String> {
let indent = match s {
"none" => xmlwriter::Indent::None,
"0" => xmlwriter::Indent::Spaces(0),
"1" => xmlwriter::Indent::Spaces(1),
"2" => xmlwriter::Indent::Spaces(2),
"3" => xmlwriter::Indent::Spaces(3),
"4" => xmlwriter::Indent::Spaces(4),
"tabs" => xmlwriter::Indent::Tabs,
_ => return Err("invalid INDENT value".to_string()),
};
Ok(indent)
}
fn parse_length(s: &str) -> Result<u32, String> {
let n: u32 = s.parse().map_err(|_| "invalid length")?;
if n > 0 {
Ok(n)
} else {
Err("LENGTH cannot be zero".to_string())
}
}
fn parse_precision(s: &str) -> Result<u8, String> {
let n: u8 = s.parse().map_err(|_| "invalid precision NUM value")?;
if (2..=8).contains(&n) {
Ok(n)
} else {
Err("precision NUM cannot be smaller than 2 or larger than 8".to_string())
}
}
#[derive(Clone, PartialEq, Debug)]
enum InputFrom<'a> {
Stdin,
File(&'a str),
}
#[derive(Clone, PartialEq, Debug)]
enum OutputTo<'a> {
Stdout,
File(&'a str),
}
fn main() {
let args = match collect_args() {
Ok(v) => v,
Err(e) => {
eprintln!("Error: {}.", e);
process::exit(1);
}
};
if !args.quiet {
if let Ok(()) = log::set_logger(&LOGGER) {
log::set_max_level(log::LevelFilter::Warn);
}
}
if let Err(e) = process(args) {
eprintln!("Error: {}.", e);
process::exit(1);
}
}
fn process(args: Args) -> Result<(), String> {
let (in_svg, out_svg) = {
let in_svg = args.input.as_str();
let out_svg = args.output.as_str();
let svg_from = if in_svg == "-" {
InputFrom::Stdin
} else if in_svg == "-c" {
return Err("-c should be set after input".to_string());
} else {
InputFrom::File(in_svg)
};
let svg_to = if out_svg == "-c" {
OutputTo::Stdout
} else {
OutputTo::File(out_svg)
};
(svg_from, svg_to)
};
let mut fontdb = usvg::fontdb::Database::new();
if !args.skip_system_fonts {
fontdb.load_system_fonts();
}
for path in &args.font_files {
if let Err(e) = fontdb.load_font_file(path) {
log::warn!("Failed to load '{}' cause {}.", path.display(), e);
}
}
for path in &args.font_dirs {
fontdb.load_fonts_dir(path);
}
let take_or = |mut family: Option<String>, fallback: &str| {
family.take().unwrap_or_else(|| fallback.to_string())
};
fontdb.set_serif_family(take_or(args.serif_family, "Times New Roman"));
fontdb.set_sans_serif_family(take_or(args.sans_serif_family, "Arial"));
fontdb.set_cursive_family(take_or(args.cursive_family, "Comic Sans MS"));
fontdb.set_fantasy_family(take_or(args.fantasy_family, "Impact"));
fontdb.set_monospace_family(take_or(args.monospace_family, "Courier New"));
if args.list_fonts {
for face in fontdb.faces() {
if let usvg::fontdb::Source::File(path) = &face.source {
let families: Vec<_> = face
.families
.iter()
.map(|f| format!("{} ({}, {})", f.0, f.1.primary_language(), f.1.region()))
.collect();
println!(
"{}: '{}', {}, {:?}, {:?}, {:?}",
path.display(),
families.join("', '"),
face.index,
face.style,
face.weight.0,
face.stretch
);
}
}
}
let resources_dir = match args.resources_dir {
Some(v) => Some(v),
None => {
match in_svg {
InputFrom::Stdin => None,
InputFrom::File(ref f) => {
std::fs::canonicalize(f)
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
}
}
}
};
let style_sheet = match args.style_sheet.as_ref() {
Some(p) => Some(
std::fs::read(&p)
.ok()
.and_then(|s| std::str::from_utf8(&s).ok().map(|s| s.to_string()))
.ok_or("failed to read stylesheet".to_string())?,
),
None => None,
};
let re_opt = usvg::Options {
resources_dir,
dpi: args.dpi as f32,
font_family: args
.font_family
.as_deref()
.unwrap_or("Times New Roman")
.to_string(),
font_size: args.font_size as f32,
languages: args.languages,
shape_rendering: args.shape_rendering,
text_rendering: args.text_rendering,
image_rendering: args.image_rendering,
default_size: usvg::Size::from_wh(args.default_width as f32, args.default_height as f32)
.unwrap(),
image_href_resolver: usvg::ImageHrefResolver::default(),
font_resolver: usvg::FontResolver::default(),
fontdb: Arc::new(fontdb),
style_sheet,
};
let input_svg = match in_svg {
InputFrom::Stdin => load_stdin(),
InputFrom::File(ref path) => std::fs::read(path).map_err(|e| e.to_string()),
}?;
let tree = usvg::Tree::from_data(&input_svg, &re_opt).map_err(|e| format!("{}", e))?;
let xml_opt = usvg::WriteOptions {
id_prefix: args.id_prefix,
preserve_text: args.preserve_text,
coordinates_precision: args.coordinates_precision.unwrap_or(8),
transforms_precision: args.transforms_precision.unwrap_or(8),
use_single_quote: false,
indent: args.indent,
attributes_indent: args.attrs_indent,
};
let s = tree.to_string(&xml_opt);
match out_svg {
OutputTo::Stdout => {
io::stdout()
.write_all(s.as_bytes())
.map_err(|_| "failed to write to the stdout".to_string())?;
}
OutputTo::File(path) => {
let mut f =
File::create(path).map_err(|_| "failed to create the output file".to_string())?;
f.write_all(s.as_bytes())
.map_err(|_| "failed to write to the output file".to_string())?;
}
}
Ok(())
}
fn load_stdin() -> Result<Vec<u8>, String> {
let mut buf = Vec::new();
let stdin = io::stdin();
let mut handle = stdin.lock();
handle
.read_to_end(&mut buf)
.map_err(|_| "failed to read from stdin".to_string())?;
Ok(buf)
}
static LOGGER: SimpleLogger = SimpleLogger;
struct SimpleLogger;
impl log::Log for SimpleLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::LevelFilter::Warn
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
let target = if record.target().len() > 0 {
record.target()
} else {
record.module_path().unwrap_or_default()
};
let line = record.line().unwrap_or(0);
let args = record.args();
match record.level() {
log::Level::Error => eprintln!("Error (in {}:{}): {}", target, line, args),
log::Level::Warn => eprintln!("Warning (in {}:{}): {}", target, line, args),
log::Level::Info => eprintln!("Info (in {}:{}): {}", target, line, args),
log::Level::Debug => eprintln!("Debug (in {}:{}): {}", target, line, args),
log::Level::Trace => eprintln!("Trace (in {}:{}): {}", target, line, args),
}
}
}
fn flush(&self) {}
}