From 950425c215b9cd819b1cffb568de7ae1270040ed Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Sat, 1 Apr 2023 17:17:41 +0200 Subject: meta: Separate C API from rust crate Part-of: --- Cargo.lock | 12 +- Cargo.toml | 1 + example.svg | 1 - gdk-pixbuf-loader/Cargo.toml | 11 +- gdk-pixbuf-loader/src/lib.rs | 4 +- librsvg-c/Cargo.toml | 23 +- librsvg-c/build.rs | 57 +- librsvg-c/src/c_api/handle.rs | 20 +- librsvg-c/src/c_api/pixbuf_utils.rs | 8 +- librsvg-c/src/c_api/sizing.rs | 12 +- librsvg-c/src/lib.rs | 2 +- librsvg-c/tests-c/Makefile.am | 95 ++ librsvg-c/tests-c/api.c | 1709 ++++++++++++++++++++++++ librsvg-c/tests-c/test-utils.c | 257 ++++ librsvg-c/tests-c/test-utils.h | 35 + librsvg-c/tests/legacy_sizing.rs | 189 +++ rsvg-bench/Cargo.toml | 2 +- rsvg-convert/Cargo.toml | 13 +- rsvg-convert/src/main.rs | 9 +- rsvg-convert/tests/fixtures | 1 + rsvg-convert/tests/internal_predicates/file.rs | 28 + rsvg-convert/tests/internal_predicates/mod.rs | 4 + rsvg-convert/tests/internal_predicates/pdf.rs | 358 +++++ rsvg-convert/tests/internal_predicates/png.rs | 193 +++ rsvg-convert/tests/internal_predicates/svg.rs | 179 +++ rsvg-convert/tests/rsvg_convert.rs | 1081 +++++++++++++++ rsvg/Cargo.toml | 13 +- rsvg/build.rs | 66 +- rsvg/example.svg | 1 + rsvg/src/api.rs | 22 +- rsvg/src/lib.rs | 12 +- rsvg/src/test_utils/compare_surfaces.rs | 112 ++ rsvg/src/test_utils/mod.rs | 119 ++ rsvg/src/test_utils/reference_utils.rs | 288 ++++ rsvg/tests/Makefile.am | 95 -- rsvg/tests/api.c | 1709 ------------------------ rsvg/tests/src/api.rs | 4 +- rsvg/tests/src/bugs.rs | 4 +- rsvg/tests/src/cmdline/mod.rs | 1 - rsvg/tests/src/cmdline/rsvg_convert.rs | 1079 --------------- rsvg/tests/src/compare_surfaces.rs | 112 -- rsvg/tests/src/filters.rs | 6 +- rsvg/tests/src/intrinsic_dimensions.rs | 4 +- rsvg/tests/src/legacy_sizing.rs | 189 --- rsvg/tests/src/main.rs | 60 - rsvg/tests/src/predicates/file.rs | 28 - rsvg/tests/src/predicates/mod.rs | 10 - rsvg/tests/src/predicates/pdf.rs | 358 ----- rsvg/tests/src/predicates/png.rs | 193 --- rsvg/tests/src/predicates/svg.rs | 179 --- rsvg/tests/src/primitives.rs | 6 +- rsvg/tests/src/reference.rs | 6 +- rsvg/tests/src/reference_utils.rs | 288 ---- rsvg/tests/src/shapes.rs | 2 +- rsvg/tests/src/text.rs | 6 +- rsvg/tests/src/utils.rs | 119 -- rsvg/tests/test-utils.c | 257 ---- rsvg/tests/test-utils.h | 35 - 58 files changed, 4780 insertions(+), 4907 deletions(-) delete mode 100644 example.svg create mode 100644 librsvg-c/tests-c/Makefile.am create mode 100644 librsvg-c/tests-c/api.c create mode 100644 librsvg-c/tests-c/test-utils.c create mode 100644 librsvg-c/tests-c/test-utils.h create mode 100644 librsvg-c/tests/legacy_sizing.rs create mode 120000 rsvg-convert/tests/fixtures create mode 100644 rsvg-convert/tests/internal_predicates/file.rs create mode 100644 rsvg-convert/tests/internal_predicates/mod.rs create mode 100644 rsvg-convert/tests/internal_predicates/pdf.rs create mode 100644 rsvg-convert/tests/internal_predicates/png.rs create mode 100644 rsvg-convert/tests/internal_predicates/svg.rs create mode 100644 rsvg-convert/tests/rsvg_convert.rs create mode 100644 rsvg/example.svg create mode 100644 rsvg/src/test_utils/compare_surfaces.rs create mode 100644 rsvg/src/test_utils/mod.rs create mode 100644 rsvg/src/test_utils/reference_utils.rs delete mode 100644 rsvg/tests/Makefile.am delete mode 100644 rsvg/tests/api.c delete mode 100644 rsvg/tests/src/cmdline/mod.rs delete mode 100644 rsvg/tests/src/cmdline/rsvg_convert.rs delete mode 100644 rsvg/tests/src/compare_surfaces.rs delete mode 100644 rsvg/tests/src/legacy_sizing.rs delete mode 100644 rsvg/tests/src/main.rs delete mode 100644 rsvg/tests/src/predicates/file.rs delete mode 100644 rsvg/tests/src/predicates/mod.rs delete mode 100644 rsvg/tests/src/predicates/pdf.rs delete mode 100644 rsvg/tests/src/predicates/png.rs delete mode 100644 rsvg/tests/src/predicates/svg.rs delete mode 100644 rsvg/tests/src/reference_utils.rs delete mode 100644 rsvg/tests/src/utils.rs delete mode 100644 rsvg/tests/test-utils.c delete mode 100644 rsvg/tests/test-utils.h diff --git a/Cargo.lock b/Cargo.lock index cb6644ef..b87beb1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1117,7 +1117,6 @@ name = "librsvg" version = "2.56.0" dependencies = [ "anyhow", - "assert_cmd", "byteorder", "cairo-rs", "cast", @@ -1164,7 +1163,7 @@ dependencies = [ [[package]] name = "librsvg-c" -version = "0.1.0" +version = "2.56.0" dependencies = [ "cairo-rs", "cast", @@ -1662,6 +1661,7 @@ dependencies = [ "glib", "libc", "librsvg", + "librsvg-c", ] [[package]] @@ -2041,16 +2041,24 @@ dependencies = [ name = "rsvg-convert" version = "2.56.0" dependencies = [ + "assert_cmd", "cairo-rs", "cast", "chrono", "clap 4.1.9", "clap_complete", "cssparser", + "float-cmp", "gio", "libc", "librsvg", + "librsvg-c", + "lopdf", + "png", + "predicates 2.1.5", "system-deps", + "tempfile", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 50c5826a..9d66510d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,5 +21,6 @@ members = [ "rsvg-bench", ] default-members = [ + "rsvg", "rsvg-convert", ] diff --git a/example.svg b/example.svg deleted file mode 100644 index 1a6c762d..00000000 --- a/example.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/gdk-pixbuf-loader/Cargo.toml b/gdk-pixbuf-loader/Cargo.toml index d9132e7a..f1458fc0 100644 --- a/gdk-pixbuf-loader/Cargo.toml +++ b/gdk-pixbuf-loader/Cargo.toml @@ -11,10 +11,11 @@ rust-version.workspace = true crate-type = ["cdylib"] [dependencies] -librsvg = { path = "../rsvg" } +cairo-rs = "0.17" +cstr = "0.2" gdk-pixbuf = "0.17" -libc = "0.2" -glib = "0.17" gio = "0.17" -cairo-rs = "0.17" -cstr = "0.2" \ No newline at end of file +glib = "0.17" +libc = "0.2" +librsvg = { path = "../rsvg" } +librsvg-c = { path = "../librsvg-c" } diff --git a/gdk-pixbuf-loader/src/lib.rs b/gdk-pixbuf-loader/src/lib.rs index 007367c1..06e541ce 100644 --- a/gdk-pixbuf-loader/src/lib.rs +++ b/gdk-pixbuf-loader/src/lib.rs @@ -16,7 +16,7 @@ use gio::prelude::MemoryInputStreamExt; use gio::MemoryInputStream; use glib::gobject_ffi::GObject; -use rsvg::rsvg_convert_only::LegacySize; +use librsvg_c::c_api::sizing::LegacySize; use rsvg::Loader; use cstr::cstr; @@ -104,7 +104,7 @@ unsafe extern "C" fn stop_load(user_data: gpointer, error: *mut *mut GError) -> } } - let pb = rsvg::c_api::pixbuf_utils::render_to_pixbuf_at_size( + let pb = librsvg_c::c_api::pixbuf_utils::render_to_pixbuf_at_size( &renderer, w as f64, h as f64, w as f64, h as f64, ) .map_err(|e| e.to_string())?; diff --git a/librsvg-c/Cargo.toml b/librsvg-c/Cargo.toml index cb2209cd..9996027b 100644 --- a/librsvg-c/Cargo.toml +++ b/librsvg-c/Cargo.toml @@ -1,19 +1,26 @@ [package] name = "librsvg-c" -version = "0.1.0" -edition = "2021" +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 [dependencies] -librsvg = { version = "*", path = "../rsvg" } -gio = "0.17" -glib = "0.17" cairo-rs = { version = "0.17", features=["v1_16", "png", "pdf", "ps", "svg"] } cast = "0.3.0" +float-cmp = "0.9.0" gdk-pixbuf = "0.17" +gio = "0.17" +glib = "0.17" libc = "0.2" -float-cmp = "0.9.0" -url = "2" +librsvg = { path = "../rsvg", features = ["c-api"] } once_cell = "1.2.0" +url = "2" [build-dependencies] -regex = "1.7.1" \ No newline at end of file +regex = "1.7.1" +librsvg = { path = "../rsvg", features = ["c-api", "test-utils"] } \ No newline at end of file diff --git a/librsvg-c/build.rs b/librsvg-c/build.rs index 7a46d6c6..0a0bdd9b 100644 --- a/librsvg-c/build.rs +++ b/librsvg-c/build.rs @@ -2,68 +2,13 @@ use regex::Regex; use std::env; use std::fs::File; use std::io::prelude::*; -use std::io::{BufReader, BufWriter, Write}; +use std::io::{BufReader, Write}; use std::path::Path; -use std::process; fn main() { - if let Err(e) = system_deps::Config::new().probe() { - eprintln!("{e}"); - process::exit(1); - } - - generate_srgb_tables(); write_version(); } -/// Converts an sRGB color value to a linear sRGB color value (undoes the gamma correction). -/// -/// The input and the output are supposed to be in the [0, 1] range. -#[inline] -fn linearize(c: f64) -> f64 { - if c <= (12.92 * 0.0031308) { - c / 12.92 - } else { - ((c + 0.055) / 1.055).powf(2.4) - } -} - -/// Converts a linear sRGB color value to a normal sRGB color value (applies the gamma correction). -/// -/// The input and the output are supposed to be in the [0, 1] range. -#[inline] -fn unlinearize(c: f64) -> f64 { - if c <= 0.0031308 { - 12.92 * c - } else { - 1.055 * c.powf(1f64 / 2.4) - 0.055 - } -} - -fn print_table(w: &mut W, name: &str, f: F, len: u32) -where - W: Write, - F: Fn(f64) -> f64, -{ - writeln!(w, "const {name}: [u8; {len}] = [").unwrap(); - - for i in 0..len { - let x = f(i as f64 / 255.0); - let v = (x * 255.0).round() as u8; - writeln!(w, " {v},").unwrap(); - } - - writeln!(w, "];").unwrap(); -} - -fn generate_srgb_tables() { - let path = Path::new(&env::var("OUT_DIR").unwrap()).join("srgb-codegen.rs"); - let mut file = BufWriter::new(File::create(path).unwrap()); - - print_table(&mut file, "LINEARIZE", linearize, 256); - print_table(&mut file, "UNLINEARIZE", unlinearize, 256); -} - fn write_version() { let mut major = None; let mut minor = None; diff --git a/librsvg-c/src/c_api/handle.rs b/librsvg-c/src/c_api/handle.rs index 1698cc17..3ab6acc5 100644 --- a/librsvg-c/src/c_api/handle.rs +++ b/librsvg-c/src/c_api/handle.rs @@ -38,14 +38,10 @@ use glib::types::instance_of; use glib::{ffi::gpointer, gobject_ffi}; use glib::{Bytes, Cast, StaticType, ToValue}; -use crate::api::{self, CairoRenderer, IntrinsicDimensions, Loader, LoadingError, SvgHandle}; - -use crate::{ - length::RsvgLength, - rsvg_log, - session::Session, - surface_utils::shared_surface::{SharedImageSurface, SurfaceType}, -}; +use rsvg::c_api_only::Session; +use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use rsvg::{rsvg_log, Length}; +use rsvg::{CairoRenderer, IntrinsicDimensions, Loader, LoadingError, SvgHandle}; use super::dpi::Dpi; use super::messages::{rsvg_g_critical, rsvg_g_warning}; @@ -59,13 +55,13 @@ include!(concat!(env!("OUT_DIR"), "/version.rs")); // This is basically the same as api::RenderingError but with extra cases for // the peculiarities of the C API. enum RenderingError { - RenderingError(api::RenderingError), + RenderingError(rsvg::RenderingError), // The RsvgHandle is created, but hasn't been loaded yet. HandleIsNotLoaded, } -impl> From for RenderingError { +impl> From for RenderingError { fn from(e: T) -> RenderingError { RenderingError::RenderingError(e.into()) } @@ -1645,9 +1641,9 @@ pub unsafe extern "C" fn rsvg_handle_set_stylesheet( pub unsafe extern "C" fn rsvg_handle_get_intrinsic_dimensions( handle: *const RsvgHandle, out_has_width: *mut glib::ffi::gboolean, - out_width: *mut RsvgLength, + out_width: *mut Length, out_has_height: *mut glib::ffi::gboolean, - out_height: *mut RsvgLength, + out_height: *mut Length, out_has_viewbox: *mut glib::ffi::gboolean, out_viewbox: *mut RsvgRectangle, ) { diff --git a/librsvg-c/src/c_api/pixbuf_utils.rs b/librsvg-c/src/c_api/pixbuf_utils.rs index 710f2c4b..da1ad804 100644 --- a/librsvg-c/src/c_api/pixbuf_utils.rs +++ b/librsvg-c/src/c_api/pixbuf_utils.rs @@ -12,10 +12,10 @@ use super::dpi::Dpi; use super::handle::{checked_i32, set_gerror}; use super::sizing::LegacySize; -use crate::api::{CairoRenderer, Loader}; -use crate::error::RenderingError; -use crate::session::Session; -use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use rsvg::c_api_only::Session; +use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; +use rsvg::RenderingError; +use rsvg::{CairoRenderer, Loader}; pub fn empty_pixbuf() -> Result { // GdkPixbuf does not allow zero-sized pixbufs, but Cairo allows zero-sized diff --git a/librsvg-c/src/c_api/sizing.rs b/librsvg-c/src/c_api/sizing.rs index e0c85bd1..4d16e483 100644 --- a/librsvg-c/src/c_api/sizing.rs +++ b/librsvg-c/src/c_api/sizing.rs @@ -12,10 +12,8 @@ use float_cmp::approx_eq; -use crate::api::{CairoRenderer, IntrinsicDimensions, RenderingError}; -use crate::dpi::Dpi; -use crate::handle::Handle; -use crate::length::*; +use rsvg::c_api_only::Handle; +use rsvg::{CairoRenderer, Dpi, IntrinsicDimensions, RenderingError}; use super::handle::CairoRectangleExt; @@ -51,9 +49,9 @@ impl<'a> LegacySize for CairoRenderer<'a> { let size_from_intrinsic_dimensions = self.intrinsic_size_in_pixels().or_else(|| { size_in_pixels_from_percentage_width_and_height( - &self.handle.handle, + self.handle(), &self.intrinsic_dimensions(), - self.dpi, + self.dpi(), ) }); @@ -94,7 +92,7 @@ fn size_in_pixels_from_percentage_width_and_height( vbox, } = *dim; - use LengthUnit::*; + use rsvg::LengthUnit::*; // Unwrap or return None if we don't know the aspect ratio -> Let the caller handle it. let vbox = vbox?; diff --git a/librsvg-c/src/lib.rs b/librsvg-c/src/lib.rs index ec407516..3b543815 100644 --- a/librsvg-c/src/lib.rs +++ b/librsvg-c/src/lib.rs @@ -1 +1 @@ -pub use c_api; +pub mod c_api; diff --git a/librsvg-c/tests-c/Makefile.am b/librsvg-c/tests-c/Makefile.am new file mode 100644 index 00000000..b645ea6d --- /dev/null +++ b/librsvg-c/tests-c/Makefile.am @@ -0,0 +1,95 @@ +include $(top_srcdir)/glib-tap.mk + +test_sources = \ + src/api.rs \ + src/bugs.rs \ + src/compare_surfaces.rs \ + src/errors.rs \ + src/filters.rs \ + src/geometries.rs \ + src/intrinsic_dimensions.rs \ + src/legacy_sizing.rs \ + src/loading_crash.rs \ + src/main.rs \ + src/primitive_geometries.rs \ + src/primitives.rs \ + src/reference.rs \ + src/reference_utils.rs \ + src/render_crash.rs \ + src/shapes.rs \ + src/text.rs \ + src/utils.rs \ + src/cmdline/mod.rs \ + src/cmdline/rsvg_convert.rs \ + src/predicates/file.rs \ + src/predicates/mod.rs \ + src/predicates/pdf.rs \ + src/predicates/png.rs \ + src/predicates/svg.rs \ + $(NULL) + +test_programs = api + +api_SOURCES = \ + api.c \ + test-utils.c \ + test-utils.h \ + $(NULL) + +api_LDADD = $(top_builddir)/librsvg_c_api.la \ + $(LIBRSVG_LIBS) \ + $(LIBM) + +api_LDFLAGS = -static + +AM_CPPFLAGS = \ + -I$(srcdir) \ + -I$(top_srcdir)/include \ + -I$(top_builddir)/include \ + -DTEST_DATA_DIR="\"$(srcdir)\"" \ + -DTEST_SRC_DIR="\"$(PWD)\"" \ + -DTOP_SRC_DIR="\"$(top_srcdir)\"" \ + $(LIBRSVG_CFLAGS) + +test_resources = \ + $(wildcard $(srcdir)/resources/*.ttf) + +test_fixtures = \ + $(wildcard $(srcdir)/fixtures/api/*.svg) \ + $(wildcard $(srcdir)/fixtures/cmdline/*.svg) \ + $(wildcard $(srcdir)/fixtures/cmdline/*.png) \ + $(wildcard $(srcdir)/fixtures/crash/*.svg) \ + $(wildcard $(srcdir)/fixtures/crash/*.png) \ + $(wildcard $(srcdir)/fixtures/errors/*) \ + $(wildcard $(srcdir)/fixtures/geometries/*) \ + $(wildcard $(srcdir)/fixtures/loading/*) \ + $(wildcard $(srcdir)/fixtures/primitive_geometries/*) \ + $(wildcard $(srcdir)/fixtures/reftests/*.css) \ + $(wildcard $(srcdir)/fixtures/reftests/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/*.png) \ + $(wildcard $(srcdir)/fixtures/reftests/*.txt) \ + $(wildcard $(srcdir)/fixtures/reftests/adwaita/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/adwaita/*.png) \ + $(wildcard $(srcdir)/fixtures/reftests/bugs/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/bugs/*.png) \ + $(wildcard $(srcdir)/fixtures/reftests/svg1.1/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/svg1.1/*.png) \ + $(wildcard $(srcdir)/fixtures/reftests/svg1.1/images/*) \ + $(wildcard $(srcdir)/fixtures/reftests/svg1.1/resources/*) \ + $(wildcard $(srcdir)/fixtures/reftests/svg2/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/svg2/*.png) \ + $(wildcard $(srcdir)/fixtures/reftests/bugs-reftests/*.svg) \ + $(wildcard $(srcdir)/fixtures/reftests/svg2-reftests/*.svg) \ + $(wildcard $(srcdir)/fixtures/render-crash/*.svg) \ + $(wildcard $(srcdir)/fixtures/text/*.svg) \ + $(wildcard $(srcdir)/fixtures/dimensions/*.svg) + +EXTRA_DIST += \ + $(test_sources) \ + $(test_resources) \ + $(test_fixtures) \ + README.md \ + $(NULL) + +clean-local: + rm -rf output diff --git a/librsvg-c/tests-c/api.c b/librsvg-c/tests-c/api.c new file mode 100644 index 00000000..f7b01c2e --- /dev/null +++ b/librsvg-c/tests-c/api.c @@ -0,0 +1,1709 @@ +/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=4 nowrap ai expandtab sw=4: */ + +/* These are the C API tests for librsvg. These test the complete C + * API, especially its historical peculiarities to ensure ABI + * compatibility. + * + * These tests are not meant to exhaustively test librsvg's features. + * For those, you should look at the Rust integration tests. See + * tests/README.md for details. + */ + +#include "config.h" + +#include +#include +#include + +#define RSVG_DISABLE_DEPRECATION_WARNINGS /* so we can test deprecated API */ +#include +#include "test-utils.h" + +/* + Untested: + rsvg_handle_internal_set_testing +*/ + +static void +handle_has_correct_type_info (void) +{ + GTypeQuery q; + RsvgHandle *handle; + + g_type_query (RSVG_TYPE_HANDLE, &q); + g_assert (q.type == RSVG_TYPE_HANDLE); + g_assert (q.type == rsvg_handle_get_type ()); + + g_assert_cmpstr (q.type_name, ==, "RsvgHandle"); + + /* These test that the sizes of the structs in the header file actually match the + * sizes of structs and the glib-subclass machinery in the Rust side. + */ + g_assert (sizeof (RsvgHandleClass) == (gsize) q.class_size); + g_assert (sizeof (RsvgHandle) == (gsize) q.instance_size); + + handle = rsvg_handle_new(); + g_assert (G_OBJECT_TYPE (handle) == RSVG_TYPE_HANDLE); + g_object_unref (handle); +} + +static void +assert_flags_value_matches (GFlagsValue *v, + guint value, + const char *value_name, + const char *value_nick) +{ + g_assert_cmpint(v->value, ==, value); + g_assert_cmpstr(v->value_name, ==, value_name); + g_assert_cmpstr(v->value_nick, ==, value_nick); +} + +static void +flags_registration (void) +{ + GType ty; + GTypeQuery q; + GTypeClass *type_class; + GFlagsClass *flags_class; + + ty = RSVG_TYPE_HANDLE_FLAGS; + + g_assert (ty != G_TYPE_INVALID); + + g_type_query (RSVG_TYPE_HANDLE_FLAGS, &q); + g_assert (q.type == ty); + g_assert (G_TYPE_IS_FLAGS (q.type)); + g_assert_cmpstr (q.type_name, ==, "RsvgHandleFlags"); + + type_class = g_type_class_ref (ty); + g_assert (G_IS_FLAGS_CLASS (type_class)); + g_assert (G_FLAGS_CLASS_TYPE (type_class) == ty); + + flags_class = G_FLAGS_CLASS (type_class); + g_assert_cmpint (flags_class->n_values, ==, 3); + + assert_flags_value_matches(&flags_class->values[0], + RSVG_HANDLE_FLAGS_NONE, + "RSVG_HANDLE_FLAGS_NONE", + "flags-none"); + + assert_flags_value_matches(&flags_class->values[1], + RSVG_HANDLE_FLAG_UNLIMITED, + "RSVG_HANDLE_FLAG_UNLIMITED", + "flag-unlimited"); + + assert_flags_value_matches(&flags_class->values[2], + RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA, + "RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA", + "flag-keep-image-data"); + + g_type_class_unref (type_class); +} + +static void +assert_enum_value_matches (GEnumValue *v, + gint value, + const char *value_name, + const char *value_nick) +{ + g_assert_cmpint (v->value, ==, value); + g_assert_cmpstr (v->value_name, ==, value_name); + g_assert_cmpstr (v->value_nick, ==, value_nick); +} + +static void +error_registration (void) +{ + GType ty; + GTypeQuery q; + GTypeClass *type_class; + GEnumClass *enum_class; + + g_assert_cmpint (RSVG_ERROR, !=, 0); + + ty = RSVG_TYPE_ERROR; + + g_assert (ty != G_TYPE_INVALID); + + g_type_query (ty, &q); + g_assert (q.type == ty); + g_assert (G_TYPE_IS_ENUM (q.type)); + g_assert_cmpstr (q.type_name, ==, "RsvgError"); + + type_class = g_type_class_ref (ty); + g_assert (G_IS_ENUM_CLASS (type_class)); + g_assert (G_ENUM_CLASS_TYPE (type_class) == ty); + + enum_class = G_ENUM_CLASS (type_class); + g_assert_cmpint (enum_class->n_values, ==, 1); + + assert_enum_value_matches (&enum_class->values[0], + RSVG_ERROR_FAILED, + "RSVG_ERROR_FAILED", + "failed"); + + g_type_class_unref (type_class); +} + +static char * +get_test_filename (const char *basename) { + return g_build_filename (test_utils_get_test_data_path (), + "api", + basename, + NULL); +} + +static RsvgHandle * +load_test_document (const char *basename) { + char *filename = get_test_filename (basename); + GError *error = NULL; + + RsvgHandle *handle = rsvg_handle_new_from_file (filename, &error); + g_free (filename); + + g_assert_nonnull (handle); + g_assert_no_error (error); + + return handle; +} + +#define EXAMPLE_WIDTH 100 +#define EXAMPLE_HEIGHT 400 + +#define XZOOM 2 +#define YZOOM 3 + +#define MAX_WIDTH 10 +#define MAX_HEIGHT 40 + +#define MAX_ZOOMED_WIDTH 20 +#define MAX_ZOOMED_HEIGHT 120 + +#define EXAMPLE_ONE_ID "#one" +#define EXAMPLE_TWO_ID "#two" +#define EXAMPLE_NONEXISTENT_ID "#nonexistent" + +#define EXAMPLE_ONE_X 0 +#define EXAMPLE_ONE_Y 0 +#define EXAMPLE_ONE_W 100 +#define EXAMPLE_ONE_H 200 + +#define EXAMPLE_TWO_X 0 +#define EXAMPLE_TWO_Y 200 +#define EXAMPLE_TWO_W 100 +#define EXAMPLE_TWO_H 200 + +static GdkPixbuf * +pixbuf_from_file (const char *filename, GError **error) +{ + return rsvg_pixbuf_from_file (filename, error); +} + +static GdkPixbuf * +pixbuf_from_file_at_zoom (const char *filename, GError **error) +{ + return rsvg_pixbuf_from_file_at_zoom (filename, (double) XZOOM, (double) YZOOM, error); +} + +static GdkPixbuf * +pixbuf_from_file_at_size (const char *filename, GError **error) +{ + return rsvg_pixbuf_from_file_at_size (filename, EXAMPLE_WIDTH * XZOOM, EXAMPLE_HEIGHT * YZOOM, error); +} + +static GdkPixbuf * +pixbuf_from_file_at_max_size (const char *filename, GError **error) +{ + return rsvg_pixbuf_from_file_at_max_size (filename, MAX_WIDTH, MAX_HEIGHT, error); +} + +static GdkPixbuf * +pixbuf_from_file_at_zoom_with_max (const char *filename, GError **error) +{ + return rsvg_pixbuf_from_file_at_zoom_with_max (filename, + XZOOM, YZOOM, + MAX_ZOOMED_WIDTH, MAX_ZOOMED_HEIGHT, + error); +} + +typedef GdkPixbuf *(* PixbufCreateFn) (const char *filename, GError **error); + +typedef struct { + const char *test_name; + PixbufCreateFn pixbuf_create_fn; + int expected_width; + int expected_height; +} PixbufTest; + +static const PixbufTest pixbuf_tests[] = { + { + "/api/pixbuf_from_file", + pixbuf_from_file, + EXAMPLE_WIDTH, + EXAMPLE_HEIGHT + }, + { + "/api/pixbuf_from_file_at_zoom", + pixbuf_from_file_at_zoom, + EXAMPLE_WIDTH * XZOOM, + EXAMPLE_HEIGHT * YZOOM + }, + { + "/api/pixbuf_from_file_at_size", + pixbuf_from_file_at_size, + EXAMPLE_WIDTH * XZOOM, + EXAMPLE_HEIGHT * YZOOM + }, + { + "/api/pixbuf_from_file_at_max_size", + pixbuf_from_file_at_max_size, + MAX_WIDTH, + MAX_HEIGHT + }, + { + "/api/pixbuf_from_file_at_zoom_with_max", + pixbuf_from_file_at_zoom_with_max, + MAX_ZOOMED_WIDTH, + MAX_ZOOMED_HEIGHT + }, +}; + +static void +test_pixbuf (gconstpointer data) +{ + const PixbufTest *test = data; + + char *filename = get_test_filename ("example.svg"); + GError *error = NULL; + + GdkPixbuf *pixbuf = test->pixbuf_create_fn (filename, &error); + + g_free (filename); + + g_assert_nonnull (pixbuf); + g_assert_no_error (error); + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, test->expected_width); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, test->expected_height); + + g_object_unref (pixbuf); +} + +static void +pixbuf_overflow (void) +{ + char *filename = get_test_filename ("example.svg"); + GError *error = NULL; + + g_assert (!rsvg_pixbuf_from_file_at_zoom (filename, 1000000.0, 1000000.0, &error)); + g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); + g_error_free (error); + g_free (filename); +} + +static void +noops (void) +{ + /* Just to test that these functions are present in the binary, I guess */ + rsvg_init (); + rsvg_term (); + rsvg_cleanup (); +} + +static void +noops_return_null (void) +{ + RsvgHandle *handle = rsvg_handle_new (); + + g_assert_null (rsvg_handle_get_title (handle)); + g_assert_null (rsvg_handle_get_desc (handle)); + g_assert_null (rsvg_handle_get_metadata (handle)); + + g_object_unref (handle); +} + +static void +set_dpi (void) +{ + RsvgHandle *handle; + RsvgDimensionData dim; + + rsvg_set_default_dpi (100.0); + + handle = load_test_document ("dpi.svg"); + + rsvg_handle_get_dimensions (handle, &dim); + g_assert_cmpint (dim.width, ==, 100); + g_assert_cmpint (dim.height, ==, 400); + + rsvg_handle_set_dpi (handle, 200.0); + rsvg_handle_get_dimensions (handle, &dim); + g_assert_cmpint (dim.width, ==, 200); + g_assert_cmpint (dim.height, ==, 800); + g_object_unref (handle); + + handle = load_test_document ("dpi.svg"); + + rsvg_handle_set_dpi_x_y (handle, 400.0, 300.0); + rsvg_handle_get_dimensions (handle, &dim); + g_assert_cmpint (dim.width, ==, 400); + g_assert_cmpint (dim.height, ==, 1200); + g_object_unref (handle); +} + +static void +base_uri (void) +{ + RsvgHandle *handle = rsvg_handle_new (); + const char *uri; + + uri = rsvg_handle_get_base_uri (handle); + g_assert_null (uri); + + rsvg_handle_set_base_uri (handle, "file:///foo/bar.svg"); + uri = rsvg_handle_get_base_uri (handle); + + g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); + + g_object_unref (handle); +} + +static void +base_gfile (void) +{ + RsvgHandle *handle = rsvg_handle_new (); + GFile *file; + const char *uri; + + uri = rsvg_handle_get_base_uri (handle); + g_assert_null (uri); + + file = g_file_new_for_uri ("file:///foo/bar.svg"); + + rsvg_handle_set_base_gfile (handle, file); + uri = rsvg_handle_get_base_uri (handle); + + g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); + + g_object_unref (file); + g_object_unref (handle); +} + +static void +handle_write_close_free (void) +{ + char *filename = get_test_filename ("dpi.svg"); + char *data; + gsize length; + gsize i; + GError *error = NULL; + + g_assert (g_file_get_contents (filename, &data, &length, &error)); + g_free (filename); + + g_assert_nonnull (data); + g_assert_no_error (error); + + RsvgHandle *handle = rsvg_handle_new_with_flags (RSVG_HANDLE_FLAGS_NONE); + + for (i = 0; i < length; i++) { + g_assert (rsvg_handle_write (handle, (guchar *) &data[i], 1, &error)); + g_assert_no_error (error); + } + + g_assert (rsvg_handle_close (handle, &error)); + g_assert_no_error (error); + + /* Test that close() is idempotent in the happy case */ + g_assert (rsvg_handle_close (handle, &error)); + g_assert_no_error (error); + + rsvg_handle_free (handle); + g_free (data); +} + +static void +handle_new_from_file (void) +{ + char *filename = get_test_filename ("dpi.svg"); + char *uri = g_strconcat ("file://", filename, NULL); + + RsvgHandle *handle; + GError *error = NULL; + + /* rsvg_handle_new_from_file() can take both filenames and URIs */ + + handle = rsvg_handle_new_from_file (filename, &error); + g_assert_nonnull (handle); + g_assert_no_error (error); + g_object_unref (handle); + + handle = rsvg_handle_new_from_file (uri, &error); + g_assert_nonnull (handle); + g_assert_no_error (error); + g_object_unref (handle); + + g_free (filename); + g_free (uri); +} + +static void +handle_new_from_data (void) +{ + char *filename = get_test_filename ("dpi.svg"); + char *data; + gsize length; + GError *error = NULL; + + g_assert (g_file_get_contents (filename, &data, &length, &error)); + g_free (filename); + + g_assert_nonnull (data); + g_assert_no_error (error); + + RsvgHandle *handle = rsvg_handle_new_from_data ((guint8 *) data, length, &error); + g_assert_nonnull (handle); + g_assert_no_error (error); + + g_object_unref (handle); + g_free (data); +} + +static void +handle_new_from_gfile_sync (void) +{ + char *filename = get_test_filename ("dpi.svg"); + GError *error = NULL; + GFile *file = g_file_new_for_path (filename); + g_assert_nonnull (file); + + g_free (filename); + + RsvgHandle *handle = rsvg_handle_new_from_gfile_sync (file, + RSVG_HANDLE_FLAGS_NONE, + NULL, + &error); + + g_assert_nonnull (handle); + g_assert_no_error (error); + + g_object_unref (handle); + g_object_unref (file); +} + +static void +handle_new_from_stream_sync (void) +{ + char *filename = get_test_filename ("dpi.svg"); + GError *error = NULL; + GFile *file = g_file_new_for_path (filename); + g_assert_nonnull (file); + + g_free (filename); + + GFileInputStream *stream = g_file_read (file, NULL, &error); + g_assert (stream != NULL); + g_assert_no_error (error); + + RsvgHandle *handle = rsvg_handle_new_from_stream_sync (G_INPUT_STREAM (stream), + file, + RSVG_HANDLE_FLAGS_NONE, + NULL, + &error); + + g_assert_nonnull (handle); + g_assert_no_error (error); + + g_object_unref (handle); + g_object_unref (file); + g_object_unref (stream); +} + +static void +handle_read_stream_sync (void) +{ + char *filename = get_test_filename ("dpi.svg"); + GError *error = NULL; + GFile *file = g_file_new_for_path (filename); + g_assert_nonnull (file); + + g_free (filename); + + GFileInputStream *stream = g_file_read (file, NULL, &error); + g_assert_nonnull (stream); + g_assert_no_error (error); + + RsvgHandle *handle = rsvg_handle_new (); + + g_assert (rsvg_handle_read_stream_sync (handle, G_INPUT_STREAM (stream), NULL, &error)); + g_assert_no_error (error); + + g_object_unref (handle); + g_object_unref (file); + g_object_unref (stream); +} + +static void +handle_has_sub (void) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + + g_assert (rsvg_handle_has_sub (handle, EXAMPLE_ONE_ID)); + g_assert (rsvg_handle_has_sub (handle, EXAMPLE_TWO_ID)); + g_assert (!rsvg_handle_has_sub (handle, "#foo")); + + g_object_unref (handle); +} + +static void +test_get_pixbuf (gboolean sub) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + + GdkPixbuf *pixbuf; + if (sub) { + pixbuf = rsvg_handle_get_pixbuf_sub (handle, EXAMPLE_ONE_ID); + } else { + pixbuf = rsvg_handle_get_pixbuf (handle); + } + + g_assert_nonnull (pixbuf); + + /* Note that rsvg_handle_get_pixbuf_sub() creates a surface the size of the + * whole SVG, not just the size of the sub-element. + */ + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, EXAMPLE_WIDTH); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, EXAMPLE_HEIGHT); + + cairo_surface_t *surface_a = test_utils_cairo_surface_from_pixbuf (pixbuf); + cairo_surface_t *surface_b = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, EXAMPLE_WIDTH, EXAMPLE_HEIGHT); + cairo_surface_t *surface_diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, EXAMPLE_WIDTH, EXAMPLE_HEIGHT); + + g_object_unref (pixbuf); + + g_assert_nonnull (surface_a); + g_assert_nonnull (surface_b); + g_assert_nonnull (surface_diff); + + cairo_t *cr = cairo_create (surface_b); + if (sub) { + g_assert (rsvg_handle_render_cairo_sub (handle, cr, EXAMPLE_ONE_ID)); + } else { + g_assert (rsvg_handle_render_cairo (handle, cr)); + } + cairo_destroy (cr); + + g_object_unref (handle); + + TestUtilsBufferDiffResult result = {0, 0}; + test_utils_compare_surfaces (surface_a, surface_b, surface_diff, &result); + + if (result.pixels_changed && result.max_diff > 0) { + g_test_fail (); + } + + cairo_surface_destroy (surface_a); + cairo_surface_destroy (surface_b); + cairo_surface_destroy (surface_diff); +} + +static void +handle_get_pixbuf (void) +{ + test_get_pixbuf (FALSE); +} + +static void +handle_get_pixbuf_sub (void) +{ + test_get_pixbuf (TRUE); +} + +static void +dimensions_and_position (void) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + RsvgDimensionData dim; + + g_assert (rsvg_handle_get_dimensions_sub (handle, &dim, EXAMPLE_TWO_ID)); + g_assert_cmpint (dim.width, ==, EXAMPLE_TWO_W); + g_assert_cmpint (dim.height, ==, EXAMPLE_TWO_H); + + RsvgPositionData pos; + g_assert (rsvg_handle_get_position_sub (handle, &pos, EXAMPLE_TWO_ID)); + g_assert_cmpint (pos.x, ==, EXAMPLE_TWO_X); + g_assert_cmpint (pos.y, ==, EXAMPLE_TWO_Y); + + g_assert_false (rsvg_handle_get_position_sub (handle, &pos, EXAMPLE_NONEXISTENT_ID)); + g_assert_false (rsvg_handle_get_dimensions_sub (handle, &dim, EXAMPLE_NONEXISTENT_ID)); + + /* Asking for "position of the whole SVG" (id=NULL) always returns (0, 0) */ + g_assert (rsvg_handle_get_position_sub (handle, &pos, NULL)); + g_assert_cmpint (pos.x, ==, 0); + g_assert_cmpint (pos.y, ==, 0); + + g_object_unref (handle); +} + +struct size_func_data +{ + gboolean called; + gboolean destroyed; + gboolean testing_size_func_calls; +}; + +static void +size_func (gint *width, gint *height, gpointer user_data) +{ + struct size_func_data *data = user_data; + + if (data->testing_size_func_calls) { + g_assert_false (data->called); + data->called = TRUE; + + g_assert_false (data->destroyed); + } + + *width = 42; + *height = 43; +} + +static void +size_func_destroy (gpointer user_data) +{ + struct size_func_data *data = user_data; + + if (data->testing_size_func_calls) { + g_assert_false (data->destroyed); + data->destroyed = TRUE; + } +} + +static void +set_size_callback (void) +{ + RsvgHandle *handle; + struct size_func_data data; + RsvgDimensionData dim; + + handle = load_test_document ("example.svg"); + + data.called = FALSE; + data.destroyed = FALSE; + data.testing_size_func_calls = TRUE; + + rsvg_handle_set_size_callback (handle, size_func, &data, size_func_destroy); + + rsvg_handle_get_dimensions (handle, &dim); + g_assert_cmpint (dim.width, ==, 42); + g_assert_cmpint (dim.height, ==, 43); + + g_object_unref (handle); + + g_assert_true (data.called); + g_assert_true (data.destroyed); +} + +static void +reset_size_callback (void) +{ + RsvgHandle *handle; + struct size_func_data data_1; + struct size_func_data data_2; + + handle = load_test_document ("example.svg"); + + data_1.called = FALSE; + data_1.destroyed = FALSE; + data_1.testing_size_func_calls = TRUE; + + rsvg_handle_set_size_callback (handle, size_func, &data_1, size_func_destroy); + + data_2.called = FALSE; + data_2.destroyed = FALSE; + data_2.testing_size_func_calls = TRUE; + + rsvg_handle_set_size_callback (handle, size_func, &data_2, size_func_destroy); + g_assert_true (data_1.destroyed); + + g_object_unref (handle); + + g_assert_true (data_2.destroyed); +} + +static void +zero_size_func (gint *width, gint *height, gpointer user_data) +{ + *width = 0; + *height = 0; +} + +static void +render_with_zero_size_callback (void) +{ + /* gdk_pixbuf_get_file_info() uses a GdkPixbufLoader, but in its + * "size-prepared" callback it saves the computed size, and then calls + * gdk_pixbuf_loader_set_size(loader, 0, 0). Presumably it does to tell + * loaders that it only wanted to know the size, but that they shouldn't + * decode or render the image to a pixbuf buffer. + * + * Librsvg used to panic when getting (0, 0) from the size_callback; this + * test is to check that there is no such crash now. Instead, librsvg + * will return a 1x1 transparent pixbuf. + */ + RsvgHandle *handle; + GdkPixbuf *pixbuf; + + handle = load_test_document ("example.svg"); + + rsvg_handle_set_size_callback (handle, zero_size_func, NULL, NULL); + + pixbuf = rsvg_handle_get_pixbuf (handle); + g_assert_nonnull (pixbuf); + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 1); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 1); + + g_object_unref (pixbuf); + g_object_unref (handle); +} + +static void +pixbuf_size_func (gint *width, gint *height, gpointer user_data) +{ + *width = 420; + *height = 430; +} + +static void +get_pixbuf_with_size_callback (void) +{ + RsvgHandle *handle = rsvg_handle_new (); + + rsvg_handle_set_size_callback (handle, pixbuf_size_func, NULL, NULL); + + char *filename = get_test_filename ("example.svg"); + guchar *data = NULL; + gsize length; + GError *error = NULL; + + g_assert (g_file_get_contents (filename, (gchar **) &data, &length, &error)); + g_assert_nonnull (data); + + g_free (filename); + + g_assert (rsvg_handle_write (handle, data, length, &error)); + g_assert_no_error (error); + + g_assert (rsvg_handle_close (handle, &error)); + g_assert_no_error (error); + + GdkPixbuf *pixbuf = rsvg_handle_get_pixbuf (handle); + g_assert_nonnull (pixbuf); + g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 420); + g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 430); + + g_object_unref (pixbuf); + g_free (data); + g_object_unref (handle); +} + +static void +detects_cairo_context_in_error (void) +{ + if (g_test_subprocess ()) { + RsvgHandle *handle = load_test_document ("example.svg"); + + /* this is wrong; it is to simulate creating a surface and a cairo_t in error */ + cairo_surface_t *surf = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, -1, -1); + cairo_t *cr = cairo_create (surf); + /* rsvg_handle_render_cairo() should return FALSE when it gets a cr in an error state */ + g_assert_false (rsvg_handle_render_cairo (handle, cr)); + + return; + } + + g_test_trap_subprocess (NULL, 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("*WARNING*cannot render on a cairo_t with a failure status*"); +} + +static gboolean +matrixes_are_equal (cairo_matrix_t *a, cairo_matrix_t *b) +{ + return (a->xx == b->xx && + a->yx == b->yx && + a->xy == b->xy && + a->yy == b->yy && + a->x0 == b->x0 && + a->y0 == b->y0); +} + +static void +can_draw_to_non_image_surface (void) +{ + cairo_rectangle_t rect; + cairo_surface_t *surface; + cairo_t *cr; + + RsvgHandle *handle = load_test_document ("example.svg"); + + rect.x = 0.0; + rect.y = 0.0; + rect.width = 100.0; + rect.height = 100.0; + + /* We create a surface that is not a Cairo image surface, + * so we can test that in fact we can render to non-image surfaces. + */ + surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, &rect); + cr = cairo_create (surface); + + cairo_translate (cr, 42.0, 42.0); + + cairo_matrix_t original_affine; + cairo_get_matrix (cr, &original_affine); + + g_assert (rsvg_handle_render_cairo (handle, cr)); + + cairo_matrix_t new_affine; + cairo_get_matrix (cr, &new_affine); + + g_assert (matrixes_are_equal (&original_affine, &new_affine)); + + g_object_unref (handle); + + cairo_destroy (cr); + cairo_surface_destroy (surface); +} + +/* Test that we preserve the affine transformation in the cr during a call + * to rsvg_handle_render_cairo_sub(). + */ +static void +render_cairo_sub (void) +{ + RsvgHandle *handle = load_test_document ("bug334-element-positions.svg"); + + cairo_surface_t *surf = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 200, 200); + cairo_t *cr = cairo_create (surf); + + cairo_translate (cr, 42.0, 42.0); + + cairo_matrix_t original_affine; + cairo_get_matrix (cr, &original_affine); + + g_assert (rsvg_handle_render_cairo_sub (handle, cr, "#button5-leader")); + + cairo_matrix_t new_affine; + cairo_get_matrix (cr, &new_affine); + + g_assert (matrixes_are_equal (&original_affine, &new_affine)); + + g_object_unref (handle); + cairo_destroy (cr); + cairo_surface_destroy (surf); +} + +static void +get_intrinsic_dimensions (void) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + + gboolean has_width; + RsvgLength width; + gboolean has_height; + RsvgLength height; + gboolean has_viewbox; + RsvgRectangle viewbox; + + rsvg_handle_get_intrinsic_dimensions (handle, &has_width, &width, &has_height, &height, &has_viewbox, &viewbox); + + g_assert (has_width); + g_assert_cmpfloat (width.length, ==, 100.0); + g_assert (width.unit == RSVG_UNIT_PX); + + g_assert (has_height); + g_assert_cmpfloat (height.length, ==, 400.0); + g_assert (height.unit == RSVG_UNIT_PX); + + g_assert (has_viewbox); + g_assert_cmpfloat (viewbox.x, ==, 0.0); + g_assert_cmpfloat (viewbox.y, ==, 0.0); + g_assert_cmpfloat (viewbox.width, ==, 100.0); + g_assert_cmpfloat (viewbox.height, ==, 400.0); + + g_object_unref (handle); +} + +static void +get_intrinsic_dimensions_missing_values (void) +{ + RsvgHandle *handle = load_test_document ("no-viewbox.svg"); + + gboolean has_width; + RsvgLength width; + gboolean has_height; + RsvgLength height; + gboolean has_viewbox; + RsvgRectangle viewbox; + + rsvg_handle_get_intrinsic_dimensions (handle, &has_width, &width, &has_height, &height, &has_viewbox, &viewbox); + g_assert_true (has_width); + g_assert_true (has_height); + g_assert_false (has_viewbox); + g_object_unref (handle); +} + +static void +get_intrinsic_size_in_pixels_yes (void) +{ + RsvgHandle *handle = load_test_document ("size.svg"); + gdouble width, height; + + rsvg_handle_set_dpi (handle, 96.0); + + /* Test optional parameters */ + g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, NULL, NULL)); + + /* Test the actual result */ + g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); + g_assert_cmpfloat (width, ==, 192.0); + g_assert_cmpfloat (height, ==, 288.0); + + g_object_unref (handle); +} + +static void +get_intrinsic_size_in_pixels_no (void) +{ + RsvgHandle *handle = load_test_document ("no-size.svg"); + gdouble width, height; + + rsvg_handle_set_dpi (handle, 96.0); + g_assert (!rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); + g_assert_cmpfloat (width, ==, 0.0); + g_assert_cmpfloat (height, ==, 0.0); + + g_object_unref (handle); +} + +static void +set_stylesheet (void) +{ + const char *css = "rect { fill: #00ff00; }"; + + RsvgHandle *handle = load_test_document ("stylesheet.svg"); + RsvgHandle *ref_handle = load_test_document ("stylesheet-ref.svg"); + + cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); + cairo_surface_t *reference = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); + + RsvgRectangle viewport = { 0.0, 0.0, 100.0, 100.0 }; + + cairo_t *output_cr = cairo_create (output); + cairo_t *ref_cr = cairo_create (reference); + + GError *error = NULL; + g_assert (rsvg_handle_set_stylesheet (handle, (const guint8 *) css, strlen (css), &error)); + g_assert_no_error (error); + + g_assert (rsvg_handle_render_document (handle, output_cr, &viewport, &error)); + g_assert_no_error (error); + + g_assert (rsvg_handle_render_document (ref_handle, ref_cr, &viewport, &error)); + g_assert_no_error (error); + + cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); + + TestUtilsBufferDiffResult result = {0, 0}; + test_utils_compare_surfaces (output, reference, diff, &result); + + if (result.pixels_changed && result.max_diff > 0) { + g_test_fail (); + } + + cairo_surface_destroy (diff); + cairo_destroy (ref_cr); + cairo_destroy (output_cr); + cairo_surface_destroy (reference); + cairo_surface_destroy (output); + g_object_unref (ref_handle); + g_object_unref (handle); +} + +static void +render_document (void) +{ + RsvgHandle *handle = load_test_document ("document.svg"); + + cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); + cairo_t *cr = cairo_create (output); + + RsvgRectangle viewport = { 50.0, 50.0, 50.0, 50.0 }; + + GError *error = NULL; + g_assert (rsvg_handle_render_document (handle, cr, &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + + cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); + cr = cairo_create (expected); + + cairo_translate (cr, 50.0, 50.0); + cairo_rectangle (cr, 10.0, 10.0, 30.0, 30.0); + cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 0.5); + cairo_fill (cr); + cairo_destroy (cr); + + cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); + + TestUtilsBufferDiffResult result = {0, 0}; + test_utils_compare_surfaces (output, expected, diff, &result); + + if (result.pixels_changed && result.max_diff > 0) { + g_test_fail (); + } + + cairo_surface_destroy (diff); + cairo_surface_destroy (expected); + cairo_surface_destroy (output); + g_object_unref (handle); +} + +static void +get_geometry_for_layer (void) +{ + RsvgHandle *handle = load_test_document ("geometry.svg"); + + RsvgRectangle viewport = { 0.0, 0.0, 100.0, 400.0 }; + RsvgRectangle ink_rect; + RsvgRectangle logical_rect; + + GError *error = NULL; + + g_assert_false (rsvg_handle_get_geometry_for_layer (handle, "#nonexistent", &viewport, + &ink_rect, &logical_rect, &error)); + g_assert_nonnull (error); + + g_clear_error (&error); + + g_assert (rsvg_handle_get_geometry_for_layer (handle, "#two", &viewport, + &ink_rect, &logical_rect, &error)); + g_assert_no_error (error); + + g_assert_cmpfloat (ink_rect.x, ==, 5.0); + g_assert_cmpfloat (ink_rect.y, ==, 195.0); + g_assert_cmpfloat (ink_rect.width, ==, 90.0); + g_assert_cmpfloat (ink_rect.height, ==, 110.0); + + g_assert_cmpfloat (logical_rect.x, ==, 10.0); + g_assert_cmpfloat (logical_rect.y, ==, 200.0); + g_assert_cmpfloat (logical_rect.width, ==, 80.0); + g_assert_cmpfloat (logical_rect.height, ==, 100.0); + + g_object_unref (handle); +} + +static void +render_layer (void) +{ + RsvgHandle *handle = load_test_document ("layers.svg"); + + cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + cairo_t *cr = cairo_create (output); + + RsvgRectangle viewport = { 100.0, 100.0, 100.0, 100.0 }; + + GError *error = NULL; + + g_assert (rsvg_handle_render_layer (handle, cr, "#bar", &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + + cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + cr = cairo_create (expected); + + cairo_translate (cr, 100.0, 100.0); + cairo_rectangle (cr, 20.0, 20.0, 30.0, 30.0); + cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 1.0); + cairo_fill (cr); + cairo_destroy (cr); + + cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + + TestUtilsBufferDiffResult result = {0, 0}; + test_utils_compare_surfaces (output, expected, diff, &result); + + if (result.pixels_changed && result.max_diff > 0) { + g_test_fail (); + } + + cairo_surface_destroy (diff); + cairo_surface_destroy (expected); + cairo_surface_destroy (output); + g_object_unref (handle); +} + +static void +untransformed_element (void) +{ + RsvgHandle *handle = load_test_document ("geometry-element.svg"); + + RsvgRectangle ink_rect; + RsvgRectangle logical_rect; + + GError *error = NULL; + + g_assert (!rsvg_handle_get_geometry_for_element (handle, "#nonexistent", + &ink_rect, &logical_rect, &error)); + g_assert_nonnull (error); + + g_clear_error (&error); + + g_assert (rsvg_handle_get_geometry_for_element (handle, "#foo", + &ink_rect, &logical_rect, &error)); + g_assert_no_error (error); + + g_assert_cmpfloat (ink_rect.x, ==, 0.0); + g_assert_cmpfloat (ink_rect.y, ==, 0.0); + g_assert_cmpfloat (ink_rect.width, ==, 40.0); + g_assert_cmpfloat (ink_rect.height, ==, 50.0); + + g_assert_cmpfloat (logical_rect.x, ==, 5.0); + g_assert_cmpfloat (logical_rect.y, ==, 5.0); + g_assert_cmpfloat (logical_rect.width, ==, 30.0); + g_assert_cmpfloat (logical_rect.height, ==, 40.0); + + cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + cairo_t *cr = cairo_create (output); + + RsvgRectangle viewport = { 100.0, 100.0, 100.0, 100.0 }; + + g_assert (rsvg_handle_render_element (handle, cr, "#foo", &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + + cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + cr = cairo_create (expected); + + cairo_translate (cr, 100.0, 100.0); + cairo_rectangle (cr, 10.0, 10.0, 60.0, 80.0); + cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 1.0); + cairo_fill_preserve (cr); + + cairo_set_line_width (cr, 20.0); + cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0); + cairo_stroke (cr); + + cairo_destroy (cr); + + cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); + + TestUtilsBufferDiffResult result = {0, 0}; + test_utils_compare_surfaces (output, expected, diff, &result); + + if (result.pixels_changed && result.max_diff > 0) { + g_test_fail (); + } + + cairo_surface_destroy (diff); + cairo_surface_destroy (expected); + cairo_surface_destroy (output); + g_object_unref (handle); +} + +/* https://gitlab.gnome.org/GNOME/librsvg/issues/385 */ +static void +no_write_before_close (void) +{ + RsvgHandle *handle = rsvg_handle_new(); + GError *error = NULL; + + g_assert_false (rsvg_handle_close (handle, &error)); + g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); + g_error_free (error); + error = NULL; + + /* Test that close() is idempotent in the error case */ + g_assert (rsvg_handle_close (handle, &error)); + g_assert_no_error (error); + + g_object_unref (handle); +} + +static void +empty_write_close (void) +{ + RsvgHandle *handle = rsvg_handle_new(); + GError *error = NULL; + guchar buf = 0; + + g_assert_true (rsvg_handle_write (handle, &buf, 0, &error)); + g_assert_no_error (error); + + g_assert_false (rsvg_handle_close (handle, &error)); + g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); + + g_error_free (error); + + g_object_unref (handle); +} + +static void +cannot_request_external_elements (void) +{ + /* We want to test that using one of the _sub() functions will fail + * if the element's id is within an external file. + */ + + RsvgHandle *handle = load_test_document ("example.svg"); + RsvgPositionData pos; + + g_assert_false (rsvg_handle_get_position_sub (handle, &pos, "dpi.svg#one")); + + g_object_unref (handle); +} + +static void +test_flags (RsvgHandleFlags flags) +{ + guint read_flags; + + RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, + "flags", flags, + NULL); + g_object_get (handle, "flags", &read_flags, NULL); + g_assert (read_flags == flags); + + g_object_unref (handle); +} + +static void +property_flags (void) +{ + test_flags (RSVG_HANDLE_FLAGS_NONE); + test_flags (RSVG_HANDLE_FLAG_UNLIMITED); + test_flags (RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA); + test_flags (RSVG_HANDLE_FLAG_UNLIMITED | RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA); +} + +static void +property_dpi (void) +{ + RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, + "dpi-x", 42.0, + "dpi-y", 43.0, + NULL); + double x, y; + + g_object_get (handle, + "dpi-x", &x, + "dpi-y", &y, + NULL); + + g_assert_cmpfloat (x, ==, 42.0); + g_assert_cmpfloat (y, ==, 43.0); + + g_object_unref (handle); +} + +static void +property_base_uri (void) +{ + RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, + "base-uri", "file:///foo/bar.svg", + NULL); + char *uri; + + g_object_get (handle, + "base-uri", &uri, + NULL); + + g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); + g_free (uri); + + g_object_unref (handle); +} + +static void +property_dimensions (void) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + + int width; + int height; + double em; + double ex; + + g_object_get (handle, + "width", &width, + "height", &height, + "em", &em, + "ex", &ex, + NULL); + + g_assert_cmpint (width, ==, EXAMPLE_WIDTH); + g_assert_cmpint (height, ==, EXAMPLE_HEIGHT); + + g_assert_cmpfloat (em, ==, (double) EXAMPLE_WIDTH); + g_assert_cmpfloat (ex, ==, (double) EXAMPLE_HEIGHT); + + g_object_unref (handle); +} + +static void +property_deprecated (void) +{ + RsvgHandle *handle = load_test_document ("example.svg"); + + char *title; + char *desc; + char *metadata; + + g_object_get (handle, + "title", &title, + "desc", &desc, + "metadata", &metadata, + NULL); + + g_assert_null (title); + g_assert_null (desc); + g_assert_null (metadata); + + g_object_unref (handle); +} + +static void +return_if_fail (void) +{ + if (g_test_subprocess ()) { + RsvgHandle *handle; + + handle = rsvg_handle_new(); + g_assert_nonnull (handle); + + /* NULL is an invalid argument... */ + rsvg_handle_set_base_uri (handle, NULL); + g_object_unref (handle); + } + + g_test_trap_subprocess (NULL, 0, 0); + /* ... and here we catch that it was validated */ + g_test_trap_assert_stderr ("*rsvg_handle_set_base_uri*assertion*failed*"); +} + +static void +return_if_fail_null_check (void) +{ + if (g_test_subprocess ()) { + /* Pass NULL as an argument, incorrectly... */ + g_assert_null (rsvg_handle_get_base_uri (NULL)); + } + + g_test_trap_subprocess (NULL, 0, 0); + /* ... and here we catch that it was validated */ + g_test_trap_assert_stderr ("*rsvg_handle_get_base_uri*assertion*handle*failed*"); +} + +static void +return_if_fail_type_check (void) +{ + if (g_test_subprocess ()) { + /* Create a random GObject that is not an RsvgHandle... */ + GInputStream *stream = g_memory_input_stream_new(); + + /* Feed it to an RsvgHandle function so it will bail out */ + g_assert_null (rsvg_handle_get_base_uri ((RsvgHandle *) stream)); + + g_object_unref (stream); + } + + g_test_trap_subprocess (NULL, 0, 0); + /* ... and here we catch that it was validated */ + g_test_trap_assert_stderr ("*rsvg_handle_get_base_uri*assertion*handle*failed*"); +} + +static void +library_version_defines (void) +{ + gchar *version = g_strdup_printf ("%u.%u.%u", + LIBRSVG_MAJOR_VERSION, LIBRSVG_MINOR_VERSION, LIBRSVG_MICRO_VERSION); + g_assert_cmpstr (version, ==, LIBRSVG_VERSION); + g_free (version); +} + +static void +library_version_check (void) +{ + g_assert_true(LIBRSVG_CHECK_VERSION(1, 99, 9)); + g_assert_true(LIBRSVG_CHECK_VERSION(2, 0, 0)); + g_assert_true(LIBRSVG_CHECK_VERSION(2, 50, 7)); + g_assert_false(LIBRSVG_CHECK_VERSION(2, 99, 0)); + g_assert_false(LIBRSVG_CHECK_VERSION(3, 0, 0)); +} + +static void +library_version_constants (void) +{ + g_assert_cmpuint (rsvg_major_version, ==, LIBRSVG_MAJOR_VERSION); + g_assert_cmpuint (rsvg_minor_version, ==, LIBRSVG_MINOR_VERSION); + g_assert_cmpuint (rsvg_micro_version, ==, LIBRSVG_MICRO_VERSION); +} + +typedef struct +{ + const gchar *test_name; + const gchar *file_path; + const gchar *id; + gdouble x; + gdouble y; + gdouble width; + gdouble height; + gboolean has_position; + gboolean has_dimensions; +} DimensionsFixtureData; + +static void +test_dimensions (DimensionsFixtureData *fixture) +{ + RsvgHandle *handle; + RsvgPositionData position; + RsvgDimensionData dimension; + gchar *target_file; + GError *error = NULL; + + target_file = g_build_filename (test_utils_get_test_data_path (), + fixture->file_path, NULL); + handle = rsvg_handle_new_from_file (target_file, &error); + g_free (target_file); + g_assert_no_error (error); + + if (fixture->id) { + g_assert (rsvg_handle_has_sub (handle, fixture->id)); + g_assert (rsvg_handle_get_position_sub (handle, &position, fixture->id)); + g_assert (rsvg_handle_get_dimensions_sub (handle, &dimension, fixture->id)); + } else { + rsvg_handle_get_dimensions (handle, &dimension); + } + + if (fixture->has_position) { + g_assert_cmpint (fixture->x, ==, position.x); + g_assert_cmpint (fixture->y, ==, position.y); + } + + if (fixture->has_dimensions) { + g_assert_cmpint (fixture->width, ==, dimension.width); + g_assert_cmpint (fixture->height, ==, dimension.height); + } + + g_object_unref (handle); +} + +static DimensionsFixtureData dimensions_fixtures[] = +{ + { + "/dimensions/viewbox_only", + "dimensions/bug608102.svg", + NULL, + 0, 0, 16, 16, + FALSE, TRUE + }, + { + "/dimensions/hundred_percent_width_and_height", + "dimensions/bug612951.svg", + NULL, + 0, 0, 47, 47.14, + FALSE, TRUE + }, + { + "/dimensions/viewbox_only_2", + "dimensions/bug614018.svg", + NULL, + 0, 0, 972, 546, + FALSE, TRUE + }, + { + "/dimensions/sub/rect_no_unit", + "dimensions/sub-rect-no-unit.svg", + "#rect-no-unit", + 0, 0, 44, 45, + FALSE, TRUE + }, + { + "/dimensions/with_viewbox", + "dimensions/bug521-with-viewbox.svg", + "#foo", + 50.0, 60.0, 70.0, 80.0, + TRUE, TRUE + }, + { + "/dimensions/sub/823", + "dimensions/bug823-position-sub.svg", + "#pad_width", + 444.0, 139.0, 0.0, 0.0, + TRUE, FALSE + }, +}; + +typedef struct +{ + const char *test_name; + const char *fixture; + size_t buf_size; +} LoadingTestData; + +static void +load_n_bytes_at_a_time (gconstpointer data) +{ + const LoadingTestData *fixture_data = data; + char *filename = g_build_filename (test_utils_get_test_data_path (), fixture_data->fixture, NULL); + guchar *buf = g_new (guchar, fixture_data->buf_size); + gboolean done; + + RsvgHandle *handle; + FILE *file; + + file = fopen (filename, "rb"); + g_assert_nonnull (file); + + handle = rsvg_handle_new_with_flags (RSVG_HANDLE_FLAGS_NONE); + + done = FALSE; + + do { + size_t num_read; + + num_read = fread (buf, 1, fixture_data->buf_size, file); + + if (num_read > 0) { + g_assert_true (rsvg_handle_write (handle, buf, num_read, NULL)); + } else { + g_assert_cmpint (ferror (file), ==, 0); + + if (feof (file)) { + done = TRUE; + } + } + } while (!done); + + fclose (file); + g_free (filename); + + g_assert_true (rsvg_handle_close (handle, NULL)); + + g_object_unref (handle); + + g_free (buf); +} + +static LoadingTestData loading_tests[] = { + { "/loading/one-byte-at-a-time", "loading/gnome-cool.svg", 1 }, + { "/loading/compressed-one-byte-at-a-time", "loading/gnome-cool.svgz", 1 }, + { "/loading/compressed-two-bytes-at-a-time", "loading/gnome-cool.svgz", 2 } /* to test reading the entire gzip header */ +}; + +/* Tests for the deprecated GdkPixbuf-based API */ +static void +add_pixbuf_tests (void) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (pixbuf_tests); i++) { + g_test_add_data_func (pixbuf_tests[i].test_name, &pixbuf_tests[i], test_pixbuf); + } + + g_test_add_func ("/api/pixbuf_overflow", pixbuf_overflow); +} + +/* Tests for the C API of librsvg*/ +static void +add_api_tests (void) +{ + g_test_add_func ("/api/handle_has_correct_type_info", handle_has_correct_type_info); + g_test_add_func ("/api/flags_registration", flags_registration); + g_test_add_func ("/api/error_registration", error_registration); + g_test_add_func ("/api/noops", noops); + g_test_add_func ("/api/noops_return_null", noops_return_null); + g_test_add_func ("/api/set_dpi", set_dpi); + g_test_add_func ("/api/base_uri", base_uri); + g_test_add_func ("/api/base_gfile", base_gfile); + g_test_add_func ("/api/handle_write_close_free", handle_write_close_free); + g_test_add_func ("/api/handle_new_from_file", handle_new_from_file); + g_test_add_func ("/api/handle_new_from_data", handle_new_from_data); + g_test_add_func ("/api/handle_new_from_gfile_sync", handle_new_from_gfile_sync); + g_test_add_func ("/api/handle_new_from_stream_sync", handle_new_from_stream_sync); + g_test_add_func ("/api/handle_read_stream_sync", handle_read_stream_sync); + g_test_add_func ("/api/handle_has_sub", handle_has_sub); + g_test_add_func ("/api/handle_get_pixbuf", handle_get_pixbuf); + g_test_add_func ("/api/handle_get_pixbuf_sub", handle_get_pixbuf_sub); + g_test_add_func ("/api/dimensions_and_position", dimensions_and_position); + g_test_add_func ("/api/set_size_callback", set_size_callback); + g_test_add_func ("/api/reset_size_callback", reset_size_callback); + g_test_add_func ("/api/render_with_zero_size_callback", render_with_zero_size_callback); + g_test_add_func ("/api/get_pixbuf_with_size_callback", get_pixbuf_with_size_callback); + g_test_add_func ("/api/detects_cairo_context_in_error", detects_cairo_context_in_error); + g_test_add_func ("/api/can_draw_to_non_image_surface", can_draw_to_non_image_surface); + g_test_add_func ("/api/render_cairo_sub", render_cairo_sub); + g_test_add_func ("/api/get_intrinsic_dimensions", get_intrinsic_dimensions); + g_test_add_func ("/api/get_intrinsic_dimensions_missing_values", get_intrinsic_dimensions_missing_values); + g_test_add_func ("/api/get_intrinsic_size_in_pixels/yes", get_intrinsic_size_in_pixels_yes); + g_test_add_func ("/api/get_intrinsic_size_in_pixels/no", get_intrinsic_size_in_pixels_no); + g_test_add_func ("/api/set_stylesheet", set_stylesheet); + g_test_add_func ("/api/render_document", render_document); + g_test_add_func ("/api/get_geometry_for_layer", get_geometry_for_layer); + g_test_add_func ("/api/render_layer", render_layer); + g_test_add_func ("/api/untransformed_element", untransformed_element); + g_test_add_func ("/api/no_write_before_close", no_write_before_close); + g_test_add_func ("/api/empty_write_close", empty_write_close); + g_test_add_func ("/api/cannot_request_external_elements", cannot_request_external_elements); + g_test_add_func ("/api/property_flags", property_flags); + g_test_add_func ("/api/property_dpi", property_dpi); + g_test_add_func ("/api/property_base_uri", property_base_uri); + g_test_add_func ("/api/property_dimensions", property_dimensions); + g_test_add_func ("/api/property_deprecated", property_deprecated); + g_test_add_func ("/api/return_if_fail", return_if_fail); + g_test_add_func ("/api/return_if_fail_null_check", return_if_fail_null_check); + g_test_add_func ("/api/return_if_fail_type_check", return_if_fail_type_check); + g_test_add_func ("/api/library_version_defines", library_version_defines); + g_test_add_func ("/api/library_version_check", library_version_check); + g_test_add_func ("/api/library_version_constants", library_version_constants); +} + +/* Tests for the deprecated APIs to get geometries */ +static void +add_geometry_tests (void) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (dimensions_fixtures); i++) + g_test_add_data_func (dimensions_fixtures[i].test_name, &dimensions_fixtures[i], (void*)test_dimensions); +} + +/* Tests for the deprecated API for loading bytes at a time */ +static void +add_loading_tests (void) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS (loading_tests); i++) { + g_test_add_data_func (loading_tests[i].test_name, &loading_tests[i], load_n_bytes_at_a_time); + } +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, NULL); + + test_utils_print_dependency_versions (); + + add_pixbuf_tests (); + add_api_tests (); + add_geometry_tests (); + add_loading_tests (); + + return g_test_run (); +} diff --git a/librsvg-c/tests-c/test-utils.c b/librsvg-c/tests-c/test-utils.c new file mode 100644 index 00000000..128611f4 --- /dev/null +++ b/librsvg-c/tests-c/test-utils.c @@ -0,0 +1,257 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 sts=4 ts=4 expandtab: */ + +#include "config.h" +#include "test-utils.h" + +#include +#include +#include + +#if !PANGO_VERSION_CHECK (1, 44, 0) +# include +#endif + +#include +#include FT_FREETYPE_H + + +/* Compare two buffers, returning the number of pixels that are + * different and the maximum difference of any single color channel in + * result_ret. + * + * This function should be rewritten to compare all formats supported by + * cairo_format_t instead of taking a mask as a parameter. + */ +static void +buffer_diff_core (unsigned char *_buf_a, + unsigned char *_buf_b, + unsigned char *_buf_diff, + int width, + int height, + int stride, + guint32 mask, + TestUtilsBufferDiffResult *result_ret) +{ + int x, y; + guint32 *row_a, *row_b, *row; + TestUtilsBufferDiffResult result = {0, 0}; + guint32 *buf_a = (guint32 *) _buf_a; + guint32 *buf_b = (guint32 *) _buf_b; + guint32 *buf_diff = (guint32 *) _buf_diff; + + stride /= sizeof(guint32); + for (y = 0; y < height; y++) + { + row_a = buf_a + y * stride; + row_b = buf_b + y * stride; + row = buf_diff + y * stride; + for (x = 0; x < width; x++) + { + /* check if the pixels are the same */ + if ((row_a[x] & mask) != (row_b[x] & mask)) { + int channel; + guint32 diff_pixel = 0; + + /* calculate a difference value for all 4 channels */ + for (channel = 0; channel < 4; channel++) { + int value_a = (row_a[x] >> (channel*8)) & 0xff; + int value_b = (row_b[x] >> (channel*8)) & 0xff; + unsigned int diff; + diff = abs (value_a - value_b); + if (diff > result.max_diff) + result.max_diff = diff; + diff *= 4; /* emphasize */ + if (diff) + diff += 128; /* make sure it's visible */ + if (diff > 255) + diff = 255; + diff_pixel |= diff << (channel*8); + } + + result.pixels_changed++; + if ((diff_pixel & 0x00ffffff) == 0) { + /* alpha only difference, convert to luminance */ + guint8 alpha = diff_pixel >> 24; + diff_pixel = alpha * 0x010101; + } + row[x] = diff_pixel; + } else { + row[x] = 0; + } + row[x] |= 0xff000000; /* Set ALPHA to 100% (opaque) */ + } + } + + *result_ret = result; +} + +void +test_utils_compare_surfaces (cairo_surface_t *surface_a, + cairo_surface_t *surface_b, + cairo_surface_t *surface_diff, + TestUtilsBufferDiffResult *result) +{ + /* Here, we run cairo's old buffer_diff algorithm which looks for + * pixel-perfect images. + */ + buffer_diff_core (cairo_image_surface_get_data (surface_a), + cairo_image_surface_get_data (surface_b), + cairo_image_surface_get_data (surface_diff), + cairo_image_surface_get_width (surface_a), + cairo_image_surface_get_height (surface_a), + cairo_image_surface_get_stride (surface_a), + 0xffffffff, + result); + if (result->pixels_changed == 0) + return; + + g_test_message ("%d pixels differ (with maximum difference of %d) from reference image\n", + result->pixels_changed, result->max_diff); +} + +/* Copied from gdk_cairo_surface_paint_pixbuf in gdkcairo.c, + * we do not want to depend on GDK + */ +static void +test_utils_cairo_surface_paint_pixbuf (cairo_surface_t *surface, + const GdkPixbuf *pixbuf) +{ + gint width, height; + guchar *gdk_pixels, *cairo_pixels; + int gdk_rowstride, cairo_stride; + int n_channels; + int j; + + if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) + return; + + /* This function can't just copy any pixbuf to any surface, be + * sure to read the invariants here before calling it */ + + g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE); + g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24 || + cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32); + g_assert (cairo_image_surface_get_width (surface) == gdk_pixbuf_get_width (pixbuf)); + g_assert (cairo_image_surface_get_height (surface) == gdk_pixbuf_get_height (pixbuf)); + + cairo_surface_flush (surface); + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + gdk_pixels = gdk_pixbuf_get_pixels (pixbuf); + gdk_rowstride = gdk_pixbuf_get_rowstride (pixbuf); + n_channels = gdk_pixbuf_get_n_channels (pixbuf); + cairo_stride = cairo_image_surface_get_stride (surface); + cairo_pixels = cairo_image_surface_get_data (surface); + + for (j = height; j; j--) + { + guchar *p = gdk_pixels; + guchar *q = cairo_pixels; + + if (n_channels == 3) + { + guchar *end = p + 3 * width; + + while (p < end) + { +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + q[0] = p[2]; + q[1] = p[1]; + q[2] = p[0]; +#else + q[1] = p[0]; + q[2] = p[1]; + q[3] = p[2]; +#endif + p += 3; + q += 4; + } + } + else + { + guchar *end = p + 4 * width; + guint t1,t2,t3; + +#define MULT(d,c,a,t) G_STMT_START { t = c * a + 0x80; d = ((t >> 8) + t) >> 8; } G_STMT_END + + while (p < end) + { +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + MULT(q[0], p[2], p[3], t1); + MULT(q[1], p[1], p[3], t2); + MULT(q[2], p[0], p[3], t3); + q[3] = p[3]; +#else + q[0] = p[3]; + MULT(q[1], p[0], p[3], t1); + MULT(q[2], p[1], p[3], t2); + MULT(q[3], p[2], p[3], t3); +#endif + + p += 4; + q += 4; + } + +#undef MULT + } + + gdk_pixels += gdk_rowstride; + cairo_pixels += cairo_stride; + } + + cairo_surface_mark_dirty (surface); +} + +cairo_surface_t * +test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf) +{ + cairo_surface_t *surface; + + g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL); + g_return_val_if_fail (gdk_pixbuf_get_n_channels (pixbuf) == 4, NULL); + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + gdk_pixbuf_get_width (pixbuf), + gdk_pixbuf_get_height (pixbuf)); + + test_utils_cairo_surface_paint_pixbuf (surface, pixbuf); + + return surface; +} + +static gchar *data_path = NULL; + +const gchar * +test_utils_get_test_data_path (void) +{ + if (data_path) + return data_path; + + data_path = g_test_build_filename (G_TEST_DIST, "fixtures", NULL); + + return data_path; +} + +void +test_utils_print_dependency_versions (void) +{ + FT_Library ft_lib; + FT_Int ft_major = 0; + FT_Int ft_minor = 0; + FT_Int ft_patch = 0; + + FT_Init_FreeType (&ft_lib); + FT_Library_Version (ft_lib, &ft_major, &ft_minor, &ft_patch); + FT_Done_FreeType (ft_lib); + + g_test_message ("Cairo version: %s", cairo_version_string ()); + g_test_message ("Pango version: %s", pango_version_string ()); + g_test_message ("Freetype version: %d.%d.%d", ft_major, ft_minor, ft_patch); +#if PANGO_VERSION_CHECK (1, 44, 0) + g_test_message ("Harfbuzz version: %s", hb_version_string ()); +#else + g_test_message ("Not printing Harfbuzz version since Pango is older than 1.44"); +#endif +} diff --git a/librsvg-c/tests-c/test-utils.h b/librsvg-c/tests-c/test-utils.h new file mode 100644 index 00000000..d71af69d --- /dev/null +++ b/librsvg-c/tests-c/test-utils.h @@ -0,0 +1,35 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set sw=4 sts=4 ts=4 expandtab: */ + +#ifndef TEST_UTILS_H +#define TEST_UTILS_H + +#include +#include +#include + +G_BEGIN_DECLS + +typedef struct { + unsigned int pixels_changed; + unsigned int max_diff; +} TestUtilsBufferDiffResult; + +void test_utils_compare_surfaces (cairo_surface_t *surface_a, + cairo_surface_t *surface_b, + cairo_surface_t *surface_diff, + TestUtilsBufferDiffResult *result); + +cairo_surface_t *test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf); + +typedef gboolean (* AddTestFunc) (GFile *file); + +const gchar *test_utils_get_test_data_path (void); + +void test_utils_print_dependency_versions (void); + +void test_utils_setup_font_map (void); + +G_END_DECLS + +#endif /* TEST_UTILS_H */ diff --git a/librsvg-c/tests/legacy_sizing.rs b/librsvg-c/tests/legacy_sizing.rs new file mode 100644 index 00000000..3ccc1f01 --- /dev/null +++ b/librsvg-c/tests/legacy_sizing.rs @@ -0,0 +1,189 @@ +use cairo; + +use librsvg_c::c_api::sizing::LegacySize; +use rsvg::test_utils::load_svg; +use rsvg::CairoRenderer; + +#[test] +fn just_viewbox_uses_viewbox_size() { + let svg = load_svg( + br#" + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), + cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), + ) + ); +} + +#[test] +fn no_intrinsic_size_uses_element_geometries() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), + cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), + ) + ); +} + +#[test] +fn hundred_percent_width_height_uses_viewbox() { + let svg = load_svg( + br#" + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), + cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), + ) + ); +} + +#[test] +fn hundred_percent_width_height_no_viewbox_uses_element_geometries() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), + cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), + ) + ); +} + +#[test] +fn width_and_viewbox_preserves_aspect_ratio() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + // Per the spec, the height property should default to 100%, so the above would end up + // like + // + // If that were being *rendered* to a viewport, no problem, just use units horizontally + // and 100% of the viewport vertically. + // + // But we are being asked to compute the SVG's natural geometry, so the best we can do + // is to take the aspect ratio defined by the viewBox and apply it to the width. + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), + cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), + ) + ); +} + +#[test] +fn height_and_viewbox_preserves_aspect_ratio() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + // See the comment above in width_and_viewbox_preserves_aspect_ratio(); this + // is equivalent but for the height. + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), + cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), + ) + ); +} + +#[test] +fn zero_width_vbox() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0), + cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0) + ) + ); +} + +#[test] +fn zero_height_vbox() { + let svg = load_svg( + br#" + + + +"#, + ) + .unwrap(); + + assert_eq!( + CairoRenderer::new(&svg) + .legacy_layer_geometry(None) + .unwrap(), + ( + cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0), + cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0) + ) + ); +} diff --git a/rsvg-bench/Cargo.toml b/rsvg-bench/Cargo.toml index f951faf9..4eecd476 100644 --- a/rsvg-bench/Cargo.toml +++ b/rsvg-bench/Cargo.toml @@ -16,5 +16,5 @@ documentation = "https://gnome.pages.gitlab.gnome.org/librsvg/doc/rsvg/index.htm anyhow = "1.0" cairo-rs = { version = "0.17", features=["v1_16", "png", "pdf", "ps", "svg"] } clap = { version = "4.0.17", features = ["cargo", "derive"] } # rsvg-convert -librsvg = { version = "*", path = "../rsvg" } +librsvg = { path = "../rsvg" } thiserror = "1.0" \ No newline at end of file diff --git a/rsvg-convert/Cargo.toml b/rsvg-convert/Cargo.toml index feb8aa9c..c3e103fb 100644 --- a/rsvg-convert/Cargo.toml +++ b/rsvg-convert/Cargo.toml @@ -25,7 +25,18 @@ clap_complete = "4.0.5" # rsvg-convert cssparser = "0.29.0" gio = "0.17" libc = "0.2" -librsvg = { version = "*", path = "../rsvg" } +librsvg = { path = "../rsvg" } +librsvg-c = { path = "../librsvg-c" } + +[dev-dependencies] +assert_cmd = "2.0.2" +predicates = "2.0.0" +tempfile = "3" +url = "2" +lopdf = "0.29.0" +png = "0.17.2" +float-cmp = "0.9.0" +librsvg = { path = "../rsvg", features = ["test-utils"] } [build-dependencies] system-deps = "6.0.0" \ No newline at end of file diff --git a/rsvg-convert/src/main.rs b/rsvg-convert/src/main.rs index 9c472331..dc1cb396 100644 --- a/rsvg-convert/src/main.rs +++ b/rsvg-convert/src/main.rs @@ -21,11 +21,14 @@ use self::windows_imports::*; use cssparser::{_cssparser_internal_to_lowercase, match_ignore_ascii_case}; +use librsvg_c::c_api::{handle::PathOrUrl, sizing::LegacySize}; use rsvg::rsvg_convert_only::{ - AspectRatio, CssLength, Dpi, Horizontal, LegacySize, Length, Normalize, NormalizeParams, Parse, - PathOrUrl, Rect, Signed, ULength, Unsigned, Validate, Vertical, ViewBox, + 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 rsvg::{AcceptLanguage, CairoRenderer, Color, Language, LengthUnit, Loader, RenderingError}; use std::ffi::OsString; use std::io; diff --git a/rsvg-convert/tests/fixtures b/rsvg-convert/tests/fixtures new file mode 120000 index 00000000..ab9214e0 --- /dev/null +++ b/rsvg-convert/tests/fixtures @@ -0,0 +1 @@ +../../rsvg/tests/fixtures/ \ No newline at end of file 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 { + DetailPredicate:: { + 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 { + DetailPredicate:: { + 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) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::CreationDate(when), + } + } + + pub fn with_link(self: Self, link: &str) -> DetailPredicate { + DetailPredicate:: { + p: self, + d: Detail::Link(link.to_string()), + } + } + + pub fn with_text(self: Self, text: &str) -> DetailPredicate { + DetailPredicate:: { + 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> { + 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 { + p: PdfPredicate, + d: Detail, +} + +#[derive(Debug)] +enum Detail { + PageCount(usize), + PageSize(Dimensions, usize), + CreationDate(DateTime), + 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) -> lopdf::Result { + 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; + fn get_creation_date(&self) -> Option>; + 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 { + 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> { + 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; +} + +impl ObjExt for lopdf::Object { + fn as_float(&self) -> lopdf::Result { + 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 { + 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> { + 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 { + 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> { + 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 { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for DetailPredicate { + 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 { + // 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 { + SizePredicate:: { p: self, w, h } + } + + pub fn with_contents>(self: Self, reference: P) -> ReferencePredicate { + let mut path = PathBuf::new(); + path.push(reference); + ReferencePredicate:: { 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> { + 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 { + p: PngPredicate, + w: u32, + h: u32, +} + +impl SizePredicate { + 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> { + 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 { + 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> { + 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 { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for SizePredicate { + 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 { + p: PngPredicate, + path: PathBuf, +} + +impl ReferencePredicate { + fn diff_acceptable(diff: &BufferDiff) -> bool { + match diff { + BufferDiff::DifferentSizes => false, + BufferDiff::Diff(diff) => !diff.inacceptable(), + } + } + + fn diff_surface(&self, surface: &SharedImageSurface) -> Option { + 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> { + 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 { + 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> { + 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 { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for ReferencePredicate { + 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 { + DetailPredicate:: { + p: self, + d: Detail::Size(Dimensions { + w: width, + h: height, + }), + } + } +} + +fn svg_from_bytes(data: &[u8]) -> Result { + 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> { + 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 { + 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; +} + +impl DetailPredicate { + 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> { + 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 { + 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> { + 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 { + fn children<'a>(&'a self) -> Box> + 'a> { + let params = vec![Child::new("predicate", &self.p)]; + Box::new(params.into_iter()) + } +} + +impl fmt::Display for DetailPredicate { + 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..dcd04838 --- /dev/null +++ b/rsvg-convert/tests/rsvg_convert.rs @@ -0,0 +1,1081 @@ +//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

(file: P) -> Command + where + P: AsRef, + { + 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/api/dpi.svg") + .arg(option) + .assert() + .success(); + } + + fn option_yields_output(option: &str, output_pred: I) + where + I: IntoOutputPredicate

, + 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/dimensions/bug521-with-viewbox.svg") + .assert() + .success() + .stdout(file::is_png()); +} + +#[test] +fn argument_is_input_filename() { + let input = Path::new("tests/fixtures/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/api/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/api/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/cmdline/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/cmdline/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/text/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/bug521-with-viewbox.svg") + .arg("--height=200ex") + .assert() + .failure() + .stderr(contains("supported units")); +} + +#[test] +fn invalid_length() { + RsvgConvert::new_with_input("tests/fixtures/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/api/dpi.svg") + .assert() + .success() + .stdout(file::is_png().with_size(96, 384)); +} + +#[test] +fn x_resolution() { + RsvgConvert::new_with_input("tests/fixtures/api/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/api/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/api/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/api/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/api/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/api/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/api/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/cmdline/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/cmdline/zero-offset-png.png")); +} + +#[test] +fn offset_png() { + RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/offset-png.png")); +} + +#[cfg(system_deps_have_cairo_pdf)] +#[test] +fn unscaled_pdf_size() { + RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/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/cmdline/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/cmdline/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/dimensions/bug521-with-viewbox.svg"); + let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); + let three = Path::new("tests/fixtures/api/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/cmdline/dimensions-in.svg") + .arg("--format=pdf") + .arg("--page-width=210mm") + .assert() + .failure() + .stderr(contains("both").and(contains("options"))); + + RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/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/api/dpi.svg") + .arg("-b") + .arg("#aabbcc") + .assert() + .success(); +} + +#[test] +fn background_color_option_invalid_color_yields_error() { + RsvgConvert::new_with_input("tests/fixtures/api/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/cmdline/gimp-wilber.svg") + .arg("--background-color=purple") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/cmdline/gimp-wilber-ref.png")); +} + +#[test] +fn stylesheet_option() { + RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") + .arg("--stylesheet=tests/fixtures/dimensions/empty.svg") + .assert() + .success(); +} + +#[test] +fn stylesheet_short_option() { + RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") + .arg("-s") + .arg("tests/fixtures/dimensions/empty.svg") + .assert() + .success(); +} + +#[test] +fn stylesheet_option_error() { + RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") + .arg("--stylesheet=foobar") + .assert() + .failure() + .stderr(starts_with("Error reading stylesheet")); +} + +#[test] +fn export_id_option() { + RsvgConvert::new_with_input("tests/fixtures/api/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/cmdline/bug601-zero-stroke-width.svg") + .arg("--export-id=foo") + .assert() + .success() + .stdout( + file::is_png().with_contents( + "tests/fixtures/cmdline/bug601-zero-stroke-width-render-only-foo.png", + ), + ); +} + +#[test] +fn export_id_short_option() { + RsvgConvert::new_with_input("tests/fixtures/api/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/api/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/api/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/api/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/api/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/render-crash/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/cmdline/accept-language.svg") + .arg("--accept-language=es-MX") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/cmdline/accept-language-es.png")); + + RsvgConvert::new_with_input("tests/fixtures/cmdline/accept-language.svg") + .arg("--accept-language=de") + .assert() + .success() + .stdout(file::is_png().with_contents("tests/fixtures/cmdline/accept-language-de.png")); +} + +#[test] +fn accept_language_fallback() { + RsvgConvert::new_with_input("tests/fixtures/cmdline/accept-language.svg") + .arg("--accept-language=fr") + .assert() + .success() + .stdout( + file::is_png().with_contents("tests/fixtures/cmdline/accept-language-fallback.png"), + ); +} + +#[test] +fn accept_language_invalid_tag() { + // underscores are not valid in BCP47 language tags + RsvgConvert::new_with_input("tests/fixtures/cmdline/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, 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 { + 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()) +} diff --git a/rsvg/Cargo.toml b/rsvg/Cargo.toml index 4f6fa35e..5469ad5d 100644 --- a/rsvg/Cargo.toml +++ b/rsvg/Cargo.toml @@ -11,6 +11,9 @@ edition.workspace = true rust-version.workspace = true [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 } gdk-pixbuf = { name = "gdk-pixbuf-2.0", version = "2.20" } gio = { name = "gio-2.0", version = "2.24" } glib = { name = "glib-2.0", version = "2.50" } @@ -36,6 +39,10 @@ harfbuzz = "2.0" freetype2 = "20.0.14" libxml2 = { name = "libxml-2.0", version = "2.9" } +[features] +c-api = [] +test-utils = ["yeslogic-fontconfig-sys"] + [lib] name = "rsvg" crate-type = [ "staticlib", "rlib" ] @@ -69,14 +76,13 @@ regex = "1.7.1" rgb = { version="0.8", features=["argb"] } selectors = "0.24.0" string_cache = "0.8.0" -#thiserror = "1.0" tinyvec = { version = "1.2.0", features = ["alloc"] } url = "2" xml5ever = "0.17.0" +yeslogic-fontconfig-sys = { version = "4.0.1", optional = true } [dev-dependencies] anyhow = "1.0" -assert_cmd = "2.0.2" chrono = { version = "0.4.23", default-features = false, features = ["clock", "std"] } criterion = "0.4" lopdf = "0.29.0" @@ -88,10 +94,9 @@ quick-error = "2.0.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tempfile = "3" -yeslogic-fontconfig-sys = "4.0.1" +#librsvg = { path = ".", features = ["tests"] } [build-dependencies] -regex = "1.7.1" system-deps = "6.0.0" [[bench]] diff --git a/rsvg/build.rs b/rsvg/build.rs index 7a46d6c6..92037d16 100644 --- a/rsvg/build.rs +++ b/rsvg/build.rs @@ -1,8 +1,6 @@ -use regex::Regex; use std::env; use std::fs::File; -use std::io::prelude::*; -use std::io::{BufReader, BufWriter, Write}; +use std::io::{BufWriter, Write}; use std::path::Path; use std::process; @@ -13,7 +11,6 @@ fn main() { } generate_srgb_tables(); - write_version(); } /// Converts an sRGB color value to a linear sRGB color value (undoes the gamma correction). @@ -63,64 +60,3 @@ fn generate_srgb_tables() { print_table(&mut file, "LINEARIZE", linearize, 256); print_table(&mut file, "UNLINEARIZE", unlinearize, 256); } - -fn write_version() { - let mut major = None; - let mut minor = None; - let mut micro = None; - - { - let file = File::open("../configure.ac") - .expect("builds must take place within the librsvg source tree"); - - let major_regex = Regex::new(r#"^m4_define\(\[rsvg_major_version\],\[(\d+)\]\)"#).unwrap(); - let minor_regex = Regex::new(r#"^m4_define\(\[rsvg_minor_version\],\[(\d+)\]\)"#).unwrap(); - let micro_regex = Regex::new(r#"^m4_define\(\[rsvg_micro_version\],\[(\d+)\]\)"#).unwrap(); - - for line in BufReader::new(file).lines() { - match line { - Ok(line) => { - if let Some(nums) = major_regex.captures(&line) { - major = Some(String::from( - nums.get(1).expect("major_regex matched once").as_str(), - )); - } else if let Some(nums) = minor_regex.captures(&line) { - minor = Some(String::from( - nums.get(1).expect("minor_regex matched once").as_str(), - )); - } else if let Some(nums) = micro_regex.captures(&line) { - micro = Some(String::from( - nums.get(1).expect("micro_regex matched once").as_str(), - )); - } - } - - Err(e) => panic!("could not parse version from configure.ac: {e}"), - } - } - } - - let output = Path::new(&env::var("OUT_DIR").unwrap()).join("version.rs"); - let mut file = File::create(output).expect("open version.rs for writing"); - file.write_all( - format!( - r#" -use std::os::raw::c_uint; - -#[no_mangle] -pub static rsvg_major_version: c_uint = {}; - -#[no_mangle] -pub static rsvg_minor_version: c_uint = {}; - -#[no_mangle] -pub static rsvg_micro_version: c_uint = {}; -"#, - major.expect("major version is set"), - minor.expect("minor version is set"), - micro.expect("micro version is set") - ) - .as_bytes(), - ) - .expect("write version.rs"); -} diff --git a/rsvg/example.svg b/rsvg/example.svg new file mode 100644 index 00000000..1a6c762d --- /dev/null +++ b/rsvg/example.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rsvg/src/api.rs b/rsvg/src/api.rs index beaee9a9..34945afa 100644 --- a/rsvg/src/api.rs +++ b/rsvg/src/api.rs @@ -6,6 +6,7 @@ pub use crate::{ accept_language::{AcceptLanguage, Language}, + dpi::Dpi, error::{ImplementationLimit, LoadingError, RenderingError}, length::{LengthUnit, RsvgLength as Length}, }; @@ -22,7 +23,6 @@ use locale_config::{LanguageRange, Locale}; use crate::{ accept_language::{LanguageTags, UserLanguage}, - dpi::Dpi, handle::{Handle, LoadOptions}, session::Session, url_resolver::UrlResolver, @@ -64,14 +64,19 @@ impl Loader { /// ``` #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::new_with_session(Session::default()) + Self { + unlimited_size: false, + keep_image_data: false, + session: Session::default(), + } } /// Creates a `Loader` from a pre-created [`Session`]. /// /// This is useful when a `Loader` must be created by the C API, which should already /// have created a session for logging. - pub(crate) fn new_with_session(session: Session) -> Self { + #[cfg(feature = "c-api")] + pub fn new_with_session(session: Session) -> Self { Self { unlimited_size: false, keep_image_data: false, @@ -615,6 +620,17 @@ impl<'a> CairoRenderer<'a> { ) } + /// Returns DPI TODO + pub fn dpi(&self) -> Dpi { + self.dpi + } + + /// Questionable Special function TODO + #[cfg(feature = "c-api")] + pub fn handle(&self) -> &Handle { + &self.handle.handle + } + /// Turns on test mode. Do not use this function; it is for librsvg's test suite only. pub fn test_mode(self, is_testing: bool) -> Self { CairoRenderer { is_testing, ..self } diff --git a/rsvg/src/lib.rs b/rsvg/src/lib.rs index bfe2e73d..6d470d59 100644 --- a/rsvg/src/lib.rs +++ b/rsvg/src/lib.rs @@ -218,12 +218,23 @@ mod url_resolver; mod viewbox; mod xml; +#[cfg(feature = "test-utils")] +#[doc(hidden)] +pub mod test_utils; + #[doc(hidden)] pub mod bench_only { pub use crate::path_builder::PathBuilder; pub use crate::path_parser::Lexer; } +#[doc(hidden)] +#[cfg(feature = "c-api")] +pub mod c_api_only { + pub use crate::handle::Handle; + pub use crate::session::Session; +} + #[doc(hidden)] pub mod doctest_only { pub use crate::aspect_ratio::AspectRatio; @@ -239,7 +250,6 @@ pub mod doctest_only { #[doc(hidden)] pub mod rsvg_convert_only { pub use crate::aspect_ratio::AspectRatio; - pub use crate::dpi::Dpi; pub use crate::error::ParseError; pub use crate::length::{ CssLength, Horizontal, Length, Normalize, NormalizeParams, Signed, ULength, Unsigned, diff --git a/rsvg/src/test_utils/compare_surfaces.rs b/rsvg/src/test_utils/compare_surfaces.rs new file mode 100644 index 00000000..06100db9 --- /dev/null +++ b/rsvg/src/test_utils/compare_surfaces.rs @@ -0,0 +1,112 @@ +use std::fmt; + +use crate::surface_utils::{ + iterators::Pixels, + shared_surface::{SharedImageSurface, SurfaceType}, + ImageSurfaceDataExt, Pixel, PixelOps, +}; + +use rgb::{ComponentMap, RGB}; + +pub enum BufferDiff { + DifferentSizes, + Diff(Diff), +} + +pub struct Diff { + pub num_pixels_changed: usize, + pub max_diff: u8, + pub surface: SharedImageSurface, +} + +impl fmt::Display for BufferDiff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BufferDiff::DifferentSizes => write!(f, "different sizes"), + BufferDiff::Diff(diff) => diff.fmt(f), + } + } +} + +impl fmt::Display for Diff { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} pixels are different, with a maximum difference of {}", + self.num_pixels_changed, self.max_diff + ) + } +} + +#[inline] +fn emphasize(p: &Pixel) -> Pixel { + let emphasize_component = |c| { + // emphasize + let mut c = c as u32 * 4; + // make sure it's visible + if c > 0 { + c += 128; + } + c.min(255) as u8 + }; + p.map(emphasize_component) +} + +pub fn compare_surfaces( + surf_a: &SharedImageSurface, + surf_b: &SharedImageSurface, +) -> Result { + let a_width = surf_a.width(); + let a_height = surf_a.height(); + + let b_width = surf_b.width(); + let b_height = surf_b.height(); + + if a_width != b_width || a_height != b_height { + return Ok(BufferDiff::DifferentSizes); + } + + let mut surf_diff = cairo::ImageSurface::create(cairo::Format::ARgb32, a_width, a_height)?; + let diff_stride = surf_diff.stride() as usize; + + let mut num_pixels_changed = 0; + let mut max_diff = 0; + + let black = Pixel::default().alpha(255); + + { + let mut diff_data = surf_diff.data().unwrap(); + + for ((xa, ya, pixel_a), (_, _, pixel_b)) in Pixels::new(surf_a).zip(Pixels::new(surf_b)) { + let dest = if pixel_a != pixel_b { + num_pixels_changed += 1; + + let pixel_diff = pixel_a.diff(&pixel_b); + + max_diff = pixel_diff.iter().fold(max_diff, |acc, c| acc.max(c)); + + let pixel_diff = emphasize(&pixel_diff); + + if pixel_diff.rgb() == RGB::default() { + // alpha only difference; convert alpha to gray + let a = pixel_diff.a; + pixel_diff.map_rgb(|_| a) + } else { + pixel_diff.alpha(255) + } + } else { + black + }; + + diff_data.set_pixel(diff_stride, dest, xa, ya); + } + } + + let surface = SharedImageSurface::wrap(surf_diff, SurfaceType::SRgb)?; + + Ok(BufferDiff::Diff(Diff { + num_pixels_changed, + max_diff, + surface, + })) +} diff --git a/rsvg/src/test_utils/mod.rs b/rsvg/src/test_utils/mod.rs new file mode 100644 index 00000000..c5aabb91 --- /dev/null +++ b/rsvg/src/test_utils/mod.rs @@ -0,0 +1,119 @@ +pub mod compare_surfaces; +pub mod reference_utils; + +use cairo; +use gio; +use glib; +use glib::translate::*; +use libc; +use std::env; +use std::ffi::CString; +use std::sync::Once; + +use crate::{ + surface_utils::shared_surface::{SharedImageSurface, SurfaceType}, + CairoRenderer, Loader, LoadingError, RenderingError, SvgHandle, +}; + +pub fn load_svg(input: &'static [u8]) -> Result { + let bytes = glib::Bytes::from_static(input); + let stream = gio::MemoryInputStream::from_bytes(&bytes); + + Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>) +} + +#[derive(Copy, Clone)] +pub struct SurfaceSize(pub i32, pub i32); + +pub fn render_document( + svg: &SvgHandle, + surface_size: SurfaceSize, + cr_transform: F, + viewport: cairo::Rectangle, +) -> Result { + let renderer = CairoRenderer::new(svg); + + let SurfaceSize(width, height) = surface_size; + + let output = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); + + let res = { + let cr = cairo::Context::new(&output).expect("Failed to create a cairo context"); + cr_transform(&cr); + Ok(renderer.render_document(&cr, &viewport)?) + }; + + res.and_then(|_| Ok(SharedImageSurface::wrap(output, SurfaceType::SRgb)?)) +} + +#[cfg(system_deps_have_pangoft2)] +mod pango_ft2 { + use super::*; + use glib::prelude::*; + use pangocairo::FontMap; + + extern "C" { + // pango_fc_font_map_set_config (PangoFcFontMap *fcfontmap, + // FcConfig *fcconfig); + // This is not bound in gtk-rs, and PangoFcFontMap is not even exposed, so we'll bind it by hand. + fn pango_fc_font_map_set_config( + font_map: *mut libc::c_void, + config: *mut fontconfig_sys::FcConfig, + ); + } + + pub unsafe fn load_test_fonts() { + let font_paths = [ + "tests/resources/Ahem.ttf", + "tests/resources/NotoSansHebrew-Regular.ttf", + "tests/resources/Roboto-Regular.ttf", + "tests/resources/Roboto-Italic.ttf", + "tests/resources/Roboto-Bold.ttf", + "tests/resources/Roboto-BoldItalic.ttf", + ]; + + let config = fontconfig_sys::FcConfigCreate(); + if fontconfig_sys::FcConfigSetCurrent(config) == 0 { + panic!("Could not set a fontconfig configuration"); + } + + for path in &font_paths { + let path_cstring = CString::new(*path).unwrap(); + + if fontconfig_sys::FcConfigAppFontAddFile(config, path_cstring.as_ptr() as *const _) + == 0 + { + panic!("Could not load font file {} for tests; aborting", path,); + } + } + + let font_map = FontMap::for_font_type(cairo::FontType::FontTypeFt).unwrap(); + let raw_font_map: *mut pango::ffi::PangoFontMap = font_map.to_glib_none().0; + + pango_fc_font_map_set_config(raw_font_map as *mut _, config); + fontconfig_sys::FcConfigDestroy(config); + + FontMap::set_default(Some(&font_map.downcast::().unwrap())); + } +} + +#[cfg(system_deps_have_pangoft2)] +pub fn setup_font_map() { + unsafe { + self::pango_ft2::load_test_fonts(); + } +} + +#[cfg(not(system_deps_have_pangoft2))] +pub fn setup_font_map() {} + +pub fn setup_language() { + static ONCE: Once = Once::new(); + + ONCE.call_once(|| { + // For systemLanguage attribute tests. + // The trailing ":" is intentional to test gitlab#425. + env::set_var("LANGUAGE", "de:en_US:en:"); + env::set_var("LC_ALL", "de:en_US:en:"); + }); +} diff --git a/rsvg/src/test_utils/reference_utils.rs b/rsvg/src/test_utils/reference_utils.rs new file mode 100644 index 00000000..9f4a51d5 --- /dev/null +++ b/rsvg/src/test_utils/reference_utils.rs @@ -0,0 +1,288 @@ +//! Utilities for the reference image test suite. +//! +//! This module has utility functions that are used in the test suite +//! to compare rendered surfaces to reference images. + +use cairo; + +use std::convert::TryFrom; +use std::env; +use std::fs::{self, File}; +use std::io::{BufReader, Read}; +use std::path::{Path, PathBuf}; +use std::sync::Once; + +use crate::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; + +use super::compare_surfaces::{compare_surfaces, BufferDiff, Diff}; + +pub struct Reference(SharedImageSurface); + +impl Reference { + pub fn from_png

(path: P) -> Result + where + P: AsRef, + { + let file = File::open(path).map_err(cairo::IoError::Io)?; + let mut reader = BufReader::new(file); + let surface = surface_from_png(&mut reader)?; + Self::from_surface(surface) + } + + pub fn from_surface(surface: cairo::ImageSurface) -> Result { + let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; + Ok(Self(shared)) + } +} + +pub trait Compare { + fn compare(self, surface: &SharedImageSurface) -> Result; +} + +impl Compare for &Reference { + fn compare(self, surface: &SharedImageSurface) -> Result { + compare_surfaces(&self.0, surface).map_err(cairo::IoError::from) + } +} + +impl Compare for Result { + fn compare(self, surface: &SharedImageSurface) -> Result { + self.map(|reference| reference.compare(surface)) + .and_then(std::convert::identity) + } +} + +pub trait Evaluate { + fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str); +} + +impl Evaluate for BufferDiff { + /// Evaluates a BufferDiff and panics if there are relevant differences + /// + /// The `output_base_name` is used to write test results if the + /// surfaces are different. If this is `foo`, this will write + /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a + /// visual diff between `output_surf` and the `Reference` that this + /// diff was created from. + /// + /// # Panics + /// + /// Will panic if the surfaces are too different to be acceptable. + fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) { + match self { + BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"), + + BufferDiff::Diff(diff) => { + if diff.distinguishable() { + println!( + "{}: {} pixels changed with maximum difference of {}", + output_base_name, diff.num_pixels_changed, diff.max_diff, + ); + + write_to_file(output_surf, output_base_name, "out"); + write_to_file(&diff.surface, output_base_name, "diff"); + + if diff.inacceptable() { + panic!("surfaces are too different"); + } + } + } + } + } +} + +impl Evaluate for Result { + fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) { + self.as_ref() + .map(|diff| diff.evaluate(output_surface, output_base_name)) + .unwrap(); + } +} + +fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) { + let path = output_dir().join(format!("{}-{}.png", output_base_name, suffix)); + println!("{}: {}", suffix, path.to_string_lossy()); + let mut output_file = File::create(path).unwrap(); + input + .clone() + .into_image_surface() + .unwrap() + .write_to_png(&mut output_file) + .unwrap(); +} + +/// Creates a directory for test output and returns its path. +/// +/// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment +/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is +/// a platform dependent location for temporary files. +/// +/// # Panics +/// +/// Will panic if the output directory can not be created. +pub fn output_dir() -> PathBuf { + let tempdir = || { + let mut path = env::temp_dir(); + path.push("rsvg-test-output"); + path + }; + let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from); + + fs::create_dir_all(&path).expect("could not create output directory for tests"); + + path +} + +fn tolerable_difference() -> u8 { + static mut TOLERANCE: u8 = 8; + + static ONCE: Once = Once::new(); + ONCE.call_once(|| unsafe { + if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") { + let value: usize = str + .parse() + .expect("Can not parse RSVG_TEST_TOLERANCE as a number"); + TOLERANCE = + u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255"); + } + }); + + unsafe { TOLERANCE } +} + +pub trait Deviation { + fn distinguishable(&self) -> bool; + fn inacceptable(&self) -> bool; +} + +impl Deviation for Diff { + fn distinguishable(&self) -> bool { + self.max_diff > 2 + } + + fn inacceptable(&self) -> bool { + self.max_diff > tolerable_difference() + } +} + +/// Creates a cairo::ImageSurface from a stream of PNG data. +/// +/// The surface is converted to ARGB if needed. Use this helper function with `Reference`. +pub fn surface_from_png(stream: &mut R) -> Result +where + R: Read, +{ + let png = cairo::ImageSurface::create_from_png(stream)?; + let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?; + { + // convert to ARGB; the PNG may come as Rgb24 + let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context"); + cr.set_source_surface(&png, 0.0, 0.0).unwrap(); + cr.paint().unwrap(); + } + Ok(argb) +} + +/// Macro test that compares render outputs +/// +/// Takes in SurfaceSize width and height, setting the cairo surface +#[macro_export] +macro_rules! test_compare_render_output { + ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => { + #[test] + fn $test_name() { + $crate::test_utils::setup_font_map(); + + let sx: i32 = $width; + let sy: i32 = $height; + let svg = load_svg($test).unwrap(); + let output_surf = render_document( + &svg, + SurfaceSize(sx, sy), + |_| (), + cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)), + ) + .unwrap(); + + let reference = load_svg($reference).unwrap(); + let reference_surf = render_document( + &reference, + SurfaceSize(sx, sy), + |_| (), + cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)), + ) + .unwrap(); + + Reference::from_surface(reference_surf.into_image_surface().unwrap()) + .compare(&output_surf) + .evaluate(&output_surf, stringify!($test_name)); + } + }; +} + +/// Render two SVG files and compare them. +/// +/// This is used to implement reference tests, or reftests. Use it like this: +/// +/// ```ignore +/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg"); +/// ``` +/// +/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions, +/// and that they produce the same rendered output. +#[macro_export] +macro_rules! test_svg_reference { + ($test_name:ident, $test_filename:expr, $reference_filename:expr) => { + #[test] + fn $test_name() { + use cairo; + use rsvg::{CairoRenderer, Loader}; + use $crate::test_utils::reference_utils::{Compare, Evaluate, Reference}; + use $crate::test_utils::{render_document, setup_font_map, SurfaceSize}; + + setup_font_map(); + + let svg = Loader::new() + .read_path($test_filename) + .expect("reading SVG test file"); + let reference = Loader::new() + .read_path($reference_filename) + .expect("reading reference file"); + + let svg_renderer = CairoRenderer::new(&svg); + let ref_renderer = CairoRenderer::new(&reference); + + let svg_dim = svg_renderer.intrinsic_dimensions(); + let ref_dim = ref_renderer.intrinsic_dimensions(); + + assert_eq!( + svg_dim, ref_dim, + "sizes of SVG document and reference file are different" + ); + + let pixels = svg_renderer + .intrinsic_size_in_pixels() + .unwrap_or((100.0, 100.0)); + + let output_surf = render_document( + &svg, + SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), + |_| (), + cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), + ) + .unwrap(); + + let reference_surf = render_document( + &reference, + SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), + |_| (), + cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), + ) + .unwrap(); + + Reference::from_surface(reference_surf.into_image_surface().unwrap()) + .compare(&output_surf) + .evaluate(&output_surf, stringify!($test_name)); + } + }; +} diff --git a/rsvg/tests/Makefile.am b/rsvg/tests/Makefile.am deleted file mode 100644 index b645ea6d..00000000 --- a/rsvg/tests/Makefile.am +++ /dev/null @@ -1,95 +0,0 @@ -include $(top_srcdir)/glib-tap.mk - -test_sources = \ - src/api.rs \ - src/bugs.rs \ - src/compare_surfaces.rs \ - src/errors.rs \ - src/filters.rs \ - src/geometries.rs \ - src/intrinsic_dimensions.rs \ - src/legacy_sizing.rs \ - src/loading_crash.rs \ - src/main.rs \ - src/primitive_geometries.rs \ - src/primitives.rs \ - src/reference.rs \ - src/reference_utils.rs \ - src/render_crash.rs \ - src/shapes.rs \ - src/text.rs \ - src/utils.rs \ - src/cmdline/mod.rs \ - src/cmdline/rsvg_convert.rs \ - src/predicates/file.rs \ - src/predicates/mod.rs \ - src/predicates/pdf.rs \ - src/predicates/png.rs \ - src/predicates/svg.rs \ - $(NULL) - -test_programs = api - -api_SOURCES = \ - api.c \ - test-utils.c \ - test-utils.h \ - $(NULL) - -api_LDADD = $(top_builddir)/librsvg_c_api.la \ - $(LIBRSVG_LIBS) \ - $(LIBM) - -api_LDFLAGS = -static - -AM_CPPFLAGS = \ - -I$(srcdir) \ - -I$(top_srcdir)/include \ - -I$(top_builddir)/include \ - -DTEST_DATA_DIR="\"$(srcdir)\"" \ - -DTEST_SRC_DIR="\"$(PWD)\"" \ - -DTOP_SRC_DIR="\"$(top_srcdir)\"" \ - $(LIBRSVG_CFLAGS) - -test_resources = \ - $(wildcard $(srcdir)/resources/*.ttf) - -test_fixtures = \ - $(wildcard $(srcdir)/fixtures/api/*.svg) \ - $(wildcard $(srcdir)/fixtures/cmdline/*.svg) \ - $(wildcard $(srcdir)/fixtures/cmdline/*.png) \ - $(wildcard $(srcdir)/fixtures/crash/*.svg) \ - $(wildcard $(srcdir)/fixtures/crash/*.png) \ - $(wildcard $(srcdir)/fixtures/errors/*) \ - $(wildcard $(srcdir)/fixtures/geometries/*) \ - $(wildcard $(srcdir)/fixtures/loading/*) \ - $(wildcard $(srcdir)/fixtures/primitive_geometries/*) \ - $(wildcard $(srcdir)/fixtures/reftests/*.css) \ - $(wildcard $(srcdir)/fixtures/reftests/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/*.png) \ - $(wildcard $(srcdir)/fixtures/reftests/*.txt) \ - $(wildcard $(srcdir)/fixtures/reftests/adwaita/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/adwaita/*.png) \ - $(wildcard $(srcdir)/fixtures/reftests/bugs/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/bugs/*.png) \ - $(wildcard $(srcdir)/fixtures/reftests/svg1.1/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/svg1.1/*.png) \ - $(wildcard $(srcdir)/fixtures/reftests/svg1.1/images/*) \ - $(wildcard $(srcdir)/fixtures/reftests/svg1.1/resources/*) \ - $(wildcard $(srcdir)/fixtures/reftests/svg2/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/svg2/*.png) \ - $(wildcard $(srcdir)/fixtures/reftests/bugs-reftests/*.svg) \ - $(wildcard $(srcdir)/fixtures/reftests/svg2-reftests/*.svg) \ - $(wildcard $(srcdir)/fixtures/render-crash/*.svg) \ - $(wildcard $(srcdir)/fixtures/text/*.svg) \ - $(wildcard $(srcdir)/fixtures/dimensions/*.svg) - -EXTRA_DIST += \ - $(test_sources) \ - $(test_resources) \ - $(test_fixtures) \ - README.md \ - $(NULL) - -clean-local: - rm -rf output diff --git a/rsvg/tests/api.c b/rsvg/tests/api.c deleted file mode 100644 index f7b01c2e..00000000 --- a/rsvg/tests/api.c +++ /dev/null @@ -1,1709 +0,0 @@ -/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ -/* vim: set ts=4 nowrap ai expandtab sw=4: */ - -/* These are the C API tests for librsvg. These test the complete C - * API, especially its historical peculiarities to ensure ABI - * compatibility. - * - * These tests are not meant to exhaustively test librsvg's features. - * For those, you should look at the Rust integration tests. See - * tests/README.md for details. - */ - -#include "config.h" - -#include -#include -#include - -#define RSVG_DISABLE_DEPRECATION_WARNINGS /* so we can test deprecated API */ -#include -#include "test-utils.h" - -/* - Untested: - rsvg_handle_internal_set_testing -*/ - -static void -handle_has_correct_type_info (void) -{ - GTypeQuery q; - RsvgHandle *handle; - - g_type_query (RSVG_TYPE_HANDLE, &q); - g_assert (q.type == RSVG_TYPE_HANDLE); - g_assert (q.type == rsvg_handle_get_type ()); - - g_assert_cmpstr (q.type_name, ==, "RsvgHandle"); - - /* These test that the sizes of the structs in the header file actually match the - * sizes of structs and the glib-subclass machinery in the Rust side. - */ - g_assert (sizeof (RsvgHandleClass) == (gsize) q.class_size); - g_assert (sizeof (RsvgHandle) == (gsize) q.instance_size); - - handle = rsvg_handle_new(); - g_assert (G_OBJECT_TYPE (handle) == RSVG_TYPE_HANDLE); - g_object_unref (handle); -} - -static void -assert_flags_value_matches (GFlagsValue *v, - guint value, - const char *value_name, - const char *value_nick) -{ - g_assert_cmpint(v->value, ==, value); - g_assert_cmpstr(v->value_name, ==, value_name); - g_assert_cmpstr(v->value_nick, ==, value_nick); -} - -static void -flags_registration (void) -{ - GType ty; - GTypeQuery q; - GTypeClass *type_class; - GFlagsClass *flags_class; - - ty = RSVG_TYPE_HANDLE_FLAGS; - - g_assert (ty != G_TYPE_INVALID); - - g_type_query (RSVG_TYPE_HANDLE_FLAGS, &q); - g_assert (q.type == ty); - g_assert (G_TYPE_IS_FLAGS (q.type)); - g_assert_cmpstr (q.type_name, ==, "RsvgHandleFlags"); - - type_class = g_type_class_ref (ty); - g_assert (G_IS_FLAGS_CLASS (type_class)); - g_assert (G_FLAGS_CLASS_TYPE (type_class) == ty); - - flags_class = G_FLAGS_CLASS (type_class); - g_assert_cmpint (flags_class->n_values, ==, 3); - - assert_flags_value_matches(&flags_class->values[0], - RSVG_HANDLE_FLAGS_NONE, - "RSVG_HANDLE_FLAGS_NONE", - "flags-none"); - - assert_flags_value_matches(&flags_class->values[1], - RSVG_HANDLE_FLAG_UNLIMITED, - "RSVG_HANDLE_FLAG_UNLIMITED", - "flag-unlimited"); - - assert_flags_value_matches(&flags_class->values[2], - RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA, - "RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA", - "flag-keep-image-data"); - - g_type_class_unref (type_class); -} - -static void -assert_enum_value_matches (GEnumValue *v, - gint value, - const char *value_name, - const char *value_nick) -{ - g_assert_cmpint (v->value, ==, value); - g_assert_cmpstr (v->value_name, ==, value_name); - g_assert_cmpstr (v->value_nick, ==, value_nick); -} - -static void -error_registration (void) -{ - GType ty; - GTypeQuery q; - GTypeClass *type_class; - GEnumClass *enum_class; - - g_assert_cmpint (RSVG_ERROR, !=, 0); - - ty = RSVG_TYPE_ERROR; - - g_assert (ty != G_TYPE_INVALID); - - g_type_query (ty, &q); - g_assert (q.type == ty); - g_assert (G_TYPE_IS_ENUM (q.type)); - g_assert_cmpstr (q.type_name, ==, "RsvgError"); - - type_class = g_type_class_ref (ty); - g_assert (G_IS_ENUM_CLASS (type_class)); - g_assert (G_ENUM_CLASS_TYPE (type_class) == ty); - - enum_class = G_ENUM_CLASS (type_class); - g_assert_cmpint (enum_class->n_values, ==, 1); - - assert_enum_value_matches (&enum_class->values[0], - RSVG_ERROR_FAILED, - "RSVG_ERROR_FAILED", - "failed"); - - g_type_class_unref (type_class); -} - -static char * -get_test_filename (const char *basename) { - return g_build_filename (test_utils_get_test_data_path (), - "api", - basename, - NULL); -} - -static RsvgHandle * -load_test_document (const char *basename) { - char *filename = get_test_filename (basename); - GError *error = NULL; - - RsvgHandle *handle = rsvg_handle_new_from_file (filename, &error); - g_free (filename); - - g_assert_nonnull (handle); - g_assert_no_error (error); - - return handle; -} - -#define EXAMPLE_WIDTH 100 -#define EXAMPLE_HEIGHT 400 - -#define XZOOM 2 -#define YZOOM 3 - -#define MAX_WIDTH 10 -#define MAX_HEIGHT 40 - -#define MAX_ZOOMED_WIDTH 20 -#define MAX_ZOOMED_HEIGHT 120 - -#define EXAMPLE_ONE_ID "#one" -#define EXAMPLE_TWO_ID "#two" -#define EXAMPLE_NONEXISTENT_ID "#nonexistent" - -#define EXAMPLE_ONE_X 0 -#define EXAMPLE_ONE_Y 0 -#define EXAMPLE_ONE_W 100 -#define EXAMPLE_ONE_H 200 - -#define EXAMPLE_TWO_X 0 -#define EXAMPLE_TWO_Y 200 -#define EXAMPLE_TWO_W 100 -#define EXAMPLE_TWO_H 200 - -static GdkPixbuf * -pixbuf_from_file (const char *filename, GError **error) -{ - return rsvg_pixbuf_from_file (filename, error); -} - -static GdkPixbuf * -pixbuf_from_file_at_zoom (const char *filename, GError **error) -{ - return rsvg_pixbuf_from_file_at_zoom (filename, (double) XZOOM, (double) YZOOM, error); -} - -static GdkPixbuf * -pixbuf_from_file_at_size (const char *filename, GError **error) -{ - return rsvg_pixbuf_from_file_at_size (filename, EXAMPLE_WIDTH * XZOOM, EXAMPLE_HEIGHT * YZOOM, error); -} - -static GdkPixbuf * -pixbuf_from_file_at_max_size (const char *filename, GError **error) -{ - return rsvg_pixbuf_from_file_at_max_size (filename, MAX_WIDTH, MAX_HEIGHT, error); -} - -static GdkPixbuf * -pixbuf_from_file_at_zoom_with_max (const char *filename, GError **error) -{ - return rsvg_pixbuf_from_file_at_zoom_with_max (filename, - XZOOM, YZOOM, - MAX_ZOOMED_WIDTH, MAX_ZOOMED_HEIGHT, - error); -} - -typedef GdkPixbuf *(* PixbufCreateFn) (const char *filename, GError **error); - -typedef struct { - const char *test_name; - PixbufCreateFn pixbuf_create_fn; - int expected_width; - int expected_height; -} PixbufTest; - -static const PixbufTest pixbuf_tests[] = { - { - "/api/pixbuf_from_file", - pixbuf_from_file, - EXAMPLE_WIDTH, - EXAMPLE_HEIGHT - }, - { - "/api/pixbuf_from_file_at_zoom", - pixbuf_from_file_at_zoom, - EXAMPLE_WIDTH * XZOOM, - EXAMPLE_HEIGHT * YZOOM - }, - { - "/api/pixbuf_from_file_at_size", - pixbuf_from_file_at_size, - EXAMPLE_WIDTH * XZOOM, - EXAMPLE_HEIGHT * YZOOM - }, - { - "/api/pixbuf_from_file_at_max_size", - pixbuf_from_file_at_max_size, - MAX_WIDTH, - MAX_HEIGHT - }, - { - "/api/pixbuf_from_file_at_zoom_with_max", - pixbuf_from_file_at_zoom_with_max, - MAX_ZOOMED_WIDTH, - MAX_ZOOMED_HEIGHT - }, -}; - -static void -test_pixbuf (gconstpointer data) -{ - const PixbufTest *test = data; - - char *filename = get_test_filename ("example.svg"); - GError *error = NULL; - - GdkPixbuf *pixbuf = test->pixbuf_create_fn (filename, &error); - - g_free (filename); - - g_assert_nonnull (pixbuf); - g_assert_no_error (error); - g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, test->expected_width); - g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, test->expected_height); - - g_object_unref (pixbuf); -} - -static void -pixbuf_overflow (void) -{ - char *filename = get_test_filename ("example.svg"); - GError *error = NULL; - - g_assert (!rsvg_pixbuf_from_file_at_zoom (filename, 1000000.0, 1000000.0, &error)); - g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); - g_error_free (error); - g_free (filename); -} - -static void -noops (void) -{ - /* Just to test that these functions are present in the binary, I guess */ - rsvg_init (); - rsvg_term (); - rsvg_cleanup (); -} - -static void -noops_return_null (void) -{ - RsvgHandle *handle = rsvg_handle_new (); - - g_assert_null (rsvg_handle_get_title (handle)); - g_assert_null (rsvg_handle_get_desc (handle)); - g_assert_null (rsvg_handle_get_metadata (handle)); - - g_object_unref (handle); -} - -static void -set_dpi (void) -{ - RsvgHandle *handle; - RsvgDimensionData dim; - - rsvg_set_default_dpi (100.0); - - handle = load_test_document ("dpi.svg"); - - rsvg_handle_get_dimensions (handle, &dim); - g_assert_cmpint (dim.width, ==, 100); - g_assert_cmpint (dim.height, ==, 400); - - rsvg_handle_set_dpi (handle, 200.0); - rsvg_handle_get_dimensions (handle, &dim); - g_assert_cmpint (dim.width, ==, 200); - g_assert_cmpint (dim.height, ==, 800); - g_object_unref (handle); - - handle = load_test_document ("dpi.svg"); - - rsvg_handle_set_dpi_x_y (handle, 400.0, 300.0); - rsvg_handle_get_dimensions (handle, &dim); - g_assert_cmpint (dim.width, ==, 400); - g_assert_cmpint (dim.height, ==, 1200); - g_object_unref (handle); -} - -static void -base_uri (void) -{ - RsvgHandle *handle = rsvg_handle_new (); - const char *uri; - - uri = rsvg_handle_get_base_uri (handle); - g_assert_null (uri); - - rsvg_handle_set_base_uri (handle, "file:///foo/bar.svg"); - uri = rsvg_handle_get_base_uri (handle); - - g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); - - g_object_unref (handle); -} - -static void -base_gfile (void) -{ - RsvgHandle *handle = rsvg_handle_new (); - GFile *file; - const char *uri; - - uri = rsvg_handle_get_base_uri (handle); - g_assert_null (uri); - - file = g_file_new_for_uri ("file:///foo/bar.svg"); - - rsvg_handle_set_base_gfile (handle, file); - uri = rsvg_handle_get_base_uri (handle); - - g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); - - g_object_unref (file); - g_object_unref (handle); -} - -static void -handle_write_close_free (void) -{ - char *filename = get_test_filename ("dpi.svg"); - char *data; - gsize length; - gsize i; - GError *error = NULL; - - g_assert (g_file_get_contents (filename, &data, &length, &error)); - g_free (filename); - - g_assert_nonnull (data); - g_assert_no_error (error); - - RsvgHandle *handle = rsvg_handle_new_with_flags (RSVG_HANDLE_FLAGS_NONE); - - for (i = 0; i < length; i++) { - g_assert (rsvg_handle_write (handle, (guchar *) &data[i], 1, &error)); - g_assert_no_error (error); - } - - g_assert (rsvg_handle_close (handle, &error)); - g_assert_no_error (error); - - /* Test that close() is idempotent in the happy case */ - g_assert (rsvg_handle_close (handle, &error)); - g_assert_no_error (error); - - rsvg_handle_free (handle); - g_free (data); -} - -static void -handle_new_from_file (void) -{ - char *filename = get_test_filename ("dpi.svg"); - char *uri = g_strconcat ("file://", filename, NULL); - - RsvgHandle *handle; - GError *error = NULL; - - /* rsvg_handle_new_from_file() can take both filenames and URIs */ - - handle = rsvg_handle_new_from_file (filename, &error); - g_assert_nonnull (handle); - g_assert_no_error (error); - g_object_unref (handle); - - handle = rsvg_handle_new_from_file (uri, &error); - g_assert_nonnull (handle); - g_assert_no_error (error); - g_object_unref (handle); - - g_free (filename); - g_free (uri); -} - -static void -handle_new_from_data (void) -{ - char *filename = get_test_filename ("dpi.svg"); - char *data; - gsize length; - GError *error = NULL; - - g_assert (g_file_get_contents (filename, &data, &length, &error)); - g_free (filename); - - g_assert_nonnull (data); - g_assert_no_error (error); - - RsvgHandle *handle = rsvg_handle_new_from_data ((guint8 *) data, length, &error); - g_assert_nonnull (handle); - g_assert_no_error (error); - - g_object_unref (handle); - g_free (data); -} - -static void -handle_new_from_gfile_sync (void) -{ - char *filename = get_test_filename ("dpi.svg"); - GError *error = NULL; - GFile *file = g_file_new_for_path (filename); - g_assert_nonnull (file); - - g_free (filename); - - RsvgHandle *handle = rsvg_handle_new_from_gfile_sync (file, - RSVG_HANDLE_FLAGS_NONE, - NULL, - &error); - - g_assert_nonnull (handle); - g_assert_no_error (error); - - g_object_unref (handle); - g_object_unref (file); -} - -static void -handle_new_from_stream_sync (void) -{ - char *filename = get_test_filename ("dpi.svg"); - GError *error = NULL; - GFile *file = g_file_new_for_path (filename); - g_assert_nonnull (file); - - g_free (filename); - - GFileInputStream *stream = g_file_read (file, NULL, &error); - g_assert (stream != NULL); - g_assert_no_error (error); - - RsvgHandle *handle = rsvg_handle_new_from_stream_sync (G_INPUT_STREAM (stream), - file, - RSVG_HANDLE_FLAGS_NONE, - NULL, - &error); - - g_assert_nonnull (handle); - g_assert_no_error (error); - - g_object_unref (handle); - g_object_unref (file); - g_object_unref (stream); -} - -static void -handle_read_stream_sync (void) -{ - char *filename = get_test_filename ("dpi.svg"); - GError *error = NULL; - GFile *file = g_file_new_for_path (filename); - g_assert_nonnull (file); - - g_free (filename); - - GFileInputStream *stream = g_file_read (file, NULL, &error); - g_assert_nonnull (stream); - g_assert_no_error (error); - - RsvgHandle *handle = rsvg_handle_new (); - - g_assert (rsvg_handle_read_stream_sync (handle, G_INPUT_STREAM (stream), NULL, &error)); - g_assert_no_error (error); - - g_object_unref (handle); - g_object_unref (file); - g_object_unref (stream); -} - -static void -handle_has_sub (void) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - - g_assert (rsvg_handle_has_sub (handle, EXAMPLE_ONE_ID)); - g_assert (rsvg_handle_has_sub (handle, EXAMPLE_TWO_ID)); - g_assert (!rsvg_handle_has_sub (handle, "#foo")); - - g_object_unref (handle); -} - -static void -test_get_pixbuf (gboolean sub) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - - GdkPixbuf *pixbuf; - if (sub) { - pixbuf = rsvg_handle_get_pixbuf_sub (handle, EXAMPLE_ONE_ID); - } else { - pixbuf = rsvg_handle_get_pixbuf (handle); - } - - g_assert_nonnull (pixbuf); - - /* Note that rsvg_handle_get_pixbuf_sub() creates a surface the size of the - * whole SVG, not just the size of the sub-element. - */ - g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, EXAMPLE_WIDTH); - g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, EXAMPLE_HEIGHT); - - cairo_surface_t *surface_a = test_utils_cairo_surface_from_pixbuf (pixbuf); - cairo_surface_t *surface_b = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, EXAMPLE_WIDTH, EXAMPLE_HEIGHT); - cairo_surface_t *surface_diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, EXAMPLE_WIDTH, EXAMPLE_HEIGHT); - - g_object_unref (pixbuf); - - g_assert_nonnull (surface_a); - g_assert_nonnull (surface_b); - g_assert_nonnull (surface_diff); - - cairo_t *cr = cairo_create (surface_b); - if (sub) { - g_assert (rsvg_handle_render_cairo_sub (handle, cr, EXAMPLE_ONE_ID)); - } else { - g_assert (rsvg_handle_render_cairo (handle, cr)); - } - cairo_destroy (cr); - - g_object_unref (handle); - - TestUtilsBufferDiffResult result = {0, 0}; - test_utils_compare_surfaces (surface_a, surface_b, surface_diff, &result); - - if (result.pixels_changed && result.max_diff > 0) { - g_test_fail (); - } - - cairo_surface_destroy (surface_a); - cairo_surface_destroy (surface_b); - cairo_surface_destroy (surface_diff); -} - -static void -handle_get_pixbuf (void) -{ - test_get_pixbuf (FALSE); -} - -static void -handle_get_pixbuf_sub (void) -{ - test_get_pixbuf (TRUE); -} - -static void -dimensions_and_position (void) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - RsvgDimensionData dim; - - g_assert (rsvg_handle_get_dimensions_sub (handle, &dim, EXAMPLE_TWO_ID)); - g_assert_cmpint (dim.width, ==, EXAMPLE_TWO_W); - g_assert_cmpint (dim.height, ==, EXAMPLE_TWO_H); - - RsvgPositionData pos; - g_assert (rsvg_handle_get_position_sub (handle, &pos, EXAMPLE_TWO_ID)); - g_assert_cmpint (pos.x, ==, EXAMPLE_TWO_X); - g_assert_cmpint (pos.y, ==, EXAMPLE_TWO_Y); - - g_assert_false (rsvg_handle_get_position_sub (handle, &pos, EXAMPLE_NONEXISTENT_ID)); - g_assert_false (rsvg_handle_get_dimensions_sub (handle, &dim, EXAMPLE_NONEXISTENT_ID)); - - /* Asking for "position of the whole SVG" (id=NULL) always returns (0, 0) */ - g_assert (rsvg_handle_get_position_sub (handle, &pos, NULL)); - g_assert_cmpint (pos.x, ==, 0); - g_assert_cmpint (pos.y, ==, 0); - - g_object_unref (handle); -} - -struct size_func_data -{ - gboolean called; - gboolean destroyed; - gboolean testing_size_func_calls; -}; - -static void -size_func (gint *width, gint *height, gpointer user_data) -{ - struct size_func_data *data = user_data; - - if (data->testing_size_func_calls) { - g_assert_false (data->called); - data->called = TRUE; - - g_assert_false (data->destroyed); - } - - *width = 42; - *height = 43; -} - -static void -size_func_destroy (gpointer user_data) -{ - struct size_func_data *data = user_data; - - if (data->testing_size_func_calls) { - g_assert_false (data->destroyed); - data->destroyed = TRUE; - } -} - -static void -set_size_callback (void) -{ - RsvgHandle *handle; - struct size_func_data data; - RsvgDimensionData dim; - - handle = load_test_document ("example.svg"); - - data.called = FALSE; - data.destroyed = FALSE; - data.testing_size_func_calls = TRUE; - - rsvg_handle_set_size_callback (handle, size_func, &data, size_func_destroy); - - rsvg_handle_get_dimensions (handle, &dim); - g_assert_cmpint (dim.width, ==, 42); - g_assert_cmpint (dim.height, ==, 43); - - g_object_unref (handle); - - g_assert_true (data.called); - g_assert_true (data.destroyed); -} - -static void -reset_size_callback (void) -{ - RsvgHandle *handle; - struct size_func_data data_1; - struct size_func_data data_2; - - handle = load_test_document ("example.svg"); - - data_1.called = FALSE; - data_1.destroyed = FALSE; - data_1.testing_size_func_calls = TRUE; - - rsvg_handle_set_size_callback (handle, size_func, &data_1, size_func_destroy); - - data_2.called = FALSE; - data_2.destroyed = FALSE; - data_2.testing_size_func_calls = TRUE; - - rsvg_handle_set_size_callback (handle, size_func, &data_2, size_func_destroy); - g_assert_true (data_1.destroyed); - - g_object_unref (handle); - - g_assert_true (data_2.destroyed); -} - -static void -zero_size_func (gint *width, gint *height, gpointer user_data) -{ - *width = 0; - *height = 0; -} - -static void -render_with_zero_size_callback (void) -{ - /* gdk_pixbuf_get_file_info() uses a GdkPixbufLoader, but in its - * "size-prepared" callback it saves the computed size, and then calls - * gdk_pixbuf_loader_set_size(loader, 0, 0). Presumably it does to tell - * loaders that it only wanted to know the size, but that they shouldn't - * decode or render the image to a pixbuf buffer. - * - * Librsvg used to panic when getting (0, 0) from the size_callback; this - * test is to check that there is no such crash now. Instead, librsvg - * will return a 1x1 transparent pixbuf. - */ - RsvgHandle *handle; - GdkPixbuf *pixbuf; - - handle = load_test_document ("example.svg"); - - rsvg_handle_set_size_callback (handle, zero_size_func, NULL, NULL); - - pixbuf = rsvg_handle_get_pixbuf (handle); - g_assert_nonnull (pixbuf); - g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 1); - g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 1); - - g_object_unref (pixbuf); - g_object_unref (handle); -} - -static void -pixbuf_size_func (gint *width, gint *height, gpointer user_data) -{ - *width = 420; - *height = 430; -} - -static void -get_pixbuf_with_size_callback (void) -{ - RsvgHandle *handle = rsvg_handle_new (); - - rsvg_handle_set_size_callback (handle, pixbuf_size_func, NULL, NULL); - - char *filename = get_test_filename ("example.svg"); - guchar *data = NULL; - gsize length; - GError *error = NULL; - - g_assert (g_file_get_contents (filename, (gchar **) &data, &length, &error)); - g_assert_nonnull (data); - - g_free (filename); - - g_assert (rsvg_handle_write (handle, data, length, &error)); - g_assert_no_error (error); - - g_assert (rsvg_handle_close (handle, &error)); - g_assert_no_error (error); - - GdkPixbuf *pixbuf = rsvg_handle_get_pixbuf (handle); - g_assert_nonnull (pixbuf); - g_assert_cmpint (gdk_pixbuf_get_width (pixbuf), ==, 420); - g_assert_cmpint (gdk_pixbuf_get_height (pixbuf), ==, 430); - - g_object_unref (pixbuf); - g_free (data); - g_object_unref (handle); -} - -static void -detects_cairo_context_in_error (void) -{ - if (g_test_subprocess ()) { - RsvgHandle *handle = load_test_document ("example.svg"); - - /* this is wrong; it is to simulate creating a surface and a cairo_t in error */ - cairo_surface_t *surf = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, -1, -1); - cairo_t *cr = cairo_create (surf); - /* rsvg_handle_render_cairo() should return FALSE when it gets a cr in an error state */ - g_assert_false (rsvg_handle_render_cairo (handle, cr)); - - return; - } - - g_test_trap_subprocess (NULL, 0, 0); - g_test_trap_assert_failed (); - g_test_trap_assert_stderr ("*WARNING*cannot render on a cairo_t with a failure status*"); -} - -static gboolean -matrixes_are_equal (cairo_matrix_t *a, cairo_matrix_t *b) -{ - return (a->xx == b->xx && - a->yx == b->yx && - a->xy == b->xy && - a->yy == b->yy && - a->x0 == b->x0 && - a->y0 == b->y0); -} - -static void -can_draw_to_non_image_surface (void) -{ - cairo_rectangle_t rect; - cairo_surface_t *surface; - cairo_t *cr; - - RsvgHandle *handle = load_test_document ("example.svg"); - - rect.x = 0.0; - rect.y = 0.0; - rect.width = 100.0; - rect.height = 100.0; - - /* We create a surface that is not a Cairo image surface, - * so we can test that in fact we can render to non-image surfaces. - */ - surface = cairo_recording_surface_create (CAIRO_CONTENT_COLOR_ALPHA, &rect); - cr = cairo_create (surface); - - cairo_translate (cr, 42.0, 42.0); - - cairo_matrix_t original_affine; - cairo_get_matrix (cr, &original_affine); - - g_assert (rsvg_handle_render_cairo (handle, cr)); - - cairo_matrix_t new_affine; - cairo_get_matrix (cr, &new_affine); - - g_assert (matrixes_are_equal (&original_affine, &new_affine)); - - g_object_unref (handle); - - cairo_destroy (cr); - cairo_surface_destroy (surface); -} - -/* Test that we preserve the affine transformation in the cr during a call - * to rsvg_handle_render_cairo_sub(). - */ -static void -render_cairo_sub (void) -{ - RsvgHandle *handle = load_test_document ("bug334-element-positions.svg"); - - cairo_surface_t *surf = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 200, 200); - cairo_t *cr = cairo_create (surf); - - cairo_translate (cr, 42.0, 42.0); - - cairo_matrix_t original_affine; - cairo_get_matrix (cr, &original_affine); - - g_assert (rsvg_handle_render_cairo_sub (handle, cr, "#button5-leader")); - - cairo_matrix_t new_affine; - cairo_get_matrix (cr, &new_affine); - - g_assert (matrixes_are_equal (&original_affine, &new_affine)); - - g_object_unref (handle); - cairo_destroy (cr); - cairo_surface_destroy (surf); -} - -static void -get_intrinsic_dimensions (void) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - - gboolean has_width; - RsvgLength width; - gboolean has_height; - RsvgLength height; - gboolean has_viewbox; - RsvgRectangle viewbox; - - rsvg_handle_get_intrinsic_dimensions (handle, &has_width, &width, &has_height, &height, &has_viewbox, &viewbox); - - g_assert (has_width); - g_assert_cmpfloat (width.length, ==, 100.0); - g_assert (width.unit == RSVG_UNIT_PX); - - g_assert (has_height); - g_assert_cmpfloat (height.length, ==, 400.0); - g_assert (height.unit == RSVG_UNIT_PX); - - g_assert (has_viewbox); - g_assert_cmpfloat (viewbox.x, ==, 0.0); - g_assert_cmpfloat (viewbox.y, ==, 0.0); - g_assert_cmpfloat (viewbox.width, ==, 100.0); - g_assert_cmpfloat (viewbox.height, ==, 400.0); - - g_object_unref (handle); -} - -static void -get_intrinsic_dimensions_missing_values (void) -{ - RsvgHandle *handle = load_test_document ("no-viewbox.svg"); - - gboolean has_width; - RsvgLength width; - gboolean has_height; - RsvgLength height; - gboolean has_viewbox; - RsvgRectangle viewbox; - - rsvg_handle_get_intrinsic_dimensions (handle, &has_width, &width, &has_height, &height, &has_viewbox, &viewbox); - g_assert_true (has_width); - g_assert_true (has_height); - g_assert_false (has_viewbox); - g_object_unref (handle); -} - -static void -get_intrinsic_size_in_pixels_yes (void) -{ - RsvgHandle *handle = load_test_document ("size.svg"); - gdouble width, height; - - rsvg_handle_set_dpi (handle, 96.0); - - /* Test optional parameters */ - g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, NULL, NULL)); - - /* Test the actual result */ - g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); - g_assert_cmpfloat (width, ==, 192.0); - g_assert_cmpfloat (height, ==, 288.0); - - g_object_unref (handle); -} - -static void -get_intrinsic_size_in_pixels_no (void) -{ - RsvgHandle *handle = load_test_document ("no-size.svg"); - gdouble width, height; - - rsvg_handle_set_dpi (handle, 96.0); - g_assert (!rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); - g_assert_cmpfloat (width, ==, 0.0); - g_assert_cmpfloat (height, ==, 0.0); - - g_object_unref (handle); -} - -static void -set_stylesheet (void) -{ - const char *css = "rect { fill: #00ff00; }"; - - RsvgHandle *handle = load_test_document ("stylesheet.svg"); - RsvgHandle *ref_handle = load_test_document ("stylesheet-ref.svg"); - - cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); - cairo_surface_t *reference = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); - - RsvgRectangle viewport = { 0.0, 0.0, 100.0, 100.0 }; - - cairo_t *output_cr = cairo_create (output); - cairo_t *ref_cr = cairo_create (reference); - - GError *error = NULL; - g_assert (rsvg_handle_set_stylesheet (handle, (const guint8 *) css, strlen (css), &error)); - g_assert_no_error (error); - - g_assert (rsvg_handle_render_document (handle, output_cr, &viewport, &error)); - g_assert_no_error (error); - - g_assert (rsvg_handle_render_document (ref_handle, ref_cr, &viewport, &error)); - g_assert_no_error (error); - - cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); - - TestUtilsBufferDiffResult result = {0, 0}; - test_utils_compare_surfaces (output, reference, diff, &result); - - if (result.pixels_changed && result.max_diff > 0) { - g_test_fail (); - } - - cairo_surface_destroy (diff); - cairo_destroy (ref_cr); - cairo_destroy (output_cr); - cairo_surface_destroy (reference); - cairo_surface_destroy (output); - g_object_unref (ref_handle); - g_object_unref (handle); -} - -static void -render_document (void) -{ - RsvgHandle *handle = load_test_document ("document.svg"); - - cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); - cairo_t *cr = cairo_create (output); - - RsvgRectangle viewport = { 50.0, 50.0, 50.0, 50.0 }; - - GError *error = NULL; - g_assert (rsvg_handle_render_document (handle, cr, &viewport, &error)); - g_assert_no_error (error); - - cairo_destroy (cr); - - cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); - cr = cairo_create (expected); - - cairo_translate (cr, 50.0, 50.0); - cairo_rectangle (cr, 10.0, 10.0, 30.0, 30.0); - cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 0.5); - cairo_fill (cr); - cairo_destroy (cr); - - cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 150, 150); - - TestUtilsBufferDiffResult result = {0, 0}; - test_utils_compare_surfaces (output, expected, diff, &result); - - if (result.pixels_changed && result.max_diff > 0) { - g_test_fail (); - } - - cairo_surface_destroy (diff); - cairo_surface_destroy (expected); - cairo_surface_destroy (output); - g_object_unref (handle); -} - -static void -get_geometry_for_layer (void) -{ - RsvgHandle *handle = load_test_document ("geometry.svg"); - - RsvgRectangle viewport = { 0.0, 0.0, 100.0, 400.0 }; - RsvgRectangle ink_rect; - RsvgRectangle logical_rect; - - GError *error = NULL; - - g_assert_false (rsvg_handle_get_geometry_for_layer (handle, "#nonexistent", &viewport, - &ink_rect, &logical_rect, &error)); - g_assert_nonnull (error); - - g_clear_error (&error); - - g_assert (rsvg_handle_get_geometry_for_layer (handle, "#two", &viewport, - &ink_rect, &logical_rect, &error)); - g_assert_no_error (error); - - g_assert_cmpfloat (ink_rect.x, ==, 5.0); - g_assert_cmpfloat (ink_rect.y, ==, 195.0); - g_assert_cmpfloat (ink_rect.width, ==, 90.0); - g_assert_cmpfloat (ink_rect.height, ==, 110.0); - - g_assert_cmpfloat (logical_rect.x, ==, 10.0); - g_assert_cmpfloat (logical_rect.y, ==, 200.0); - g_assert_cmpfloat (logical_rect.width, ==, 80.0); - g_assert_cmpfloat (logical_rect.height, ==, 100.0); - - g_object_unref (handle); -} - -static void -render_layer (void) -{ - RsvgHandle *handle = load_test_document ("layers.svg"); - - cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - cairo_t *cr = cairo_create (output); - - RsvgRectangle viewport = { 100.0, 100.0, 100.0, 100.0 }; - - GError *error = NULL; - - g_assert (rsvg_handle_render_layer (handle, cr, "#bar", &viewport, &error)); - g_assert_no_error (error); - - cairo_destroy (cr); - - cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - cr = cairo_create (expected); - - cairo_translate (cr, 100.0, 100.0); - cairo_rectangle (cr, 20.0, 20.0, 30.0, 30.0); - cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 1.0); - cairo_fill (cr); - cairo_destroy (cr); - - cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - - TestUtilsBufferDiffResult result = {0, 0}; - test_utils_compare_surfaces (output, expected, diff, &result); - - if (result.pixels_changed && result.max_diff > 0) { - g_test_fail (); - } - - cairo_surface_destroy (diff); - cairo_surface_destroy (expected); - cairo_surface_destroy (output); - g_object_unref (handle); -} - -static void -untransformed_element (void) -{ - RsvgHandle *handle = load_test_document ("geometry-element.svg"); - - RsvgRectangle ink_rect; - RsvgRectangle logical_rect; - - GError *error = NULL; - - g_assert (!rsvg_handle_get_geometry_for_element (handle, "#nonexistent", - &ink_rect, &logical_rect, &error)); - g_assert_nonnull (error); - - g_clear_error (&error); - - g_assert (rsvg_handle_get_geometry_for_element (handle, "#foo", - &ink_rect, &logical_rect, &error)); - g_assert_no_error (error); - - g_assert_cmpfloat (ink_rect.x, ==, 0.0); - g_assert_cmpfloat (ink_rect.y, ==, 0.0); - g_assert_cmpfloat (ink_rect.width, ==, 40.0); - g_assert_cmpfloat (ink_rect.height, ==, 50.0); - - g_assert_cmpfloat (logical_rect.x, ==, 5.0); - g_assert_cmpfloat (logical_rect.y, ==, 5.0); - g_assert_cmpfloat (logical_rect.width, ==, 30.0); - g_assert_cmpfloat (logical_rect.height, ==, 40.0); - - cairo_surface_t *output = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - cairo_t *cr = cairo_create (output); - - RsvgRectangle viewport = { 100.0, 100.0, 100.0, 100.0 }; - - g_assert (rsvg_handle_render_element (handle, cr, "#foo", &viewport, &error)); - g_assert_no_error (error); - - cairo_destroy (cr); - - cairo_surface_t *expected = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - cr = cairo_create (expected); - - cairo_translate (cr, 100.0, 100.0); - cairo_rectangle (cr, 10.0, 10.0, 60.0, 80.0); - cairo_set_source_rgba (cr, 0.0, 0.0, 1.0, 1.0); - cairo_fill_preserve (cr); - - cairo_set_line_width (cr, 20.0); - cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 1.0); - cairo_stroke (cr); - - cairo_destroy (cr); - - cairo_surface_t *diff = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 300, 300); - - TestUtilsBufferDiffResult result = {0, 0}; - test_utils_compare_surfaces (output, expected, diff, &result); - - if (result.pixels_changed && result.max_diff > 0) { - g_test_fail (); - } - - cairo_surface_destroy (diff); - cairo_surface_destroy (expected); - cairo_surface_destroy (output); - g_object_unref (handle); -} - -/* https://gitlab.gnome.org/GNOME/librsvg/issues/385 */ -static void -no_write_before_close (void) -{ - RsvgHandle *handle = rsvg_handle_new(); - GError *error = NULL; - - g_assert_false (rsvg_handle_close (handle, &error)); - g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); - g_error_free (error); - error = NULL; - - /* Test that close() is idempotent in the error case */ - g_assert (rsvg_handle_close (handle, &error)); - g_assert_no_error (error); - - g_object_unref (handle); -} - -static void -empty_write_close (void) -{ - RsvgHandle *handle = rsvg_handle_new(); - GError *error = NULL; - guchar buf = 0; - - g_assert_true (rsvg_handle_write (handle, &buf, 0, &error)); - g_assert_no_error (error); - - g_assert_false (rsvg_handle_close (handle, &error)); - g_assert_error (error, RSVG_ERROR, RSVG_ERROR_FAILED); - - g_error_free (error); - - g_object_unref (handle); -} - -static void -cannot_request_external_elements (void) -{ - /* We want to test that using one of the _sub() functions will fail - * if the element's id is within an external file. - */ - - RsvgHandle *handle = load_test_document ("example.svg"); - RsvgPositionData pos; - - g_assert_false (rsvg_handle_get_position_sub (handle, &pos, "dpi.svg#one")); - - g_object_unref (handle); -} - -static void -test_flags (RsvgHandleFlags flags) -{ - guint read_flags; - - RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, - "flags", flags, - NULL); - g_object_get (handle, "flags", &read_flags, NULL); - g_assert (read_flags == flags); - - g_object_unref (handle); -} - -static void -property_flags (void) -{ - test_flags (RSVG_HANDLE_FLAGS_NONE); - test_flags (RSVG_HANDLE_FLAG_UNLIMITED); - test_flags (RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA); - test_flags (RSVG_HANDLE_FLAG_UNLIMITED | RSVG_HANDLE_FLAG_KEEP_IMAGE_DATA); -} - -static void -property_dpi (void) -{ - RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, - "dpi-x", 42.0, - "dpi-y", 43.0, - NULL); - double x, y; - - g_object_get (handle, - "dpi-x", &x, - "dpi-y", &y, - NULL); - - g_assert_cmpfloat (x, ==, 42.0); - g_assert_cmpfloat (y, ==, 43.0); - - g_object_unref (handle); -} - -static void -property_base_uri (void) -{ - RsvgHandle *handle = g_object_new (RSVG_TYPE_HANDLE, - "base-uri", "file:///foo/bar.svg", - NULL); - char *uri; - - g_object_get (handle, - "base-uri", &uri, - NULL); - - g_assert_cmpstr (uri, ==, "file:///foo/bar.svg"); - g_free (uri); - - g_object_unref (handle); -} - -static void -property_dimensions (void) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - - int width; - int height; - double em; - double ex; - - g_object_get (handle, - "width", &width, - "height", &height, - "em", &em, - "ex", &ex, - NULL); - - g_assert_cmpint (width, ==, EXAMPLE_WIDTH); - g_assert_cmpint (height, ==, EXAMPLE_HEIGHT); - - g_assert_cmpfloat (em, ==, (double) EXAMPLE_WIDTH); - g_assert_cmpfloat (ex, ==, (double) EXAMPLE_HEIGHT); - - g_object_unref (handle); -} - -static void -property_deprecated (void) -{ - RsvgHandle *handle = load_test_document ("example.svg"); - - char *title; - char *desc; - char *metadata; - - g_object_get (handle, - "title", &title, - "desc", &desc, - "metadata", &metadata, - NULL); - - g_assert_null (title); - g_assert_null (desc); - g_assert_null (metadata); - - g_object_unref (handle); -} - -static void -return_if_fail (void) -{ - if (g_test_subprocess ()) { - RsvgHandle *handle; - - handle = rsvg_handle_new(); - g_assert_nonnull (handle); - - /* NULL is an invalid argument... */ - rsvg_handle_set_base_uri (handle, NULL); - g_object_unref (handle); - } - - g_test_trap_subprocess (NULL, 0, 0); - /* ... and here we catch that it was validated */ - g_test_trap_assert_stderr ("*rsvg_handle_set_base_uri*assertion*failed*"); -} - -static void -return_if_fail_null_check (void) -{ - if (g_test_subprocess ()) { - /* Pass NULL as an argument, incorrectly... */ - g_assert_null (rsvg_handle_get_base_uri (NULL)); - } - - g_test_trap_subprocess (NULL, 0, 0); - /* ... and here we catch that it was validated */ - g_test_trap_assert_stderr ("*rsvg_handle_get_base_uri*assertion*handle*failed*"); -} - -static void -return_if_fail_type_check (void) -{ - if (g_test_subprocess ()) { - /* Create a random GObject that is not an RsvgHandle... */ - GInputStream *stream = g_memory_input_stream_new(); - - /* Feed it to an RsvgHandle function so it will bail out */ - g_assert_null (rsvg_handle_get_base_uri ((RsvgHandle *) stream)); - - g_object_unref (stream); - } - - g_test_trap_subprocess (NULL, 0, 0); - /* ... and here we catch that it was validated */ - g_test_trap_assert_stderr ("*rsvg_handle_get_base_uri*assertion*handle*failed*"); -} - -static void -library_version_defines (void) -{ - gchar *version = g_strdup_printf ("%u.%u.%u", - LIBRSVG_MAJOR_VERSION, LIBRSVG_MINOR_VERSION, LIBRSVG_MICRO_VERSION); - g_assert_cmpstr (version, ==, LIBRSVG_VERSION); - g_free (version); -} - -static void -library_version_check (void) -{ - g_assert_true(LIBRSVG_CHECK_VERSION(1, 99, 9)); - g_assert_true(LIBRSVG_CHECK_VERSION(2, 0, 0)); - g_assert_true(LIBRSVG_CHECK_VERSION(2, 50, 7)); - g_assert_false(LIBRSVG_CHECK_VERSION(2, 99, 0)); - g_assert_false(LIBRSVG_CHECK_VERSION(3, 0, 0)); -} - -static void -library_version_constants (void) -{ - g_assert_cmpuint (rsvg_major_version, ==, LIBRSVG_MAJOR_VERSION); - g_assert_cmpuint (rsvg_minor_version, ==, LIBRSVG_MINOR_VERSION); - g_assert_cmpuint (rsvg_micro_version, ==, LIBRSVG_MICRO_VERSION); -} - -typedef struct -{ - const gchar *test_name; - const gchar *file_path; - const gchar *id; - gdouble x; - gdouble y; - gdouble width; - gdouble height; - gboolean has_position; - gboolean has_dimensions; -} DimensionsFixtureData; - -static void -test_dimensions (DimensionsFixtureData *fixture) -{ - RsvgHandle *handle; - RsvgPositionData position; - RsvgDimensionData dimension; - gchar *target_file; - GError *error = NULL; - - target_file = g_build_filename (test_utils_get_test_data_path (), - fixture->file_path, NULL); - handle = rsvg_handle_new_from_file (target_file, &error); - g_free (target_file); - g_assert_no_error (error); - - if (fixture->id) { - g_assert (rsvg_handle_has_sub (handle, fixture->id)); - g_assert (rsvg_handle_get_position_sub (handle, &position, fixture->id)); - g_assert (rsvg_handle_get_dimensions_sub (handle, &dimension, fixture->id)); - } else { - rsvg_handle_get_dimensions (handle, &dimension); - } - - if (fixture->has_position) { - g_assert_cmpint (fixture->x, ==, position.x); - g_assert_cmpint (fixture->y, ==, position.y); - } - - if (fixture->has_dimensions) { - g_assert_cmpint (fixture->width, ==, dimension.width); - g_assert_cmpint (fixture->height, ==, dimension.height); - } - - g_object_unref (handle); -} - -static DimensionsFixtureData dimensions_fixtures[] = -{ - { - "/dimensions/viewbox_only", - "dimensions/bug608102.svg", - NULL, - 0, 0, 16, 16, - FALSE, TRUE - }, - { - "/dimensions/hundred_percent_width_and_height", - "dimensions/bug612951.svg", - NULL, - 0, 0, 47, 47.14, - FALSE, TRUE - }, - { - "/dimensions/viewbox_only_2", - "dimensions/bug614018.svg", - NULL, - 0, 0, 972, 546, - FALSE, TRUE - }, - { - "/dimensions/sub/rect_no_unit", - "dimensions/sub-rect-no-unit.svg", - "#rect-no-unit", - 0, 0, 44, 45, - FALSE, TRUE - }, - { - "/dimensions/with_viewbox", - "dimensions/bug521-with-viewbox.svg", - "#foo", - 50.0, 60.0, 70.0, 80.0, - TRUE, TRUE - }, - { - "/dimensions/sub/823", - "dimensions/bug823-position-sub.svg", - "#pad_width", - 444.0, 139.0, 0.0, 0.0, - TRUE, FALSE - }, -}; - -typedef struct -{ - const char *test_name; - const char *fixture; - size_t buf_size; -} LoadingTestData; - -static void -load_n_bytes_at_a_time (gconstpointer data) -{ - const LoadingTestData *fixture_data = data; - char *filename = g_build_filename (test_utils_get_test_data_path (), fixture_data->fixture, NULL); - guchar *buf = g_new (guchar, fixture_data->buf_size); - gboolean done; - - RsvgHandle *handle; - FILE *file; - - file = fopen (filename, "rb"); - g_assert_nonnull (file); - - handle = rsvg_handle_new_with_flags (RSVG_HANDLE_FLAGS_NONE); - - done = FALSE; - - do { - size_t num_read; - - num_read = fread (buf, 1, fixture_data->buf_size, file); - - if (num_read > 0) { - g_assert_true (rsvg_handle_write (handle, buf, num_read, NULL)); - } else { - g_assert_cmpint (ferror (file), ==, 0); - - if (feof (file)) { - done = TRUE; - } - } - } while (!done); - - fclose (file); - g_free (filename); - - g_assert_true (rsvg_handle_close (handle, NULL)); - - g_object_unref (handle); - - g_free (buf); -} - -static LoadingTestData loading_tests[] = { - { "/loading/one-byte-at-a-time", "loading/gnome-cool.svg", 1 }, - { "/loading/compressed-one-byte-at-a-time", "loading/gnome-cool.svgz", 1 }, - { "/loading/compressed-two-bytes-at-a-time", "loading/gnome-cool.svgz", 2 } /* to test reading the entire gzip header */ -}; - -/* Tests for the deprecated GdkPixbuf-based API */ -static void -add_pixbuf_tests (void) -{ - int i; - - for (i = 0; i < G_N_ELEMENTS (pixbuf_tests); i++) { - g_test_add_data_func (pixbuf_tests[i].test_name, &pixbuf_tests[i], test_pixbuf); - } - - g_test_add_func ("/api/pixbuf_overflow", pixbuf_overflow); -} - -/* Tests for the C API of librsvg*/ -static void -add_api_tests (void) -{ - g_test_add_func ("/api/handle_has_correct_type_info", handle_has_correct_type_info); - g_test_add_func ("/api/flags_registration", flags_registration); - g_test_add_func ("/api/error_registration", error_registration); - g_test_add_func ("/api/noops", noops); - g_test_add_func ("/api/noops_return_null", noops_return_null); - g_test_add_func ("/api/set_dpi", set_dpi); - g_test_add_func ("/api/base_uri", base_uri); - g_test_add_func ("/api/base_gfile", base_gfile); - g_test_add_func ("/api/handle_write_close_free", handle_write_close_free); - g_test_add_func ("/api/handle_new_from_file", handle_new_from_file); - g_test_add_func ("/api/handle_new_from_data", handle_new_from_data); - g_test_add_func ("/api/handle_new_from_gfile_sync", handle_new_from_gfile_sync); - g_test_add_func ("/api/handle_new_from_stream_sync", handle_new_from_stream_sync); - g_test_add_func ("/api/handle_read_stream_sync", handle_read_stream_sync); - g_test_add_func ("/api/handle_has_sub", handle_has_sub); - g_test_add_func ("/api/handle_get_pixbuf", handle_get_pixbuf); - g_test_add_func ("/api/handle_get_pixbuf_sub", handle_get_pixbuf_sub); - g_test_add_func ("/api/dimensions_and_position", dimensions_and_position); - g_test_add_func ("/api/set_size_callback", set_size_callback); - g_test_add_func ("/api/reset_size_callback", reset_size_callback); - g_test_add_func ("/api/render_with_zero_size_callback", render_with_zero_size_callback); - g_test_add_func ("/api/get_pixbuf_with_size_callback", get_pixbuf_with_size_callback); - g_test_add_func ("/api/detects_cairo_context_in_error", detects_cairo_context_in_error); - g_test_add_func ("/api/can_draw_to_non_image_surface", can_draw_to_non_image_surface); - g_test_add_func ("/api/render_cairo_sub", render_cairo_sub); - g_test_add_func ("/api/get_intrinsic_dimensions", get_intrinsic_dimensions); - g_test_add_func ("/api/get_intrinsic_dimensions_missing_values", get_intrinsic_dimensions_missing_values); - g_test_add_func ("/api/get_intrinsic_size_in_pixels/yes", get_intrinsic_size_in_pixels_yes); - g_test_add_func ("/api/get_intrinsic_size_in_pixels/no", get_intrinsic_size_in_pixels_no); - g_test_add_func ("/api/set_stylesheet", set_stylesheet); - g_test_add_func ("/api/render_document", render_document); - g_test_add_func ("/api/get_geometry_for_layer", get_geometry_for_layer); - g_test_add_func ("/api/render_layer", render_layer); - g_test_add_func ("/api/untransformed_element", untransformed_element); - g_test_add_func ("/api/no_write_before_close", no_write_before_close); - g_test_add_func ("/api/empty_write_close", empty_write_close); - g_test_add_func ("/api/cannot_request_external_elements", cannot_request_external_elements); - g_test_add_func ("/api/property_flags", property_flags); - g_test_add_func ("/api/property_dpi", property_dpi); - g_test_add_func ("/api/property_base_uri", property_base_uri); - g_test_add_func ("/api/property_dimensions", property_dimensions); - g_test_add_func ("/api/property_deprecated", property_deprecated); - g_test_add_func ("/api/return_if_fail", return_if_fail); - g_test_add_func ("/api/return_if_fail_null_check", return_if_fail_null_check); - g_test_add_func ("/api/return_if_fail_type_check", return_if_fail_type_check); - g_test_add_func ("/api/library_version_defines", library_version_defines); - g_test_add_func ("/api/library_version_check", library_version_check); - g_test_add_func ("/api/library_version_constants", library_version_constants); -} - -/* Tests for the deprecated APIs to get geometries */ -static void -add_geometry_tests (void) -{ - int i; - - for (i = 0; i < G_N_ELEMENTS (dimensions_fixtures); i++) - g_test_add_data_func (dimensions_fixtures[i].test_name, &dimensions_fixtures[i], (void*)test_dimensions); -} - -/* Tests for the deprecated API for loading bytes at a time */ -static void -add_loading_tests (void) -{ - int i; - - for (i = 0; i < G_N_ELEMENTS (loading_tests); i++) { - g_test_add_data_func (loading_tests[i].test_name, &loading_tests[i], load_n_bytes_at_a_time); - } -} - -int -main (int argc, char **argv) -{ - g_test_init (&argc, &argv, NULL); - - test_utils_print_dependency_versions (); - - add_pixbuf_tests (); - add_api_tests (); - add_geometry_tests (); - add_loading_tests (); - - return g_test_run (); -} diff --git a/rsvg/tests/src/api.rs b/rsvg/tests/src/api.rs index 7846b2cb..88735ec0 100644 --- a/rsvg/tests/src/api.rs +++ b/rsvg/tests/src/api.rs @@ -2,8 +2,8 @@ use cairo; use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; use rsvg::{CairoRenderer, RenderingError}; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::load_svg; +use rsvg::test_utils::load_svg; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; #[test] fn has_element_with_id_works() { diff --git a/rsvg/tests/src/bugs.rs b/rsvg/tests/src/bugs.rs index 6fb75f7c..447e5159 100644 --- a/rsvg/tests/src/bugs.rs +++ b/rsvg/tests/src/bugs.rs @@ -2,8 +2,8 @@ use cairo; use matches::matches; use rsvg::{CairoRenderer, Loader, LoadingError, SvgHandle}; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::{load_svg, render_document, setup_font_map, setup_language, SurfaceSize}; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, setup_font_map, setup_language, SurfaceSize}; // https://gitlab.gnome.org/GNOME/librsvg/issues/335 #[test] diff --git a/rsvg/tests/src/cmdline/mod.rs b/rsvg/tests/src/cmdline/mod.rs deleted file mode 100644 index c694bf2e..00000000 --- a/rsvg/tests/src/cmdline/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod rsvg_convert; diff --git a/rsvg/tests/src/cmdline/rsvg_convert.rs b/rsvg/tests/src/cmdline/rsvg_convert.rs deleted file mode 100644 index 88e63ce8..00000000 --- a/rsvg/tests/src/cmdline/rsvg_convert.rs +++ /dev/null @@ -1,1079 +0,0 @@ -use crate::predicates::ends_with_pkg_version; -use crate::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

(file: P) -> Command - where - P: AsRef, - { - 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/api/dpi.svg") - .arg(option) - .assert() - .success(); - } - - fn option_yields_output(option: &str, output_pred: I) - where - I: IntoOutputPredicate

, - 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/dimensions/bug521-with-viewbox.svg") - .assert() - .success() - .stdout(file::is_png()); -} - -#[test] -fn argument_is_input_filename() { - let input = Path::new("tests/fixtures/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/api/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/api/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/cmdline/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/cmdline/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/text/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/bug521-with-viewbox.svg") - .arg("--height=200ex") - .assert() - .failure() - .stderr(contains("supported units")); -} - -#[test] -fn invalid_length() { - RsvgConvert::new_with_input("tests/fixtures/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/dimensions/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/api/dpi.svg") - .assert() - .success() - .stdout(file::is_png().with_size(96, 384)); -} - -#[test] -fn x_resolution() { - RsvgConvert::new_with_input("tests/fixtures/api/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/api/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/api/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/api/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/api/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/api/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/api/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/cmdline/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/cmdline/zero-offset-png.png")); -} - -#[test] -fn offset_png() { - RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/offset-png.png")); -} - -#[cfg(system_deps_have_cairo_pdf)] -#[test] -fn unscaled_pdf_size() { - RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/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/cmdline/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/cmdline/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/dimensions/bug521-with-viewbox.svg"); - let two = Path::new("tests/fixtures/dimensions/sub-rect-no-unit.svg"); - let three = Path::new("tests/fixtures/api/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/cmdline/dimensions-in.svg") - .arg("--format=pdf") - .arg("--page-width=210mm") - .assert() - .failure() - .stderr(contains("both").and(contains("options"))); - - RsvgConvert::new_with_input("tests/fixtures/cmdline/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/cmdline/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/api/dpi.svg") - .arg("-b") - .arg("#aabbcc") - .assert() - .success(); -} - -#[test] -fn background_color_option_invalid_color_yields_error() { - RsvgConvert::new_with_input("tests/fixtures/api/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/cmdline/gimp-wilber.svg") - .arg("--background-color=purple") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/cmdline/gimp-wilber-ref.png")); -} - -#[test] -fn stylesheet_option() { - RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") - .arg("--stylesheet=tests/fixtures/dimensions/empty.svg") - .assert() - .success(); -} - -#[test] -fn stylesheet_short_option() { - RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") - .arg("-s") - .arg("tests/fixtures/dimensions/empty.svg") - .assert() - .success(); -} - -#[test] -fn stylesheet_option_error() { - RsvgConvert::new_with_input("tests/fixtures/api/dpi.svg") - .arg("--stylesheet=foobar") - .assert() - .failure() - .stderr(starts_with("Error reading stylesheet")); -} - -#[test] -fn export_id_option() { - RsvgConvert::new_with_input("tests/fixtures/api/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/cmdline/bug601-zero-stroke-width.svg") - .arg("--export-id=foo") - .assert() - .success() - .stdout( - file::is_png().with_contents( - "tests/fixtures/cmdline/bug601-zero-stroke-width-render-only-foo.png", - ), - ); -} - -#[test] -fn export_id_short_option() { - RsvgConvert::new_with_input("tests/fixtures/api/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/api/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/api/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/api/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/api/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/render-crash/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/cmdline/accept-language.svg") - .arg("--accept-language=es-MX") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/cmdline/accept-language-es.png")); - - RsvgConvert::new_with_input("tests/fixtures/cmdline/accept-language.svg") - .arg("--accept-language=de") - .assert() - .success() - .stdout(file::is_png().with_contents("tests/fixtures/cmdline/accept-language-de.png")); -} - -#[test] -fn accept_language_fallback() { - RsvgConvert::new_with_input("tests/fixtures/cmdline/accept-language.svg") - .arg("--accept-language=fr") - .assert() - .success() - .stdout( - file::is_png().with_contents("tests/fixtures/cmdline/accept-language-fallback.png"), - ); -} - -#[test] -fn accept_language_invalid_tag() { - // underscores are not valid in BCP47 language tags - RsvgConvert::new_with_input("tests/fixtures/cmdline/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, str> { - starts_with("rsvg-convert version ").and(ends_with_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 { - 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()) -} diff --git a/rsvg/tests/src/compare_surfaces.rs b/rsvg/tests/src/compare_surfaces.rs deleted file mode 100644 index 0a170441..00000000 --- a/rsvg/tests/src/compare_surfaces.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::fmt; - -use rsvg::surface_utils::{ - iterators::Pixels, - shared_surface::{SharedImageSurface, SurfaceType}, - ImageSurfaceDataExt, Pixel, PixelOps, -}; - -use rgb::{ComponentMap, RGB}; - -pub enum BufferDiff { - DifferentSizes, - Diff(Diff), -} - -pub struct Diff { - pub num_pixels_changed: usize, - pub max_diff: u8, - pub surface: SharedImageSurface, -} - -impl fmt::Display for BufferDiff { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - BufferDiff::DifferentSizes => write!(f, "different sizes"), - BufferDiff::Diff(diff) => diff.fmt(f), - } - } -} - -impl fmt::Display for Diff { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} pixels are different, with a maximum difference of {}", - self.num_pixels_changed, self.max_diff - ) - } -} - -#[inline] -fn emphasize(p: &Pixel) -> Pixel { - let emphasize_component = |c| { - // emphasize - let mut c = c as u32 * 4; - // make sure it's visible - if c > 0 { - c += 128; - } - c.min(255) as u8 - }; - p.map(emphasize_component) -} - -pub fn compare_surfaces( - surf_a: &SharedImageSurface, - surf_b: &SharedImageSurface, -) -> Result { - let a_width = surf_a.width(); - let a_height = surf_a.height(); - - let b_width = surf_b.width(); - let b_height = surf_b.height(); - - if a_width != b_width || a_height != b_height { - return Ok(BufferDiff::DifferentSizes); - } - - let mut surf_diff = cairo::ImageSurface::create(cairo::Format::ARgb32, a_width, a_height)?; - let diff_stride = surf_diff.stride() as usize; - - let mut num_pixels_changed = 0; - let mut max_diff = 0; - - let black = Pixel::default().alpha(255); - - { - let mut diff_data = surf_diff.data().unwrap(); - - for ((xa, ya, pixel_a), (_, _, pixel_b)) in Pixels::new(surf_a).zip(Pixels::new(surf_b)) { - let dest = if pixel_a != pixel_b { - num_pixels_changed += 1; - - let pixel_diff = pixel_a.diff(&pixel_b); - - max_diff = pixel_diff.iter().fold(max_diff, |acc, c| acc.max(c)); - - let pixel_diff = emphasize(&pixel_diff); - - if pixel_diff.rgb() == RGB::default() { - // alpha only difference; convert alpha to gray - let a = pixel_diff.a; - pixel_diff.map_rgb(|_| a) - } else { - pixel_diff.alpha(255) - } - } else { - black - }; - - diff_data.set_pixel(diff_stride, dest, xa, ya); - } - } - - let surface = SharedImageSurface::wrap(surf_diff, SurfaceType::SRgb)?; - - Ok(BufferDiff::Diff(Diff { - num_pixels_changed, - max_diff, - surface, - })) -} diff --git a/rsvg/tests/src/filters.rs b/rsvg/tests/src/filters.rs index 9517e651..28fc867c 100644 --- a/rsvg/tests/src/filters.rs +++ b/rsvg/tests/src/filters.rs @@ -1,8 +1,8 @@ use cairo; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::{load_svg, render_document, SurfaceSize}; -use crate::{test_compare_render_output, test_svg_reference}; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, SurfaceSize}; +use rsvg::{test_compare_render_output, test_svg_reference}; #[test] fn invalid_filter_reference_cancels_filter_chain() { diff --git a/rsvg/tests/src/intrinsic_dimensions.rs b/rsvg/tests/src/intrinsic_dimensions.rs index 4ca31f93..fdbec145 100644 --- a/rsvg/tests/src/intrinsic_dimensions.rs +++ b/rsvg/tests/src/intrinsic_dimensions.rs @@ -2,8 +2,8 @@ use cairo; use rsvg::{CairoRenderer, IntrinsicDimensions, Length, LengthUnit, RenderingError}; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::{load_svg, render_document, SurfaceSize}; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, SurfaceSize}; #[test] fn no_intrinsic_dimensions() { diff --git a/rsvg/tests/src/legacy_sizing.rs b/rsvg/tests/src/legacy_sizing.rs deleted file mode 100644 index 78f2a13f..00000000 --- a/rsvg/tests/src/legacy_sizing.rs +++ /dev/null @@ -1,189 +0,0 @@ -use cairo; - -use rsvg::{rsvg_convert_only::LegacySize, CairoRenderer}; - -use crate::utils::load_svg; - -#[test] -fn just_viewbox_uses_viewbox_size() { - let svg = load_svg( - br#" - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), - cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), - ) - ); -} - -#[test] -fn no_intrinsic_size_uses_element_geometries() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), - cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), - ) - ); -} - -#[test] -fn hundred_percent_width_height_uses_viewbox() { - let svg = load_svg( - br#" - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), - cairo::Rectangle::new(0.0, 0.0, 100.0, 200.0), - ) - ); -} - -#[test] -fn hundred_percent_width_height_no_viewbox_uses_element_geometries() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), - cairo::Rectangle::new(10.0, 20.0, 30.0, 40.0), - ) - ); -} - -#[test] -fn width_and_viewbox_preserves_aspect_ratio() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - // Per the spec, the height property should default to 100%, so the above would end up - // like - // - // If that were being *rendered* to a viewport, no problem, just use units horizontally - // and 100% of the viewport vertically. - // - // But we are being asked to compute the SVG's natural geometry, so the best we can do - // is to take the aspect ratio defined by the viewBox and apply it to the width. - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), - cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), - ) - ); -} - -#[test] -fn height_and_viewbox_preserves_aspect_ratio() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - // See the comment above in width_and_viewbox_preserves_aspect_ratio(); this - // is equivalent but for the height. - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), - cairo::Rectangle::new(0.0, 0.0, 60.0, 80.0), - ) - ); -} - -#[test] -fn zero_width_vbox() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0), - cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0) - ) - ); -} - -#[test] -fn zero_height_vbox() { - let svg = load_svg( - br#" - - - -"#, - ) - .unwrap(); - - assert_eq!( - CairoRenderer::new(&svg) - .legacy_layer_geometry(None) - .unwrap(), - ( - cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0), - cairo::Rectangle::new(0.0, 0.0, 0.0, 0.0) - ) - ); -} diff --git a/rsvg/tests/src/main.rs b/rsvg/tests/src/main.rs deleted file mode 100644 index 467cbb47..00000000 --- a/rsvg/tests/src/main.rs +++ /dev/null @@ -1,60 +0,0 @@ -#[cfg(test)] -mod api; - -#[cfg(test)] -mod bugs; - -#[cfg(test)] -mod cmdline; - -#[cfg(test)] -mod compare_surfaces; - -#[cfg(test)] -mod errors; - -#[cfg(test)] -mod filters; - -#[cfg(test)] -mod geometries; - -#[cfg(test)] -mod intrinsic_dimensions; - -#[cfg(test)] -mod legacy_sizing; - -#[cfg(test)] -mod loading_crash; - -#[cfg(test)] -mod predicates; - -#[cfg(test)] -mod primitive_geometries; - -#[cfg(test)] -mod primitives; - -#[cfg(test)] -mod reference; - -#[cfg(test)] -mod reference_utils; - -#[cfg(test)] -mod render_crash; - -#[cfg(test)] -mod shapes; - -#[cfg(test)] -mod text; - -#[cfg(test)] -mod utils; - -fn main() { - println!("Use 'cargo test' to run the tests."); -} diff --git a/rsvg/tests/src/predicates/file.rs b/rsvg/tests/src/predicates/file.rs deleted file mode 100644 index 997b3690..00000000 --- a/rsvg/tests/src/predicates/file.rs +++ /dev/null @@ -1,28 +0,0 @@ -use predicates::prelude::*; -use predicates::str::StartsWithPredicate; - -use crate::predicates::pdf::PdfPredicate; -use crate::predicates::png::PngPredicate; -use crate::predicates::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/tests/src/predicates/mod.rs b/rsvg/tests/src/predicates/mod.rs deleted file mode 100644 index e14061f0..00000000 --- a/rsvg/tests/src/predicates/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod file; -mod pdf; -mod png; -mod svg; - -use predicates::str; - -pub fn ends_with_pkg_version() -> str::EndsWithPredicate { - str::ends_with(env!("CARGO_PKG_VERSION")) -} diff --git a/rsvg/tests/src/predicates/pdf.rs b/rsvg/tests/src/predicates/pdf.rs deleted file mode 100644 index f7872d71..00000000 --- a/rsvg/tests/src/predicates/pdf.rs +++ /dev/null @@ -1,358 +0,0 @@ -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 { - DetailPredicate:: { - 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 { - DetailPredicate:: { - 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) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::CreationDate(when), - } - } - - pub fn with_link(self: Self, link: &str) -> DetailPredicate { - DetailPredicate:: { - p: self, - d: Detail::Link(link.to_string()), - } - } - - pub fn with_text(self: Self, text: &str) -> DetailPredicate { - DetailPredicate:: { - 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> { - 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 { - p: PdfPredicate, - d: Detail, -} - -#[derive(Debug)] -enum Detail { - PageCount(usize), - PageSize(Dimensions, usize), - CreationDate(DateTime), - 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) -> lopdf::Result { - 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; - fn get_creation_date(&self) -> Option>; - 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 { - 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> { - 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; -} - -impl ObjExt for lopdf::Object { - fn as_float(&self) -> lopdf::Result { - 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 { - 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> { - 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 { - 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> { - 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 { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for DetailPredicate { - 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 { - // 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/tests/src/predicates/png.rs b/rsvg/tests/src/predicates/png.rs deleted file mode 100644 index 452e756e..00000000 --- a/rsvg/tests/src/predicates/png.rs +++ /dev/null @@ -1,193 +0,0 @@ -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 crate::compare_surfaces::BufferDiff; -use crate::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 { - SizePredicate:: { p: self, w, h } - } - - pub fn with_contents>(self: Self, reference: P) -> ReferencePredicate { - let mut path = PathBuf::new(); - path.push(reference); - ReferencePredicate:: { 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> { - 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 { - p: PngPredicate, - w: u32, - h: u32, -} - -impl SizePredicate { - 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> { - 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 { - 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> { - 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 { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for SizePredicate { - 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 { - p: PngPredicate, - path: PathBuf, -} - -impl ReferencePredicate { - fn diff_acceptable(diff: &BufferDiff) -> bool { - match diff { - BufferDiff::DifferentSizes => false, - BufferDiff::Diff(diff) => !diff.inacceptable(), - } - } - - fn diff_surface(&self, surface: &SharedImageSurface) -> Option { - 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> { - 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 { - 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> { - 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 { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for ReferencePredicate { - 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/tests/src/predicates/svg.rs b/rsvg/tests/src/predicates/svg.rs deleted file mode 100644 index cd6071b7..00000000 --- a/rsvg/tests/src/predicates/svg.rs +++ /dev/null @@ -1,179 +0,0 @@ -use float_cmp::approx_eq; -use gio::MemoryInputStream; -use glib::Bytes; -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 { - DetailPredicate:: { - p: self, - d: Detail::Size(Dimensions { - w: width, - h: height, - }), - } - } -} - -fn svg_from_bytes(data: &[u8]) -> Result { - 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> { - 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 { - 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; -} - -impl DetailPredicate { - 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> { - 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 { - 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> { - 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 { - fn children<'a>(&'a self) -> Box> + 'a> { - let params = vec![Child::new("predicate", &self.p)]; - Box::new(params.into_iter()) - } -} - -impl fmt::Display for DetailPredicate { - 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/tests/src/primitives.rs b/rsvg/tests/src/primitives.rs index b12906f4..b24c1a6a 100644 --- a/rsvg/tests/src/primitives.rs +++ b/rsvg/tests/src/primitives.rs @@ -1,8 +1,8 @@ use cairo; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::test_compare_render_output; -use crate::utils::{load_svg, render_document, SurfaceSize}; +use rsvg::test_compare_render_output; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, SurfaceSize}; #[test] fn simple_opacity_with_transform() { diff --git a/rsvg/tests/src/reference.rs b/rsvg/tests/src/reference.rs index 37f1a562..37eed1b1 100644 --- a/rsvg/tests/src/reference.rs +++ b/rsvg/tests/src/reference.rs @@ -12,9 +12,9 @@ use rsvg::{ CairoRenderer, IntrinsicDimensions, Length, Loader, }; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::{load_svg, render_document, setup_font_map, setup_language, SurfaceSize}; -use crate::{test_compare_render_output, test_svg_reference}; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, setup_font_map, setup_language, SurfaceSize}; +use rsvg::{test_compare_render_output, test_svg_reference}; use std::path::{Path, PathBuf}; diff --git a/rsvg/tests/src/reference_utils.rs b/rsvg/tests/src/reference_utils.rs deleted file mode 100644 index 8232d4c9..00000000 --- a/rsvg/tests/src/reference_utils.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! Utilities for the reference image test suite. -//! -//! This module has utility functions that are used in the test suite -//! to compare rendered surfaces to reference images. - -use cairo; - -use std::convert::TryFrom; -use std::env; -use std::fs::{self, File}; -use std::io::{BufReader, Read}; -use std::path::{Path, PathBuf}; -use std::sync::Once; - -use rsvg::surface_utils::shared_surface::{SharedImageSurface, SurfaceType}; - -use crate::compare_surfaces::{compare_surfaces, BufferDiff, Diff}; - -pub struct Reference(SharedImageSurface); - -impl Reference { - pub fn from_png

(path: P) -> Result - where - P: AsRef, - { - let file = File::open(path).map_err(|e| cairo::IoError::Io(e))?; - let mut reader = BufReader::new(file); - let surface = surface_from_png(&mut reader)?; - Self::from_surface(surface) - } - - pub fn from_surface(surface: cairo::ImageSurface) -> Result { - let shared = SharedImageSurface::wrap(surface, SurfaceType::SRgb)?; - Ok(Self(shared)) - } -} - -pub trait Compare { - fn compare(self, surface: &SharedImageSurface) -> Result; -} - -impl Compare for &Reference { - fn compare(self, surface: &SharedImageSurface) -> Result { - compare_surfaces(&self.0, surface).map_err(cairo::IoError::from) - } -} - -impl Compare for Result { - fn compare(self, surface: &SharedImageSurface) -> Result { - self.map(|reference| reference.compare(surface)) - .and_then(std::convert::identity) - } -} - -pub trait Evaluate { - fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str); -} - -impl Evaluate for BufferDiff { - /// Evaluates a BufferDiff and panics if there are relevant differences - /// - /// The `output_base_name` is used to write test results if the - /// surfaces are different. If this is `foo`, this will write - /// `foo-out.png` with the `output_surf` and `foo-diff.png` with a - /// visual diff between `output_surf` and the `Reference` that this - /// diff was created from. - /// - /// # Panics - /// - /// Will panic if the surfaces are too different to be acceptable. - fn evaluate(&self, output_surf: &SharedImageSurface, output_base_name: &str) { - match self { - BufferDiff::DifferentSizes => unreachable!("surfaces should be of the same size"), - - BufferDiff::Diff(diff) => { - if diff.distinguishable() { - println!( - "{}: {} pixels changed with maximum difference of {}", - output_base_name, diff.num_pixels_changed, diff.max_diff, - ); - - write_to_file(output_surf, output_base_name, "out"); - write_to_file(&diff.surface, output_base_name, "diff"); - - if diff.inacceptable() { - panic!("surfaces are too different"); - } - } - } - } - } -} - -impl Evaluate for Result { - fn evaluate(&self, output_surface: &SharedImageSurface, output_base_name: &str) { - self.as_ref() - .map(|diff| diff.evaluate(output_surface, output_base_name)) - .unwrap(); - } -} - -fn write_to_file(input: &SharedImageSurface, output_base_name: &str, suffix: &str) { - let path = output_dir().join(&format!("{}-{}.png", output_base_name, suffix)); - println!("{}: {}", suffix, path.to_string_lossy()); - let mut output_file = File::create(path).unwrap(); - input - .clone() - .into_image_surface() - .unwrap() - .write_to_png(&mut output_file) - .unwrap(); -} - -/// Creates a directory for test output and returns its path. -/// -/// The location for the output directory is taken from the `TESTS_OUTPUT_DIR` environment -/// variable if that is set. Otherwise std::env::temp_dir() will be used, which is -/// a platform dependent location for temporary files. -/// -/// # Panics -/// -/// Will panic if the output directory can not be created. -pub fn output_dir() -> PathBuf { - let tempdir = || { - let mut path = env::temp_dir(); - path.push("rsvg-test-output"); - path - }; - let path = env::var_os("TESTS_OUTPUT_DIR").map_or_else(tempdir, PathBuf::from); - - fs::create_dir_all(&path).expect("could not create output directory for tests"); - - path -} - -fn tolerable_difference() -> u8 { - static mut TOLERANCE: u8 = 8; - - static ONCE: Once = Once::new(); - ONCE.call_once(|| unsafe { - if let Ok(str) = env::var("RSVG_TEST_TOLERANCE") { - let value: usize = str - .parse() - .expect("Can not parse RSVG_TEST_TOLERANCE as a number"); - TOLERANCE = - u8::try_from(value).expect("RSVG_TEST_TOLERANCE should be between 0 and 255"); - } - }); - - unsafe { TOLERANCE } -} - -pub trait Deviation { - fn distinguishable(&self) -> bool; - fn inacceptable(&self) -> bool; -} - -impl Deviation for Diff { - fn distinguishable(&self) -> bool { - self.max_diff > 2 - } - - fn inacceptable(&self) -> bool { - self.max_diff > tolerable_difference() - } -} - -/// Creates a cairo::ImageSurface from a stream of PNG data. -/// -/// The surface is converted to ARGB if needed. Use this helper function with `Reference`. -pub fn surface_from_png(stream: &mut R) -> Result -where - R: Read, -{ - let png = cairo::ImageSurface::create_from_png(stream)?; - let argb = cairo::ImageSurface::create(cairo::Format::ARgb32, png.width(), png.height())?; - { - // convert to ARGB; the PNG may come as Rgb24 - let cr = cairo::Context::new(&argb).expect("Failed to create a cairo context"); - cr.set_source_surface(&png, 0.0, 0.0).unwrap(); - cr.paint().unwrap(); - } - Ok(argb) -} - -/// Macro test that compares render outputs -/// -/// Takes in SurfaceSize width and height, setting the cairo surface -#[macro_export] -macro_rules! test_compare_render_output { - ($test_name:ident, $width:expr, $height:expr, $test:expr, $reference:expr $(,)?) => { - #[test] - fn $test_name() { - crate::utils::setup_font_map(); - - let sx: i32 = $width; - let sy: i32 = $height; - let svg = load_svg($test).unwrap(); - let output_surf = render_document( - &svg, - SurfaceSize(sx, sy), - |_| (), - cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)), - ) - .unwrap(); - - let reference = load_svg($reference).unwrap(); - let reference_surf = render_document( - &reference, - SurfaceSize(sx, sy), - |_| (), - cairo::Rectangle::new(0.0, 0.0, f64::from(sx), f64::from(sy)), - ) - .unwrap(); - - Reference::from_surface(reference_surf.into_image_surface().unwrap()) - .compare(&output_surf) - .evaluate(&output_surf, stringify!($test_name)); - } - }; -} - -/// Render two SVG files and compare them. -/// -/// This is used to implement reference tests, or reftests. Use it like this: -/// -/// ```ignore -/// test_svg_reference!(test_name, "tests/fixtures/blah/foo.svg", "tests/fixtures/blah/foo-ref.svg"); -/// ``` -/// -/// This will ensure that `foo.svg` and `foo-ref.svg` have exactly the same intrinsic dimensions, -/// and that they produce the same rendered output. -#[macro_export] -macro_rules! test_svg_reference { - ($test_name:ident, $test_filename:expr, $reference_filename:expr) => { - #[test] - fn $test_name() { - use crate::reference_utils::{Compare, Evaluate, Reference}; - use crate::utils::{render_document, setup_font_map, SurfaceSize}; - use cairo; - use rsvg::{CairoRenderer, Loader}; - - setup_font_map(); - - let svg = Loader::new() - .read_path($test_filename) - .expect("reading SVG test file"); - let reference = Loader::new() - .read_path($reference_filename) - .expect("reading reference file"); - - let svg_renderer = CairoRenderer::new(&svg); - let ref_renderer = CairoRenderer::new(&reference); - - let svg_dim = svg_renderer.intrinsic_dimensions(); - let ref_dim = ref_renderer.intrinsic_dimensions(); - - assert_eq!( - svg_dim, ref_dim, - "sizes of SVG document and reference file are different" - ); - - let pixels = svg_renderer - .intrinsic_size_in_pixels() - .unwrap_or((100.0, 100.0)); - - let output_surf = render_document( - &svg, - SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), - |_| (), - cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), - ) - .unwrap(); - - let reference_surf = render_document( - &reference, - SurfaceSize(pixels.0.ceil() as i32, pixels.1.ceil() as i32), - |_| (), - cairo::Rectangle::new(0.0, 0.0, pixels.0, pixels.1), - ) - .unwrap(); - - Reference::from_surface(reference_surf.into_image_surface().unwrap()) - .compare(&output_surf) - .evaluate(&output_surf, stringify!($test_name)); - } - }; -} diff --git a/rsvg/tests/src/shapes.rs b/rsvg/tests/src/shapes.rs index 8545b79d..23a75d49 100644 --- a/rsvg/tests/src/shapes.rs +++ b/rsvg/tests/src/shapes.rs @@ -1,4 +1,4 @@ -use crate::test_svg_reference; +use rsvg::test_svg_reference; test_svg_reference!( ellipse_auto_rx_ry, diff --git a/rsvg/tests/src/text.rs b/rsvg/tests/src/text.rs index 257d7f0c..05c3de3e 100644 --- a/rsvg/tests/src/text.rs +++ b/rsvg/tests/src/text.rs @@ -2,9 +2,9 @@ use cairo; use float_cmp::approx_eq; use rsvg::{CairoRenderer, Loader}; -use crate::reference_utils::{Compare, Evaluate, Reference}; -use crate::utils::{load_svg, render_document, setup_font_map, SurfaceSize}; -use crate::{test_compare_render_output, test_svg_reference}; +use rsvg::test_utils::reference_utils::{Compare, Evaluate, Reference}; +use rsvg::test_utils::{load_svg, render_document, setup_font_map, SurfaceSize}; +use rsvg::{test_compare_render_output, test_svg_reference}; // From https://www.w3.org/Style/CSS/Test/Fonts/Ahem/ // diff --git a/rsvg/tests/src/utils.rs b/rsvg/tests/src/utils.rs deleted file mode 100644 index 158fc77e..00000000 --- a/rsvg/tests/src/utils.rs +++ /dev/null @@ -1,119 +0,0 @@ -#![cfg(test)] -#![allow(unused)] - -use cairo; -use gio; -use glib; -use glib::translate::*; -use libc; -use std::env; -use std::ffi::CString; -use std::sync::Once; - -use rsvg::{ - surface_utils::shared_surface::{SharedImageSurface, SurfaceType}, - CairoRenderer, Loader, LoadingError, RenderingError, SvgHandle, -}; - -pub fn load_svg(input: &'static [u8]) -> Result { - let bytes = glib::Bytes::from_static(input); - let stream = gio::MemoryInputStream::from_bytes(&bytes); - - Loader::new().read_stream(&stream, None::<&gio::File>, None::<&gio::Cancellable>) -} - -#[derive(Copy, Clone)] -pub struct SurfaceSize(pub i32, pub i32); - -pub fn render_document( - svg: &SvgHandle, - surface_size: SurfaceSize, - cr_transform: F, - viewport: cairo::Rectangle, -) -> Result { - let renderer = CairoRenderer::new(svg); - - let SurfaceSize(width, height) = surface_size; - - let output = cairo::ImageSurface::create(cairo::Format::ARgb32, width, height).unwrap(); - - let res = { - let cr = cairo::Context::new(&output).expect("Failed to create a cairo context"); - cr_transform(&cr); - Ok(renderer.render_document(&cr, &viewport)?) - }; - - res.and_then(|_| Ok(SharedImageSurface::wrap(output, SurfaceType::SRgb)?)) -} - -#[cfg(system_deps_have_pangoft2)] -mod pango_ft2 { - use super::*; - use glib::prelude::*; - use pangocairo::FontMap; - - extern "C" { - // pango_fc_font_map_set_config (PangoFcFontMap *fcfontmap, - // FcConfig *fcconfig); - // This is not bound in gtk-rs, and PangoFcFontMap is not even exposed, so we'll bind it by hand. - fn pango_fc_font_map_set_config( - font_map: *mut libc::c_void, - config: *mut fontconfig_sys::FcConfig, - ); - } - - pub unsafe fn load_test_fonts() { - let font_paths = [ - "tests/resources/Ahem.ttf", - "tests/resources/NotoSansHebrew-Regular.ttf", - "tests/resources/Roboto-Regular.ttf", - "tests/resources/Roboto-Italic.ttf", - "tests/resources/Roboto-Bold.ttf", - "tests/resources/Roboto-BoldItalic.ttf", - ]; - - let config = fontconfig_sys::FcConfigCreate(); - if fontconfig_sys::FcConfigSetCurrent(config) == 0 { - panic!("Could not set a fontconfig configuration"); - } - - for path in &font_paths { - let path_cstring = CString::new(*path).unwrap(); - - if fontconfig_sys::FcConfigAppFontAddFile(config, path_cstring.as_ptr() as *const _) - == 0 - { - panic!("Could not load font file {} for tests; aborting", path,); - } - } - - let font_map = FontMap::for_font_type(cairo::FontType::FontTypeFt).unwrap(); - let raw_font_map: *mut pango::ffi::PangoFontMap = font_map.to_glib_none().0; - - pango_fc_font_map_set_config(raw_font_map as *mut _, config); - fontconfig_sys::FcConfigDestroy(config); - - FontMap::set_default(Some(&font_map.downcast::().unwrap())); - } -} - -#[cfg(system_deps_have_pangoft2)] -pub fn setup_font_map() { - unsafe { - self::pango_ft2::load_test_fonts(); - } -} - -#[cfg(not(system_deps_have_pangoft2))] -pub fn setup_font_map() {} - -pub fn setup_language() { - static ONCE: Once = Once::new(); - - ONCE.call_once(|| { - // For systemLanguage attribute tests. - // The trailing ":" is intentional to test gitlab#425. - env::set_var("LANGUAGE", "de:en_US:en:"); - env::set_var("LC_ALL", "de:en_US:en:"); - }); -} diff --git a/rsvg/tests/test-utils.c b/rsvg/tests/test-utils.c deleted file mode 100644 index 128611f4..00000000 --- a/rsvg/tests/test-utils.c +++ /dev/null @@ -1,257 +0,0 @@ -/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ -/* vim: set sw=4 sts=4 ts=4 expandtab: */ - -#include "config.h" -#include "test-utils.h" - -#include -#include -#include - -#if !PANGO_VERSION_CHECK (1, 44, 0) -# include -#endif - -#include -#include FT_FREETYPE_H - - -/* Compare two buffers, returning the number of pixels that are - * different and the maximum difference of any single color channel in - * result_ret. - * - * This function should be rewritten to compare all formats supported by - * cairo_format_t instead of taking a mask as a parameter. - */ -static void -buffer_diff_core (unsigned char *_buf_a, - unsigned char *_buf_b, - unsigned char *_buf_diff, - int width, - int height, - int stride, - guint32 mask, - TestUtilsBufferDiffResult *result_ret) -{ - int x, y; - guint32 *row_a, *row_b, *row; - TestUtilsBufferDiffResult result = {0, 0}; - guint32 *buf_a = (guint32 *) _buf_a; - guint32 *buf_b = (guint32 *) _buf_b; - guint32 *buf_diff = (guint32 *) _buf_diff; - - stride /= sizeof(guint32); - for (y = 0; y < height; y++) - { - row_a = buf_a + y * stride; - row_b = buf_b + y * stride; - row = buf_diff + y * stride; - for (x = 0; x < width; x++) - { - /* check if the pixels are the same */ - if ((row_a[x] & mask) != (row_b[x] & mask)) { - int channel; - guint32 diff_pixel = 0; - - /* calculate a difference value for all 4 channels */ - for (channel = 0; channel < 4; channel++) { - int value_a = (row_a[x] >> (channel*8)) & 0xff; - int value_b = (row_b[x] >> (channel*8)) & 0xff; - unsigned int diff; - diff = abs (value_a - value_b); - if (diff > result.max_diff) - result.max_diff = diff; - diff *= 4; /* emphasize */ - if (diff) - diff += 128; /* make sure it's visible */ - if (diff > 255) - diff = 255; - diff_pixel |= diff << (channel*8); - } - - result.pixels_changed++; - if ((diff_pixel & 0x00ffffff) == 0) { - /* alpha only difference, convert to luminance */ - guint8 alpha = diff_pixel >> 24; - diff_pixel = alpha * 0x010101; - } - row[x] = diff_pixel; - } else { - row[x] = 0; - } - row[x] |= 0xff000000; /* Set ALPHA to 100% (opaque) */ - } - } - - *result_ret = result; -} - -void -test_utils_compare_surfaces (cairo_surface_t *surface_a, - cairo_surface_t *surface_b, - cairo_surface_t *surface_diff, - TestUtilsBufferDiffResult *result) -{ - /* Here, we run cairo's old buffer_diff algorithm which looks for - * pixel-perfect images. - */ - buffer_diff_core (cairo_image_surface_get_data (surface_a), - cairo_image_surface_get_data (surface_b), - cairo_image_surface_get_data (surface_diff), - cairo_image_surface_get_width (surface_a), - cairo_image_surface_get_height (surface_a), - cairo_image_surface_get_stride (surface_a), - 0xffffffff, - result); - if (result->pixels_changed == 0) - return; - - g_test_message ("%d pixels differ (with maximum difference of %d) from reference image\n", - result->pixels_changed, result->max_diff); -} - -/* Copied from gdk_cairo_surface_paint_pixbuf in gdkcairo.c, - * we do not want to depend on GDK - */ -static void -test_utils_cairo_surface_paint_pixbuf (cairo_surface_t *surface, - const GdkPixbuf *pixbuf) -{ - gint width, height; - guchar *gdk_pixels, *cairo_pixels; - int gdk_rowstride, cairo_stride; - int n_channels; - int j; - - if (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS) - return; - - /* This function can't just copy any pixbuf to any surface, be - * sure to read the invariants here before calling it */ - - g_assert (cairo_surface_get_type (surface) == CAIRO_SURFACE_TYPE_IMAGE); - g_assert (cairo_image_surface_get_format (surface) == CAIRO_FORMAT_RGB24 || - cairo_image_surface_get_format (surface) == CAIRO_FORMAT_ARGB32); - g_assert (cairo_image_surface_get_width (surface) == gdk_pixbuf_get_width (pixbuf)); - g_assert (cairo_image_surface_get_height (surface) == gdk_pixbuf_get_height (pixbuf)); - - cairo_surface_flush (surface); - - width = gdk_pixbuf_get_width (pixbuf); - height = gdk_pixbuf_get_height (pixbuf); - gdk_pixels = gdk_pixbuf_get_pixels (pixbuf); - gdk_rowstride = gdk_pixbuf_get_rowstride (pixbuf); - n_channels = gdk_pixbuf_get_n_channels (pixbuf); - cairo_stride = cairo_image_surface_get_stride (surface); - cairo_pixels = cairo_image_surface_get_data (surface); - - for (j = height; j; j--) - { - guchar *p = gdk_pixels; - guchar *q = cairo_pixels; - - if (n_channels == 3) - { - guchar *end = p + 3 * width; - - while (p < end) - { -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - q[0] = p[2]; - q[1] = p[1]; - q[2] = p[0]; -#else - q[1] = p[0]; - q[2] = p[1]; - q[3] = p[2]; -#endif - p += 3; - q += 4; - } - } - else - { - guchar *end = p + 4 * width; - guint t1,t2,t3; - -#define MULT(d,c,a,t) G_STMT_START { t = c * a + 0x80; d = ((t >> 8) + t) >> 8; } G_STMT_END - - while (p < end) - { -#if G_BYTE_ORDER == G_LITTLE_ENDIAN - MULT(q[0], p[2], p[3], t1); - MULT(q[1], p[1], p[3], t2); - MULT(q[2], p[0], p[3], t3); - q[3] = p[3]; -#else - q[0] = p[3]; - MULT(q[1], p[0], p[3], t1); - MULT(q[2], p[1], p[3], t2); - MULT(q[3], p[2], p[3], t3); -#endif - - p += 4; - q += 4; - } - -#undef MULT - } - - gdk_pixels += gdk_rowstride; - cairo_pixels += cairo_stride; - } - - cairo_surface_mark_dirty (surface); -} - -cairo_surface_t * -test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf) -{ - cairo_surface_t *surface; - - g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL); - g_return_val_if_fail (gdk_pixbuf_get_n_channels (pixbuf) == 4, NULL); - - surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, - gdk_pixbuf_get_width (pixbuf), - gdk_pixbuf_get_height (pixbuf)); - - test_utils_cairo_surface_paint_pixbuf (surface, pixbuf); - - return surface; -} - -static gchar *data_path = NULL; - -const gchar * -test_utils_get_test_data_path (void) -{ - if (data_path) - return data_path; - - data_path = g_test_build_filename (G_TEST_DIST, "fixtures", NULL); - - return data_path; -} - -void -test_utils_print_dependency_versions (void) -{ - FT_Library ft_lib; - FT_Int ft_major = 0; - FT_Int ft_minor = 0; - FT_Int ft_patch = 0; - - FT_Init_FreeType (&ft_lib); - FT_Library_Version (ft_lib, &ft_major, &ft_minor, &ft_patch); - FT_Done_FreeType (ft_lib); - - g_test_message ("Cairo version: %s", cairo_version_string ()); - g_test_message ("Pango version: %s", pango_version_string ()); - g_test_message ("Freetype version: %d.%d.%d", ft_major, ft_minor, ft_patch); -#if PANGO_VERSION_CHECK (1, 44, 0) - g_test_message ("Harfbuzz version: %s", hb_version_string ()); -#else - g_test_message ("Not printing Harfbuzz version since Pango is older than 1.44"); -#endif -} diff --git a/rsvg/tests/test-utils.h b/rsvg/tests/test-utils.h deleted file mode 100644 index d71af69d..00000000 --- a/rsvg/tests/test-utils.h +++ /dev/null @@ -1,35 +0,0 @@ -/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ -/* vim: set sw=4 sts=4 ts=4 expandtab: */ - -#ifndef TEST_UTILS_H -#define TEST_UTILS_H - -#include -#include -#include - -G_BEGIN_DECLS - -typedef struct { - unsigned int pixels_changed; - unsigned int max_diff; -} TestUtilsBufferDiffResult; - -void test_utils_compare_surfaces (cairo_surface_t *surface_a, - cairo_surface_t *surface_b, - cairo_surface_t *surface_diff, - TestUtilsBufferDiffResult *result); - -cairo_surface_t *test_utils_cairo_surface_from_pixbuf (const GdkPixbuf *pixbuf); - -typedef gboolean (* AddTestFunc) (GFile *file); - -const gchar *test_utils_get_test_data_path (void); - -void test_utils_print_dependency_versions (void); - -void test_utils_setup_font_map (void); - -G_END_DECLS - -#endif /* TEST_UTILS_H */ -- cgit v1.2.1