summaryrefslogtreecommitdiff
path: root/rsvg_convert
diff options
context:
space:
mode:
Diffstat (limited to 'rsvg_convert')
-rw-r--r--rsvg_convert/Cargo.toml50
-rw-r--r--rsvg_convert/build.rs3
-rw-r--r--rsvg_convert/src/main.rs1552
-rw-r--r--rsvg_convert/tests/fixtures/a-link.svg6
-rw-r--r--rsvg_convert/tests/fixtures/accept-language-de.pngbin0 -> 173 bytes
-rw-r--r--rsvg_convert/tests/fixtures/accept-language-es.pngbin0 -> 172 bytes
-rw-r--r--rsvg_convert/tests/fixtures/accept-language-fallback.pngbin0 -> 173 bytes
-rw-r--r--rsvg_convert/tests/fixtures/accept-language.svg7
-rw-r--r--rsvg_convert/tests/fixtures/bug521-with-viewbox.svg4
-rw-r--r--rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg11
-rw-r--r--rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.pngbin0 -> 95 bytes
-rw-r--r--rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg6
-rw-r--r--rsvg_convert/tests/fixtures/bug677-partial-pixel.svg7
-rw-r--r--rsvg_convert/tests/fixtures/dimensions-in.svg4
-rw-r--r--rsvg_convert/tests/fixtures/dpi.svg5
-rw-r--r--rsvg_convert/tests/fixtures/empty.svg3
-rw-r--r--rsvg_convert/tests/fixtures/example.svg5
-rw-r--r--rsvg_convert/tests/fixtures/geometry-element.svg6
-rw-r--r--rsvg_convert/tests/fixtures/gimp-wilber-ref.pngbin0 -> 2503 bytes
-rw-r--r--rsvg_convert/tests/fixtures/gimp-wilber.svg978
-rw-r--r--rsvg_convert/tests/fixtures/hello-world.svg11
-rw-r--r--rsvg_convert/tests/fixtures/offset-png.pngbin0 -> 1426 bytes
-rw-r--r--rsvg_convert/tests/fixtures/sub-rect-no-unit.svg13
-rw-r--r--rsvg_convert/tests/fixtures/text-a-link.svg14
-rw-r--r--rsvg_convert/tests/fixtures/zero-offset-png.pngbin0 -> 1419 bytes
-rw-r--r--rsvg_convert/tests/internal_predicates/file.rs28
-rw-r--r--rsvg_convert/tests/internal_predicates/mod.rs4
-rw-r--r--rsvg_convert/tests/internal_predicates/pdf.rs358
-rw-r--r--rsvg_convert/tests/internal_predicates/png.rs193
-rw-r--r--rsvg_convert/tests/internal_predicates/svg.rs179
-rw-r--r--rsvg_convert/tests/rsvg_convert.rs1078
31 files changed, 4525 insertions, 0 deletions
diff --git a/rsvg_convert/Cargo.toml b/rsvg_convert/Cargo.toml
new file mode 100644
index 00000000..035b2419
--- /dev/null
+++ b/rsvg_convert/Cargo.toml
@@ -0,0 +1,50 @@
+[package]
+name = "rsvg_convert"
+version.workspace = true
+authors.workspace = true
+description.workspace = true
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+
+# So that we can use an rsvg-convert name instead of the default rsvg_convert
+autobins = false
+
+[package.metadata.system-deps]
+cairo-pdf = { version = "1.16", optional = true }
+cairo-ps = { version = "1.16", optional = true }
+cairo-svg = { version = "1.16", optional = true }
+
+[dependencies]
+# Keep these in sync with respect to the cairo-rs version:
+# src/lib.rs - toplevel example in the docs
+cairo-rs = { version = "0.17", features=["v1_16", "png", "pdf", "ps", "svg"] }
+cast = "0.3.0"
+chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] }
+clap = { version = "4.0.17", features = ["cargo", "derive"] } # rsvg-convert
+clap_complete = "4.0.5" # rsvg-convert
+cssparser = "0.29.0"
+gio = "0.17"
+glib = "0.17"
+libc = "0.2"
+librsvg = { path = "../rsvg" }
+librsvg-c = { path = "../librsvg-c" }
+
+[dev-dependencies]
+assert_cmd = "2.0.2"
+predicates = "3.0.3"
+tempfile = "3"
+url = "2"
+lopdf = "0.30.0"
+png = "0.17.2"
+float-cmp = "0.9.0"
+librsvg = { path = "../rsvg", features = ["test-utils"] }
+
+[build-dependencies]
+system-deps = "6.0.0"
+
+[[bin]]
+name = "rsvg-convert"
+path = "src/main.rs"
diff --git a/rsvg_convert/build.rs b/rsvg_convert/build.rs
new file mode 100644
index 00000000..eec6d526
--- /dev/null
+++ b/rsvg_convert/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ system_deps::Config::new().probe().unwrap();
+}
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),
+ );
+ }
+}
diff --git a/rsvg_convert/tests/fixtures/a-link.svg b/rsvg_convert/tests/fixtures/a-link.svg
new file mode 100644
index 00000000..1ae8ace5
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/a-link.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
+ <a href="https://example.com">
+ <rect x="100" y="100" width="200" height="200" fill="lime"/>
+ </a>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/accept-language-de.png b/rsvg_convert/tests/fixtures/accept-language-de.png
new file mode 100644
index 00000000..cc797dc2
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/accept-language-de.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/accept-language-es.png b/rsvg_convert/tests/fixtures/accept-language-es.png
new file mode 100644
index 00000000..4cf3a21f
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/accept-language-es.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/accept-language-fallback.png b/rsvg_convert/tests/fixtures/accept-language-fallback.png
new file mode 100644
index 00000000..43b20f01
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/accept-language-fallback.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/accept-language.svg b/rsvg_convert/tests/fixtures/accept-language.svg
new file mode 100644
index 00000000..c132b65d
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/accept-language.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <switch allowReorder="yes">
+ <rect systemLanguage="de" fill="red" width="10" height="10" />
+ <rect systemLanguage="es" fill="lime" width="10" height="10" />
+ <rect fill="yellow" id="rect3" width="10" height="10" />
+ </switch>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg b/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg
new file mode 100644
index 00000000..c3f34e6d
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/bug521-with-viewbox.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="200" height="100" viewBox="0 0 2000 1000">
+ <rect id="foo" x="500" y="600" width="700" height="800"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg b/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg
new file mode 100644
index 00000000..1cee7759
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/bug591-vbox-overflow.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg ion="1.1" baseProfile="b" id="svg-root"
+ width="100%" height="100%" viewBox="0 0 4822222222222222222220 360"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="httk">
+ <g>
+ <g opacity="0.5">
+ <rect x="60" y="230" width="80" height="40" fill="+0000ff" opacity=".5"/>
+ <rect x="71" y="240" width="80" height="40" fill="#00ff00" opacity=".5"/>
+</g>
+ </g>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png
new file mode 100644
index 00000000..1ed070ca
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width-render-only-foo.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg
new file mode 100644
index 00000000..ee96d474
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/bug601-zero-stroke-width.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <g id="foo">
+ <rect x="50" y="50" width="10" height="10" style="stroke-width: 0; stroke: black; fill: blue"/>
+ </g>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg b/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg
new file mode 100644
index 00000000..aeac8c30
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/bug677-partial-pixel.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="1.2" height="1.8">
+ <!-- Output should be an image 2x2 pixels in size, with partial coverage for the pixels
+ on the right and bottom.
+ -->
+ <rect x="0" y="0" width="1.2" height="1.8" fill="lime"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/dimensions-in.svg b/rsvg_convert/tests/fixtures/dimensions-in.svg
new file mode 100644
index 00000000..aa4f3219
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/dimensions-in.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="1in" height="1in" viewBox="0 0 1 1">
+ <rect x="0" y="0" width="1" height="1" fill="blue"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/dpi.svg b/rsvg_convert/tests/fixtures/dpi.svg
new file mode 100644
index 00000000..499ee206
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/dpi.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="1in" height="4in" viewBox="0 0 100 400">
+ <rect id="one" x="0" y="0" width="100" height="200" fill="rgb(0,255,0)"/>
+ <rect id="two" x="0" y="200" width="100" height="200" fill="rgb(0,0,255)"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/empty.svg b/rsvg_convert/tests/fixtures/empty.svg
new file mode 100644
index 00000000..01a940a2
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/empty.svg
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/example.svg b/rsvg_convert/tests/fixtures/example.svg
new file mode 100644
index 00000000..850fba3e
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/example.svg
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="400" viewBox="0 0 100 400">
+ <rect id="one" x="0" y="0" width="100" height="200" fill="rgb(0,255,0)"/>
+ <rect id="two" x="0" y="200" width="100" height="200" fill="rgb(0,0,255)"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/geometry-element.svg b/rsvg_convert/tests/fixtures/geometry-element.svg
new file mode 100644
index 00000000..3d707cdc
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/geometry-element.svg
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <g transform="rotate(45)" stroke-width="10" stroke="#000000">
+ <rect id="foo" x="10" y="20" width="30" height="40" fill="#0000ff"/>
+ </g>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/gimp-wilber-ref.png b/rsvg_convert/tests/fixtures/gimp-wilber-ref.png
new file mode 100644
index 00000000..606f2a4d
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/gimp-wilber-ref.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/gimp-wilber.svg b/rsvg_convert/tests/fixtures/gimp-wilber.svg
new file mode 100644
index 00000000..97c821a7
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/gimp-wilber.svg
@@ -0,0 +1,978 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:export-ydpi="120"
+ inkscape:export-xdpi="120"
+ inkscape:export-filename="/home/jimmac/src/cvs/gnome/gimp/themes/Default/images/stock-wilber-64.png"
+ width="48px"
+ height="48px"
+ id="svg11300"
+ sodipodi:version="0.32"
+ inkscape:version="0.44+devel"
+ sodipodi:docbase="/home/jimmac/src/cvs/gnome/gimp/themes/Default/images"
+ sodipodi:docname="stock-wilber.svg">
+ <defs
+ id="defs3">
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8534"
+ id="linearGradient8951"
+ gradientUnits="userSpaceOnUse"
+ x1="26.162951"
+ y1="30.543303"
+ x2="24.328892"
+ y2="30.985245" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2446"
+ id="linearGradient8949"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.755165,0.395143,-0.395143,0.755165,-25.91245,6.532586)"
+ x1="13.236155"
+ y1="37.752247"
+ x2="7.7521091"
+ y2="42.282146" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6963"
+ id="radialGradient8947"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.572694,0,0,1.532639,-55.36682,-21.35823)"
+ cx="15.415101"
+ cy="35.356506"
+ fx="15.415101"
+ fy="35.356506"
+ r="7.5791559" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6939"
+ id="linearGradient8945"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-46.40695,-1.802856)"
+ x1="19.394735"
+ y1="30.001331"
+ x2="23.109331"
+ y2="33.438831" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6951"
+ id="linearGradient8943"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-49.30496,1.877723)"
+ x1="37.017639"
+ y1="19.239889"
+ x2="27.753893"
+ y2="11.182488" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6468"
+ id="radialGradient8941"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.318488,0,0,1.318488,-22.1264,-6.241691)"
+ cx="69.473244"
+ cy="19.597878"
+ fx="69.473244"
+ fy="19.597878"
+ r="3.5153139" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6452"
+ id="linearGradient8939"
+ gradientUnits="userSpaceOnUse"
+ x1="6.3051529"
+ y1="23.362427"
+ x2="5.9846287"
+ y2="31.57" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8542"
+ id="radialGradient8937"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.916159,9.318684e-2,-0.108765,1.069309,3.253668,-3.029272)"
+ cx="6.0242186"
+ cy="25.271027"
+ fx="6.0242186"
+ fy="25.271027"
+ r="4.8310289" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8524"
+ id="linearGradient8935"
+ gradientUnits="userSpaceOnUse"
+ x1="14.96875"
+ y1="19.110678"
+ x2="39.524544"
+ y2="46.98568" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6482"
+ id="linearGradient8933"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-3.836549,0.345971)"
+ x1="32.350136"
+ y1="28.083355"
+ x2="21.213203"
+ y2="30.293064" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6500"
+ id="radialGradient8931"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.77275,0,0,1.29668,-16.3404,-6.615959)"
+ cx="18.557627"
+ cy="22.300018"
+ fx="18.557627"
+ fy="22.300018"
+ r="19.2292" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6492"
+ id="radialGradient8929"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.284247,0,24.29088)"
+ cx="23"
+ cy="33.9375"
+ fx="23"
+ fy="33.9375"
+ r="18.25" />
+ <linearGradient
+ id="linearGradient8542">
+ <stop
+ style="stop-color:#5b676b;stop-opacity:1;"
+ offset="0"
+ id="stop8544" />
+ <stop
+ style="stop-color:#141718;stop-opacity:1;"
+ offset="1"
+ id="stop8546" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient8534">
+ <stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0"
+ id="stop8536" />
+ <stop
+ style="stop-color:black;stop-opacity:0;"
+ offset="1"
+ id="stop8538" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient8524">
+ <stop
+ style="stop-color:white;stop-opacity:1;"
+ offset="0"
+ id="stop8526" />
+ <stop
+ style="stop-color:white;stop-opacity:0;"
+ offset="1"
+ id="stop8528" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient2446">
+ <stop
+ style="stop-color:#ffffff;stop-opacity:1;"
+ offset="0"
+ id="stop2448" />
+ <stop
+ style="stop-color:#ffffff;stop-opacity:0;"
+ offset="1"
+ id="stop2450" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6963">
+ <stop
+ style="stop-color:#696969;stop-opacity:1;"
+ offset="0"
+ id="stop6965" />
+ <stop
+ style="stop-color:#000000;stop-opacity:1;"
+ offset="1"
+ id="stop6967" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6939">
+ <stop
+ style="stop-color:#bdbdbd;stop-opacity:1;"
+ offset="0"
+ id="stop6941" />
+ <stop
+ id="stop6947"
+ offset="0.33333334"
+ style="stop-color:#e2e2e2;stop-opacity:1;" />
+ <stop
+ style="stop-color:#a3a3a3;stop-opacity:1;"
+ offset="0.66666669"
+ id="stop6949" />
+ <stop
+ style="stop-color:#dddddd;stop-opacity:1;"
+ offset="1"
+ id="stop6943" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6951">
+ <stop
+ style="stop-color:#6e3d09;stop-opacity:1;"
+ offset="0"
+ id="stop6953" />
+ <stop
+ id="stop6959"
+ offset="0.24242425"
+ style="stop-color:#ea8113;stop-opacity:1;" />
+ <stop
+ style="stop-color:#5c3307;stop-opacity:1;"
+ offset="0.62121212"
+ id="stop6961" />
+ <stop
+ style="stop-color:#e07c12;stop-opacity:1;"
+ offset="1"
+ id="stop6955" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6500">
+ <stop
+ style="stop-color:#857c63;stop-opacity:1;"
+ offset="0"
+ id="stop6502" />
+ <stop
+ style="stop-color:#221f19;stop-opacity:1;"
+ offset="1"
+ id="stop6504" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient6492">
+ <stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0"
+ id="stop6494" />
+ <stop
+ style="stop-color:black;stop-opacity:0;"
+ offset="1"
+ id="stop6496" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient6482">
+ <stop
+ style="stop-color:black;stop-opacity:1;"
+ offset="0"
+ id="stop6484" />
+ <stop
+ style="stop-color:black;stop-opacity:0;"
+ offset="1"
+ id="stop6486" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient6468">
+ <stop
+ style="stop-color:white;stop-opacity:1;"
+ offset="0"
+ id="stop6470" />
+ <stop
+ style="stop-color:#b9b9b0;stop-opacity:1;"
+ offset="1"
+ id="stop6472" />
+ </linearGradient>
+ <linearGradient
+ inkscape:collect="always"
+ id="linearGradient6452">
+ <stop
+ style="stop-color:white;stop-opacity:1;"
+ offset="0"
+ id="stop6454" />
+ <stop
+ style="stop-color:white;stop-opacity:0;"
+ offset="1"
+ id="stop6456" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient11520">
+ <stop
+ id="stop11522"
+ offset="0.0000000"
+ style="stop-color:#ffffff;stop-opacity:1.0000000;" />
+ <stop
+ id="stop11524"
+ offset="1.0000000"
+ style="stop-color:#dcdcdc;stop-opacity:1.0000000;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient11508"
+ inkscape:collect="always">
+ <stop
+ id="stop11510"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop11512"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient11494"
+ inkscape:collect="always">
+ <stop
+ id="stop11496"
+ offset="0"
+ style="stop-color:#ef2929;stop-opacity:1;" />
+ <stop
+ id="stop11498"
+ offset="1"
+ style="stop-color:#ef2929;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient11415">
+ <stop
+ id="stop11417"
+ offset="0.0000000"
+ style="stop-color:#204a87;stop-opacity:0.0000000;" />
+ <stop
+ style="stop-color:#204a87;stop-opacity:1.0000000;"
+ offset="0.50000000"
+ id="stop11423" />
+ <stop
+ id="stop11419"
+ offset="1"
+ style="stop-color:#204a87;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ id="linearGradient11399"
+ inkscape:collect="always">
+ <stop
+ id="stop11401"
+ offset="0"
+ style="stop-color:#000000;stop-opacity:1;" />
+ <stop
+ id="stop11403"
+ offset="1"
+ style="stop-color:#000000;stop-opacity:0;" />
+ </linearGradient>
+ <linearGradient
+ gradientTransform="translate(-60.28571,-0.285714)"
+ y2="34.462429"
+ x2="43.615788"
+ y1="3.7744560"
+ x1="15.828360"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient11425"
+ xlink:href="#linearGradient11415"
+ inkscape:collect="always" />
+ <linearGradient
+ gradientTransform="translate(-60.57143,0.000000)"
+ y2="39.033859"
+ x2="35.679932"
+ y1="9.3458843"
+ x1="9.6957054"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient11427"
+ xlink:href="#linearGradient11415"
+ inkscape:collect="always" />
+ <linearGradient
+ y2="33.462429"
+ x2="26.758644"
+ y1="19.774456"
+ x1="13.267134"
+ gradientTransform="translate(-60.85714,0.428571)"
+ gradientUnits="userSpaceOnUse"
+ id="linearGradient11439"
+ xlink:href="#linearGradient11415"
+ inkscape:collect="always" />
+ <radialGradient
+ r="8.5000000"
+ fy="39.142857"
+ fx="12.071428"
+ cy="39.142857"
+ cx="12.071428"
+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.487395,0.000000,20.06483)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient11441"
+ xlink:href="#linearGradient11399"
+ inkscape:collect="always" />
+ <radialGradient
+ gradientTransform="matrix(1.243453,2.106784e-16,-2.106784e-16,1.243453,-6.713754,-3.742847)"
+ gradientUnits="userSpaceOnUse"
+ r="3.8335034"
+ fy="15.048258"
+ fx="27.577173"
+ cy="15.048258"
+ cx="27.577173"
+ id="radialGradient11500"
+ xlink:href="#linearGradient11494"
+ inkscape:collect="always" />
+ <radialGradient
+ r="3.8335034"
+ fy="16.049133"
+ fx="27.577173"
+ cy="16.049133"
+ cx="27.577173"
+ gradientTransform="matrix(1.243453,2.106784e-16,-2.106784e-16,1.243453,-6.713754,-3.742847)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient11504"
+ xlink:href="#linearGradient11494"
+ inkscape:collect="always" />
+ <radialGradient
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.338462,2.166583e-14,29.48178)"
+ r="6.5659914"
+ fy="44.565483"
+ fx="30.203562"
+ cy="44.565483"
+ cx="30.203562"
+ id="radialGradient11514"
+ xlink:href="#linearGradient11508"
+ inkscape:collect="always" />
+ <radialGradient
+ gradientTransform="matrix(1.995058,-1.651527e-32,0.000000,1.995058,-24.32488,-35.70087)"
+ gradientUnits="userSpaceOnUse"
+ r="20.530962"
+ fy="35.878170"
+ fx="24.445690"
+ cy="35.878170"
+ cx="24.445690"
+ id="radialGradient11526"
+ xlink:href="#linearGradient11520"
+ inkscape:collect="always" />
+ <radialGradient
+ r="6.5659914"
+ fy="44.565483"
+ fx="30.203562"
+ cy="44.565483"
+ cx="30.203562"
+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.338462,3.185827e-15,29.48178)"
+ gradientUnits="userSpaceOnUse"
+ id="radialGradient11532"
+ xlink:href="#linearGradient11508"
+ inkscape:collect="always" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11508"
+ id="radialGradient1348"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.338462,-1.353344e-14,29.48178)"
+ cx="30.203562"
+ cy="44.565483"
+ fx="30.203562"
+ fy="44.565483"
+ r="6.5659914" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11520"
+ id="radialGradient1350"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.995058,-1.651527e-32,0.000000,1.995058,-24.32488,-35.70087)"
+ cx="24.445690"
+ cy="35.878170"
+ fx="24.445690"
+ fy="35.878170"
+ r="20.530962" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11494"
+ id="radialGradient1352"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.243453,2.106784e-16,-2.106784e-16,1.243453,-6.713754,-3.742847)"
+ cx="27.577173"
+ cy="16.049133"
+ fx="27.577173"
+ fy="16.049133"
+ r="3.8335034" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11494"
+ id="radialGradient1354"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.243453,2.106784e-16,-2.106784e-16,1.243453,-6.713754,-3.742847)"
+ cx="27.577173"
+ cy="15.048258"
+ fx="27.577173"
+ fy="15.048258"
+ r="3.8335034" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11508"
+ id="radialGradient1356"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.000000,0.000000,0.000000,0.338462,2.220359e-14,29.48178)"
+ cx="30.203562"
+ cy="44.565483"
+ fx="30.203562"
+ fy="44.565483"
+ r="6.5659914" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient11520"
+ id="radialGradient1366"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(2.049266,-1.696401e-32,0.000000,2.049266,-25.65002,-37.31089)"
+ cx="24.445690"
+ cy="35.878170"
+ fx="24.445690"
+ fy="35.878170"
+ r="20.530962" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6452"
+ id="linearGradient6458"
+ x1="6.3051529"
+ y1="23.362427"
+ x2="5.9846287"
+ y2="31.57"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6468"
+ id="radialGradient6474"
+ cx="69.473244"
+ cy="19.597878"
+ fx="69.473244"
+ fy="19.597878"
+ r="3.5153138"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.318488,1.207574e-15,-1.207574e-15,1.318488,-22.1264,-6.241691)" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6482"
+ id="linearGradient6488"
+ x1="32.350136"
+ y1="28.083355"
+ x2="21.213203"
+ y2="30.293064"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-3.836549,0.345971)" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6492"
+ id="radialGradient6498"
+ cx="23"
+ cy="33.9375"
+ fx="23"
+ fy="33.9375"
+ r="18.25"
+ gradientTransform="matrix(1,0,0,0.284247,0,24.29088)"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6500"
+ id="radialGradient8522"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.77275,-1.847562e-16,1.351402e-16,1.29668,-16.3404,-6.615959)"
+ cx="18.557627"
+ cy="22.300018"
+ fx="18.557627"
+ fy="22.300018"
+ r="19.2292" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8524"
+ id="linearGradient8530"
+ x1="14.96875"
+ y1="19.110678"
+ x2="39.524544"
+ y2="46.98568"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8534"
+ id="linearGradient8540"
+ x1="26.162951"
+ y1="30.543303"
+ x2="24.328892"
+ y2="30.985245"
+ gradientUnits="userSpaceOnUse" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8542"
+ id="radialGradient8548"
+ cx="6.0242186"
+ cy="25.271027"
+ fx="6.0242186"
+ fy="25.271027"
+ r="4.8310288"
+ gradientTransform="matrix(0.916159,9.318684e-2,-0.108765,1.069309,3.253668,-3.029272)"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8534"
+ id="linearGradient4467"
+ gradientUnits="userSpaceOnUse"
+ x1="26.162951"
+ y1="30.543303"
+ x2="24.328892"
+ y2="30.985245" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6492"
+ id="radialGradient2360"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1,0,0,0.284247,0,24.29088)"
+ cx="23"
+ cy="33.9375"
+ fx="23"
+ fy="33.9375"
+ r="18.25" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6500"
+ id="radialGradient2362"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.77275,0,0,1.29668,-16.3404,-6.615959)"
+ cx="18.557627"
+ cy="22.300018"
+ fx="18.557627"
+ fy="22.300018"
+ r="19.2292" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6482"
+ id="linearGradient2364"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="translate(-3.83655,0.345971)"
+ x1="32.350136"
+ y1="28.083355"
+ x2="21.213203"
+ y2="30.293064" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8524"
+ id="linearGradient2366"
+ gradientUnits="userSpaceOnUse"
+ x1="14.96875"
+ y1="19.110678"
+ x2="39.524544"
+ y2="46.98568" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient8542"
+ id="radialGradient2368"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.916159,9.318684e-2,-0.108765,1.069309,3.253668,-3.029272)"
+ cx="6.0242186"
+ cy="25.271027"
+ fx="6.0242186"
+ fy="25.271027"
+ r="4.8310288" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6452"
+ id="linearGradient2370"
+ gradientUnits="userSpaceOnUse"
+ x1="6.3051529"
+ y1="23.362427"
+ x2="5.9846287"
+ y2="31.57" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6468"
+ id="radialGradient2372"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(1.318488,0,0,1.318488,-22.1264,-6.241691)"
+ cx="69.473244"
+ cy="19.597878"
+ fx="69.473244"
+ fy="19.597878"
+ r="3.5153138" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6951"
+ id="linearGradient2374"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-0.117687,-0.528197,0.528197,-0.117687,46.2238,49.69112)"
+ x1="37.017639"
+ y1="19.239889"
+ x2="27.753893"
+ y2="11.182488" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6939"
+ id="linearGradient2376"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-0.117687,-0.528197,0.528197,-0.117687,40.56367,46.8748)"
+ x1="19.394735"
+ y1="30.001331"
+ x2="23.109331"
+ y2="33.438831" />
+ <radialGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient6963"
+ id="radialGradient2378"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(-9.389073e-2,-1.406075,1.370264,-9.149987e-2,18.28382,59.78933)"
+ cx="15.415101"
+ cy="35.356506"
+ fx="15.415101"
+ fy="35.356506"
+ r="7.5791561" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient2446"
+ id="linearGradient2380"
+ gradientUnits="userSpaceOnUse"
+ gradientTransform="matrix(0.11984,-0.445379,0.445379,0.11984,42.0839,34.30798)"
+ x1="13.236155"
+ y1="37.752247"
+ x2="7.7521091"
+ y2="42.282146" />
+ </defs>
+ <sodipodi:namedview
+ stroke="#ef2929"
+ fill="#eeeeec"
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666"
+ borderopacity="0.15294118"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="-31.831093"
+ inkscape:cy="9.6042458"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ inkscape:grid-bbox="true"
+ inkscape:document-units="px"
+ inkscape:showpageshadow="false"
+ inkscape:window-width="1191"
+ inkscape:window-height="1078"
+ inkscape:window-x="269"
+ inkscape:window-y="51" />
+ <metadata
+ id="metadata4">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:creator>
+ <cc:Agent>
+ <dc:title>Jakub Steiner</dc:title>
+ </cc:Agent>
+ </dc:creator>
+ <dc:source>http://jimmac.musichall.cz</dc:source>
+ <cc:license
+ rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" />
+ <dc:title />
+ </cc:Work>
+ <cc:License
+ rdf:about="http://creativecommons.org/licenses/by-sa/2.0/">
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Reproduction" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/Distribution" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/Notice" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/Attribution" />
+ <cc:permits
+ rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
+ <cc:requires
+ rdf:resource="http://web.resource.org/cc/ShareAlike" />
+ </cc:License>
+ </rdf:RDF>
+ </metadata>
+ <g
+ id="layer1"
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer">
+ <g
+ transform="matrix(1.016627,0,0,1.016627,0.434805,-0.792136)"
+ id="g8883">
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:url(#radialGradient8929);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8885"
+ sodipodi:cx="23"
+ sodipodi:cy="33.9375"
+ sodipodi:rx="18.25"
+ sodipodi:ry="5.1875"
+ d="M 41.25 33.9375 A 18.25 5.1875 0 1 1 4.75,33.9375 A 18.25 5.1875 0 1 1 41.25 33.9375 z"
+ transform="matrix(1,0,0,1.53012,0.125,-19.99096)" />
+ <path
+ style="opacity:1;color:black;fill:url(#radialGradient8931);fill-opacity:1;fill-rule:evenodd;stroke:#2e3436;stroke-width:0.98364494;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 10.374369,12.467884 C 10.374369,12.467884 13.248878,18.395518 19.973611,18.228291 C 34.066126,17.874738 36.309226,10.582719 37.832786,8.7660099 C 42.895143,51.417634 6.0135488,33.362123 4.7175144,26.256467 C 11.965359,24.135147 10.197592,20.069282 10.197592,20.069282 L 10.374369,12.467884 z "
+ id="path8887"
+ sodipodi:nodetypes="cscccc" />
+ <path
+ style="opacity:1;color:black;fill:url(#linearGradient8933);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 15.73779,30.066049 C 22.47669,31.413886 25.908481,30.164142 27.916965,28.613273 C 27.386635,27.928263 26.480655,27.176962 26.480655,27.176962 C 26.480655,27.176962 28.833972,27.830904 29.662635,28.900535 C 30.488925,29.967103 29.969443,30.624242 29.753196,31.988905 C 29.271785,30.790306 28.373215,30.340813 28.251562,29.864573 C 26.445294,32.3615 21.94512,32.257773 15.73779,30.066049 z "
+ id="path8889"
+ sodipodi:nodetypes="cccsccc" />
+ <path
+ sodipodi:type="inkscape:offset"
+ inkscape:radius="-1.073054"
+ inkscape:original="M 37.5 8.75 C 37.304927 8.7198838 37.083027 8.9384197 36.90625 9.46875 C 36.552697 10.529411 34.061264 17.865197 19.96875 18.21875 C 13.244017 18.385977 10.375 12.46875 10.375 12.46875 L 10.1875 20.0625 C 10.1875 20.0625 11.966595 24.128679 4.71875 26.25 C 6.014785 33.355656 42.502444 51.25055 37.90625 9.53125 C 37.843713 9.0411177 37.695073 8.7801162 37.5 8.75 z "
+ xlink:href="#path4323"
+ style="opacity:0.18539327;color:black;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient8935);stroke-width:0.98364494;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8891"
+ inkscape:href="#path4323"
+ d="M 36.96875,11.84375 C 36.406772,12.770645 35.562258,13.876916 34.28125,14.9375 C 31.649332,17.116542 27.230687,19.099847 20,19.28125 C 15.775627,19.386299 13.047259,17.347101 11.375,15.53125 L 11.25,20 C 11.386107,20.418802 11.665455,21.390498 11.1875,22.71875 C 10.673186,24.148046 9.0329864,25.610113 6.21875,26.71875 C 6.4690804,27.240783 6.7142344,27.76237 7.46875,28.5 C 8.4967004,29.504945 9.9257833,30.588049 11.625,31.5625 C 15.023433,33.511402 19.426583,35.055712 23.53125,35.125 C 27.635917,35.194288 31.388376,33.89045 33.96875,30.125 C 36.347494,26.653782 37.651223,20.777057 36.96875,11.84375 z " />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:url(#radialGradient8937);fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.98364455;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8893"
+ sodipodi:cx="6.0987959"
+ sodipodi:cy="27.228739"
+ sodipodi:rx="4.3310289"
+ sodipodi:ry="6.0987959"
+ d="M 10.429825 27.228739 A 4.3310289 6.0987959 0 1 1 1.767767,27.228739 A 4.3310289 6.0987959 0 1 1 10.429825 27.228739 z"
+ transform="matrix(0.810984,-0.585069,0.585069,0.810984,-14.77791,6.947121)" />
+ <path
+ sodipodi:type="inkscape:offset"
+ inkscape:radius="-1.044355"
+ inkscape:original="M 6.09375 21.125 C 3.703022 21.125 1.78125 23.852215 1.78125 27.21875 C 1.78125 30.585285 3.703022 33.312501 6.09375 33.3125 C 8.484478 33.3125 10.4375 30.585285 10.4375 27.21875 C 10.4375 23.852215 8.484478 21.124999 6.09375 21.125 z "
+ xlink:href="#path5198"
+ style="opacity:0.28089887;color:black;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient8939);stroke-width:0.98364493;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8895"
+ inkscape:href="#path5198"
+ d="M 6.09375,22.15625 C 5.2955008,22.15625 4.5406196,22.602421 3.90625,23.5 C 3.2718804,24.397579 2.8125,25.734204 2.8125,27.21875 C 2.8125,28.703296 3.2718804,30.039921 3.90625,30.9375 C 4.5406196,31.835079 5.2955011,32.28125 6.09375,32.28125 C 6.8919992,32.28125 7.6710339,31.804861 8.3125,30.90625 C 8.9539661,30.007639 9.40625,28.700064 9.40625,27.21875 C 9.40625,25.737436 8.9539662,24.429861 8.3125,23.53125 C 7.6710338,22.632639 6.8919989,22.15625 6.09375,22.15625 z "
+ transform="matrix(0.800389,-0.599481,0.599481,0.800389,-15.2744,7.32784)" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8897"
+ sodipodi:cx="4.0658641"
+ sodipodi:cy="23.251263"
+ sodipodi:rx="1.767767"
+ sodipodi:ry="2.1213202"
+ d="M 5.833631 23.251263 A 1.767767 2.1213202 0 1 1 2.2980971,23.251263 A 1.767767 2.1213202 0 1 1 5.833631 23.251263 z" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:0.98364494;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8899"
+ sodipodi:cx="69.473244"
+ sodipodi:cy="21.837049"
+ sodipodi:rx="3.1819806"
+ sodipodi:ry="3.1819806"
+ d="M 72.655224 21.837049 A 3.1819806 3.1819806 0 1 1 66.291263,21.837049 A 3.1819806 3.1819806 0 1 1 72.655224 21.837049 z"
+ transform="translate(-55.86145,0)" />
+ <path
+ transform="matrix(1.5,0,0,1.5,-82.16821,-10.91852)"
+ d="M 72.655224 21.837049 A 3.1819806 3.1819806 0 1 1 66.291263,21.837049 A 3.1819806 3.1819806 0 1 1 72.655224 21.837049 z"
+ sodipodi:ry="3.1819806"
+ sodipodi:rx="3.1819806"
+ sodipodi:cy="21.837049"
+ sodipodi:cx="69.473244"
+ id="path8901"
+ style="opacity:1;color:black;fill:url(#radialGradient8941);fill-opacity:1;fill-rule:evenodd;stroke:#888a85;stroke-width:0.65576329;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:#2e3436;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8903"
+ sodipodi:cx="21.743534"
+ sodipodi:cy="21.837049"
+ sodipodi:rx="2.2980971"
+ sodipodi:ry="2.2980971"
+ d="M 24.041631 21.837049 A 2.2980971 2.2980971 0 1 1 19.445437,21.837049 A 2.2980971 2.2980971 0 1 1 24.041631 21.837049 z"
+ transform="translate(1.414216,0.707108)" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8905"
+ sodipodi:cx="21.38998"
+ sodipodi:cy="20.953165"
+ sodipodi:rx="1.767767"
+ sodipodi:ry="1.767767"
+ d="M 23.157747 20.953165 A 1.767767 1.767767 0 1 1 19.622213,20.953165 A 1.767767 1.767767 0 1 1 23.157747 20.953165 z"
+ transform="matrix(0.9,0,0,0.9,3.022883,2.625648)" />
+ <path
+ d="M 24.041631 21.837049 A 2.2980971 2.2980971 0 1 1 19.445437,21.837049 A 2.2980971 2.2980971 0 1 1 24.041631 21.837049 z"
+ sodipodi:ry="2.2980971"
+ sodipodi:rx="2.2980971"
+ sodipodi:cy="21.837049"
+ sodipodi:cx="21.743534"
+ id="path8907"
+ style="opacity:1;color:black;fill:#2e3436;fill-opacity:1;fill-rule:evenodd;stroke:black;stroke-width:1.62499952;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ sodipodi:type="arc"
+ transform="matrix(0.615385,0,0,0.615385,1.258896,8.840808)" />
+ <path
+ transform="matrix(0.553846,0,0,0.553846,2.248846,10.02145)"
+ d="M 23.157747 20.953165 A 1.767767 1.767767 0 1 1 19.622213,20.953165 A 1.767767 1.767767 0 1 1 23.157747 20.953165 z"
+ sodipodi:ry="1.767767"
+ sodipodi:rx="1.767767"
+ sodipodi:cy="20.953165"
+ sodipodi:cx="21.38998"
+ id="path8909"
+ style="opacity:1;color:black;fill:white;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ sodipodi:type="arc" />
+ <g
+ id="g8911"
+ transform="matrix(-0.114852,-0.389864,0.389864,-0.114852,24.71507,29.68942)">
+ <path
+ sodipodi:nodetypes="ccssc"
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ id="path8913"
+ d="M -23.364209,23.568336 L -19.654111,27.037511 C -11.307128,20.656664 -2.5600338,6.9381374 -2.5600338,6.9381374 C -0.58371893,4.6957251 -2.6769537,3.1876548 -4.7249404,4.5402186 C -4.7249404,4.5402186 -17.695303,14.655085 -23.364209,23.568336 z "
+ style="opacity:1;color:black;fill:url(#linearGradient8943);fill-opacity:1;fill-rule:nonzero;stroke:#673907;stroke-width:2.42021061;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ sodipodi:nodetypes="ccccccccc"
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ id="path8915"
+ d="M -30.449715,32.813894 L -28.203395,34.747714 L -19.710629,27.380683 L -19.435568,26.674855 L -18.341163,26.65704 C -18.778663,25.09454 -21.282677,22.273585 -23.157677,22.273585 L -23.075658,23.36366 L -23.745649,23.743687 L -30.449715,32.813894 z "
+ style="opacity:1;color:black;fill:url(#linearGradient8945);fill-opacity:1;fill-rule:nonzero;stroke:#888a85;stroke-width:2.42021061;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ id="path8917"
+ d="M -23.801371,28.376767 L -28.166304,33.643238"
+ style="opacity:1;color:black;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:white;stroke-width:2.46045327;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ sodipodi:nodetypes="cssc"
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ id="path8919"
+ d="M -45.341208,42.769835 C -34.89725,42.769835 -26.971169,44.013565 -25.470603,36.415097 C -24.261354,30.291783 -32.681137,27.357729 -36.853473,32.824236 C -40.87275,38.090207 -45.341208,42.769835 -45.341208,42.769835 z "
+ style="opacity:1;color:black;fill:url(#radialGradient8947);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.2;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ <path
+ transform="matrix(2.069903,0,0,2.069903,-44.75012,-41.50978)"
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ d="M 8.8749989 37.75 A 1.2499999 1.2499999 0 1 1 6.3749992,37.75 A 1.2499999 1.2499999 0 1 1 8.8749989 37.75 z"
+ sodipodi:ry="1.2499999"
+ sodipodi:rx="1.2499999"
+ sodipodi:cy="37.75"
+ sodipodi:cx="7.624999"
+ id="path8921"
+ style="opacity:0.52777782;color:black;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ sodipodi:type="arc" />
+ <path
+ sodipodi:type="arc"
+ style="opacity:1;color:black;fill:white;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ id="path8923"
+ sodipodi:cx="7.624999"
+ sodipodi:cy="37.75"
+ sodipodi:rx="1.2499999"
+ sodipodi:ry="1.2499999"
+ d="M 8.8749989 37.75 A 1.2499999 1.2499999 0 1 1 6.3749992,37.75 A 1.2499999 1.2499999 0 1 1 8.8749989 37.75 z"
+ inkscape:r_cx="true"
+ inkscape:r_cy="true"
+ transform="matrix(1.396669,0,0,1.396669,-32.05526,-25.87664)" />
+ <path
+ sodipodi:nodetypes="cssc"
+ inkscape:r_cy="true"
+ inkscape:r_cx="true"
+ id="path8925"
+ d="M -38.543723,40.909242 C -38.543723,40.909242 -34.822203,41.003542 -32.427185,39.497247 C -31.579834,38.964324 -30.911411,40.147232 -31.933366,40.584614 C -34.14076,41.529346 -38.543723,40.909242 -38.543723,40.909242 z "
+ style="opacity:0.42777776;color:black;fill:url(#linearGradient8949);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" />
+ </g>
+ <path
+ style="opacity:1;color:black;fill:url(#linearGradient8951);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible"
+ d="M 23.003067,31.736544 C 24.500439,31.879636 25.852696,31.464331 26.41496,31.262497 C 26.513185,30.707111 26.951512,29.64124 28.461048,29.571029 L 27.930718,28.642952 C 27.930718,28.642952 25.964077,29.990873 23.864854,30.388621 L 23.003067,31.736544 z "
+ id="path8927"
+ sodipodi:nodetypes="cccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/hello-world.svg b/rsvg_convert/tests/fixtures/hello-world.svg
new file mode 100644
index 00000000..45a65c0f
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/hello-world.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
+ <style>
+ text {
+ font: 50px Sans;
+ fill: black;
+ }
+ </style>
+
+ <text x="50" y="100">Hello world!</text>
+ <text transform="translate(50, 200) rotate(45)">Hello again!</text>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/offset-png.png b/rsvg_convert/tests/fixtures/offset-png.png
new file mode 100644
index 00000000..a4bc7e2a
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/offset-png.png
Binary files differ
diff --git a/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg b/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg
new file mode 100644
index 00000000..fb8312ac
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/sub-rect-no-unit.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="123pt"
+ height="123pt"
+ id="svg1">
+ <rect id="rect-no-unit" width="44" height="45" x="46" y="47"/>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/text-a-link.svg b/rsvg_convert/tests/fixtures/text-a-link.svg
new file mode 100644
index 00000000..d205c768
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/text-a-link.svg
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
+ <text x="100" y="100">
+ <a href="https://example.com">
+ This is a link to example.com
+ </a>
+
+ <tspan x="100" dy="2em">
+ <a href="https://another.example.com">
+ This is a link to another.example.com
+ </a>
+ </tspan>
+ </text>
+</svg>
diff --git a/rsvg_convert/tests/fixtures/zero-offset-png.png b/rsvg_convert/tests/fixtures/zero-offset-png.png
new file mode 100644
index 00000000..adbe9524
--- /dev/null
+++ b/rsvg_convert/tests/fixtures/zero-offset-png.png
Binary files differ
diff --git a/rsvg_convert/tests/internal_predicates/file.rs b/rsvg_convert/tests/internal_predicates/file.rs
new file mode 100644
index 00000000..a7af5acf
--- /dev/null
+++ b/rsvg_convert/tests/internal_predicates/file.rs
@@ -0,0 +1,28 @@
+use predicates::prelude::*;
+use predicates::str::StartsWithPredicate;
+
+use super::pdf::PdfPredicate;
+use super::png::PngPredicate;
+use super::svg::SvgPredicate;
+
+/// Predicates to check that some output ([u8]) is of a certain file type
+
+pub fn is_png() -> PngPredicate {
+ PngPredicate {}
+}
+
+pub fn is_ps() -> StartsWithPredicate {
+ predicate::str::starts_with("%!PS-Adobe-3.0\n")
+}
+
+pub fn is_eps() -> StartsWithPredicate {
+ predicate::str::starts_with("%!PS-Adobe-3.0 EPSF-3.0\n")
+}
+
+pub fn is_pdf() -> PdfPredicate {
+ PdfPredicate {}
+}
+
+pub fn is_svg() -> SvgPredicate {
+ SvgPredicate {}
+}
diff --git a/rsvg_convert/tests/internal_predicates/mod.rs b/rsvg_convert/tests/internal_predicates/mod.rs
new file mode 100644
index 00000000..7e15354d
--- /dev/null
+++ b/rsvg_convert/tests/internal_predicates/mod.rs
@@ -0,0 +1,4 @@
+pub mod file;
+mod pdf;
+mod png;
+mod svg;
diff --git a/rsvg_convert/tests/internal_predicates/pdf.rs b/rsvg_convert/tests/internal_predicates/pdf.rs
new file mode 100644
index 00000000..f7872d71
--- /dev/null
+++ b/rsvg_convert/tests/internal_predicates/pdf.rs
@@ -0,0 +1,358 @@
+use chrono::{DateTime, Utc};
+use float_cmp::approx_eq;
+use lopdf::{self, Dictionary, Object};
+use predicates::prelude::*;
+use predicates::reflection::{Case, Child, PredicateReflection, Product};
+use std::cmp;
+use std::fmt;
+
+/// Checks that the variable of type [u8] can be parsed as a PDF file.
+#[derive(Debug)]
+pub struct PdfPredicate {}
+
+impl PdfPredicate {
+ pub fn with_page_count(self: Self, num_pages: usize) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::PageCount(num_pages),
+ }
+ }
+
+ pub fn with_page_size(
+ self: Self,
+ idx: usize,
+ width_in_points: f32,
+ height_in_points: f32,
+ ) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::PageSize(
+ Dimensions {
+ w: width_in_points,
+ h: height_in_points,
+ unit: 1.0,
+ },
+ idx,
+ ),
+ }
+ }
+
+ pub fn with_creation_date(self: Self, when: DateTime<Utc>) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::CreationDate(when),
+ }
+ }
+
+ pub fn with_link(self: Self, link: &str) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::Link(link.to_string()),
+ }
+ }
+
+ pub fn with_text(self: Self, text: &str) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::Text(text.to_string()),
+ }
+ }
+}
+
+impl Predicate<[u8]> for PdfPredicate {
+ fn eval(&self, data: &[u8]) -> bool {
+ lopdf::Document::load_mem(data).is_ok()
+ }
+
+ fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ match lopdf::Document::load_mem(data) {
+ Ok(_) => None,
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for PdfPredicate {}
+
+impl fmt::Display for PdfPredicate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "is a PDF")
+ }
+}
+
+/// Extends a PdfPredicate by a check for page count, page size or creation date.
+#[derive(Debug)]
+pub struct DetailPredicate<PdfPredicate> {
+ p: PdfPredicate,
+ d: Detail,
+}
+
+#[derive(Debug)]
+enum Detail {
+ PageCount(usize),
+ PageSize(Dimensions, usize),
+ CreationDate(DateTime<Utc>),
+ Link(String),
+ Text(String),
+}
+
+/// A PDF page's dimensions from its `MediaBox`.
+///
+/// Note that `w` and `h` given in `UserUnit`, which is by default 1.0 = 1/72 inch.
+#[derive(Debug)]
+struct Dimensions {
+ w: f32,
+ h: f32,
+ unit: f32, // UserUnit, in points (1/72 of an inch)
+}
+
+impl Dimensions {
+ pub fn from_media_box(obj: &lopdf::Object, unit: Option<f32>) -> lopdf::Result<Dimensions> {
+ let a = obj.as_array()?;
+ Ok(Dimensions {
+ w: a[2].as_float()?,
+ h: a[3].as_float()?,
+ unit: unit.unwrap_or(1.0),
+ })
+ }
+
+ pub fn width_in_pt(self: &Self) -> f32 {
+ self.w * self.unit
+ }
+
+ pub fn height_in_pt(self: &Self) -> f32 {
+ self.h * self.unit
+ }
+}
+
+impl fmt::Display for Dimensions {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{} pt x {} pt", self.width_in_pt(), self.height_in_pt())
+ }
+}
+
+impl cmp::PartialEq for Dimensions {
+ fn eq(&self, other: &Self) -> bool {
+ approx_eq!(
+ f32,
+ self.width_in_pt(),
+ other.width_in_pt(),
+ epsilon = 0.0001
+ ) && approx_eq!(
+ f32,
+ self.height_in_pt(),
+ other.height_in_pt(),
+ epsilon = 0.0001
+ )
+ }
+}
+
+impl cmp::Eq for Dimensions {}
+
+trait Details {
+ fn get_page_count(&self) -> usize;
+ fn get_page_size(&self, idx: usize) -> Option<Dimensions>;
+ fn get_creation_date(&self) -> Option<DateTime<Utc>>;
+ fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object>;
+ fn get_from_page<'a>(
+ self: &'a Self,
+ idx: usize,
+ key: &[u8],
+ ) -> lopdf::Result<&'a lopdf::Object>;
+}
+
+impl DetailPredicate<PdfPredicate> {
+ fn eval_doc(&self, doc: &lopdf::Document) -> bool {
+ match &self.d {
+ Detail::PageCount(n) => doc.get_page_count() == *n,
+ Detail::PageSize(d, idx) => doc.get_page_size(*idx).map_or(false, |dim| dim == *d),
+ Detail::CreationDate(d) => doc.get_creation_date().map_or(false, |date| date == *d),
+ Detail::Link(link) => document_has_link(doc, &link),
+ Detail::Text(text) => document_has_text(doc, &text),
+ }
+ }
+
+ fn find_case_for_doc<'a>(&'a self, expected: bool, doc: &lopdf::Document) -> Option<Case<'a>> {
+ if self.eval_doc(doc) == expected {
+ let product = self.product_for_doc(doc);
+ Some(Case::new(Some(self), false).add_product(product))
+ } else {
+ None
+ }
+ }
+
+ fn product_for_doc(&self, doc: &lopdf::Document) -> Product {
+ match &self.d {
+ Detail::PageCount(_) => Product::new(
+ "actual page count",
+ format!("{} page(s)", doc.get_page_count()),
+ ),
+ Detail::PageSize(_, idx) => Product::new(
+ "actual page size",
+ match doc.get_page_size(*idx) {
+ Some(dim) => format!("{}", dim),
+ None => "None".to_string(),
+ },
+ ),
+ Detail::CreationDate(_) => Product::new(
+ "actual creation date",
+ format!("{:?}", doc.get_creation_date()),
+ ),
+ Detail::Link(_) => Product::new(
+ "actual link contents",
+ "FIXME: who knows, but it's not what we expected".to_string(),
+ ),
+ Detail::Text(_) => {
+ Product::new("actual text contents", doc.extract_text(&[1]).unwrap())
+ }
+ }
+ }
+}
+
+// Extensions to lopdf::Object; can be removed after lopdf 0.26
+trait ObjExt {
+ /// Get the object value as a float.
+ /// Unlike as_f32() this will also cast an Integer to a Real.
+ fn as_float(&self) -> lopdf::Result<f32>;
+}
+
+impl ObjExt for lopdf::Object {
+ fn as_float(&self) -> lopdf::Result<f32> {
+ match *self {
+ lopdf::Object::Integer(ref value) => Ok(*value as f32),
+ lopdf::Object::Real(ref value) => Ok(*value),
+ _ => Err(lopdf::Error::Type),
+ }
+ }
+}
+
+impl Details for lopdf::Document {
+ fn get_page_count(self: &Self) -> usize {
+ self.get_pages().len()
+ }
+
+ fn get_page_size(self: &Self, idx: usize) -> Option<Dimensions> {
+ match self.get_from_page(idx, b"MediaBox") {
+ Ok(obj) => {
+ let unit = self
+ .get_from_page(idx, b"UserUnit")
+ .and_then(ObjExt::as_float)
+ .ok();
+ Dimensions::from_media_box(obj, unit).ok()
+ }
+ Err(_) => None,
+ }
+ }
+
+ fn get_creation_date(self: &Self) -> Option<DateTime<Utc>> {
+ match self.get_from_trailer(b"CreationDate") {
+ Ok(obj) => obj.as_datetime().map(|date| date.with_timezone(&Utc)),
+ Err(_) => None,
+ }
+ }
+
+ fn get_from_trailer<'a>(self: &'a Self, key: &[u8]) -> lopdf::Result<&'a lopdf::Object> {
+ let id = self.trailer.get(b"Info")?.as_reference()?;
+ self.get_object(id)?.as_dict()?.get(key)
+ }
+
+ fn get_from_page<'a>(
+ self: &'a Self,
+ idx: usize,
+ key: &[u8],
+ ) -> lopdf::Result<&'a lopdf::Object> {
+ let mut iter = self.page_iter();
+ for _ in 0..idx {
+ let _ = iter.next();
+ }
+ match iter.next() {
+ Some(id) => self.get_object(id)?.as_dict()?.get(key),
+ None => Err(lopdf::Error::ObjectNotFound),
+ }
+ }
+}
+
+impl Predicate<[u8]> for DetailPredicate<PdfPredicate> {
+ fn eval(&self, data: &[u8]) -> bool {
+ match lopdf::Document::load_mem(data) {
+ Ok(doc) => self.eval_doc(&doc),
+ _ => false,
+ }
+ }
+
+ fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ match lopdf::Document::load_mem(data) {
+ Ok(doc) => self.find_case_for_doc(expected, &doc),
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for DetailPredicate<PdfPredicate> {
+ fn children<'a>(&'a self) -> Box<dyn Iterator<Item = Child<'a>> + 'a> {
+ let params = vec![Child::new("predicate", &self.p)];
+ Box::new(params.into_iter())
+ }
+}
+
+impl fmt::Display for DetailPredicate<PdfPredicate> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match &self.d {
+ Detail::PageCount(n) => write!(f, "is a PDF with {} page(s)", n),
+ Detail::PageSize(d, _) => write!(f, "is a PDF sized {}", d),
+ Detail::CreationDate(d) => write!(f, "is a PDF created {:?}", d),
+ Detail::Link(l) => write!(f, "is a PDF with a link to {}", l),
+ Detail::Text(t) => write!(f, "is a PDF with \"{}\" in its text content", t),
+ }
+ }
+}
+
+// This is an extremely trivial test for a string being present in the document's
+// text objects.
+fn document_has_text(document: &lopdf::Document, needle: &str) -> bool {
+ if let Ok(haystack) = text_from_first_page(document) {
+ haystack.contains(needle)
+ } else {
+ false
+ }
+}
+
+// We do a super simple test that a PDF actually contains an Annotation object
+// with a particular link. We don't test that this annotation is actually linked
+// from a page; that would be nicer.
+fn document_has_link(document: &lopdf::Document, link_text: &str) -> bool {
+ document
+ .objects
+ .iter()
+ .map(|(_obj_id, object)| object)
+ .any(|obj| object_is_annotation_with_link(obj, link_text))
+}
+
+fn object_is_annotation_with_link(object: &Object, link_text: &str) -> bool {
+ object
+ .as_dict()
+ .map(|dict| dict_is_annotation(dict) && dict_has_a_with_link(dict, link_text))
+ .unwrap_or(false)
+}
+
+fn dict_is_annotation(dict: &Dictionary) -> bool {
+ dict.get(b"Type")
+ .and_then(|type_val| type_val.as_name_str())
+ .map(|name| name == "Annot")
+ .unwrap_or(false)
+}
+
+fn dict_has_a_with_link(dict: &Dictionary, link_text: &str) -> bool {
+ dict.get(b"A")
+ .and_then(|obj| obj.as_dict())
+ .and_then(|dict| dict.get(b"URI"))
+ .and_then(|obj| obj.as_str())
+ .map(|string| string == link_text.as_bytes())
+ .unwrap_or(false)
+}
+
+fn text_from_first_page(doc: &lopdf::Document) -> lopdf::Result<String> {
+ // This is extremely simplistic; lopdf just concatenates all the text in the page
+ // into a single string.
+ doc.extract_text(&[1])
+}
diff --git a/rsvg_convert/tests/internal_predicates/png.rs b/rsvg_convert/tests/internal_predicates/png.rs
new file mode 100644
index 00000000..f629b510
--- /dev/null
+++ b/rsvg_convert/tests/internal_predicates/png.rs
@@ -0,0 +1,193 @@
+use png;
+use predicates::prelude::*;
+use predicates::reflection::{Case, Child, PredicateReflection, Product};
+use std::fmt;
+use std::io::BufReader;
+use std::path::{Path, PathBuf};
+
+use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType};
+
+use rsvg::test_utils::compare_surfaces::BufferDiff;
+use rsvg::test_utils::reference_utils::{surface_from_png, Compare, Deviation, Reference};
+
+/// Checks that the variable of type [u8] can be parsed as a PNG file.
+#[derive(Debug)]
+pub struct PngPredicate {}
+
+impl PngPredicate {
+ pub fn with_size(self: Self, w: u32, h: u32) -> SizePredicate<Self> {
+ SizePredicate::<Self> { p: self, w, h }
+ }
+
+ pub fn with_contents<P: AsRef<Path>>(self: Self, reference: P) -> ReferencePredicate<Self> {
+ let mut path = PathBuf::new();
+ path.push(reference);
+ ReferencePredicate::<Self> { p: self, path }
+ }
+}
+
+impl Predicate<[u8]> for PngPredicate {
+ fn eval(&self, data: &[u8]) -> bool {
+ let decoder = png::Decoder::new(data);
+ decoder.read_info().is_ok()
+ }
+
+ fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ let decoder = png::Decoder::new(data);
+ match decoder.read_info() {
+ Ok(_) => None,
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for PngPredicate {}
+
+impl fmt::Display for PngPredicate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "is a PNG")
+ }
+}
+
+/// Extends a PngPredicate by a check for a given size of the PNG file.
+#[derive(Debug)]
+pub struct SizePredicate<PngPredicate> {
+ p: PngPredicate,
+ w: u32,
+ h: u32,
+}
+
+impl SizePredicate<PngPredicate> {
+ fn eval_info(&self, info: &png::Info) -> bool {
+ info.width == self.w && info.height == self.h
+ }
+
+ fn find_case_for_info<'a>(&'a self, expected: bool, info: &png::Info) -> Option<Case<'a>> {
+ if self.eval_info(info) == expected {
+ let product = self.product_for_info(info);
+ Some(Case::new(Some(self), false).add_product(product))
+ } else {
+ None
+ }
+ }
+
+ fn product_for_info(&self, info: &png::Info) -> Product {
+ let actual_size = format!("{} x {}", info.width, info.height);
+ Product::new("actual size", actual_size)
+ }
+}
+
+impl Predicate<[u8]> for SizePredicate<PngPredicate> {
+ fn eval(&self, data: &[u8]) -> bool {
+ let decoder = png::Decoder::new(data);
+ match decoder.read_info() {
+ Ok(reader) => self.eval_info(&reader.info()),
+ _ => false,
+ }
+ }
+
+ fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ let decoder = png::Decoder::new(data);
+ match decoder.read_info() {
+ Ok(reader) => self.find_case_for_info(expected, reader.info()),
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for SizePredicate<PngPredicate> {
+ fn children<'a>(&'a self) -> Box<dyn Iterator<Item = Child<'a>> + 'a> {
+ let params = vec![Child::new("predicate", &self.p)];
+ Box::new(params.into_iter())
+ }
+}
+
+impl fmt::Display for SizePredicate<PngPredicate> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "is a PNG with size {} x {}", self.w, self.h)
+ }
+}
+
+/// Extends a PngPredicate by a comparison to the contents of a reference file
+#[derive(Debug)]
+pub struct ReferencePredicate<PngPredicate> {
+ p: PngPredicate,
+ path: PathBuf,
+}
+
+impl ReferencePredicate<PngPredicate> {
+ fn diff_acceptable(diff: &BufferDiff) -> bool {
+ match diff {
+ BufferDiff::DifferentSizes => false,
+ BufferDiff::Diff(diff) => !diff.inacceptable(),
+ }
+ }
+
+ fn diff_surface(&self, surface: &SharedImageSurface) -> Option<BufferDiff> {
+ let reference = Reference::from_png(&self.path)
+ .unwrap_or_else(|_| panic!("could not open {:?}", self.path));
+ if let Ok(diff) = reference.compare(&surface) {
+ if !Self::diff_acceptable(&diff) {
+ return Some(diff);
+ }
+ }
+ None
+ }
+
+ fn find_case_for_surface<'a>(
+ &'a self,
+ expected: bool,
+ surface: &SharedImageSurface,
+ ) -> Option<Case<'a>> {
+ let diff = self.diff_surface(&surface);
+ if diff.is_some() != expected {
+ let product = self.product_for_diff(&diff.unwrap());
+ Some(Case::new(Some(self), false).add_product(product))
+ } else {
+ None
+ }
+ }
+
+ fn product_for_diff(&self, diff: &BufferDiff) -> Product {
+ let difference = format!("{}", diff);
+ Product::new("images differ", difference)
+ }
+}
+
+impl Predicate<[u8]> for ReferencePredicate<PngPredicate> {
+ fn eval(&self, data: &[u8]) -> bool {
+ if let Ok(surface) = surface_from_png(&mut BufReader::new(data)) {
+ let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap();
+ self.diff_surface(&surface).is_some()
+ } else {
+ false
+ }
+ }
+
+ fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ match surface_from_png(&mut BufReader::new(data)) {
+ Ok(surface) => {
+ let surface = SharedImageSurface::wrap(surface, SurfaceType::SRgb).unwrap();
+ self.find_case_for_surface(expected, &surface)
+ }
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for ReferencePredicate<PngPredicate> {
+ fn children<'a>(&'a self) -> Box<dyn Iterator<Item = Child<'a>> + 'a> {
+ let params = vec![Child::new("predicate", &self.p)];
+ Box::new(params.into_iter())
+ }
+}
+
+impl fmt::Display for ReferencePredicate<PngPredicate> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "is a PNG that matches the reference {}",
+ self.path.display()
+ )
+ }
+}
diff --git a/rsvg_convert/tests/internal_predicates/svg.rs b/rsvg_convert/tests/internal_predicates/svg.rs
new file mode 100644
index 00000000..70473812
--- /dev/null
+++ b/rsvg_convert/tests/internal_predicates/svg.rs
@@ -0,0 +1,179 @@
+use float_cmp::approx_eq;
+use gio::glib::Bytes;
+use gio::MemoryInputStream;
+use predicates::prelude::*;
+use predicates::reflection::{Case, Child, PredicateReflection, Product};
+use std::cmp;
+use std::fmt;
+
+use rsvg::{CairoRenderer, Length, Loader, LoadingError, SvgHandle};
+
+/// Checks that the variable of type [u8] can be parsed as a SVG file.
+#[derive(Debug)]
+pub struct SvgPredicate {}
+
+impl SvgPredicate {
+ pub fn with_size(self: Self, width: Length, height: Length) -> DetailPredicate<Self> {
+ DetailPredicate::<Self> {
+ p: self,
+ d: Detail::Size(Dimensions {
+ w: width,
+ h: height,
+ }),
+ }
+ }
+}
+
+fn svg_from_bytes(data: &[u8]) -> Result<SvgHandle, LoadingError> {
+ let bytes = Bytes::from(data);
+ let stream = MemoryInputStream::from_bytes(&bytes);
+ Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>)
+}
+
+impl Predicate<[u8]> for SvgPredicate {
+ fn eval(&self, data: &[u8]) -> bool {
+ svg_from_bytes(data).is_ok()
+ }
+
+ fn find_case<'a>(&'a self, _expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ match svg_from_bytes(data) {
+ Ok(_) => None,
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for SvgPredicate {}
+
+impl fmt::Display for SvgPredicate {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "is an SVG")
+ }
+}
+
+/// Extends a SVG Predicate by a check for its size
+#[derive(Debug)]
+pub struct DetailPredicate<SvgPredicate> {
+ p: SvgPredicate,
+ d: Detail,
+}
+
+#[derive(Debug)]
+enum Detail {
+ Size(Dimensions),
+}
+
+/// SVG's dimensions
+#[derive(Debug)]
+struct Dimensions {
+ w: Length,
+ h: Length,
+}
+
+impl Dimensions {
+ pub fn width(self: &Self) -> f64 {
+ self.w.length
+ }
+
+ pub fn height(self: &Self) -> f64 {
+ self.h.length
+ }
+}
+
+impl fmt::Display for Dimensions {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "{}{} x {}{}",
+ self.width(),
+ self.w.unit,
+ self.height(),
+ self.h.unit
+ )
+ }
+}
+
+impl cmp::PartialEq for Dimensions {
+ fn eq(&self, other: &Self) -> bool {
+ approx_eq!(f64, self.width(), other.width(), epsilon = 0.000_001)
+ && approx_eq!(f64, self.height(), other.height(), epsilon = 0.000_001)
+ && (self.w.unit == self.h.unit)
+ && (self.h.unit == other.h.unit)
+ && (other.h.unit == other.w.unit)
+ }
+}
+
+impl cmp::Eq for Dimensions {}
+
+trait Details {
+ fn get_size(&self) -> Option<Dimensions>;
+}
+
+impl DetailPredicate<SvgPredicate> {
+ fn eval_doc(&self, handle: &SvgHandle) -> bool {
+ match &self.d {
+ Detail::Size(d) => {
+ let renderer = CairoRenderer::new(handle);
+ let dimensions = renderer.intrinsic_dimensions();
+ (dimensions.width, dimensions.height) == (d.w, d.h)
+ }
+ }
+ }
+
+ fn find_case_for_doc<'a>(&'a self, expected: bool, handle: &SvgHandle) -> Option<Case<'a>> {
+ if self.eval_doc(handle) == expected {
+ let product = self.product_for_doc(handle);
+ Some(Case::new(Some(self), false).add_product(product))
+ } else {
+ None
+ }
+ }
+
+ fn product_for_doc(&self, handle: &SvgHandle) -> Product {
+ match &self.d {
+ Detail::Size(_) => {
+ let renderer = CairoRenderer::new(handle);
+ let dimensions = renderer.intrinsic_dimensions();
+
+ Product::new(
+ "actual size",
+ format!(
+ "width={:?}, height={:?}",
+ dimensions.width, dimensions.height
+ ),
+ )
+ }
+ }
+ }
+}
+
+impl Predicate<[u8]> for DetailPredicate<SvgPredicate> {
+ fn eval(&self, data: &[u8]) -> bool {
+ match svg_from_bytes(data) {
+ Ok(handle) => self.eval_doc(&handle),
+ _ => false,
+ }
+ }
+
+ fn find_case<'a>(&'a self, expected: bool, data: &[u8]) -> Option<Case<'a>> {
+ match svg_from_bytes(data) {
+ Ok(handle) => self.find_case_for_doc(expected, &handle),
+ Err(e) => Some(Case::new(Some(self), false).add_product(Product::new("Error", e))),
+ }
+ }
+}
+
+impl PredicateReflection for DetailPredicate<SvgPredicate> {
+ fn children<'a>(&'a self) -> Box<dyn Iterator<Item = Child<'a>> + 'a> {
+ let params = vec![Child::new("predicate", &self.p)];
+ Box::new(params.into_iter())
+ }
+}
+
+impl fmt::Display for DetailPredicate<SvgPredicate> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match &self.d {
+ Detail::Size(d) => write!(f, "is an SVG sized {}", d),
+ }
+ }
+}
diff --git a/rsvg_convert/tests/rsvg_convert.rs b/rsvg_convert/tests/rsvg_convert.rs
new file mode 100644
index 00000000..f58edf6c
--- /dev/null
+++ b/rsvg_convert/tests/rsvg_convert.rs
@@ -0,0 +1,1078 @@
+//use crate::predicates::ends_with_pkg_version;
+mod internal_predicates;
+use internal_predicates::file;
+
+use assert_cmd::assert::IntoOutputPredicate;
+use assert_cmd::Command;
+#[cfg(system_deps_have_cairo_pdf)]
+use chrono::{TimeZone, Utc};
+use predicates::boolean::*;
+use predicates::prelude::*;
+use predicates::str::*;
+use rsvg::{Length, LengthUnit};
+use std::path::Path;
+use tempfile::Builder;
+use url::Url;
+
+// What should be tested here?
+// The goal is to test the code in rsvg-convert, not the entire library.
+//
+// - command-line options that affect size (width, height, zoom, resolution) ✔
+// - pixel dimensions of the output (should be sufficient to do that for PNG) ✔
+// - limit on output size (32767 pixels) ✔
+// - output formats (PNG, PDF, PS, EPS, SVG) ✔
+// - multi-page output (for PDF) ✔
+// - output file option ✔
+// - SOURCE_DATA_EPOCH environment variable for PDF output ✔
+// - background color option ✔
+// - optional CSS stylesheet ✔
+// - error handling for missing SVG dimensions ✔
+// - error handling for export lookup ID ✔
+// - error handling for invalid input ✔
+
+struct RsvgConvert {}
+
+impl RsvgConvert {
+ fn new() -> Command {
+ Command::cargo_bin("rsvg-convert").unwrap()
+ }
+
+ fn new_with_input<P>(file: P) -> Command
+ where
+ P: AsRef<Path>,
+ {
+ let mut command = RsvgConvert::new();
+ match command.pipe_stdin(&file) {
+ Ok(_) => command,
+ Err(e) => panic!("Error opening file '{}': {}", file.as_ref().display(), e),
+ }
+ }
+
+ fn accepts_arg(option: &str) {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg(option)
+ .assert()
+ .success();
+ }
+
+ fn option_yields_output<I, P>(option: &str, output_pred: I)
+ where
+ I: IntoOutputPredicate<P>,
+ P: Predicate<[u8]>,
+ {
+ RsvgConvert::new()
+ .arg(option)
+ .assert()
+ .success()
+ .stdout(output_pred);
+ }
+}
+
+#[test]
+fn converts_svg_from_stdin_to_png() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .assert()
+ .success()
+ .stdout(file::is_png());
+}
+
+#[test]
+fn argument_is_input_filename() {
+ let input = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ RsvgConvert::new()
+ .arg(input)
+ .assert()
+ .success()
+ .stdout(file::is_png());
+}
+
+#[test]
+fn argument_is_url() {
+ let path = Path::new("tests/fixtures/bug521-with-viewbox.svg")
+ .canonicalize()
+ .unwrap();
+ let url = Url::from_file_path(path).unwrap();
+ let stringified = url.as_str();
+ assert!(stringified.starts_with("file://"));
+
+ RsvgConvert::new()
+ .arg(stringified)
+ .assert()
+ .success()
+ .stdout(file::is_png());
+}
+
+#[test]
+fn output_format_png() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format=png")
+ .assert()
+ .success()
+ .stdout(file::is_png());
+}
+
+#[cfg(system_deps_have_cairo_ps)]
+#[test]
+fn output_format_ps() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format=ps")
+ .assert()
+ .success()
+ .stdout(file::is_ps());
+}
+
+#[cfg(system_deps_have_cairo_ps)]
+#[test]
+fn output_format_eps() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format=eps")
+ .assert()
+ .success()
+ .stdout(file::is_eps());
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn output_format_pdf() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format=pdf")
+ .assert()
+ .success()
+ .stdout(file::is_pdf());
+}
+
+#[cfg(system_deps_have_cairo_svg)]
+#[test]
+fn output_format_svg_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("-f")
+ .arg("svg")
+ .assert()
+ .success()
+ .stdout(file::is_svg());
+}
+
+#[cfg(system_deps_have_cairo_svg)]
+#[test]
+fn user_specified_width_and_height() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format")
+ .arg("svg")
+ .arg("--width")
+ .arg("42cm")
+ .arg("--height")
+ .arg("43cm")
+ .assert()
+ .success()
+ .stdout(file::is_svg().with_size(
+ Length::new(42.0, LengthUnit::Cm),
+ Length::new(43.0, LengthUnit::Cm),
+ ));
+}
+
+#[cfg(system_deps_have_cairo_svg)]
+#[test]
+fn user_specified_width_and_height_px_output() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format")
+ .arg("svg")
+ .arg("--width")
+ .arg("1920")
+ .arg("--height")
+ .arg("508mm")
+ .assert()
+ .success()
+ .stdout(file::is_svg().with_size(
+ Length::new(1920.0, LengthUnit::Px),
+ Length::new(1920.0, LengthUnit::Px),
+ ));
+}
+
+#[cfg(system_deps_have_cairo_svg)]
+#[test]
+fn user_specified_width_and_height_a4() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--format")
+ .arg("svg")
+ .arg("--page-width")
+ .arg("210mm")
+ .arg("--page-height")
+ .arg("297mm")
+ .arg("--left")
+ .arg("1cm")
+ .arg("--top")
+ .arg("1cm")
+ .arg("--width")
+ .arg("190mm")
+ .arg("--height")
+ .arg("277mm")
+ .assert()
+ .success()
+ .stdout(file::is_svg().with_size(
+ Length::new(210.0, LengthUnit::Mm),
+ Length::new(297.0, LengthUnit::Mm),
+ ));
+}
+
+#[test]
+fn output_file_option() {
+ let output = {
+ let tempfile = Builder::new().suffix(".png").tempfile().unwrap();
+ tempfile.path().to_path_buf()
+ };
+ assert!(predicates::path::is_file().not().eval(&output));
+
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg(format!("--output={}", output.display()))
+ .assert()
+ .success()
+ .stdout(is_empty());
+
+ assert!(predicates::path::is_file().eval(&output));
+ std::fs::remove_file(&output).unwrap();
+}
+
+#[test]
+fn output_file_short_option() {
+ let output = {
+ let tempfile = Builder::new().suffix(".png").tempfile().unwrap();
+ tempfile.path().to_path_buf()
+ };
+ assert!(predicates::path::is_file().not().eval(&output));
+
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("-o")
+ .arg(format!("{}", output.display()))
+ .assert()
+ .success()
+ .stdout(is_empty());
+
+ assert!(predicates::path::is_file().eval(&output));
+ std::fs::remove_file(&output).unwrap();
+}
+
+#[test]
+fn overwrites_existing_output_file() {
+ let output = {
+ let tempfile = Builder::new().suffix(".png").tempfile().unwrap();
+ tempfile.path().to_path_buf()
+ };
+ assert!(predicates::path::is_file().not().eval(&output));
+
+ for _ in 0..2 {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg(format!("--output={}", output.display()))
+ .assert()
+ .success()
+ .stdout(is_empty());
+
+ assert!(predicates::path::is_file().eval(&output));
+ }
+
+ std::fs::remove_file(&output).unwrap();
+}
+
+#[test]
+fn empty_input_yields_error() {
+ let starts_with = starts_with("Error reading SVG");
+ let ends_with = ends_with("Input file is too short").trim();
+ RsvgConvert::new()
+ .assert()
+ .failure()
+ .stderr(starts_with.and(ends_with));
+}
+
+#[test]
+fn empty_svg_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/empty.svg")
+ .assert()
+ .failure()
+ .stderr("The SVG stdin has no dimensions\n");
+}
+
+#[test]
+fn multiple_input_files_not_allowed_for_png_output() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ RsvgConvert::new()
+ .arg(one)
+ .arg(two)
+ .assert()
+ .failure()
+ .stderr(contains(
+ "Multiple SVG files are only allowed for PDF and (E)PS output",
+ ));
+}
+
+#[cfg(system_deps_have_cairo_ps)]
+#[test]
+fn multiple_input_files_accepted_for_eps_output() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ RsvgConvert::new()
+ .arg("--format=eps")
+ .arg(one)
+ .arg(two)
+ .assert()
+ .success()
+ .stdout(file::is_eps());
+}
+
+#[cfg(system_deps_have_cairo_ps)]
+#[test]
+fn multiple_input_files_accepted_for_ps_output() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ RsvgConvert::new()
+ .arg("--format=ps")
+ .arg(one)
+ .arg(two)
+ .assert()
+ .success()
+ .stdout(file::is_ps());
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn multiple_input_files_create_multi_page_pdf_output() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ let three = Path::new("tests/fixtures/example.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg(one)
+ .arg(two)
+ .arg(three)
+ .assert()
+ .success()
+ .stdout(
+ file::is_pdf()
+ .with_page_count(3)
+ .and(file::is_pdf().with_page_size(0, 150.0, 75.0))
+ .and(file::is_pdf().with_page_size(1, 123.0, 123.0))
+ .and(file::is_pdf().with_page_size(2, 75.0, 300.0)),
+ );
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn multiple_input_files_create_multi_page_pdf_output_fixed_size() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ let three = Path::new("tests/fixtures/example.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg("--page-width=8.5in")
+ .arg("--page-height=11in")
+ .arg("--width=7.5in")
+ .arg("--height=10in")
+ .arg("--left=0.5in")
+ .arg("--top=0.5in")
+ .arg("--keep-aspect-ratio")
+ .arg(one)
+ .arg(two)
+ .arg(three)
+ .assert()
+ .success()
+ .stdout(
+ file::is_pdf()
+ .with_page_count(3)
+ // https://www.wolframalpha.com/input/?i=convert+11+inches+to+desktop+publishing+points
+ .and(file::is_pdf().with_page_size(0, 612.0, 792.0))
+ .and(file::is_pdf().with_page_size(1, 612.0, 792.0))
+ .and(file::is_pdf().with_page_size(2, 612.0, 792.0)),
+ );
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_has_link() {
+ let input = Path::new("tests/fixtures/a-link.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg(input)
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_link("https://example.com"));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_has_link_inside_text() {
+ let input = Path::new("tests/fixtures/text-a-link.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg(input)
+ .assert()
+ .success()
+ .stdout(
+ file::is_pdf()
+ .with_link("https://example.com")
+ .and(file::is_pdf().with_link("https://another.example.com")),
+ );
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_has_text() {
+ let input = Path::new("tests/fixtures/hello-world.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg(input)
+ .assert()
+ .success()
+ .stdout(
+ file::is_pdf()
+ .with_text("Hello world!")
+ .and(file::is_pdf().with_text("Hello again!")),
+ );
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn env_source_data_epoch_controls_pdf_creation_date() {
+ let input = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let date = 1581411039; // seconds since epoch
+ RsvgConvert::new()
+ .env("SOURCE_DATE_EPOCH", format!("{}", date))
+ .arg("--format=pdf")
+ .arg(input)
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_creation_date(Utc.timestamp_opt(date, 0).unwrap()));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn env_source_data_epoch_no_digits() {
+ // intentionally not testing for the full error string here
+ let input = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ RsvgConvert::new()
+ .env("SOURCE_DATE_EPOCH", "foobar")
+ .arg("--format=pdf")
+ .arg(input)
+ .assert()
+ .failure()
+ .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH"));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn env_source_data_epoch_trailing_garbage() {
+ // intentionally not testing for the full error string here
+ let input = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .env("SOURCE_DATE_EPOCH", "1234556+")
+ .arg(input)
+ .assert()
+ .failure()
+ .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH"));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn env_source_data_epoch_empty() {
+ // intentionally not testing for the full error string here
+ let input = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .env("SOURCE_DATE_EPOCH", "")
+ .arg(input)
+ .assert()
+ .failure()
+ .stderr(starts_with("Environment variable $SOURCE_DATE_EPOCH"));
+}
+
+#[test]
+fn width_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--width=300")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(300, 150));
+}
+
+#[test]
+fn height_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--height=200")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(400, 200));
+}
+
+#[test]
+fn width_and_height_options() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--width=300")
+ .arg("--height=200")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(300, 200));
+}
+
+#[test]
+fn unsupported_unit_in_width_and_height() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--height=200ex")
+ .assert()
+ .failure()
+ .stderr(contains("supported units"));
+}
+
+#[test]
+fn invalid_length() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--page-width=foo")
+ .assert()
+ .failure()
+ .stderr(contains("can not be parsed as a length"));
+}
+
+#[test]
+fn zoom_factor() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--zoom=0.8")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(160, 80));
+}
+
+#[test]
+fn zoom_factor_and_larger_size() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--width=400")
+ .arg("--height=200")
+ .arg("--zoom=1.5")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(300, 150));
+}
+
+#[test]
+fn zoom_factor_and_smaller_size() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--width=400")
+ .arg("--height=200")
+ .arg("--zoom=3.5")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(400, 200));
+}
+
+#[test]
+fn x_zoom_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--x-zoom=2")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(400, 100));
+}
+
+#[test]
+fn x_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("-x")
+ .arg("2.0")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(400, 100));
+}
+
+#[test]
+fn y_zoom_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--y-zoom=2.0")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(200, 200));
+}
+
+#[test]
+fn y_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("-y")
+ .arg("2")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(200, 200));
+}
+
+#[test]
+fn huge_zoom_factor_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--zoom=1000")
+ .assert()
+ .failure()
+ .stderr(starts_with(
+ "The resulting image would be larger than 32767 pixels",
+ ));
+}
+
+#[test]
+fn negative_zoom_factor_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--zoom=-2")
+ .assert()
+ .failure()
+ .stderr(contains("Invalid zoom"));
+}
+
+#[test]
+fn invalid_zoom_factor_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/bug521-with-viewbox.svg")
+ .arg("--zoom=foo")
+ .assert()
+ .failure()
+ .stderr(contains("invalid value"));
+}
+
+#[test]
+fn default_resolution_is_96dpi() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(96, 384));
+}
+
+#[test]
+fn x_resolution() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--dpi-x=300")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(300, 384));
+}
+
+#[test]
+fn x_resolution_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-d")
+ .arg("45")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(45, 384));
+}
+
+#[test]
+fn y_resolution() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--dpi-y=300")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(96, 1200));
+}
+
+#[test]
+fn y_resolution_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-p")
+ .arg("45")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(96, 180));
+}
+
+#[test]
+fn x_and_y_resolution() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--dpi-x=300")
+ .arg("--dpi-y=150")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(300, 600));
+}
+
+#[test]
+fn zero_resolution_is_invalid() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--dpi-x=0")
+ .arg("--dpi-y=0")
+ .assert()
+ .failure()
+ .stderr(contains("Invalid resolution"));
+}
+
+#[test]
+fn negative_resolution_is_invalid() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--dpi-x=-100")
+ .arg("--dpi-y=-100")
+ .assert()
+ .failure()
+ .stderr(contains("Invalid resolution"));
+}
+
+#[test]
+fn zero_offset_png() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--page-width=640")
+ .arg("--page-height=480")
+ .arg("--width=200")
+ .arg("--height=100")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/zero-offset-png.png"));
+}
+
+#[test]
+fn offset_png() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--page-width=640")
+ .arg("--page-height=480")
+ .arg("--width=200")
+ .arg("--height=100")
+ .arg("--left=100")
+ .arg("--top=50")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/offset-png.png"));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn unscaled_pdf_size() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_page_size(0, 72.0, 72.0));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_size_width_height() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .arg("--width=2in")
+ .arg("--height=3in")
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_page_size(0, 144.0, 216.0));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_size_width_height_proportional() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .arg("--width=2in")
+ .arg("--height=3in")
+ .arg("--keep-aspect-ratio")
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_page_size(0, 144.0, 144.0));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn pdf_page_size() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .arg("--page-width=210mm")
+ .arg("--page-height=297mm")
+ .assert()
+ .success()
+ .stdout(file::is_pdf().with_page_size(0, 210.0 / 25.4 * 72.0, 297.0 / 25.4 * 72.0));
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn multiple_input_files_create_multi_page_pdf_size_override() {
+ let one = Path::new("tests/fixtures/bug521-with-viewbox.svg");
+ let two = Path::new("tests/fixtures/sub-rect-no-unit.svg");
+ let three = Path::new("tests/fixtures/example.svg");
+ RsvgConvert::new()
+ .arg("--format=pdf")
+ .arg("--width=300pt")
+ .arg("--height=200pt")
+ .arg(one)
+ .arg(two)
+ .arg(three)
+ .assert()
+ .success()
+ .stdout(
+ file::is_pdf()
+ .with_page_count(3)
+ .and(file::is_pdf().with_page_size(0, 300.0, 200.0))
+ .and(file::is_pdf().with_page_size(1, 300.0, 200.0))
+ .and(file::is_pdf().with_page_size(2, 300.0, 200.0)),
+ );
+}
+
+#[cfg(system_deps_have_cairo_pdf)]
+#[test]
+fn missing_page_size_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .arg("--page-width=210mm")
+ .assert()
+ .failure()
+ .stderr(contains("both").and(contains("options")));
+
+ RsvgConvert::new_with_input("tests/fixtures/dimensions-in.svg")
+ .arg("--format=pdf")
+ .arg("--page-height=297mm")
+ .assert()
+ .failure()
+ .stderr(contains("both").and(contains("options")));
+}
+
+#[test]
+fn does_not_clip_partial_coverage_pixels() {
+ RsvgConvert::new_with_input("tests/fixtures/bug677-partial-pixel.svg")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(2, 2));
+}
+
+#[test]
+fn background_color_option_with_valid_color() {
+ RsvgConvert::accepts_arg("--background-color=LimeGreen");
+}
+
+#[test]
+fn background_color_option_none() {
+ RsvgConvert::accepts_arg("--background-color=None");
+}
+
+#[test]
+fn background_color_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-b")
+ .arg("#aabbcc")
+ .assert()
+ .success();
+}
+
+#[test]
+fn background_color_option_invalid_color_yields_error() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--background-color=foobar")
+ .assert()
+ .failure()
+ .stderr(contains("Invalid").and(contains("color")));
+}
+
+#[test]
+fn background_color_is_rendered() {
+ RsvgConvert::new_with_input("tests/fixtures/gimp-wilber.svg")
+ .arg("--background-color=purple")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/gimp-wilber-ref.png"));
+}
+
+#[test]
+fn stylesheet_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--stylesheet=tests/fixtures/empty.svg")
+ .assert()
+ .success();
+}
+
+#[test]
+fn stylesheet_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-s")
+ .arg("tests/fixtures/empty.svg")
+ .assert()
+ .success();
+}
+
+#[test]
+fn stylesheet_option_error() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--stylesheet=foobar")
+ .assert()
+ .failure()
+ .stderr(starts_with("Error reading stylesheet"));
+}
+
+#[test]
+fn export_id_option() {
+ RsvgConvert::new_with_input("tests/fixtures/geometry-element.svg")
+ .arg("--export-id=foo")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(40, 50));
+}
+
+#[test]
+fn export_id_with_zero_stroke_width() {
+ // https://gitlab.gnome.org/GNOME/librsvg/-/issues/601
+ //
+ // This tests a bug that manifested itself easily with the --export-id option, but it
+ // is not a bug with the option itself. An object with stroke_width=0 was causing
+ // an extra point at the origin to be put in the bounding box, so the final image
+ // spanned the origin to the actual visible bounds of the rendered object.
+ //
+ // We can probably test this more cleanly once we have a render tree.
+ RsvgConvert::new_with_input("tests/fixtures/bug601-zero-stroke-width.svg")
+ .arg("--export-id=foo")
+ .assert()
+ .success()
+ .stdout(
+ file::is_png()
+ .with_contents("tests/fixtures/bug601-zero-stroke-width-render-only-foo.png"),
+ );
+}
+
+#[test]
+fn export_id_short_option() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-i")
+ .arg("two")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(100, 200));
+}
+
+#[test]
+fn export_id_with_hash_prefix() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("-i")
+ .arg("#two")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(100, 200));
+}
+
+#[test]
+fn export_id_option_error() {
+ RsvgConvert::new_with_input("tests/fixtures/dpi.svg")
+ .arg("--export-id=foobar")
+ .assert()
+ .failure()
+ .stderr(starts_with("File stdin does not have an object with id \""));
+}
+
+#[test]
+fn unlimited_option() {
+ RsvgConvert::accepts_arg("--unlimited");
+}
+
+#[test]
+fn unlimited_short_option() {
+ RsvgConvert::accepts_arg("-u");
+}
+
+#[test]
+fn keep_aspect_ratio_option() {
+ let input = Path::new("tests/fixtures/dpi.svg");
+ RsvgConvert::new_with_input(input)
+ .arg("--width=500")
+ .arg("--height=1000")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(500, 1000));
+ RsvgConvert::new_with_input(input)
+ .arg("--width=500")
+ .arg("--height=1000")
+ .arg("--keep-aspect-ratio")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(250, 1000));
+}
+
+#[test]
+fn keep_aspect_ratio_short_option() {
+ let input = Path::new("tests/fixtures/dpi.svg");
+ RsvgConvert::new_with_input(input)
+ .arg("--width=1000")
+ .arg("--height=500")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(1000, 500));
+ RsvgConvert::new_with_input(input)
+ .arg("--width=1000")
+ .arg("--height=500")
+ .arg("-a")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_size(125, 500));
+}
+
+#[test]
+fn overflowing_size_is_detected() {
+ RsvgConvert::new_with_input("tests/fixtures/bug591-vbox-overflow.svg")
+ .assert()
+ .failure()
+ .stderr(starts_with(
+ "The resulting image would be larger than 32767 pixels",
+ ));
+}
+
+#[test]
+fn accept_language_given() {
+ RsvgConvert::new_with_input("tests/fixtures/accept-language.svg")
+ .arg("--accept-language=es-MX")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/accept-language-es.png"));
+
+ RsvgConvert::new_with_input("tests/fixtures/accept-language.svg")
+ .arg("--accept-language=de")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/accept-language-de.png"));
+}
+
+#[test]
+fn accept_language_fallback() {
+ RsvgConvert::new_with_input("tests/fixtures/accept-language.svg")
+ .arg("--accept-language=fr")
+ .assert()
+ .success()
+ .stdout(file::is_png().with_contents("tests/fixtures/accept-language-fallback.png"));
+}
+
+#[test]
+fn accept_language_invalid_tag() {
+ // underscores are not valid in BCP47 language tags
+ RsvgConvert::new_with_input("tests/fixtures/accept-language.svg")
+ .arg("--accept-language=foo_bar")
+ .assert()
+ .failure()
+ .stderr(contains("invalid language tag"));
+}
+
+#[test]
+fn keep_image_data_option() {
+ RsvgConvert::accepts_arg("--keep-image-data");
+}
+
+#[test]
+fn no_keep_image_data_option() {
+ RsvgConvert::accepts_arg("--no-keep-image-data");
+}
+
+fn is_version_output() -> AndPredicate<StartsWithPredicate, TrimPredicate<EndsWithPredicate>, str> {
+ starts_with("rsvg-convert version ")
+ .and(predicates::str::ends_with(env!("CARGO_PKG_VERSION")).trim())
+}
+
+#[test]
+fn version_option() {
+ RsvgConvert::option_yields_output("--version", is_version_output())
+}
+
+#[test]
+fn version_short_option() {
+ RsvgConvert::option_yields_output("-v", is_version_output())
+}
+
+fn is_usage_output() -> OrPredicate<ContainsPredicate, ContainsPredicate, str> {
+ contains("Usage:").or(contains("USAGE:"))
+}
+
+#[test]
+fn help_option() {
+ RsvgConvert::option_yields_output("--help", is_usage_output())
+}
+
+#[test]
+fn help_short_option() {
+ RsvgConvert::option_yields_output("-?", is_usage_output())
+}