summaryrefslogtreecommitdiff
path: root/rsvg_convert/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rsvg_convert/src/main.rs')
-rw-r--r--rsvg_convert/src/main.rs1552
1 files changed, 1552 insertions, 0 deletions
diff --git a/rsvg_convert/src/main.rs b/rsvg_convert/src/main.rs
new file mode 100644
index 00000000..0a8c527d
--- /dev/null
+++ b/rsvg_convert/src/main.rs
@@ -0,0 +1,1552 @@
+use clap::crate_version;
+use clap_complete::{Generator, Shell};
+
+use gio::prelude::*;
+use gio::{Cancellable, FileCreateFlags, InputStream, OutputStream};
+
+#[cfg(unix)]
+use gio::{UnixInputStream, UnixOutputStream};
+
+#[cfg(windows)]
+mod windows_imports {
+ pub use gio::{Win32InputStream, WriteOutputStream};
+ pub use glib::ffi::gboolean;
+ pub use glib::translate::*;
+ pub use libc::c_void;
+ pub use std::io;
+ pub use std::os::windows::io::AsRawHandle;
+}
+#[cfg(windows)]
+use self::windows_imports::*;
+
+use cssparser::{_cssparser_internal_to_lowercase, match_ignore_ascii_case};
+
+use librsvg_c::{handle::PathOrUrl, sizing::LegacySize};
+use rsvg::rsvg_convert_only::{
+ AspectRatio, CssLength, Horizontal, Length, Normalize, NormalizeParams, Parse, Signed, ULength,
+ Unsigned, Validate, Vertical, ViewBox,
+};
+use rsvg::{
+ AcceptLanguage, CairoRenderer, Color, Dpi, Language, LengthUnit, Loader, Rect, RenderingError,
+};
+
+use std::ffi::OsString;
+use std::io;
+use std::ops::Deref;
+use std::path::PathBuf;
+
+#[derive(Debug)]
+pub struct Error(String);
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
+impl From<cairo::Error> for Error {
+ fn from(s: cairo::Error) -> Self {
+ match s {
+ cairo::Error::InvalidSize => Self(String::from(
+ "The resulting image would be larger than 32767 pixels on either dimension.\n\
+ Librsvg currently cannot render to images bigger than that.\n\
+ Please specify a smaller size.",
+ )),
+ e => Self(format!("{e}")),
+ }
+ }
+}
+
+macro_rules! impl_error_from {
+ ($err:ty) => {
+ impl From<$err> for Error {
+ fn from(e: $err) -> Self {
+ Self(format!("{e}"))
+ }
+ }
+ };
+}
+
+impl_error_from!(RenderingError);
+impl_error_from!(cairo::IoError);
+impl_error_from!(cairo::StreamWithError);
+impl_error_from!(clap::Error);
+
+macro_rules! error {
+ ($($arg:tt)*) => (Error(std::format!($($arg)*)));
+}
+
+#[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, PartialEq)]
+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 {
+ size: Size,
+ keep_aspect_ratio: bool,
+ },
+ FitWidth(f64),
+ FitHeight(f64),
+ ScaleWithMaxSize {
+ scale: Scale,
+ max_width: Option<f64>,
+ max_height: Option<f64>,
+ keep_aspect_ratio: bool,
+ },
+}
+
+impl ResizeStrategy {
+ pub fn apply(self, input: &Size) -> Option<Size> {
+ if input.w == 0.0 || input.h == 0.0 {
+ return None;
+ }
+
+ let output_size = match self {
+ ResizeStrategy::Scale(s) => Size::new(input.w * s.x, input.h * s.y),
+
+ ResizeStrategy::Fit {
+ size,
+ keep_aspect_ratio,
+ } => {
+ if keep_aspect_ratio {
+ let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap();
+ let rect = aspect.compute(
+ &ViewBox::from(Rect::from_size(input.w, input.h)),
+ &Rect::from_size(size.w, size.h),
+ );
+ Size::new(rect.width(), rect.height())
+ } else {
+ size
+ }
+ }
+
+ ResizeStrategy::FitWidth(w) => Size::new(w, input.h * w / input.w),
+
+ ResizeStrategy::FitHeight(h) => Size::new(input.w * h / input.h, h),
+
+ ResizeStrategy::ScaleWithMaxSize {
+ scale,
+ max_width,
+ max_height,
+ keep_aspect_ratio,
+ } => {
+ let scaled = Size::new(input.w * scale.x, input.h * scale.y);
+
+ match (max_width, max_height, keep_aspect_ratio) {
+ (None, None, _) => scaled,
+
+ (Some(max_width), Some(max_height), false) => {
+ if scaled.w <= max_width && scaled.h <= max_height {
+ scaled
+ } else {
+ Size::new(max_width, max_height)
+ }
+ }
+
+ (Some(max_width), Some(max_height), true) => {
+ if scaled.w <= max_width && scaled.h <= max_height {
+ scaled
+ } else {
+ let aspect = AspectRatio::parse_str("xMinYMin meet").unwrap();
+ let rect = aspect.compute(
+ &ViewBox::from(Rect::from_size(scaled.w, scaled.h)),
+ &Rect::from_size(max_width, max_height),
+ );
+ Size::new(rect.width(), rect.height())
+ }
+ }
+
+ (Some(max_width), None, false) => {
+ if scaled.w <= max_width {
+ scaled
+ } else {
+ Size::new(max_width, scaled.h)
+ }
+ }
+
+ (Some(max_width), None, true) => {
+ if scaled.w <= max_width {
+ scaled
+ } else {
+ let factor = max_width / scaled.w;
+ Size::new(max_width, scaled.h * factor)
+ }
+ }
+
+ (None, Some(max_height), false) => {
+ if scaled.h <= max_height {
+ scaled
+ } else {
+ Size::new(scaled.w, max_height)
+ }
+ }
+
+ (None, Some(max_height), true) => {
+ if scaled.h <= max_height {
+ scaled
+ } else {
+ let factor = max_height / scaled.h;
+ Size::new(scaled.w * factor, max_height)
+ }
+ }
+ }
+ }
+ };
+
+ Some(output_size)
+ }
+}
+
+enum Surface {
+ Png(cairo::ImageSurface, OutputStream),
+ #[cfg(system_deps_have_cairo_pdf)]
+ Pdf(cairo::PdfSurface, Size),
+ #[cfg(system_deps_have_cairo_ps)]
+ Ps(cairo::PsSurface, Size),
+ #[cfg(system_deps_have_cairo_svg)]
+ Svg(cairo::SvgSurface, Size),
+}
+
+impl Deref for Surface {
+ type Target = cairo::Surface;
+
+ fn deref(&self) -> &cairo::Surface {
+ match self {
+ Self::Png(surface, _) => surface,
+ #[cfg(system_deps_have_cairo_pdf)]
+ Self::Pdf(surface, _) => surface,
+ #[cfg(system_deps_have_cairo_ps)]
+ Self::Ps(surface, _) => surface,
+ #[cfg(system_deps_have_cairo_svg)]
+ Self::Svg(surface, _) => surface,
+ }
+ }
+}
+
+impl AsRef<cairo::Surface> for Surface {
+ fn as_ref(&self) -> &cairo::Surface {
+ self
+ }
+}
+
+impl Surface {
+ pub fn new(
+ format: Format,
+ size: Size,
+ stream: OutputStream,
+ unit: LengthUnit,
+ ) -> Result<Self, Error> {
+ 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, unit),
+ }
+ }
+
+ fn new_for_png(size: Size, stream: OutputStream) -> Result<Self, Error> {
+ // We use ceil() to avoid chopping off the last pixel if it is partially covered.
+ let w = checked_i32(size.w.ceil())?;
+ let h = checked_i32(size.h.ceil())?;
+ let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w, h)?;
+ Ok(Self::Png(surface, stream))
+ }
+
+ #[cfg(system_deps_have_cairo_pdf)]
+ fn new_for_pdf(size: Size, stream: OutputStream) -> Result<Self, Error> {
+ 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))
+ }
+
+ #[cfg(not(system_deps_have_cairo_pdf))]
+ fn new_for_pdf(_size: Size, _stream: OutputStream) -> Result<Self, Error> {
+ Err(Error("unsupported format".to_string()))
+ }
+
+ #[cfg(system_deps_have_cairo_ps)]
+ fn new_for_ps(size: Size, stream: OutputStream, eps: bool) -> Result<Self, Error> {
+ let surface = cairo::PsSurface::for_stream(size.w, size.h, stream.into_write())?;
+ surface.set_eps(eps);
+ Ok(Self::Ps(surface, size))
+ }
+
+ #[cfg(not(system_deps_have_cairo_ps))]
+ fn new_for_ps(_size: Size, _stream: OutputStream, _eps: bool) -> Result<Self, Error> {
+ Err(Error("unsupported format".to_string()))
+ }
+
+ #[cfg(system_deps_have_cairo_svg)]
+ fn new_for_svg(size: Size, stream: OutputStream, unit: LengthUnit) -> Result<Self, Error> {
+ let mut surface = cairo::SvgSurface::for_stream(size.w, size.h, stream.into_write())?;
+
+ let svg_unit = match unit {
+ LengthUnit::Cm => cairo::SvgUnit::Cm,
+ LengthUnit::In => cairo::SvgUnit::In,
+ LengthUnit::Mm => cairo::SvgUnit::Mm,
+ LengthUnit::Pc => cairo::SvgUnit::Pc,
+ LengthUnit::Pt => cairo::SvgUnit::Pt,
+ _ => cairo::SvgUnit::User,
+ };
+
+ surface.set_document_unit(svg_unit);
+ Ok(Self::Svg(surface, size))
+ }
+
+ #[cfg(not(system_deps_have_cairo_svg))]
+ fn new_for_svg(_size: Size, _stream: OutputStream, u: LengthUnit) -> Result<Self, Error> {
+ Err(Error("unsupported format".to_string()))
+ }
+
+ #[allow(clippy::too_many_arguments)] // yeah, yeah, we'll refactor it eventually
+ pub fn render(
+ &self,
+ renderer: &CairoRenderer,
+ left: f64,
+ top: f64,
+ final_size: Size,
+ geometry: cairo::Rectangle,
+ background_color: Option<Color>,
+ id: Option<&str>,
+ ) -> Result<(), Error> {
+ let cr = cairo::Context::new(self)?;
+
+ if let Some(Color::RGBA(rgba)) = background_color {
+ cr.set_source_rgba(
+ rgba.red_f32().into(),
+ rgba.green_f32().into(),
+ rgba.blue_f32().into(),
+ rgba.alpha_f32().into(),
+ );
+
+ cr.paint()?;
+ }
+
+ cr.translate(left, top);
+
+ // Note that we don't scale the viewport; we change the cr's transform instead. This
+ // is because SVGs are rendered proportionally to fit within the viewport, regardless
+ // of the viewport's proportions. Rsvg-convert allows non-proportional scaling, so
+ // we do that with a separate transform.
+
+ let scale = Scale {
+ x: final_size.w / geometry.width(),
+ y: final_size.h / geometry.height(),
+ };
+
+ cr.scale(scale.x, scale.y);
+
+ let viewport = cairo::Rectangle::new(0.0, 0.0, geometry.width(), geometry.height());
+
+ match id {
+ None => renderer.render_document(&cr, &viewport)?,
+ Some(_) => renderer.render_element(&cr, id, &viewport)?,
+ }
+
+ if !matches!(self, Self::Png(_, _)) {
+ cr.show_page()?;
+ }
+
+ Ok(())
+ }
+
+ pub fn finish(self) -> Result<(), Error> {
+ match self {
+ Self::Png(surface, stream) => surface.write_to_png(&mut stream.into_write())?,
+ _ => self.finish_output_stream().map(|_| ())?,
+ }
+
+ Ok(())
+ }
+}
+
+fn checked_i32(x: f64) -> Result<i32, cairo::Error> {
+ cast::i32(x).map_err(|_| cairo::Error::InvalidSize)
+}
+
+mod metadata {
+ use super::Error;
+ use chrono::prelude::*;
+ use std::env;
+ use std::str::FromStr;
+
+ pub fn creation_date() -> Result<Option<String>, Error> {
+ match env::var("SOURCE_DATE_EPOCH") {
+ Ok(epoch) => match i64::from_str(&epoch) {
+ Ok(seconds) => {
+ let datetime = Utc.timestamp_opt(seconds, 0).unwrap();
+ Ok(Some(datetime.to_rfc3339()))
+ }
+ Err(e) => Err(error!("Environment variable $SOURCE_DATE_EPOCH: {}", e)),
+ },
+ Err(env::VarError::NotPresent) => Ok(None),
+ Err(env::VarError::NotUnicode(_)) => Err(error!(
+ "Environment variable $SOURCE_DATE_EPOCH is not valid Unicode"
+ )),
+ }
+ }
+}
+
+struct Stdin;
+
+impl Stdin {
+ #[cfg(unix)]
+ pub fn stream() -> InputStream {
+ let stream = unsafe { UnixInputStream::with_fd(0) };
+ stream.upcast::<InputStream>()
+ }
+
+ #[cfg(windows)]
+ pub fn stream() -> InputStream {
+ let stream = unsafe { Win32InputStream::with_handle(io::stdin()) };
+ stream.upcast::<InputStream>()
+ }
+}
+
+struct Stdout;
+
+impl Stdout {
+ #[cfg(unix)]
+ pub fn stream() -> OutputStream {
+ let stream = unsafe { UnixOutputStream::with_fd(1) };
+ stream.upcast::<OutputStream>()
+ }
+
+ #[cfg(windows)]
+ pub fn stream() -> OutputStream {
+ // Ideally, we could use a Win32OutputStream, but when it's used with a file redirect,
+ // it gets buggy.
+ // https://gitlab.gnome.org/GNOME/librsvg/-/issues/812
+ let stream = WriteOutputStream::new(io::stdout());
+ stream.upcast::<OutputStream>()
+ }
+}
+
+#[derive(Clone, Debug)]
+enum Input {
+ Stdin,
+ Named(PathOrUrl),
+}
+
+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::Named(p) => p.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),
+ }
+ }
+}
+
+// Keep this enum in sync with supported_formats in parse_args()
+#[derive(Clone, Copy, Debug)]
+enum Format {
+ Png,
+ Pdf,
+ Ps,
+ Eps,
+ Svg,
+}
+
+struct Converter {
+ pub dpi_x: Resolution,
+ pub dpi_y: Resolution,
+ pub zoom: Scale,
+ pub width: Option<ULength<Horizontal>>,
+ pub height: Option<ULength<Vertical>>,
+ pub left: Option<Length<Horizontal>>,
+ pub top: Option<Length<Vertical>>,
+ pub page_size: Option<(ULength<Horizontal>, ULength<Vertical>)>,
+ pub format: Format,
+ pub export_id: Option<String>,
+ pub keep_aspect_ratio: bool,
+ pub background_color: Option<Color>,
+ pub stylesheet: Option<PathBuf>,
+ pub language: Language,
+ pub unlimited: bool,
+ pub keep_image_data: bool,
+ pub input: Vec<Input>,
+ pub output: Output,
+ pub testing: bool,
+}
+
+impl Converter {
+ pub fn convert(self) -> Result<(), Error> {
+ let stylesheet = match self.stylesheet {
+ Some(ref p) => std::fs::read_to_string(p)
+ .map(Some)
+ .map_err(|e| error!("Error reading stylesheet: {}", e))?,
+ None => None,
+ };
+
+ let mut surface: Option<Surface> = None;
+
+ // Use user units per default
+ let mut unit = LengthUnit::Px;
+
+ fn set_unit<N: Normalize, V: Validate>(
+ l: CssLength<N, V>,
+ p: &NormalizeParams,
+ u: LengthUnit,
+ ) -> f64 {
+ match u {
+ LengthUnit::Pt => l.to_points(p),
+ LengthUnit::In => l.to_inches(p),
+ LengthUnit::Cm => l.to_cm(p),
+ LengthUnit::Mm => l.to_mm(p),
+ LengthUnit::Pc => l.to_picas(p),
+ _ => l.to_user(p),
+ }
+ }
+
+ for (page_idx, input) in self.input.iter().enumerate() {
+ let (stream, basefile) = match input {
+ Input::Stdin => (Stdin::stream(), None),
+ Input::Named(p) => {
+ let file = p.get_gfile();
+ let stream = file
+ .read(None::<&Cancellable>)
+ .map_err(|e| error!("Error reading file \"{}\": {}", input, e))?;
+ (stream.upcast::<InputStream>(), Some(file))
+ }
+ };
+
+ let mut handle = Loader::new()
+ .with_unlimited_size(self.unlimited)
+ .keep_image_data(self.keep_image_data)
+ .read_stream(&stream, basefile.as_ref(), None::<&Cancellable>)
+ .map_err(|e| error!("Error reading SVG {}: {}", input, e))?;
+
+ if let Some(ref css) = stylesheet {
+ handle
+ .set_stylesheet(css)
+ .map_err(|e| error!("Error applying stylesheet: {}", e))?;
+ }
+
+ let renderer = CairoRenderer::new(&handle)
+ .with_dpi(self.dpi_x.0, self.dpi_y.0)
+ .with_language(&self.language)
+ .test_mode(self.testing);
+
+ let geometry = natural_geometry(&renderer, input, self.export_id.as_deref())?;
+
+ let natural_size = Size::new(geometry.width(), geometry.height());
+
+ let params = NormalizeParams::from_dpi(Dpi::new(self.dpi_x.0, self.dpi_y.0));
+
+ // Convert natural size and requested size to pixels or points, depending on the target format,
+ let (natural_size, requested_width, requested_height, page_size) = match self.format {
+ Format::Png => {
+ // PNG surface requires units in pixels
+ (
+ natural_size,
+ self.width.map(|l| l.to_user(&params)),
+ self.height.map(|l| l.to_user(&params)),
+ self.page_size.map(|(w, h)| Size {
+ w: w.to_user(&params),
+ h: h.to_user(&params),
+ }),
+ )
+ }
+
+ Format::Pdf | Format::Ps | Format::Eps => {
+ // These surfaces require units in points
+ unit = LengthUnit::Pt;
+
+ (
+ Size {
+ w: ULength::<Horizontal>::new(natural_size.w, LengthUnit::Px)
+ .to_points(&params),
+ h: ULength::<Vertical>::new(natural_size.h, LengthUnit::Px)
+ .to_points(&params),
+ },
+ self.width.map(|l| l.to_points(&params)),
+ self.height.map(|l| l.to_points(&params)),
+ self.page_size.map(|(w, h)| Size {
+ w: w.to_points(&params),
+ h: h.to_points(&params),
+ }),
+ )
+ }
+
+ Format::Svg => {
+ let (w_unit, h_unit) =
+ (self.width.map(|l| l.unit), self.height.map(|l| l.unit));
+
+ unit = match (w_unit, h_unit) {
+ (None, None) => LengthUnit::Px,
+ (None, u) | (u, None) => u.unwrap(),
+ (u1, u2) => {
+ if u1 == u2 {
+ u1.unwrap()
+ } else {
+ LengthUnit::Px
+ }
+ }
+ };
+
+ // Supported SVG units are px, in, cm, mm, pt, pc
+ (
+ Size {
+ w: set_unit(
+ ULength::<Horizontal>::new(natural_size.w, LengthUnit::Px),
+ &params,
+ unit,
+ ),
+ h: set_unit(
+ ULength::<Vertical>::new(natural_size.h, LengthUnit::Px),
+ &params,
+ unit,
+ ),
+ },
+ self.width.map(|l| set_unit(l, &params, unit)),
+ self.height.map(|l| set_unit(l, &params, unit)),
+ self.page_size.map(|(w, h)| Size {
+ w: set_unit(w, &params, unit),
+ h: set_unit(h, &params, unit),
+ }),
+ )
+ }
+ };
+
+ let strategy = match (requested_width, requested_height) {
+ // when w and h are not specified, scale to the requested zoom (if any)
+ (None, None) => ResizeStrategy::Scale(self.zoom),
+
+ // when w and h are specified, but zoom is not, scale to the requested size
+ (Some(width), Some(height)) if self.zoom.is_identity() => ResizeStrategy::Fit {
+ size: Size::new(width, height),
+ keep_aspect_ratio: self.keep_aspect_ratio,
+ },
+
+ // if only one between w and h is specified and there is no zoom, scale to the
+ // requested w or h and use the same scaling factor for the other
+ (Some(w), None) if self.zoom.is_identity() => ResizeStrategy::FitWidth(w),
+ (None, Some(h)) if self.zoom.is_identity() => ResizeStrategy::FitHeight(h),
+
+ // otherwise scale the image, but cap the zoom to match the requested size
+ _ => ResizeStrategy::ScaleWithMaxSize {
+ scale: self.zoom,
+ max_width: requested_width,
+ max_height: requested_height,
+ keep_aspect_ratio: self.keep_aspect_ratio,
+ },
+ };
+
+ let final_size = self.final_size(&strategy, &natural_size, input)?;
+
+ // Create the surface once on the first input,
+ // except for PDF, PS, and EPS, which allow differently-sized pages.
+ let page_size = page_size.unwrap_or(final_size);
+ let s = match &mut surface {
+ Some(s) => {
+ match s {
+ #[cfg(system_deps_have_cairo_pdf)]
+ Surface::Pdf(pdf, size) => {
+ pdf.set_size(page_size.w, page_size.h).map_err(|e| {
+ error!(
+ "Error setting PDF page #{} size {}: {}",
+ page_idx + 1,
+ input,
+ e
+ )
+ })?;
+ *size = page_size;
+ }
+ #[cfg(system_deps_have_cairo_ps)]
+ Surface::Ps(ps, size) => {
+ ps.set_size(page_size.w, page_size.h);
+ *size = page_size;
+ }
+ _ => {}
+ }
+ s
+ }
+ surface @ None => surface.insert(self.create_surface(page_size, unit)?),
+ };
+
+ let left = self.left.map(|l| set_unit(l, &params, unit)).unwrap_or(0.0);
+ let top = self.top.map(|l| set_unit(l, &params, unit)).unwrap_or(0.0);
+
+ s.render(
+ &renderer,
+ left,
+ top,
+ final_size,
+ geometry,
+ self.background_color,
+ self.export_id.as_deref(),
+ )
+ .map_err(|e| error!("Error rendering SVG {}: {}", input, e))?
+ }
+
+ if let Some(s) = surface.take() {
+ s.finish()
+ .map_err(|e| error!("Error saving output {}: {}", self.output, e))?
+ };
+
+ Ok(())
+ }
+
+ fn final_size(
+ &self,
+ strategy: &ResizeStrategy,
+ natural_size: &Size,
+ input: &Input,
+ ) -> Result<Size, Error> {
+ strategy
+ .apply(natural_size)
+ .ok_or_else(|| error!("The SVG {} has no dimensions", input))
+ }
+
+ fn create_surface(&self, size: Size, unit: LengthUnit) -> Result<Surface, Error> {
+ let output_stream = match self.output {
+ Output::Stdout => Stdout::stream(),
+ Output::Path(ref p) => {
+ let file = gio::File::for_path(p);
+ let stream = file
+ .replace(None, false, FileCreateFlags::NONE, None::<&Cancellable>)
+ .map_err(|e| error!("Error opening output \"{}\": {}", self.output, e))?;
+ stream.upcast::<OutputStream>()
+ }
+ };
+
+ Surface::new(self.format, size, output_stream, unit)
+ }
+}
+
+fn natural_geometry(
+ renderer: &CairoRenderer,
+ input: &Input,
+ export_id: Option<&str>,
+) -> Result<cairo::Rectangle, Error> {
+ match export_id {
+ None => renderer.legacy_layer_geometry(None),
+ Some(id) => renderer.geometry_for_element(Some(id)),
+ }
+ .map(|(ink_r, _)| ink_r)
+ .map_err(|e| match e {
+ RenderingError::IdNotFound => error!(
+ "File {} does not have an object with id \"{}\")",
+ input,
+ export_id.unwrap()
+ ),
+ _ => error!("Error rendering SVG {}: {}", input, e),
+ })
+}
+
+fn build_cli() -> clap::Command {
+ let supported_formats = vec![
+ "png",
+ #[cfg(system_deps_have_cairo_pdf)]
+ "pdf",
+ #[cfg(system_deps_have_cairo_ps)]
+ "ps",
+ #[cfg(system_deps_have_cairo_ps)]
+ "eps",
+ #[cfg(system_deps_have_cairo_svg)]
+ "svg",
+ ];
+
+ clap::Command::new("rsvg-convert")
+ .version(concat!("version ", crate_version!()))
+ .about("Convert SVG files to other image formats")
+ .disable_version_flag(true)
+ .disable_help_flag(true)
+ .arg(
+ clap::Arg::new("help")
+ .short('?')
+ .long("help")
+ .help("Display the help")
+ .action(clap::ArgAction::Help)
+ )
+ .arg(
+ clap::Arg::new("version")
+ .short('v')
+ .long("version")
+ .help("Display the version information")
+ .action(clap::ArgAction::Version)
+ )
+ .arg(
+ clap::Arg::new("res_x")
+ .short('d')
+ .long("dpi-x")
+ .num_args(1)
+ .value_name("number")
+ .default_value("96")
+ .value_parser(parse_resolution)
+ .help("Pixels per inch")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("res_y")
+ .short('p')
+ .long("dpi-y")
+ .num_args(1)
+ .value_name("number")
+ .default_value("96")
+ .value_parser(parse_resolution)
+ .help("Pixels per inch")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("zoom_x")
+ .short('x')
+ .long("x-zoom")
+ .num_args(1)
+ .value_name("number")
+ .conflicts_with("zoom")
+ .value_parser(parse_zoom_factor)
+ .help("Horizontal zoom factor")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("zoom_y")
+ .short('y')
+ .long("y-zoom")
+ .num_args(1)
+ .value_name("number")
+ .conflicts_with("zoom")
+ .value_parser(parse_zoom_factor)
+ .help("Vertical zoom factor")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("zoom")
+ .short('z')
+ .long("zoom")
+ .num_args(1)
+ .value_name("number")
+ .value_parser(parse_zoom_factor)
+ .help("Zoom factor")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("size_x")
+ .short('w')
+ .long("width")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Horizontal, Unsigned>)
+ .help("Width [defaults to the width of the SVG]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("size_y")
+ .short('h')
+ .long("height")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Vertical, Unsigned>)
+ .help("Height [defaults to the height of the SVG]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("top")
+ .long("top")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Vertical, Signed>)
+ .help("Distance between top edge of page and the image [defaults to 0]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("left")
+ .long("left")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Horizontal, Signed>)
+ .help("Distance between left edge of page and the image [defaults to 0]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("page_width")
+ .long("page-width")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Horizontal, Unsigned>)
+ .help("Width of output media [defaults to the width of the SVG]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("page_height")
+ .long("page-height")
+ .num_args(1)
+ .value_name("length")
+ .value_parser(parse_length::<Vertical, Unsigned>)
+ .help("Height of output media [defaults to the height of the SVG]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("format")
+ .short('f')
+ .long("format")
+ .num_args(1)
+ .value_parser(clap::builder::PossibleValuesParser::new(supported_formats.as_slice()))
+ .ignore_case(true)
+ .default_value("png")
+ .help("Output format")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("output")
+ .short('o')
+ .long("output")
+ .num_args(1)
+ .value_parser(clap::value_parser!(PathBuf))
+ .help("Output filename [defaults to stdout]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("export_id")
+ .short('i')
+ .long("export-id")
+ .value_parser(clap::builder::NonEmptyStringValueParser::new())
+ .value_name("object id")
+ .help("SVG id of object to export [default is to export all objects]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("accept-language")
+ .short('l')
+ .long("accept-language")
+ .value_parser(clap::builder::NonEmptyStringValueParser::new())
+ .value_name("languages")
+ .help("Languages to accept, for example \"es-MX,de,en\" [default uses language from the environment]")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("keep_aspect")
+ .short('a')
+ .long("keep-aspect-ratio")
+ .help("Preserve the aspect ratio")
+ .action(clap::ArgAction::SetTrue),
+ )
+ .arg(
+ clap::Arg::new("background")
+ .short('b')
+ .long("background-color")
+ .num_args(1)
+ .value_name("color")
+ .value_parser(clap::builder::NonEmptyStringValueParser::new())
+ .default_value("none")
+ .help("Set the background color using a CSS color spec")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("stylesheet")
+ .short('s')
+ .long("stylesheet")
+ .num_args(1)
+ .value_parser(clap::value_parser!(PathBuf))
+ .value_name("filename.css")
+ .help("Filename of CSS stylesheet to apply")
+ .action(clap::ArgAction::Set),
+ )
+ .arg(
+ clap::Arg::new("unlimited")
+ .short('u')
+ .long("unlimited")
+ .help("Allow huge SVG files")
+ .action(clap::ArgAction::SetTrue),
+ )
+ .arg(
+ clap::Arg::new("keep_image_data")
+ .long("keep-image-data")
+ .help("Keep image data")
+ .conflicts_with("no_keep_image_data")
+ .action(clap::ArgAction::SetTrue),
+ )
+ .arg(
+ clap::Arg::new("no_keep_image_data")
+ .long("no-keep-image-data")
+ .help("Do not keep image data")
+ .conflicts_with("keep_image_data")
+ .action(clap::ArgAction::SetTrue),
+ )
+ .arg(
+ clap::Arg::new("testing")
+ .long("testing")
+ .help("Render images for librsvg's test suite")
+ .hide(true)
+ .action(clap::ArgAction::SetTrue),
+ )
+ .arg(
+ clap::Arg::new("completion")
+ .long("completion")
+ .help("Output shell completion for the given shell")
+ .num_args(1)
+ .action(clap::ArgAction::Set)
+ .value_parser(clap::value_parser!(Shell)),
+ )
+ .arg(
+ clap::Arg::new("FILE")
+ .value_parser(clap::value_parser!(OsString))
+ .help("The input file(s) to convert")
+ .num_args(1..)
+ .action(clap::ArgAction::Append),
+ )
+}
+
+fn print_completions<G: Generator>(gen: G, cmd: &mut clap::Command) {
+ clap_complete::generate(gen, cmd, cmd.get_name().to_string(), &mut io::stdout());
+}
+
+fn parse_args() -> Result<Converter, Error> {
+ let cli = build_cli();
+ let matches = cli.get_matches();
+
+ if let Some(shell) = matches.get_one::<Shell>("completion").copied() {
+ let mut cmd = build_cli();
+ eprintln!("Generating completion file for {shell}");
+ print_completions(shell, &mut cmd);
+ std::process::exit(0);
+ }
+
+ let format_str: &String = matches
+ .get_one("format")
+ .expect("already provided default_value");
+
+ let format = match_ignore_ascii_case! {
+ format_str,
+ "png" => Format::Png,
+ "pdf" => Format::Pdf,
+ "ps" => Format::Ps,
+ "eps" => Format::Eps,
+ "svg" => Format::Svg,
+ _ => unreachable!("clap should already have the list of possible values"),
+ };
+
+ let keep_image_data = match format {
+ Format::Ps | Format::Eps | Format::Pdf => !matches.get_flag("no_keep_image_data"),
+ _ => matches.get_flag("keep_image_data"),
+ };
+
+ let language = match matches.get_one::<String>("accept-language") {
+ None => Language::FromEnvironment,
+ Some(s) => AcceptLanguage::parse(s)
+ .map(Language::AcceptLanguage)
+ .map_err(|e| {
+ let desc = format!("{e}");
+ clap::Error::raw(clap::error::ErrorKind::InvalidValue, desc)
+ })?,
+ };
+
+ let background_str: &String = matches
+ .get_one("background")
+ .expect("already provided default_value");
+ let background_color: Option<Color> = parse_background_color(background_str)
+ .map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))?;
+
+ // librsvg expects ids starting with '#', so it can lookup ids in externs like "subfile.svg#subid".
+ // For the user's convenience, we prepend '#' automatically; we only support specifying ids from
+ // the toplevel, and don't expect users to lookup things in externs.
+ let lookup_id = |id: &String| {
+ if id.starts_with('#') {
+ id.clone()
+ } else {
+ format!("#{id}")
+ }
+ };
+
+ let width: Option<ULength<Horizontal>> = matches.get_one("size_x").copied();
+ let height: Option<ULength<Vertical>> = matches.get_one("size_y").copied();
+
+ let left: Option<Length<Horizontal>> = matches.get_one("left").copied();
+ let top: Option<Length<Vertical>> = matches.get_one("top").copied();
+
+ let page_width: Option<ULength<Horizontal>> = matches.get_one("page_width").copied();
+ let page_height: Option<ULength<Vertical>> = matches.get_one("page_height").copied();
+
+ let page_size = match (page_width, page_height) {
+ (None, None) => None,
+ (Some(_), None) | (None, Some(_)) => {
+ return Err(error!(
+ "Please specify both the --page-width and --page-height options together."
+ ));
+ }
+ (Some(w), Some(h)) => Some((w, h)),
+ };
+
+ let dpi_x = *matches
+ .get_one::<Resolution>("res_x")
+ .expect("already provided default_value");
+ let dpi_y = *matches
+ .get_one::<Resolution>("res_y")
+ .expect("already provided default_value");
+
+ let zoom: Option<ZoomFactor> = matches.get_one("zoom").copied();
+ let zoom_x: Option<ZoomFactor> = matches.get_one("zoom_x").copied();
+ let zoom_y: Option<ZoomFactor> = matches.get_one("zoom_y").copied();
+
+ let input = match matches.get_many::<std::ffi::OsString>("FILE") {
+ Some(values) => values
+ .map(|f| PathOrUrl::from_os_str(f).map_err(Error))
+ .map(|r| r.map(Input::Named))
+ .collect::<Result<Vec<Input>, Error>>()?,
+
+ None => vec![Input::Stdin],
+ };
+
+ if input.len() > 1 && !matches!(format, Format::Ps | Format::Eps | Format::Pdf) {
+ return Err(error!(
+ "Multiple SVG files are only allowed for PDF and (E)PS output."
+ ));
+ }
+
+ let export_id: Option<String> = matches.get_one::<String>("export_id").map(lookup_id);
+
+ let output = match matches.get_one::<PathBuf>("output") {
+ None => Output::Stdout,
+ Some(path) => Output::Path(path.clone()),
+ };
+
+ Ok(Converter {
+ dpi_x,
+ dpi_y,
+ zoom: Scale {
+ x: zoom.or(zoom_x).map(|factor| factor.0).unwrap_or(1.0),
+ y: zoom.or(zoom_y).map(|factor| factor.0).unwrap_or(1.0),
+ },
+ width,
+ height,
+ left,
+ top,
+ page_size,
+ format,
+ export_id,
+ keep_aspect_ratio: matches.get_flag("keep_aspect"),
+ background_color,
+ stylesheet: matches.get_one("stylesheet").cloned(),
+ unlimited: matches.get_flag("unlimited"),
+ keep_image_data,
+ language,
+ input,
+ output,
+ testing: matches.get_flag("testing"),
+ })
+}
+
+#[derive(Copy, Clone)]
+struct Resolution(f64);
+
+fn parse_resolution(v: &str) -> Result<Resolution, String> {
+ match v.parse::<f64>() {
+ Ok(res) if res > 0.0 => Ok(Resolution(res)),
+ Ok(_) => Err(String::from("Invalid resolution")),
+ Err(e) => Err(format!("{e}")),
+ }
+}
+
+#[derive(Copy, Clone)]
+struct ZoomFactor(f64);
+
+fn parse_zoom_factor(v: &str) -> Result<ZoomFactor, String> {
+ match v.parse::<f64>() {
+ Ok(res) if res > 0.0 => Ok(ZoomFactor(res)),
+ 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::error::ErrorKind::UnknownArgument => Ok(None),
+ _ => Err(e),
+ },
+ |v| Ok(Some(v)),
+ )
+ }
+}
+
+fn parse_background_color(s: &str) -> Result<Option<Color>, String> {
+ match s {
+ "none" | "None" => Ok(None),
+ _ => <Color as Parse>::parse_str(s).map(Some).map_err(|_| {
+ format!("Invalid value: The argument '{s}' can not be parsed as a CSS color value")
+ }),
+ }
+}
+
+fn is_absolute_unit(u: LengthUnit) -> bool {
+ use LengthUnit::*;
+
+ match u {
+ Percent | Em | Ex => false,
+ Px | In | Cm | Mm | Pt | Pc => true,
+ }
+}
+
+fn parse_length<N: Normalize, V: Validate>(s: &str) -> Result<CssLength<N, V>, String> {
+ <CssLength<N, V> as Parse>::parse_str(s)
+ .map_err(|_| format!("Invalid value: The argument '{s}' can not be parsed as a length"))
+ .and_then(|l| {
+ if is_absolute_unit(l.unit) {
+ Ok(l)
+ } else {
+ Err(format!(
+ "Invalid value '{s}': supported units are px, in, cm, mm, pt, pc"
+ ))
+ }
+ })
+}
+
+fn main() {
+ if let Err(e) = parse_args().and_then(|converter| converter.convert()) {
+ std::eprintln!("{e}");
+ std::process::exit(1);
+ }
+}
+
+#[cfg(test)]
+mod color_tests {
+ use super::*;
+
+ #[test]
+ fn valid_color_is_ok() {
+ assert!(parse_background_color("Red").is_ok());
+ }
+
+ #[test]
+ fn none_is_handled_as_transparent() {
+ assert_eq!(parse_background_color("None").unwrap(), None,);
+ }
+
+ #[test]
+ fn invalid_is_handled_as_invalid_value() {
+ assert!(parse_background_color("foo").is_err());
+ }
+}
+
+#[cfg(test)]
+mod sizing_tests {
+ use super::*;
+
+ #[test]
+ fn detects_empty_size() {
+ let strategy = ResizeStrategy::Scale(Scale { x: 42.0, y: 42.0 });
+ assert!(strategy.apply(&Size::new(0.0, 0.0)).is_none());
+ }
+
+ #[test]
+ fn scale() {
+ let strategy = ResizeStrategy::Scale(Scale { x: 2.0, y: 3.0 });
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
+ Size::new(2.0, 6.0),
+ );
+ }
+
+ #[test]
+ fn fit_non_proportional() {
+ let strategy = ResizeStrategy::Fit {
+ size: Size::new(40.0, 10.0),
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(2.0, 1.0)).unwrap(),
+ Size::new(40.0, 10.0),
+ );
+ }
+
+ #[test]
+ fn fit_proportional_wider_than_tall() {
+ let strategy = ResizeStrategy::Fit {
+ size: Size::new(40.0, 10.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(2.0, 1.0)).unwrap(),
+ Size::new(20.0, 10.0),
+ );
+ }
+
+ #[test]
+ fn fit_proportional_taller_than_wide() {
+ let strategy = ResizeStrategy::Fit {
+ size: Size::new(100.0, 50.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
+ Size::new(25.0, 50.0),
+ );
+ }
+
+ #[test]
+ fn fit_width() {
+ let strategy = ResizeStrategy::FitWidth(100.0);
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
+ Size::new(100.0, 200.0),
+ );
+ }
+
+ #[test]
+ fn fit_height() {
+ let strategy = ResizeStrategy::FitHeight(100.0);
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
+ Size::new(50.0, 100.0),
+ );
+ }
+
+ #[test]
+ fn scale_no_max_size_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 2.0, y: 3.0 },
+ max_width: None,
+ max_height: None,
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 2.0)).unwrap(),
+ Size::new(2.0, 6.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_and_height_fits_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 2.0, y: 3.0 },
+ max_width: Some(10.0),
+ max_height: Some(20.0),
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(4.0, 2.0)).unwrap(),
+ Size::new(8.0, 6.0)
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_and_height_fits_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 2.0, y: 3.0 },
+ max_width: Some(10.0),
+ max_height: Some(20.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(4.0, 2.0)).unwrap(),
+ Size::new(8.0, 6.0)
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_and_height_doesnt_fit_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 10.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: Some(20.0),
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(4.0, 5.0)).unwrap(),
+ Size::new(10.0, 20.0)
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_and_height_doesnt_fit_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 10.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: Some(15.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ // this will end up with a 40:120 aspect ratio
+ strategy.apply(&Size::new(4.0, 6.0)).unwrap(),
+ // which should be shrunk to 1:3 that fits in (10, 15) per the max_width/max_height above
+ Size::new(5.0, 15.0)
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_fits_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 5.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: None,
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 10.0)).unwrap(),
+ Size::new(5.0, 200.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_fits_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 5.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: None,
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(1.0, 10.0)).unwrap(),
+ Size::new(5.0, 200.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_height_fits_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 20.0, y: 5.0 },
+ max_width: None,
+ max_height: Some(10.0),
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(10.0, 1.0)).unwrap(),
+ Size::new(200.0, 5.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_height_fits_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 20.0, y: 5.0 },
+ max_width: None,
+ max_height: Some(10.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(10.0, 1.0)).unwrap(),
+ Size::new(200.0, 5.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_doesnt_fit_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 10.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: None,
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
+ Size::new(10.0, 200.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_width_doesnt_fit_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 10.0, y: 20.0 },
+ max_width: Some(10.0),
+ max_height: None,
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
+ Size::new(10.0, 40.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_height_doesnt_fit_non_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 10.0, y: 20.0 },
+ max_width: None,
+ max_height: Some(10.0),
+ keep_aspect_ratio: false,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
+ Size::new(50.0, 10.0),
+ );
+ }
+
+ #[test]
+ fn scale_with_max_height_doesnt_fit_proportional() {
+ let strategy = ResizeStrategy::ScaleWithMaxSize {
+ scale: Scale { x: 8.0, y: 20.0 },
+ max_width: None,
+ max_height: Some(10.0),
+ keep_aspect_ratio: true,
+ };
+
+ assert_eq!(
+ strategy.apply(&Size::new(5.0, 10.0)).unwrap(),
+ Size::new(2.0, 10.0),
+ );
+ }
+}