From 34021cbd58529cb4f736dd73eee8a2a20816a0cc Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Sun, 16 Aug 2026 10:43:19 +0000 Subject: [PATCH 3/7] cairo: Add rsvg_handle_render_document() and rsvg_handle_render_layer() These render into a viewport supplied by the caller rather than at the document's own size, which is the whole reason applications reach for them: an SVG cursor drawn at an arbitrary pixel size, an icon drawn at the display's scale factor, a thumbnail composited into a corner. For the outermost this means the caller's viewport overrides its width and height, and a viewBox is synthesized from those dimensions when the document has none, following SVG2's initial coordinate system. Do neither and a 24x24 document asked for at 256x256 paints 24x24 in the corner, which is worse than the build failure it replaces. The new drawing-context constructor is a sibling of rsvg_cairo_new_drawing_ctx() rather than a rewrite of it, and the outermost- override in rsvg_node_svg_draw() is gated on a field that only these entry points set, so rsvg_handle_render_cairo() keeps running exactly the code it ran before. Also initialize vb_stack, which rsvg_cairo_new_drawing_ctx() left uninitialized before _rsvg_push_view_box() prepended to it. Harmless in practice, as nothing ever dereferences the stale tail. --- rsvg-cairo-render.c | 209 ++++++++++++++++++++++++++++++++++++++++++++ rsvg-cairo.h | 5 ++ rsvg-private.h | 8 ++ rsvg-structure.c | 38 ++++++-- rsvg.symbols | 2 + 5 files changed, 255 insertions(+), 7 deletions(-) diff --git a/rsvg-cairo-render.c b/rsvg-cairo-render.c index 6876bbf47..b63c909f8 100644 --- a/rsvg-cairo-render.c +++ b/rsvg-cairo-render.c @@ -177,11 +177,13 @@ rsvg_cairo_new_drawing_ctx (cairo_t * cr, RsvgHandle * handle) draw->dpi_y = handle->priv->dpi_y; draw->vb.rect.width = data.em; draw->vb.rect.height = data.ex; + draw->vb_stack = NULL; draw->num_elements_acquired = 0; draw->pango_context = NULL; draw->drawsub_stack = NULL; draw->acquired_nodes = NULL; draw->is_testing = handle->priv->is_testing; + draw->toplevel_viewport.active = FALSE; rsvg_state_push (draw); state = rsvg_current_state (draw); @@ -280,3 +282,210 @@ rsvg_handle_render_cairo (RsvgHandle * handle, cairo_t * cr) { return rsvg_handle_render_cairo_sub (handle, cr, NULL); } + +/* ------------------------------------------------------------------------ + * Viewport-based API, compatible with librsvg 2.46 and later. + * + * These share the node tree and the bounding-box machinery with the legacy + * entry points above, but build their drawing context from a caller-supplied + * viewport instead of from the document's own size. The legacy path is left + * untouched on purpose. + * ------------------------------------------------------------------------ */ + +/* Like rsvg_cairo_new_drawing_ctx(), but the coordinate system is established + * from @viewport rather than from the document's own size. When @fit_toplevel + * is TRUE the outermost is fitted into @viewport instead of using its own + * width/height; this is off when drawing a single element in isolation, which + * never goes through the outermost . Returns NULL for a degenerate + * viewport. + */ +static RsvgDrawingCtx * +rsvg_cairo_new_drawing_ctx_for_viewport (cairo_t * cr, + RsvgHandle * handle, + const RsvgRectangle * viewport, + gboolean fit_toplevel) +{ + RsvgDrawingCtx *draw; + RsvgCairoRender *render; + RsvgState *state; + cairo_matrix_t affine, offset_affine; + double bbx0, bby0, bbx1, bby1; + + if (!(viewport->width > 0.0) || !(viewport->height > 0.0)) + return NULL; + + cairo_get_matrix (cr, &affine); + + /* "The origin of both coordinate systems must be at the origin of the SVG + * viewport" - https://www.w3.org/TR/SVG2/coords.html#InitialCoordinateSystem + */ + cairo_matrix_init_translate (&offset_affine, viewport->x, viewport->y); + cairo_matrix_multiply (&affine, &offset_affine, &affine); + + /* Bounding box of the viewport as transformed by the current cairo matrix; + * this sizes the intermediate surfaces used for filters, masks and groups. */ + rsvg_cairo_transformed_image_bounding_box (&affine, + viewport->width, viewport->height, + &bbx0, &bby0, &bbx1, &bby1); + + render = rsvg_cairo_render_new (cr, bbx1 - bbx0, bby1 - bby0); + if (!render) + return NULL; + + draw = g_new (RsvgDrawingCtx, 1); + + draw->render = (RsvgRender *) render; + render->offset_x = bbx0; + render->offset_y = bby0; + + draw->state = NULL; + + draw->defs = handle->priv->defs; + draw->dpi_x = handle->priv->dpi_x; + draw->dpi_y = handle->priv->dpi_y; + draw->vb.rect.x = 0; + draw->vb.rect.y = 0; + draw->vb.rect.width = viewport->width; + draw->vb.rect.height = viewport->height; + draw->vb.active = TRUE; + draw->vb_stack = NULL; + draw->num_elements_acquired = 0; + draw->pango_context = NULL; + draw->drawsub_stack = NULL; + draw->acquired_nodes = NULL; + draw->is_testing = handle->priv->is_testing; + + draw->toplevel_viewport.rect.x = 0; + draw->toplevel_viewport.rect.y = 0; + draw->toplevel_viewport.rect.width = viewport->width; + draw->toplevel_viewport.rect.height = viewport->height; + draw->toplevel_viewport.active = fit_toplevel; + + rsvg_state_push (draw); + state = rsvg_current_state (draw); + + cairo_matrix_multiply (&state->affine, &affine, &state->affine); + + /* Compensated for in _set_rsvg_affine(), as in rsvg_cairo_new_drawing_ctx(). */ + state->affine.x0 -= render->offset_x; + state->affine.y0 -= render->offset_y; + + rsvg_bbox_init (&render->bbox, &state->affine); + + return draw; +} + +static gboolean +rsvg_handle_lookup_drawsub (RsvgHandle * handle, const char *id, + RsvgNode ** out_node, GError ** error) +{ + *out_node = NULL; + + if (handle->priv->state != RSVG_HANDLE_STATE_CLOSED_OK) { + g_set_error (error, RSVG_ERROR, RSVG_ERROR_FAILED, + "the handle is not loaded"); + return FALSE; + } + + if (id && *id) { + RsvgNode *node = rsvg_defs_lookup (handle->priv->defs, id); + + if (node == NULL) { + g_set_error (error, RSVG_ERROR, RSVG_ERROR_FAILED, + "element id=\"%s\" does not exist", id); + return FALSE; + } + + *out_node = node; + } + + return TRUE; +} + +/** + * rsvg_handle_render_layer: + * @handle: A #RsvgHandle + * @cr: A Cairo context + * @id: (nullable): An element's id within the SVG, starting with "##", or %NULL + * to render the whole document. + * @viewport: Viewport size at which the whole SVG would be fitted. + * @error: (optional): a location to store a #GError + * + * Renders a single SVG element in the same place as for a whole SVG document + * (a "layer"), scaling the document to fit @viewport. + * + * Returns: %TRUE on success. + * + * Since: 2.46 + */ +gboolean +rsvg_handle_render_layer (RsvgHandle * handle, + cairo_t * cr, + const char *id, + const RsvgRectangle * viewport, + GError ** error) +{ + RsvgDrawingCtx *draw; + RsvgNode *drawsub = NULL; + gboolean retval; + + g_return_val_if_fail (handle != NULL, FALSE); + g_return_val_if_fail (cr != NULL, FALSE); + g_return_val_if_fail (viewport != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (!rsvg_handle_lookup_drawsub (handle, id, &drawsub, error)) + return FALSE; + + draw = rsvg_cairo_new_drawing_ctx_for_viewport (cr, handle, viewport, TRUE); + if (!draw) { + /* A degenerate viewport has nothing to draw, which is not an error. */ + return TRUE; + } + + while (drawsub != NULL) { + draw->drawsub_stack = g_slist_prepend (draw->drawsub_stack, drawsub); + drawsub = drawsub->parent; + } + + rsvg_state_push (draw); + cairo_save (cr); + + rsvg_node_draw ((RsvgNode *) handle->priv->treebase, draw, 0); + + if (rsvg_drawing_ctx_limits_exceeded (draw)) { + g_set_error (error, RSVG_ERROR, RSVG_ERROR_FAILED, + "instancing limit exceeded while rendering"); + retval = FALSE; + } else { + retval = TRUE; + } + + cairo_restore (cr); + rsvg_state_pop (draw); + rsvg_drawing_ctx_free (draw); + + return retval; +} + +/** + * rsvg_handle_render_document: + * @handle: A #RsvgHandle + * @cr: A Cairo context + * @viewport: Viewport size at which the whole SVG would be fitted. + * @error: (optional): a location to store a #GError + * + * Renders the whole SVG document fitted to a viewport. + * + * Returns: %TRUE on success. + * + * Since: 2.46 + */ +gboolean +rsvg_handle_render_document (RsvgHandle * handle, + cairo_t * cr, + const RsvgRectangle * viewport, + GError ** error) +{ + return rsvg_handle_render_layer (handle, cr, NULL, viewport, error); +} diff --git a/rsvg-cairo.h b/rsvg-cairo.h index d9ebfb2b5..1316b0b6a 100644 --- a/rsvg-cairo.h +++ b/rsvg-cairo.h @@ -37,6 +37,11 @@ G_BEGIN_DECLS gboolean rsvg_handle_render_cairo (RsvgHandle * handle, cairo_t * cr); gboolean rsvg_handle_render_cairo_sub (RsvgHandle * handle, cairo_t * cr, const char *id); +gboolean rsvg_handle_render_document (RsvgHandle *handle, + cairo_t *cr, + const RsvgRectangle *viewport, + GError **error); + G_END_DECLS #endif diff --git a/rsvg-private.h b/rsvg-private.h index 9e3b5f66c..fc8159442 100644 --- a/rsvg-private.h +++ b/rsvg-private.h @@ -212,6 +212,14 @@ struct RsvgDrawingCtx { GSList *drawsub_stack; GSList *acquired_nodes; gboolean is_testing; + + /* Viewport supplied by the caller of rsvg_handle_render_document() and + * friends. When active, the outermost is fitted into this rectangle + * instead of using its own width/height, and a viewBox is synthesized from + * those dimensions if the document does not have one. This stays inactive + * for rsvg_handle_render_cairo(), whose behaviour must not change. + */ + RsvgViewBox toplevel_viewport; }; /*Abstract base class for context for our backends (one as yet)*/ diff --git a/rsvg-structure.c b/rsvg-structure.c index 4d8b0ce8e..3f44333ef 100644 --- a/rsvg-structure.c +++ b/rsvg-structure.c @@ -265,6 +265,7 @@ rsvg_node_svg_draw (RsvgNode * self, RsvgDrawingCtx * ctx, int dominate) cairo_matrix_t affine, affine_old, affine_new; guint i; double nx, ny, nw, nh; + RsvgViewBox vbox; sself = (RsvgNodeSvg *) self; nx = _rsvg_css_normalize_length (&sself->x, ctx, 'h'); @@ -272,26 +273,49 @@ rsvg_node_svg_draw (RsvgNode * self, RsvgDrawingCtx * ctx, int dominate) nw = _rsvg_css_normalize_length (&sself->w, ctx, 'h'); nh = _rsvg_css_normalize_length (&sself->h, ctx, 'v'); + vbox = sself->vbox; + + /* When the caller supplied an explicit viewport, as with + * rsvg_handle_render_document(), the outermost is fitted into that + * rectangle rather than into its own width/height, and a viewBox is + * synthesized from the intrinsic dimensions if the document lacks one. + * See https://www.w3.org/TR/SVG2/coords.html#InitialCoordinateSystem . + */ + if (ctx->toplevel_viewport.active && self->parent == NULL) { + if (!vbox.active && nw > 0.0 && nh > 0.0) { + vbox.rect.x = 0.0; + vbox.rect.y = 0.0; + vbox.rect.width = nw; + vbox.rect.height = nh; + vbox.active = TRUE; + } + + nx = 0.0; + ny = 0.0; + nw = ctx->toplevel_viewport.rect.width; + nh = ctx->toplevel_viewport.rect.height; + } + rsvg_state_reinherit_top (ctx, self->state, dominate); state = rsvg_current_state (ctx); affine_old = state->affine; - if (sself->vbox.active) { + if (vbox.active) { double x = nx, y = ny, w = nw, h = nh; rsvg_preserve_aspect_ratio (sself->preserve_aspect_ratio, - sself->vbox.rect.width, sself->vbox.rect.height, + vbox.rect.width, vbox.rect.height, &w, &h, &x, &y); cairo_matrix_init (&affine, - w / sself->vbox.rect.width, + w / vbox.rect.width, 0, 0, - h / sself->vbox.rect.height, - x - sself->vbox.rect.x * w / sself->vbox.rect.width, - y - sself->vbox.rect.y * h / sself->vbox.rect.height); + h / vbox.rect.height, + x - vbox.rect.x * w / vbox.rect.width, + y - vbox.rect.y * h / vbox.rect.height); cairo_matrix_multiply (&state->affine, &affine, &state->affine); - _rsvg_push_view_box (ctx, sself->vbox.rect.width, sself->vbox.rect.height); + _rsvg_push_view_box (ctx, vbox.rect.width, vbox.rect.height); } else { cairo_matrix_init_translate (&affine, nx, ny); cairo_matrix_multiply (&state->affine, &affine, &state->affine); diff --git a/rsvg.symbols b/rsvg.symbols index 44bce7189..e7f750708 100644 --- a/rsvg.symbols +++ b/rsvg.symbols @@ -30,6 +30,8 @@ rsvg_set_default_dpi_x_y /* rsvg-cairo.h */ rsvg_handle_render_cairo rsvg_handle_render_cairo_sub +rsvg_handle_render_document +rsvg_handle_render_layer /* rsvg-css.h---semi-public for rsvg-convert */ rsvg_css_parse_color -- 2.43.0