diff options
author | Sven Neumann <sven@svenfoo.org> | 2020-10-26 10:44:37 +0100 |
---|---|---|
committer | Sven Neumann <sven@svenfoo.org> | 2021-02-03 11:09:56 +0100 |
commit | d1a837acd3b97f93c88096c3c367d1694c1dde2e (patch) | |
tree | b6f619a9710df076d4a78400457419060e449b87 | |
parent | d3b2db371f4f3dd79f574cbd027451beee6dacf7 (diff) | |
download | librsvg-d1a837acd3b97f93c88096c3c367d1694c1dde2e.tar.gz |
rsvg-convert: Start work on a rustified version
Only command-line parsing is implemented so far. For now this is
using version 2 of the clap crate. As soon as a stable version 3
release is available, this code should be revisited.
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Cargo.lock | 26 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | src/bin/rsvg-convert/cli.rs | 317 | ||||
-rw-r--r-- | src/bin/rsvg-convert/main.rs | 10 |
5 files changed, 354 insertions, 1 deletions
@@ -17,7 +17,6 @@ librsvg.spec libtool ltmain.sh missing -rsvg rsvg-convert stamp-h1 test-driver @@ -22,6 +22,15 @@ dependencies = [ ] [[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] name = "approx" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -188,9 +197,13 @@ version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ + "ansi_term", + "atty", "bitflags", + "strsim", "textwrap", "unicode-width", + "vec_map", ] [[package]] @@ -802,6 +815,7 @@ dependencies = [ "cairo-sys-rs", "cast", "chrono", + "clap", "criterion", "cssparser", "data-url", @@ -1828,6 +1842,12 @@ dependencies = [ ] [[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] name = "syn" version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2041,6 +2061,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" [[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] name = "version_check" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -16,6 +16,7 @@ bitflags = "1.0" cairo-rs = { version="0.8.0", features=["v1_16", "png", "pdf", "svg"] } cairo-sys-rs = "0.9.0" cast = "0.2.3" +clap = "~2.33.0" cssparser = "0.27.1" data-url = "0.1" encoding = "0.2.33" diff --git a/src/bin/rsvg-convert/cli.rs b/src/bin/rsvg-convert/cli.rs new file mode 100644 index 00000000..d5b05731 --- /dev/null +++ b/src/bin/rsvg-convert/cli.rs @@ -0,0 +1,317 @@ +// command-line interface for rsvg-convert + +use std::path::PathBuf; + +use librsvg::{Color, Parse}; + +arg_enum! { + #[derive(Debug)] + pub enum Format { + Png, + Pdf, + Ps, + Eps, + Svg, + } +} + +#[derive(Debug)] +pub struct Args { + resolution: (f32, f32), + zoom: (f32, f32), + width: Option<u32>, + height: Option<u32>, + format: Format, + output: Option<PathBuf>, + export_id: Option<String>, + keep_aspect_ratio: bool, + background_color: Option<Color>, + stylesheet: Option<PathBuf>, + unlimited: bool, + keep_image_data: bool, + input: Vec<PathBuf>, +} + +impl Args { + pub fn new() -> Result<Self, clap::Error> { + let app = clap::App::new("rsvg-convert") + .version(crate_version!()) + .about("SVG converter") + .help_short("?") + .version_short("v") + .arg( + clap::Arg::with_name("res_x") + .short("d") + .long("dpi-x") + .takes_value(true) + .value_name("float") + .default_value("90") + .help("Pixels per inch"), + ) + .arg( + clap::Arg::with_name("res_y") + .short("p") + .long("dpi-y") + .takes_value(true) + .value_name("float") + .default_value("90") + .help("Pixels per inch"), + ) + .arg( + clap::Arg::with_name("zoom_x") + .short("x") + .long("x-zoom") + .takes_value(true) + .value_name("float") + .default_value("1.0") + .conflicts_with("zoom") + .help("Horizontal zoom factor"), + ) + .arg( + clap::Arg::with_name("zoom_y") + .short("y") + .long("y-zoom") + .takes_value(true) + .value_name("float") + .default_value("1.0") + .conflicts_with("zoom") + .help("Vertical zoom factor"), + ) + .arg( + clap::Arg::with_name("zoom") + .short("z") + .long("zoom") + .takes_value(true) + .value_name("float") + .default_value("1.0") + .help("Zoom factor"), + ) + .arg( + clap::Arg::with_name("size_x") + .short("w") + .long("width") + .takes_value(true) + .value_name("pixels") + .help("Width [defaults to the width of the SVG]"), + ) + .arg( + clap::Arg::with_name("size_y") + .short("h") + .long("height") + .takes_value(true) + .value_name("pixels") + .help("Height [defaults to the height of the SVG]"), + ) + .arg( + clap::Arg::with_name("format") + .short("f") + .long("format") + .takes_value(true) + .possible_values(&Format::variants()) + .case_insensitive(true) + .default_value("png") + .help("Output format"), + ) + .arg( + clap::Arg::with_name("output") + .short("o") + .long("output") + .empty_values(false) + .help("Output filename [defaults to stdout]"), + ) + .arg( + clap::Arg::with_name("export_id") + .short("i") + .long("export-id") + .empty_values(false) + .value_name("object id") + .help("SVG id of object to export [default is to export all objects]"), + ) + .arg( + clap::Arg::with_name("keep_aspect") + .short("a") + .long("keep-aspect-ratio") + .help("Preserve the aspect ratio"), + ) + .arg( + clap::Arg::with_name("background") + .short("b") + .long("background-color") + .takes_value(true) + .value_name("color") + .help("Set the background color using a CSS color spec"), + ) + .arg( + clap::Arg::with_name("stylesheet") + .short("s") + .long("stylesheet") + .empty_values(false) + .help("Filename of CSS stylesheet to apply"), + ) + .arg( + clap::Arg::with_name("unlimited") + .short("u") + .long("unlimited") + .help("Allow huge SVG files"), + ) + .arg( + clap::Arg::with_name("keep_image_data") + .long("keep-image-data") + .help("Keep image data"), + ) + .arg( + clap::Arg::with_name("no_keep_image_data") + .long("no-keep-image-data") + .help("Do not keep image data"), + ) + .arg( + clap::Arg::with_name("FILE") + .help("The input file(s) to convert") + .multiple(true), + ); + + let matches = app.get_matches(); + + let format = value_t!(matches, "format", Format)?; + + let keep_image_data = match format { + Format::Ps | Format::Eps | Format::Pdf => !matches.is_present("no_keep_image_data"), + _ => matches.is_present("keep_image_data"), + }; + + let background_color = value_t!(matches, "background", String).and_then(parse_color_string); + + let lookup_id = |id: String| { + // RsvgHandle::has_sub() expects ids to have a '#' prepended to them, + // so it can lookup ids in externs like "subfile.svg#subid". For the + // user's convenience, we include this '#' automatically; we only + // support specifying ids from the toplevel, and don't expect users to + // lookup things in externs. + if id.starts_with('#') { + id + } else { + format!("#{}", id) + } + }; + + let args = Args { + resolution: ( + value_t!(matches, "res_x", f32)?, + value_t!(matches, "res_y", f32)?, + ), + zoom: if matches.is_present("zoom") { + let zoom = value_t!(matches, "zoom", f32)?; + (zoom, zoom) + } else { + let zoom_x = value_t!(matches, "zoom_x", f32)?; + let zoom_y = value_t!(matches, "zoom_y", f32)?; + (zoom_x, zoom_y) + }, + width: value_t!(matches, "size_x", u32).or_none()?, + height: value_t!(matches, "size_y", u32).or_none()?, + format, + output: matches.value_of_os("output").map(PathBuf::from), + export_id: value_t!(matches, "export_id", String) + .or_none()? + .map(lookup_id), + keep_aspect_ratio: matches.is_present("keep_aspect"), + background_color: background_color.or_none()?, + stylesheet: matches.value_of_os("stylesheet").map(PathBuf::from), + unlimited: matches.is_present("unlimited"), + keep_image_data, + input: match matches.values_of_os("FILE") { + Some(values) => values.map(PathBuf::from).collect(), + None => Vec::new(), + }, + }; + + if args.input.len() > 1 { + match args.format { + Format::Ps | Format::Eps | Format::Pdf => (), + _ => { + return Err(clap::Error::with_description( + "Multiple SVG files are only allowed for PDF and (E)PS output.", + clap::ErrorKind::TooManyValues, + )) + } + } + } + + Ok(args) + } +} + +trait NotFound { + type Ok; + type Error; + + fn or_none(self) -> Result<Option<Self::Ok>, Self::Error>; +} + +impl<T> NotFound for Result<T, clap::Error> { + type Ok = T; + type Error = clap::Error; + + /// Maps the Result to an Option, translating the ArgumentNotFound error to + /// Ok(None), while mapping other kinds of errors to Err(e). + /// + /// This allows to get proper error reporting for invalid values on optional + /// arguments. + fn or_none(self) -> Result<Option<T>, clap::Error> { + self.map_or_else( + |e| match e.kind { + clap::ErrorKind::ArgumentNotFound => Ok(None), + _ => Err(e), + }, + |v| Ok(Some(v)), + ) + } +} + +fn parse_color_string(str: String) -> Result<Color, clap::Error> { + parse_color_str(&str) +} + +fn parse_color_str(str: &str) -> Result<Color, clap::Error> { + match str { + "none" | "None" => Err(clap::Error::with_description( + str, + clap::ErrorKind::ArgumentNotFound, + )), + _ => <Color as Parse>::parse_str(str).map_err(|_| { + let desc = format!( + "Invalid value: The argument '{}' can not be parsed as a CSS color value", + str + ); + clap::Error::with_description(&desc, clap::ErrorKind::InvalidValue) + }), + } +} + +#[cfg(test)] +mod tests { + mod color { + use super::super::*; + + #[test] + fn valid_color_is_ok() { + assert!(parse_color_str("Red").is_ok()); + } + + #[test] + fn none_is_handled_as_not_found() { + assert_eq!( + parse_color_str("None").map_err(|e| e.kind), + Err(clap::ErrorKind::ArgumentNotFound) + ); + } + + #[test] + fn invalid_is_handled_as_invalid_value() { + assert_eq!( + parse_color_str("foo").map_err(|e| e.kind), + Err(clap::ErrorKind::InvalidValue) + ); + } + } +} diff --git a/src/bin/rsvg-convert/main.rs b/src/bin/rsvg-convert/main.rs new file mode 100644 index 00000000..c6e6cb7c --- /dev/null +++ b/src/bin/rsvg-convert/main.rs @@ -0,0 +1,10 @@ +#[macro_use] +extern crate clap; + +mod cli; + +fn main() { + let args = cli::Args::new().unwrap_or_else(|e| e.exit()); + + println!("{:?}", args); +} |