[librsvg/rustify-rsvg-convert: 43/78] rsvg-convert: consolidate everything in main.rs
- From: Sven Neumann <sneumann src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [librsvg/rustify-rsvg-convert: 43/78] rsvg-convert: consolidate everything in main.rs
- Date: Wed, 3 Feb 2021 10:18:30 +0000 (UTC)
commit ae1047a8eaaee67d897bc6b59e1d5add62b4e365
Author: Paolo Borelli <pborelli gnome org>
Date: Sat Jan 9 15:17:22 2021 +0100
rsvg-convert: consolidate everything in main.rs
The entire program is 700 lines, including tests, I think having a
single source file is simpler.
src/bin/rsvg-convert/cli.rs | 347 ----------------------
src/bin/rsvg-convert/input.rs | 17 --
src/bin/rsvg-convert/main.rs | 625 ++++++++++++++++++++++++++++++++++++++--
src/bin/rsvg-convert/output.rs | 17 --
src/bin/rsvg-convert/size.rs | 106 -------
src/bin/rsvg-convert/surface.rs | 147 ----------
6 files changed, 608 insertions(+), 651 deletions(-)
---
diff --git a/src/bin/rsvg-convert/main.rs b/src/bin/rsvg-convert/main.rs
index d611dbca..381b32d0 100644
--- a/src/bin/rsvg-convert/main.rs
+++ b/src/bin/rsvg-convert/main.rs
@@ -1,26 +1,589 @@
#[macro_use]
extern crate clap;
-mod cli;
-mod input;
-mod output;
-mod size;
-mod surface;
-
-use cssparser::Color;
use gio::prelude::*;
use gio::{
Cancellable, FileCreateFlags, FileExt, InputStream, OutputStream, UnixInputStream,
UnixOutputStream,
};
use librsvg::rsvg_convert_only::LegacySize;
-use librsvg::{CairoRenderer, Loader, RenderingError};
+use librsvg::{CairoRenderer, Color, Loader, Parse, RenderingError};
+use std::ops::Deref;
+use std::path::PathBuf;
+
+#[derive(Clone, Copy, Debug)]
+struct Scale {
+ pub x: f64,
+ pub y: f64,
+}
+
+impl Scale {
+ #[allow(clippy::float_cmp)]
+ pub fn is_identity(&self) -> bool {
+ self.x == 1.0 && self.y == 1.0
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct Size {
+ pub w: f64,
+ pub h: f64,
+}
+
+impl Size {
+ pub fn new(w: f64, h: f64) -> Self {
+ Self { w, h }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+enum ResizeStrategy {
+ Scale(Scale),
+ Fit(u32, u32),
+ FitWidth(u32),
+ FitHeight(u32),
+ FitLargestScale(Scale, Option<u32>, Option<u32>),
+}
+
+impl ResizeStrategy {
+ pub fn apply(self, input: Size, keep_aspect_ratio: bool) -> Result<Size, ()> {
+ if input.w == 0.0 && input.h == 0.0 {
+ return Err(());
+ }
+
+ let output = match self {
+ ResizeStrategy::Scale(s) => Size {
+ w: input.w * s.x,
+ h: input.h * s.y,
+ },
+ ResizeStrategy::Fit(w, h) => Size {
+ w: f64::from(w),
+ h: f64::from(h),
+ },
+ ResizeStrategy::FitWidth(w) => Size {
+ w: f64::from(w),
+ h: input.h * f64::from(w) / input.w,
+ },
+ ResizeStrategy::FitHeight(h) => Size {
+ w: input.w * f64::from(h) / input.h,
+ h: f64::from(h),
+ },
+ ResizeStrategy::FitLargestScale(s, w, h) => {
+ let scaled_input_w = input.w * s.x;
+ let scaled_input_h = input.h * s.y;
+
+ let f = match (w.map(f64::from), h.map(f64::from)) {
+ (Some(w), Some(h)) if w < scaled_input_w || h < scaled_input_h => {
+ let sx = w / scaled_input_w;
+ let sy = h / scaled_input_h;
+ if sx > sy {
+ sy
+ } else {
+ sx
+ }
+ }
+ (Some(w), None) if w < scaled_input_w => w / scaled_input_w,
+ (None, Some(h)) if h < scaled_input_h => h / scaled_input_h,
+ _ => 1.0,
+ };
+
+ Size {
+ w: input.w * f * s.x,
+ h: input.h * f * s.y,
+ }
+ }
+ };
+
+ if !keep_aspect_ratio {
+ return Ok(output);
+ }
+
+ if output.w < output.h {
+ Ok(Size {
+ w: output.w,
+ h: input.h * (output.w / input.w),
+ })
+ } else {
+ Ok(Size {
+ w: input.w * (output.h / input.h),
+ h: output.h,
+ })
+ }
+ }
+}
+
+enum Surface {
+ Png(cairo::ImageSurface, OutputStream),
+ Pdf(cairo::PdfSurface, Size),
+ Ps(cairo::PsSurface, Size),
+ Svg(cairo::SvgSurface, Size),
+}
+
+impl Deref for Surface {
+ type Target = cairo::Surface;
+
+ fn deref(&self) -> &cairo::Surface {
+ match self {
+ Self::Png(surface, _) => &surface,
+ Self::Pdf(surface, _) => &surface,
+ Self::Ps(surface, _) => &surface,
+ Self::Svg(surface, _) => &surface,
+ }
+ }
+}
+
+impl Surface {
+ pub fn new(format: Format, size: Size, stream: OutputStream) -> Result<Self, cairo::Status> {
+ match format {
+ Format::Png => Self::new_for_png(size, stream),
+ Format::Pdf => Self::new_for_pdf(size, stream),
+ Format::Ps => Self::new_for_ps(size, stream, false),
+ Format::Eps => Self::new_for_ps(size, stream, true),
+ Format::Svg => Self::new_for_svg(size, stream),
+ }
+ }
+
+ fn new_for_png(size: Size, stream: OutputStream) -> Result<Self, cairo::Status> {
+ let w = checked_i32(size.w.round())?;
+ let h = checked_i32(size.h.round())?;
+ let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?;
+ Ok(Self::Png(surface, stream))
+ }
+
+ fn new_for_pdf(size: Size, stream: OutputStream) -> Result<Self, cairo::Status> {
+ let surface = cairo::PdfSurface::for_stream(size.w, size.h, stream.into_write())?;
+ if let Some(date) = metadata::creation_date() {
+ surface.set_metadata(cairo::PdfMetadata::CreateDate, &date)?;
+ }
+ Ok(Self::Pdf(surface, size))
+ }
+
+ fn new_for_ps(size: Size, stream: OutputStream, eps: bool) -> Result<Self, cairo::Status> {
+ let surface = cairo::PsSurface::for_stream(size.w, size.h, stream.into_write())?;
+ surface.set_eps(eps);
+ Ok(Self::Ps(surface, size))
+ }
+
+ fn new_for_svg(size: Size, stream: OutputStream) -> Result<Self, cairo::Status> {
+ let surface = cairo::SvgSurface::for_stream(size.w, size.h, stream.into_write())?;
+ Ok(Self::Svg(surface, size))
+ }
+
+ fn size(&self) -> Size {
+ match self {
+ Self::Png(surface, _) => Size {
+ w: surface.get_width() as f64,
+ h: surface.get_height() as f64,
+ },
+ Self::Pdf(_, size) => *size,
+ Self::Ps(_, size) => *size,
+ Self::Svg(_, size) => *size,
+ }
+ }
-use crate::cli::Args;
-use crate::input::Input;
-use crate::output::Output;
-use crate::size::{ResizeStrategy, Size};
-use crate::surface::Surface;
+ fn bounds(&self) -> cairo::Rectangle {
+ let size = self.size();
+ cairo::Rectangle {
+ x: 0.0,
+ y: 0.0,
+ width: size.w,
+ height: size.h,
+ }
+ }
+
+ pub fn render(
+ &self,
+ renderer: &CairoRenderer,
+ cr: &cairo::Context,
+ id: Option<&str>,
+ ) -> Result<(), RenderingError> {
+ let show_page = |_| self.show_page(cr);
+ renderer
+ .render_layer(cr, id, &self.bounds())
+ .and_then(show_page)
+ }
+
+ pub fn show_page(&self, cr: &cairo::Context) -> Result<(), RenderingError> {
+ match self {
+ Self::Png(_, _) => (),
+ _ => cr.show_page(),
+ }
+ Ok(())
+ }
+
+ pub fn finish(self) -> Result<(), cairo::IoError> {
+ match self {
+ Self::Png(surface, stream) => surface.write_to_png(&mut stream.into_write()),
+ _ => match self.finish_output_stream() {
+ Ok(_) => Ok(()),
+ Err(e) => Err(cairo::IoError::Io(std::io::Error::from(e))),
+ },
+ }
+ }
+}
+
+fn checked_i32(x: f64) -> Result<i32, cairo::Status> {
+ cast::i32(x).map_err(|_| cairo::Status::InvalidSize)
+}
+
+mod metadata {
+ use chrono::prelude::*;
+ use std::env;
+ use std::str::FromStr;
+
+ use super::exit;
+
+ pub fn creation_date() -> Option<String> {
+ match env::var("SOURCE_DATE_EPOCH") {
+ Ok(epoch) => {
+ let seconds = i64::from_str(&epoch)
+ .unwrap_or_else(|e| exit!("Environment variable $SOURCE_DATE_EPOCH: {}", e));
+ let datetime = Utc.timestamp(seconds, 0);
+ Some(datetime.to_rfc3339())
+ }
+ Err(env::VarError::NotPresent) => None,
+ Err(env::VarError::NotUnicode(_)) => {
+ exit!("Environment variable $SOURCE_DATE_EPOCH is not valid Unicode")
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+enum Input {
+ Stdin,
+ Path(PathBuf),
+}
+
+impl std::fmt::Display for Input {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Input::Stdin => "stdin".fmt(f),
+ Input::Path(p) => p.display().fmt(f),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+enum Output {
+ Stdout,
+ Path(PathBuf),
+}
+
+impl std::fmt::Display for Output {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Output::Stdout => "stdout".fmt(f),
+ Output::Path(p) => p.display().fmt(f),
+ }
+ }
+}
+
+arg_enum! {
+ #[derive(Clone, Copy, Debug)]
+ enum Format {
+ Png,
+ Pdf,
+ Ps,
+ Eps,
+ Svg,
+ }
+}
+
+#[derive(Debug)]
+struct Args {
+ pub dpi: (f64, f64),
+ pub zoom: Scale,
+ pub width: Option<u32>,
+ pub height: Option<u32>,
+ pub format: Format,
+ pub export_id: Option<String>,
+ pub keep_aspect_ratio: bool,
+ pub background_color: Option<Color>,
+ pub stylesheet: Option<PathBuf>,
+ pub unlimited: bool,
+ pub keep_image_data: bool,
+ pub output: Output,
+ pub input: Vec<Input>,
+}
+
+impl Args {
+ pub fn new() -> Result<Self, clap::Error> {
+ let app = clap::App::new("rsvg-convert")
+ .version(concat!("version ", crate_version!()))
+ .about("Convert SVG files to other image formats")
+ .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")
+ .validator(is_valid_resolution)
+ .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")
+ .validator(is_valid_resolution)
+ .help("Pixels per inch"),
+ )
+ .arg(
+ clap::Arg::with_name("zoom_x")
+ .short("x")
+ .long("x-zoom")
+ .takes_value(true)
+ .value_name("float")
+ .conflicts_with("zoom")
+ .validator(is_valid_zoom_factor)
+ .help("Horizontal zoom factor"),
+ )
+ .arg(
+ clap::Arg::with_name("zoom_y")
+ .short("y")
+ .long("y-zoom")
+ .takes_value(true)
+ .value_name("float")
+ .conflicts_with("zoom")
+ .validator(is_valid_zoom_factor)
+ .help("Vertical zoom factor"),
+ )
+ .arg(
+ clap::Arg::with_name("zoom")
+ .short("z")
+ .long("zoom")
+ .takes_value(true)
+ .value_name("float")
+ .validator(is_valid_zoom_factor)
+ .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 zoom = value_t!(matches, "zoom", f64).or_none()?;
+ let zoom_x = value_t!(matches, "zoom_x", f64).or_none()?;
+ let zoom_y = value_t!(matches, "zoom_y", f64).or_none()?;
+
+ let args = Args {
+ dpi: (
+ value_t!(matches, "res_x", f64)?,
+ value_t!(matches, "res_y", f64)?,
+ ),
+ zoom: Scale {
+ x: zoom.or(zoom_x).unwrap_or(1.0),
+ y: zoom.or(zoom_y).unwrap_or(1.0),
+ },
+ width: value_t!(matches, "size_x", u32).or_none()?,
+ height: value_t!(matches, "size_y", u32).or_none()?,
+ format,
+ 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,
+ output: matches
+ .value_of_os("output")
+ .map(PathBuf::from)
+ .map(Output::Path)
+ .unwrap_or(Output::Stdout),
+ input: match matches.values_of_os("FILE") {
+ Some(values) => values.map(PathBuf::from).map(Input::Path).collect(),
+ None => vec![Input::Stdin],
+ },
+ };
+
+ 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)
+ }
+}
+
+fn is_valid_resolution(v: String) -> Result<(), String> {
+ match v.parse::<f64>() {
+ Ok(res) if res > 0.0 => Ok(()),
+ Ok(_) => Err(String::from("Invalid resolution")),
+ Err(e) => Err(format!("{}", e)),
+ }
+}
+
+fn is_valid_zoom_factor(v: String) -> Result<(), String> {
+ match v.parse::<f64>() {
+ Ok(res) if res > 0.0 => Ok(()),
+ Ok(_) => Err(String::from("Invalid zoom factor")),
+ Err(e) => Err(format!("{}", e)),
+ }
+}
+
+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)
+ }),
+ }
+}
#[macro_export]
macro_rules! exit {
@@ -81,16 +644,16 @@ fn main() {
.unwrap_or_else(|e| exit!("Error applying stylesheet: {}", e));
}
- let renderer = CairoRenderer::new(&handle).with_dpi(args.dpi.x, args.dpi.y);
+ let renderer = CairoRenderer::new(&handle).with_dpi(args.dpi.0, args.dpi.1);
if target.is_none() {
let (width, height) = renderer
- .legacy_layer_size_in_pixels(args.export_id())
+ .legacy_layer_size_in_pixels(args.export_id.as_deref())
.unwrap_or_else(|e| match e {
RenderingError::IdNotFound => exit!(
"File {} does not have an object with id \"{}\")",
input,
- args.export_id().unwrap()
+ args.export_id.as_deref().unwrap()
),
_ => exit!("Error rendering SVG {}: {}", input, e),
});
@@ -155,7 +718,7 @@ fn main() {
cr.scale(args.zoom.x, args.zoom.y);
surface
- .render(&renderer, &cr, args.export_id())
+ .render(&renderer, &cr, args.export_id.as_deref())
.unwrap_or_else(|e| exit!("Error rendering SVG {}: {}", input, e));
}
}
@@ -166,3 +729,31 @@ fn main() {
.unwrap_or_else(|e| exit!("Error saving output: {}", e));
}
}
+
+#[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)
+ );
+ }
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]