[librsvg/rustify-rsvg-convert: 20/78] rsvg-convert: Start work on a rustified version




commit d1a837acd3b97f93c88096c3c367d1694c1dde2e
Author: Sven Neumann <sven svenfoo org>
Date:   Mon Oct 26 10:44:37 2020 +0100

    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.

 .gitignore                   |   1 -
 Cargo.lock                   |  26 ++++
 Cargo.toml                   |   1 +
 src/bin/rsvg-convert/cli.rs  | 317 +++++++++++++++++++++++++++++++++++++++++++
 src/bin/rsvg-convert/main.rs |  10 ++
 5 files changed, 354 insertions(+), 1 deletion(-)
---
diff --git a/.gitignore b/.gitignore
index 8e626b78..a0833030 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,7 +17,6 @@ librsvg.spec
 libtool
 ltmain.sh
 missing
-rsvg
 rsvg-convert
 stamp-h1
 test-driver
diff --git a/Cargo.lock b/Cargo.lock
index 2349a295..2f9b7e87 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -21,6 +21,15 @@ dependencies = [
  "memchr",
 ]
 
+[[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"
@@ -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",
@@ -1827,6 +1841,12 @@ dependencies = [
  "quote 1.0.7",
 ]
 
+[[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"
@@ -2040,6 +2060,12 @@ version = "0.7.5"
 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"
diff --git a/Cargo.toml b/Cargo.toml
index e9557480..9f511b0d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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);
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]