From 83035eb0e21069ac0eac2a5718d9f42644080ad3 Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Sun, 16 Aug 2026 10:43:19 +0000 Subject: [PATCH 6/7] tests: Cover the viewport and geometry API The viewport cases are the shapes real callers pass: a small icon scaled up to an arbitrary square, the same with no viewBox so one has to be synthesized, a viewport with a non-zero origin, and a non-square viewport that must letterbox rather than stretch. Each of those would have passed against an implementation that merely accepted an RsvgRectangle and ignored it. --- tests/Makefile.am | 5 + tests/api.c | 431 ++++++++++++++++++++++++++++++++++++++++++++++ tests/meson.build | 1 + 3 files changed, 437 insertions(+) create mode 100644 tests/api.c diff --git a/tests/Makefile.am b/tests/Makefile.am index 2b92cc817..8a133feab 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -8,6 +8,7 @@ test_programs = \ styles \ render-crash \ dimensions \ + api \ errors test_utils_common_sources = \ @@ -38,6 +39,10 @@ dimensions_SOURCES = \ dimensions.c \ $(test_utils_common_sources) +api_SOURCES = \ + api.c \ + $(test_utils_common_sources) + loading_SOURCES = \ loading.c \ $(test_utils_common_sources) diff --git a/tests/api.c b/tests/api.c new file mode 100644 index 000000000..cde858ce1 --- /dev/null +++ b/tests/api.c @@ -0,0 +1,431 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim: set ts=4 nowrap ai expandtab sw=4: */ + +/* Tests for the viewport and geometry API introduced in librsvg 2.46, and the + * intrinsic-size call added in 2.52. The viewport shapes exercised here are the + * ones real callers use: an icon scaled up to an arbitrary square (hyprcursor, + * RawTherapee), a viewport with a non-zero origin (xournalpp's thumbnailer), and + * intrinsic dimensions read purely for their aspect ratio (timg, coot). + */ + +#include +#include +#include +#include + +#include "rsvg.h" +#include "rsvg-compat.h" +#include "test-utils.h" + +/* A 24x24 icon that paints its whole viewBox. */ +static const char *icon24 = + "" + ""; + +/* The same icon with no viewBox, so that one has to be synthesized from the + * intrinsic width and height. */ +static const char *icon24_no_viewbox = + "" + ""; + +static RsvgHandle * +handle_from_string (const char *svg) +{ + GError *error = NULL; + RsvgHandle *handle; + + handle = rsvg_handle_new_from_data ((const guint8 *) svg, strlen (svg), &error); + g_assert_no_error (error); + g_assert (handle != NULL); + + return handle; +} + +/* Bounding box of the non-transparent pixels of a surface. */ +static void +painted_extents (cairo_surface_t *surface, int *x0, int *y0, int *x1, int *y1) +{ + int width = cairo_image_surface_get_width (surface); + int height = cairo_image_surface_get_height (surface); + int stride = cairo_image_surface_get_stride (surface); + unsigned char *data = cairo_image_surface_get_data (surface); + int x, y; + + *x0 = width; + *y0 = height; + *x1 = -1; + *y1 = -1; + + for (y = 0; y < height; y++) { + guint32 *row = (guint32 *) (data + y * stride); + + for (x = 0; x < width; x++) { + if ((row[x] >> 24) == 0) + continue; + + if (x < *x0) *x0 = x; + if (y < *y0) *y0 = y; + if (x > *x1) *x1 = x; + if (y > *y1) *y1 = y; + } + } +} + +static void +render_document_extents (const char *svg, + int surface_width, int surface_height, + const RsvgRectangle *viewport, + int *x0, int *y0, int *x1, int *y1) +{ + RsvgHandle *handle = handle_from_string (svg); + cairo_surface_t *surface; + cairo_t *cr; + GError *error = NULL; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, + surface_width, surface_height); + cr = cairo_create (surface); + + g_assert (rsvg_handle_render_document (handle, cr, viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + cairo_surface_flush (surface); + painted_extents (surface, x0, y0, x1, y1); + cairo_surface_destroy (surface); + g_object_unref (handle); +} + +/* The document's own width and height must not constrain the result; the + * caller's viewport wins. This is what makes a 24x24 cursor usable at 256x256. */ +static void +test_render_document_scales_to_viewport (void) +{ + RsvgRectangle viewport = { 0.0, 0.0, 256.0, 256.0 }; + int x0, y0, x1, y1; + + render_document_extents (icon24, 256, 256, &viewport, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 0); + g_assert_cmpint (y0, ==, 0); + g_assert_cmpint (x1, ==, 255); + g_assert_cmpint (y1, ==, 255); +} + +/* With no viewBox, one is synthesized from the intrinsic width and height, so + * the document still scales rather than being pinned to its own size. */ +static void +test_render_document_synthesizes_viewbox (void) +{ + RsvgRectangle viewport = { 0.0, 0.0, 256.0, 256.0 }; + int x0, y0, x1, y1; + + render_document_extents (icon24_no_viewbox, 256, 256, &viewport, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 0); + g_assert_cmpint (y0, ==, 0); + g_assert_cmpint (x1, ==, 255); + g_assert_cmpint (y1, ==, 255); +} + +static void +test_render_document_viewport_offset (void) +{ + RsvgRectangle viewport = { 100.0, 50.0, 64.0, 64.0 }; + int x0, y0, x1, y1; + + render_document_extents (icon24, 200, 200, &viewport, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 100); + g_assert_cmpint (y0, ==, 50); + g_assert_cmpint (x1, ==, 163); + g_assert_cmpint (y1, ==, 113); +} + +/* preserveAspectRatio defaults to xMidYMid, so a square document in a wide + * viewport is centred rather than stretched. */ +static void +test_render_document_preserves_aspect_ratio (void) +{ + RsvgRectangle viewport = { 0.0, 0.0, 200.0, 100.0 }; + int x0, y0, x1, y1; + + render_document_extents (icon24, 200, 100, &viewport, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 50); + g_assert_cmpint (y0, ==, 0); + g_assert_cmpint (x1, ==, 149); + g_assert_cmpint (y1, ==, 99); +} + +static void +test_render_document_empty_viewport (void) +{ + RsvgHandle *handle = handle_from_string (icon24); + RsvgRectangle viewport = { 0.0, 0.0, 0.0, 0.0 }; + cairo_surface_t *surface; + cairo_t *cr; + GError *error = NULL; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 10, 10); + cr = cairo_create (surface); + + /* Nothing to draw, but not a failure. */ + g_assert (rsvg_handle_render_document (handle, cr, &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + cairo_surface_destroy (surface); + g_object_unref (handle); +} + +/* Absolute lengths are normalized to inches while parsing, so a document + * authored in millimetres reports RSVG_UNIT_IN with an equivalent value. The + * ratio between the two dimensions, which is what most callers use, is exact. */ +static void +test_intrinsic_dimensions_physical_units (void) +{ + RsvgHandle *handle = handle_from_string ( + ""); + gboolean has_width, has_height, has_viewbox; + RsvgLength width, height; + RsvgRectangle viewbox; + + rsvg_handle_get_intrinsic_dimensions (handle, + &has_width, &width, + &has_height, &height, + &has_viewbox, &viewbox); + + /* SVG2 makes width and height default to 100%, so they always exist. */ + g_assert (has_width); + g_assert (has_height); + g_assert (!has_viewbox); + + g_assert_cmpint (width.unit, ==, RSVG_UNIT_IN); + g_assert_cmpint (height.unit, ==, RSVG_UNIT_IN); + g_assert_cmpfloat (fabs (width.length - 210.0 / 25.4), <, 1e-9); + g_assert_cmpfloat (fabs (height.length - 297.0 / 25.4), <, 1e-9); + g_assert_cmpfloat (fabs ((height.length / width.length) - (297.0 / 210.0)), <, 1e-9); + + g_object_unref (handle); +} + +static void +test_intrinsic_dimensions_viewbox_only (void) +{ + RsvgHandle *handle = handle_from_string ( + ""); + gboolean has_width, has_height, has_viewbox; + RsvgLength width, height; + RsvgRectangle viewbox; + + rsvg_handle_get_intrinsic_dimensions (handle, + &has_width, &width, + &has_height, &height, + &has_viewbox, &viewbox); + + g_assert (has_width); + g_assert (has_height); + g_assert_cmpint (width.unit, ==, RSVG_UNIT_PERCENT); + g_assert_cmpfloat (fabs (width.length - 1.0), <, 1e-9); + g_assert_cmpint (height.unit, ==, RSVG_UNIT_PERCENT); + + 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 +test_intrinsic_size_in_pixels (void) +{ + RsvgHandle *handle; + double width, height; + + handle = handle_from_string ( + ""); + rsvg_handle_set_dpi (handle, 96.0); + g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); + g_assert_cmpfloat (fabs (width - 192.0), <, 1e-6); + g_assert_cmpfloat (fabs (height - 288.0), <, 1e-6); + g_object_unref (handle); + + handle = handle_from_string ( + ""); + g_assert (rsvg_handle_get_intrinsic_size_in_pixels (handle, &width, &height)); + g_assert_cmpfloat (fabs (width - 20.0), <, 1e-6); + g_assert_cmpfloat (fabs (height - 30.0), <, 1e-6); + g_object_unref (handle); +} + +/* Percentages need a viewport to resolve against, so they cannot be turned + * into pixels here. */ +static void +test_intrinsic_size_in_pixels_percentages (void) +{ + RsvgHandle *handle = handle_from_string ( + ""); + double width = 1.0, height = 1.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 +test_geometry_for_layer (void) +{ + RsvgHandle *handle = handle_from_string ( + "" + ""); + RsvgRectangle viewport = { 0.0, 0.0, 100.0, 100.0 }; + RsvgRectangle ink, logical; + GError *error = NULL; + + g_assert (rsvg_handle_get_geometry_for_layer (handle, "#r", &viewport, + &ink, &logical, &error)); + g_assert_no_error (error); + g_assert_cmpfloat (fabs (ink.x - 10.0), <, 0.5); + g_assert_cmpfloat (fabs (ink.y - 20.0), <, 0.5); + g_assert_cmpfloat (fabs (ink.width - 30.0), <, 0.5); + g_assert_cmpfloat (fabs (ink.height - 40.0), <, 0.5); + + /* The geometry is reported in the caller's coordinates, so an offset + * viewport shifts it. */ + viewport.x = 7.0; + viewport.y = 11.0; + g_assert (rsvg_handle_get_geometry_for_layer (handle, "#r", &viewport, + &ink, NULL, NULL)); + g_assert_cmpfloat (fabs (ink.x - 17.0), <, 0.5); + g_assert_cmpfloat (fabs (ink.y - 31.0), <, 0.5); + + g_object_unref (handle); +} + +static void +test_geometry_for_missing_id (void) +{ + RsvgHandle *handle = handle_from_string ( + "" + ""); + RsvgRectangle viewport = { 0.0, 0.0, 100.0, 100.0 }; + RsvgRectangle ink; + GError *error = NULL; + + g_assert (!rsvg_handle_get_geometry_for_layer (handle, "#nonexistent", &viewport, + &ink, NULL, &error)); + g_assert (error != NULL); + g_error_free (error); + + g_object_unref (handle); +} + +/* An element is scaled to fit the viewport it is given, independently of where + * it sits in the document. */ +static void +test_render_element (void) +{ + RsvgHandle *handle = handle_from_string ( + "" + ""); + RsvgRectangle viewport = { 0.0, 0.0, 50.0, 50.0 }; + cairo_surface_t *surface; + cairo_t *cr; + GError *error = NULL; + int x0, y0, x1, y1; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 100, 100); + cr = cairo_create (surface); + + g_assert (rsvg_handle_render_element (handle, cr, "#r", &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + cairo_surface_flush (surface); + painted_extents (surface, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 0); + g_assert_cmpint (y0, ==, 0); + g_assert_cmpint (x1, ==, 49); + g_assert_cmpint (y1, ==, 49); + + cairo_surface_destroy (surface); + g_object_unref (handle); +} + +/* Rendering a layer with a NULL id must match rendering the whole document. */ +static void +test_render_layer_null_id (void) +{ + RsvgHandle *handle = handle_from_string (icon24); + RsvgRectangle viewport = { 0.0, 0.0, 64.0, 64.0 }; + cairo_surface_t *surface; + cairo_t *cr; + GError *error = NULL; + int x0, y0, x1, y1; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 64, 64); + cr = cairo_create (surface); + + g_assert (rsvg_handle_render_layer (handle, cr, NULL, &viewport, &error)); + g_assert_no_error (error); + + cairo_destroy (cr); + cairo_surface_flush (surface); + painted_extents (surface, &x0, &y0, &x1, &y1); + + g_assert_cmpint (x0, ==, 0); + g_assert_cmpint (y0, ==, 0); + g_assert_cmpint (x1, ==, 63); + g_assert_cmpint (y1, ==, 63); + + cairo_surface_destroy (surface); + g_object_unref (handle); +} + +int +main (int argc, char *argv[]) +{ + int result; + + RSVG_G_TYPE_INIT; + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/api/render_document/scales-to-viewport", + test_render_document_scales_to_viewport); + g_test_add_func ("/api/render_document/synthesizes-viewbox", + test_render_document_synthesizes_viewbox); + g_test_add_func ("/api/render_document/viewport-offset", + test_render_document_viewport_offset); + g_test_add_func ("/api/render_document/preserves-aspect-ratio", + test_render_document_preserves_aspect_ratio); + g_test_add_func ("/api/render_document/empty-viewport", + test_render_document_empty_viewport); + g_test_add_func ("/api/render_layer/null-id", + test_render_layer_null_id); + g_test_add_func ("/api/render_element/scales-to-viewport", + test_render_element); + g_test_add_func ("/api/geometry_for_layer/rect", + test_geometry_for_layer); + g_test_add_func ("/api/geometry_for_layer/missing-id", + test_geometry_for_missing_id); + g_test_add_func ("/api/intrinsic_dimensions/physical-units", + test_intrinsic_dimensions_physical_units); + g_test_add_func ("/api/intrinsic_dimensions/viewbox-only", + test_intrinsic_dimensions_viewbox_only); + g_test_add_func ("/api/intrinsic_size_in_pixels/absolute", + test_intrinsic_size_in_pixels); + g_test_add_func ("/api/intrinsic_size_in_pixels/percentages", + test_intrinsic_size_in_pixels_percentages); + + result = g_test_run (); + + rsvg_cleanup (); + + return result; +} diff --git a/tests/meson.build b/tests/meson.build index ca2613751..d47ace90c 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -18,6 +18,7 @@ rsvg_tests = [ }, { 'name': 'render-crash', 'sources': ['render-crash.c'] }, { 'name': 'dimensions', 'sources': ['dimensions.c'] }, + { 'name': 'api', 'sources': ['api.c'] }, { 'name': 'errors', 'sources': ['errors.c'], -- 2.43.0