summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSven Neumann <sven@svenfoo.org>2020-10-26 10:44:37 +0100
committerSven Neumann <sven@svenfoo.org>2021-02-03 11:09:56 +0100
commitd1a837acd3b97f93c88096c3c367d1694c1dde2e (patch)
treeb6f619a9710df076d4a78400457419060e449b87
parentd3b2db371f4f3dd79f574cbd027451beee6dacf7 (diff)
downloadlibrsvg-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--.gitignore1
-rw-r--r--Cargo.lock26
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/rsvg-convert/cli.rs317
-rw-r--r--src/bin/rsvg-convert/main.rs10
5 files changed, 354 insertions, 1 deletions
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
@@ -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"
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);
+}