From 83035eb0e21069ac0eac2a5718d9f42644080ad3 Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <fygrave@gmail.com>
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 <glib.h>
+#include <cairo.h>
+#include <math.h>
+#include <string.h>
+
+#include "rsvg.h"
+#include "rsvg-compat.h"
+#include "test-utils.h"
+
+/* A 24x24 icon that paints its whole viewBox. */
+static const char *icon24 =
+    "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'>"
+    "<rect x='0' y='0' width='24' height='24' fill='#ff0000'/></svg>";
+
+/* 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 =
+    "<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24'>"
+    "<rect x='0' y='0' width='24' height='24' fill='#00ff00'/></svg>";
+
+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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='210mm' height='297mm'/>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 400'/>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='2in' height='3in'/>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='20' height='30'/>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'/>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>"
+        "<rect id='r' x='10' y='20' width='30' height='40' fill='blue'/></svg>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>"
+        "<rect id='r' x='10' y='20' width='30' height='40' fill='blue'/></svg>");
+    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 (
+        "<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'>"
+        "<rect id='r' x='10' y='20' width='10' height='10' fill='blue'/></svg>");
+    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

