From cca66b9ec4e494c1d919bff0f71a820d8afab1fa Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 20:24:48 +0200 Subject: Adding upstream version 1.2.2. Signed-off-by: Daniel Baumann --- src/extension/internal/cairo-renderer.cpp | 1012 +++++++++++++++++++++++++++++ 1 file changed, 1012 insertions(+) create mode 100644 src/extension/internal/cairo-renderer.cpp (limited to 'src/extension/internal/cairo-renderer.cpp') diff --git a/src/extension/internal/cairo-renderer.cpp b/src/extension/internal/cairo-renderer.cpp new file mode 100644 index 0000000..5898b8e --- /dev/null +++ b/src/extension/internal/cairo-renderer.cpp @@ -0,0 +1,1012 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** \file + * Rendering with Cairo. + */ +/* + * Author: + * Miklos Erdelyi + * Jon A. Cruz + * Abhishek Sharma + * + * Copyright (C) 2006 Miklos Erdelyi + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" // only include where actually required! +#endif + +#ifndef PANGO_ENABLE_BACKEND +#define PANGO_ENABLE_BACKEND +#endif + +#ifndef PANGO_ENABLE_ENGINE +#define PANGO_ENABLE_ENGINE +#endif + + +#include +#include + + +#include <2geom/transforms.h> +#include <2geom/pathvector.h> +#include +#include +#include + +// include support for only the compiled-in surface types +#ifdef CAIRO_HAS_PDF_SURFACE +#include +#endif +#ifdef CAIRO_HAS_PS_SURFACE +#include +#endif + +#include "cairo-render-context.h" +#include "cairo-renderer.h" +#include "document.h" +#include "inkscape-version.h" +#include "rdf.h" +#include "style-internal.h" +#include "display/cairo-utils.h" +#include "display/curve.h" +#include "extension/system.h" +#include "filter-chemistry.h" +#include "helper/pixbuf-ops.h" +#include "helper/png-write.h" + +#include "io/sys.h" + +#include "include/source_date_epoch.h" + +#include "libnrtype/Layout-TNG.h" + +#include "object/sp-anchor.h" +#include "object/sp-clippath.h" +#include "object/sp-defs.h" +#include "object/sp-flowtext.h" +#include "object/sp-hatch-path.h" +#include "object/sp-image.h" +#include "object/sp-item-group.h" +#include "object/sp-item.h" +#include "object/sp-linear-gradient.h" +#include "object/sp-marker.h" +#include "object/sp-mask.h" +#include "object/sp-page.h" +#include "object/sp-pattern.h" +#include "object/sp-radial-gradient.h" +#include "object/sp-root.h" +#include "object/sp-shape.h" +#include "object/sp-symbol.h" +#include "object/sp-text.h" +#include "object/sp-use.h" + +#include "util/units.h" + +//#define TRACE(_args) g_printf _args +#define TRACE(_args) +//#define TEST(_args) _args +#define TEST(_args) + +namespace Inkscape { +namespace Extension { +namespace Internal { + +CairoRenderer::CairoRenderer(void) += default; + +CairoRenderer::~CairoRenderer() +{ + /* restore default signal handling for SIGPIPE */ +#if !defined(_WIN32) && !defined(__WIN32__) + (void) signal(SIGPIPE, SIG_DFL); +#endif + + return; +} + +CairoRenderContext* +CairoRenderer::createContext() +{ + CairoRenderContext *new_context = new CairoRenderContext(this); + g_assert( new_context != nullptr ); + + new_context->_state = nullptr; + + // create initial render state + CairoRenderState *state = new_context->_createState(); + state->transform = Geom::identity(); + new_context->_state_stack.push_back(state); + new_context->_state = state; + + return new_context; +} + +void +CairoRenderer::destroyContext(CairoRenderContext *ctx) +{ + delete ctx; +} + +/* + +Here comes the rendering part which could be put into the 'render' methods of SPItems' + +*/ + +/* The below functions are copy&pasted plus slightly modified from *_invoke_print functions. */ +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr); +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin = nullptr, SPPage *page = nullptr); +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx); +static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page = nullptr); +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin = nullptr); +static void sp_text_render(SPText *text, CairoRenderContext *ctx); +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx); +static void sp_image_render(SPImage *image, CairoRenderContext *ctx); +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page); +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page = nullptr); + +static void sp_shape_render_invoke_marker_rendering(SPMarker* marker, Geom::Affine tr, SPStyle* style, CairoRenderContext *ctx, SPItem *origin) +{ + bool render = true; + if (marker->markerUnits == SP_MARKER_UNITS_STROKEWIDTH) { + if (style->stroke_width.computed > 1e-9) { + tr = Geom::Scale(style->stroke_width.computed) * tr; + } else { + render = false; // stroke width zero and marker is thus scaled down to zero, skip + } + } + + if (render) { + SPItem* marker_item = sp_item_first_item_child(marker); + if (marker_item) { + tr = (Geom::Affine)marker_item->transform * (Geom::Affine)marker->c2p * tr; + Geom::Affine old_tr = marker_item->transform; + marker_item->transform = tr; + ctx->getRenderer()->renderItem (ctx, marker_item, origin); + marker_item->transform = old_tr; + } + } +} + +static void sp_shape_render(SPShape *shape, CairoRenderContext *ctx, SPItem *origin) +{ + if (!shape->curve()) { + return; + } + + Geom::OptRect pbox = shape->geometricBounds(); + + SPStyle* style = shape->style; + auto fill_origin = style->fill.paintOrigin; + auto stroke_origin = style->stroke.paintOrigin; + + if (origin) { + // If the shape is a child of a marker, we must set styles from the origin. + SPMarker *marker = nullptr; + auto parentobj = shape->parent; + while (parentobj) { + if (marker = dynamic_cast(parentobj)) { + break; + } + parentobj = parentobj->parent; + } + if (marker) { + // Origin will ultimately be either the original object the marker + // was added to OR a use tag. See sp_use_render for where this object + // comes from when it's a clone. + SPStyle *styleorig = origin->style; + bool iscolorfill = styleorig->fill.isColor() || (styleorig->fill.isPaintserver() && !styleorig->getFillPaintServer()->isValid()); + bool iscolorstroke = styleorig->stroke.isColor() || (styleorig->stroke.isPaintserver() && !styleorig->getStrokePaintServer()->isValid()); + bool fillctxfill = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool fillctxstroke = style->fill.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + bool strokectxfill = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_FILL; + bool strokectxstroke = style->stroke.paintOrigin == SP_CSS_PAINT_ORIGIN_CONTEXT_STROKE; + + if (fillctxfill || fillctxstroke) { + if (fillctxfill ? iscolorfill : iscolorstroke) { + style->fill.setColor(fillctxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (fillctxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->fill.value.href = fillctxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->fill.setNone(); + } + } + if (strokectxfill || strokectxstroke) { + if (strokectxfill ? iscolorfill : iscolorstroke) { + style->stroke.setColor(strokectxfill ? styleorig->fill.value.color : styleorig->stroke.value.color); + } else if (strokectxfill ? styleorig->fill.isPaintserver() : styleorig->stroke.isPaintserver()) { + style->stroke.value.href = strokectxfill ? styleorig->fill.value.href : styleorig->stroke.value.href; + } else { + style->stroke.setNone(); + } + } + style->fill.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + style->stroke.paintOrigin = SP_CSS_PAINT_ORIGIN_NORMAL; + } + } + + Geom::PathVector const &pathv = shape->curve()->get_pathvector(); + if (pathv.empty()) { + return; + } + + if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_NORMAL || + (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE)) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[0] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + + // START marker + for (int i = 0; i < 2; i++) { // SP_MARKER_LOC and SP_MARKER_LOC_START + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + Geom::Affine tr; + if (marker->orient_mode == MARKER_ORIENT_AUTO) { + tr = sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else if (marker->orient_mode == MARKER_ORIENT_AUTO_START_REVERSE) { + tr = Geom::Rotate::from_degrees( 180.0 ) * sp_shape_marker_get_transform_at_start(pathv.begin()->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(pathv.begin()->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + // MID marker + for (int i = 0; i < 3; i += 2) { // SP_MARKER_LOC and SP_MARKER_LOC_MID + if ( !shape->_marker[i] ) continue; + SPMarker* marker = shape->_marker[i]; + for(Geom::PathVector::const_iterator path_it = pathv.begin(); path_it != pathv.end(); ++path_it) { + // START position + if ( path_it != pathv.begin() + && ! ((path_it == (pathv.end()-1)) && (path_it->size_default() == 0)) ) // if this is the last path and it is a moveto-only, there is no mid marker there + { + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_start(path_it->front()); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(path_it->front().pointAt(0)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + // MID position + if (path_it->size_default() > 1) { + Geom::Path::const_iterator curve_it1 = path_it->begin(); // incoming curve + Geom::Path::const_iterator curve_it2 = ++(path_it->begin()); // outgoing curve + while (curve_it2 != path_it->end_default()) + { + /* Put marker between curve_it1 and curve_it2. + * Loop to end_default (so including closing segment), because when a path is closed, + * there should be a midpoint marker between last segment and closing straight line segment */ + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform(*curve_it1, *curve_it2); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(curve_it1->pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + + ++curve_it1; + ++curve_it2; + } + } + // END position + if ( path_it != (pathv.end()-1) && !path_it->empty()) { + Geom::Curve const &lastcurve = path_it->back_default(); + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + } + // END marker + for (int i = 0; i < 4; i += 3) { // SP_MARKER_LOC and SP_MARKER_LOC_END + if ( shape->_marker[i] ) { + SPMarker* marker = shape->_marker[i]; + + /* Get reference to last curve in the path. + * For moveto-only path, this returns the "closing line segment". */ + Geom::Path const &path_last = pathv.back(); + unsigned int index = path_last.size_default(); + if (index > 0) { + index--; + } + Geom::Curve const &lastcurve = path_last[index]; + + Geom::Affine tr; + if (marker->orient_mode != MARKER_ORIENT_ANGLE) { + tr = sp_shape_marker_get_transform_at_end(lastcurve); + } else { + tr = Geom::Rotate::from_degrees(marker->orient.computed) * Geom::Translate(lastcurve.pointAt(1)); + } + + sp_shape_render_invoke_marker_rendering(marker, tr, style, ctx, origin ? origin : shape); + } + } + + if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_OVER_FILL); + } else if (style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_OVER_STROKE); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_STROKE && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::STROKE_ONLY); + } else if (style->paint_order.layer[2] == SP_CSS_PAINT_ORDER_FILL && + style->paint_order.layer[1] == SP_CSS_PAINT_ORDER_MARKER ) { + ctx->renderPathVector(pathv, style, pbox, CairoRenderContext::FILL_ONLY); + } + + // Put the style's paint origin back, in case this shape is a marker + // which is rendered multple times. + style->fill.paintOrigin = fill_origin; + style->stroke.paintOrigin = stroke_origin; +} + +static void sp_group_render(SPGroup *group, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + CairoRenderer *renderer = ctx->getRenderer(); + for (auto obj : group->childList(false)) { + if (SPItem *item = dynamic_cast(obj)) { + renderer->renderItem(ctx, item, origin, page); + } + } +} + +static void sp_use_render(SPUse *use, CairoRenderContext *ctx, SPPage *page) +{ + bool translated = false; + CairoRenderer *renderer = ctx->getRenderer(); + + if ((use->x._set && use->x.computed != 0) || (use->y._set && use->y.computed != 0)) { + // FIXME: This translation sometimes isn't in the correct units; e.g. + // x="0" y="42" has a different effect than transform="translate(0,42)". + Geom::Affine tp(Geom::Translate(use->x.computed, use->y.computed)); + ctx->pushState(); + ctx->transform(tp); + translated = true; + } + + if (use->child) { + // Padding in the use object as the origin here ensures markers + // are rendered with their correct context-fill. + renderer->renderItem(ctx, use->child, use, page); + } + + if (translated) { + ctx->popState(); + } +} + +static void sp_text_render(SPText *text, CairoRenderContext *ctx) +{ + text->layout.showGlyphs(ctx); +} + +static void sp_flowtext_render(SPFlowtext *flowtext, CairoRenderContext *ctx) +{ + flowtext->layout.showGlyphs(ctx); +} + +static void sp_image_render(SPImage *image, CairoRenderContext *ctx) +{ + if (!image->pixbuf) { + return; + } + if ((image->width.computed <= 0.0) || (image->height.computed <= 0.0)) { + return; + } + + int w = image->pixbuf->width(); + int h = image->pixbuf->height(); + + double x = image->x.computed; + double y = image->y.computed; + double width = image->width.computed; + double height = image->height.computed; + + if (image->aspect_align != SP_ASPECT_NONE) { + calculatePreserveAspectRatio (image->aspect_align, image->aspect_clip, (double)w, (double)h, + &x, &y, &width, &height); + } + + if (image->aspect_clip == SP_ASPECT_SLICE && !ctx->getCurrentState()->has_overflow) { + ctx->addClippingRect(image->x.computed, image->y.computed, image->width.computed, image->height.computed); + } + + Geom::Translate tp(x, y); + Geom::Scale s(width / (double)w, height / (double)h); + Geom::Affine t(s * tp); + + ctx->renderImage(image->pixbuf, t, image->style); +} + +static void sp_anchor_render(SPAnchor *a, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + std::vector l(a->childList(false)); + if (a->href) + ctx->tagBegin(a->href); + for(auto x : l){ + SPItem *item = dynamic_cast(x); + if (item) { + renderer->renderItem(ctx, item); + } + } + if (a->href) + ctx->tagEnd(); +} + +static void sp_symbol_render(SPSymbol *symbol, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + if (!symbol->cloned) { + return; + } + + /* Cloned is actually renderable */ + ctx->pushState(); + ctx->transform(symbol->c2p); + + // apply viewbox if set + if (false /*symbol->viewBox_set*/) { + Geom::Affine vb2user; + double x, y, width, height; + double view_width, view_height; + x = 0.0; + y = 0.0; + width = 1.0; + height = 1.0; + + view_width = symbol->viewBox.width(); + view_height = symbol->viewBox.height(); + + calculatePreserveAspectRatio(symbol->aspect_align, symbol->aspect_clip, view_width, view_height, + &x, &y,&width, &height); + + // [itemTransform *] translate(x, y) * scale(w/vw, h/vh) * translate(-vx, -vy); + vb2user = Geom::identity(); + vb2user[0] = width / view_width; + vb2user[3] = height / view_height; + vb2user[4] = x - symbol->viewBox.left() * vb2user[0]; + vb2user[5] = y - symbol->viewBox.top() * vb2user[3]; + + ctx->transform(vb2user); + } + + sp_group_render(symbol, ctx, origin, page); + ctx->popState(); +} + +static void sp_root_render(SPRoot *root, CairoRenderContext *ctx) +{ + CairoRenderer *renderer = ctx->getRenderer(); + + if (!ctx->getCurrentState()->has_overflow && root->parent) + ctx->addClippingRect(root->x.computed, root->y.computed, root->width.computed, root->height.computed); + + ctx->pushState(); + renderer->setStateForItem(ctx, root); + ctx->transform(root->c2p); + sp_group_render(root, ctx); + ctx->popState(); +} + +/** + This function converts the item to a raster image and includes the image into the cairo renderer. + It is only used for filters and then only when rendering filters as bitmaps is requested. +*/ +static void sp_asbitmap_render(SPItem *item, CairoRenderContext *ctx, SPPage *page) +{ + + // The code was adapted from sp_selection_create_bitmap_copy in selection-chemistry.cpp + + // Calculate resolution + double res; + /** @TODO reimplement the resolution stuff (WHY?) + */ + res = ctx->getBitmapResolution(); + if(res == 0) { + res = Inkscape::Util::Quantity::convert(1, "in", "px"); + } + TRACE(("sp_asbitmap_render: resolution: %f\n", res )); + + // Get the bounding box of the selection in document coordinates. + Geom::OptRect bbox = item->documentVisualBounds(); + + bbox &= (page ? page->getDocumentRect() : item->document->preferredBounds()); + + // no bbox, e.g. empty group or item not overlapping its page + if (!bbox) { + return; + } + + // The width and height of the bitmap in pixels + unsigned width = ceil(bbox->width() * Inkscape::Util::Quantity::convert(res, "px", "in")); + unsigned height = ceil(bbox->height() * Inkscape::Util::Quantity::convert(res, "px", "in")); + + if (width == 0 || height == 0) return; + + // Scale to exactly fit integer bitmap inside bounding box + double scale_x = bbox->width() / width; + double scale_y = bbox->height() / height; + + // Location of bounding box in document coordinates. + double shift_x = bbox->min()[Geom::X]; + double shift_y = bbox->top(); + + // For default 96 dpi, snap bitmap to pixel grid + if (res == Inkscape::Util::Quantity::convert(1, "in", "px")) { + shift_x = round (shift_x); + shift_y = round (shift_y); + } + + // Calculate the matrix that will be applied to the image so that it exactly overlaps the source objects + + // Matrix to put bitmap in correct place on document + Geom::Affine t_on_document = (Geom::Affine)(Geom::Scale (scale_x, scale_y)) * + (Geom::Affine)(Geom::Translate (shift_x, shift_y)); + + // ctx matrix already includes item transformation. We must substract. + Geom::Affine t_item = item->i2doc_affine(); + Geom::Affine t = t_on_document * t_item.inverse(); + + // Do the export + SPDocument *document = item->document; + + std::vector items; + items.push_back(item); + + std::unique_ptr pb(sp_generate_internal_bitmap(document, *bbox, res, items, true)); + + if (pb) { + //TEST(gdk_pixbuf_save( pb, "bitmap.png", "png", NULL, NULL )); + + ctx->renderImage(pb.get(), t, item->style); + } +} + + +static void sp_item_invoke_render(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + SPRoot *root = dynamic_cast(item); + if (root) { + TRACE(("root\n")); + sp_root_render(root, ctx); + } else { + SPSymbol *symbol = dynamic_cast(item); + if (symbol) { + TRACE(("symbol\n")); + sp_symbol_render(symbol, ctx, origin, page); + } else { + SPAnchor *anchor = dynamic_cast(item); + if (anchor) { + TRACE(("\n")); + sp_anchor_render(anchor, ctx); + } else { + SPShape *shape = dynamic_cast(item); + if (shape) { + TRACE(("shape\n")); + sp_shape_render(shape, ctx, origin); + } else { + SPUse *use = dynamic_cast(item); + if (use) { + TRACE(("use begin---\n")); + sp_use_render(use, ctx, page); + TRACE(("---use end\n")); + } else { + SPText *text = dynamic_cast(item); + if (text) { + TRACE(("text\n")); + sp_text_render(text, ctx); + } else { + SPFlowtext *flowtext = dynamic_cast(item); + if (flowtext) { + TRACE(("flowtext\n")); + sp_flowtext_render(flowtext, ctx); + } else { + SPImage *image = dynamic_cast(item); + if (image) { + TRACE(("image\n")); + sp_image_render(image, ctx); + } else if (dynamic_cast(item)) { + // Marker contents shouldn't be rendered, even outside of . + return; + } else { + SPGroup *group = dynamic_cast(item); + if (group) { + TRACE(("\n")); + sp_group_render(group, ctx, origin, page); + } + } + } + } + } + } + } + } + } +} + +void +CairoRenderer::setStateForItem(CairoRenderContext *ctx, SPItem const *item) +{ + ctx->setStateForStyle(item->style); + + CairoRenderState *state = ctx->getCurrentState(); + state->clip_path = item->getClipObject(); + state->mask = item->getMaskObject(); + state->item_transform = Geom::Affine (item->transform); + + // If parent_has_userspace is true the parent state's transform + // has to be used for the mask's/clippath's context. + // This is so because we use the image's/(flow)text's transform for positioning + // instead of explicitly specifying it and letting the renderer do the + // transformation before rendering the item. + if (dynamic_cast(item) || dynamic_cast(item) || dynamic_cast(item)) { + state->parent_has_userspace = TRUE; + } + TRACE(("setStateForItem opacity: %f\n", state->opacity)); +} + +bool CairoRenderer::_shouldRasterize(CairoRenderContext *ctx, SPItem const *item) +{ + // rasterize filtered items as per user setting + // however, clipPaths ignore any filters, so do *not* rasterize + // TODO: might apply to some degree to masks with filtered elements as well; + // we need to figure out where in the stack it would be safe to rasterize + if (ctx->getFilterToBitmap() && !item->isInClipPath()) { + if (auto const *clone = dynamic_cast(item)) { + return clone->anyInChain([](SPItem const *i) { return i && i->isFiltered(); }); + } else { + return item->isFiltered(); + } + } + return false; +} + +void CairoRenderer::_doRender(SPItem *item, CairoRenderContext *ctx, SPItem *origin, SPPage *page) +{ + // Check item's visibility + if (item->isHidden() || has_hidder_filter(item)) { + return; + } + + if (_shouldRasterize(ctx, item)) { + sp_asbitmap_render(item, ctx, page); + } else { + sp_item_invoke_render(item, ctx, origin, page); + } +} + +// TODO change this to accept a const SPItem: +void CairoRenderer::renderItem(CairoRenderContext *ctx, SPItem *item, SPItem *origin, SPPage *page) +{ + ctx->pushState(); + setStateForItem(ctx, item); + + CairoRenderState *state = ctx->getCurrentState(); + state->need_layer = ( state->mask || state->clip_path || state->opacity != 1.0 ); + SPStyle* style = item->style; + SPGroup * group = dynamic_cast(item); + bool blend = false; + if (group && style->mix_blend_mode.set && style->mix_blend_mode.value != SP_CSS_BLEND_NORMAL) { + state->need_layer = true; + blend = true; + } + // Draw item on a temporary surface so a mask, clip-path, or opacity can be applied to it. + if (state->need_layer) { + state->merge_opacity = FALSE; + ctx->pushLayer(); + } + + ctx->transform(item->transform); + + _doRender(item, ctx, origin, page); + + if (state->need_layer) { + if (blend) { + ctx->popLayer(ink_css_blend_to_cairo_operator(style->mix_blend_mode.value)); // This applies clipping/masking + } else { + ctx->popLayer(); // This applies clipping/masking + } + } + ctx->popState(); +} + +void CairoRenderer::renderHatchPath(CairoRenderContext *ctx, SPHatchPath const &hatchPath, unsigned key) { + ctx->pushState(); + ctx->setStateForStyle(hatchPath.style); + ctx->transform(Geom::Translate(hatchPath.offset.computed, 0)); + + std::unique_ptr curve = hatchPath.calculateRenderCurve(key); + Geom::PathVector const & pathv =curve->get_pathvector(); + if (!pathv.empty()) { + ctx->renderPathVector(pathv, hatchPath.style, Geom::OptRect()); + } + + ctx->popState(); +} + +void CairoRenderer::setMetadata(CairoRenderContext *ctx, SPDocument *doc) { + // title + const gchar *title = rdf_get_work_entity(doc, rdf_find_entity("title")); + if (title) { + ctx->_metadata.title = title; + } + + // author + const gchar *author = rdf_get_work_entity(doc, rdf_find_entity("creator")); + if (author) { + ctx->_metadata.author = author; + } + + // subject + const gchar *subject = rdf_get_work_entity(doc, rdf_find_entity("description")); + if (subject) { + ctx->_metadata.subject = subject; + } + + // keywords + const gchar *keywords = rdf_get_work_entity(doc, rdf_find_entity("subject")); + if (keywords) { + ctx->_metadata.keywords = keywords; + } + + // copyright + const gchar *copyright = rdf_get_work_entity(doc, rdf_find_entity("rights")); + if (copyright) { + ctx->_metadata.copyright = copyright; + } + + // creator + ctx->_metadata.creator = Glib::ustring::compose("Inkscape %1 (https://inkscape.org)", + Inkscape::version_string_without_revision); + + // cdate (only used for for reproducible builds hack) + Glib::ustring cdate = ReproducibleBuilds::now_iso_8601(); + if (!cdate.empty()) { + ctx->_metadata.cdate = cdate; + } + + // mdate (currently unused) +} + +bool +CairoRenderer::setupDocument(CairoRenderContext *ctx, SPDocument *doc, bool pageBoundingBox, double bleedmargin_px, SPItem *base) +{ +// PLEASE note when making changes to the boundingbox and transform calculation, corresponding changes should be made to LaTeXTextRenderer::setupDocument !!! + + g_assert( ctx != nullptr ); + + if (!base) { + base = doc->getRoot(); + } + + Geom::Rect d; + if (pageBoundingBox) { + d = Geom::Rect::from_xywh(Geom::Point(0,0), doc->getDimensions()); + } else { + Geom::OptRect bbox = base->documentVisualBounds(); + if (!bbox) { + g_message("CairoRenderer: empty bounding box."); + return false; + } + d = *bbox; + } + d.expandBy(bleedmargin_px); + + double px_to_ctx_units = 1.0; + if (ctx->_vector_based_target) { + // convert from px to pt + px_to_ctx_units = Inkscape::Util::Quantity::convert(1, "px", "pt"); + } + + auto width = d.width() * px_to_ctx_units; + auto height = d.height() * px_to_ctx_units; + + setMetadata(ctx, doc); + + TRACE(("setupDocument: %f x %f\n", width, height)); + bool ret = ctx->setupSurface(width, height); + + if (ret) { + if (pageBoundingBox) { + // translate to set bleed/margin + Geom::Affine tp( Geom::Translate( bleedmargin_px, bleedmargin_px ) ); + ctx->transform(tp); + } else { + // this transform translates the export drawing to a virtual page (0,0)-(width,height) + Geom::Affine tp(Geom::Translate(-d.min())); + ctx->transform(tp); + } + } + + return ret; +} + +// Apply an SVG clip path +void +CairoRenderer::applyClipPath(CairoRenderContext *ctx, SPClipPath const *cp) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (cp == nullptr) + return; + + CairoRenderContext::CairoRenderMode saved_mode = ctx->getRenderMode(); + ctx->setRenderMode(CairoRenderContext::RENDER_MODE_CLIP); + + // FIXME: the access to the first clippath view to obtain the bbox is completely bogus + Geom::Affine saved_ctm; + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && cp->display->bbox) { + Geom::Rect clip_bbox = *cp->display->bbox; + Geom::Affine t(Geom::Scale(clip_bbox.dimensions())); + t[4] = clip_bbox.left(); + t[5] = clip_bbox.top(); + t *= ctx->getCurrentState()->transform; + saved_ctm = ctx->getTransform(); + ctx->setTransform(t); + } + + TRACE(("BEGIN clip\n")); + SPObject const *co = cp; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast(&child); + if (item) { + + // combine transform of the item in clippath and the item using clippath: + Geom::Affine tempmat = item->transform * ctx->getCurrentState()->item_transform; + + // render this item in clippath + ctx->pushState(); + ctx->transform(tempmat); + setStateForItem(ctx, item); + // TODO fix this call to accept const items + _doRender(const_cast(item), ctx); + ctx->popState(); + } + } + TRACE(("END clip\n")); + + // do clipping only if this was the first call to applyClipPath + if (ctx->getClipMode() == CairoRenderContext::CLIP_MODE_PATH + && saved_mode == CairoRenderContext::RENDER_MODE_NORMAL) + cairo_clip(ctx->_cr); + + if (cp->clipPathUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX) + ctx->setTransform(saved_ctm); + + ctx->setRenderMode(saved_mode); +} + +// Apply an SVG mask +void +CairoRenderer::applyMask(CairoRenderContext *ctx, SPMask const *mask) +{ + g_assert( ctx != nullptr && ctx->_is_valid ); + + if (mask == nullptr) + return; + + // FIXME: the access to the first mask view to obtain the bbox is completely bogus + // TODO: should the bbox be transformed if maskUnits != userSpaceOnUse ? + if (mask->maskContentUnits == SP_CONTENT_UNITS_OBJECTBOUNDINGBOX && mask->display->bbox) { + Geom::Rect mask_bbox = *mask->display->bbox; + Geom::Affine t(Geom::Scale(mask_bbox.dimensions())); + t[4] = mask_bbox.left(); + t[5] = mask_bbox.top(); + t *= ctx->getCurrentState()->transform; + ctx->setTransform(t); + } + + // Clip mask contents... but... + // The mask's bounding box is the "geometric bounding box" which doesn't allow for + // filters which extend outside the bounding box. So don't clip. + // ctx->addClippingRect(mask_bbox.x0, mask_bbox.y0, mask_bbox.x1 - mask_bbox.x0, mask_bbox.y1 - mask_bbox.y0); + + ctx->pushState(); + + TRACE(("BEGIN mask\n")); + SPObject const *co = mask; + for (auto& child: co->children) { + SPItem const *item = dynamic_cast(&child); + if (item) { + // TODO fix const correctness: + renderItem(ctx, const_cast(item)); + } + } + TRACE(("END mask\n")); + + ctx->popState(); +} + +void +calculatePreserveAspectRatio(unsigned int aspect_align, unsigned int aspect_clip, double vp_width, double vp_height, + double *x, double *y, double *width, double *height) +{ + if (aspect_align == SP_ASPECT_NONE) + return; + + double scalex, scaley, scale; + double new_width, new_height; + scalex = *width / vp_width; + scaley = *height / vp_height; + scale = (aspect_clip == SP_ASPECT_MEET) ? MIN(scalex, scaley) : MAX(scalex, scaley); + new_width = vp_width * scale; + new_height = vp_height * scale; + /* Now place viewbox to requested position */ + switch (aspect_align) { + case SP_ASPECT_XMIN_YMIN: + break; + case SP_ASPECT_XMID_YMIN: + *x -= 0.5 * (new_width - *width); + break; + case SP_ASPECT_XMAX_YMIN: + *x -= 1.0 * (new_width - *width); + break; + case SP_ASPECT_XMIN_YMID: + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMID: + *x -= 0.5 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMID: + *x -= 1.0 * (new_width - *width); + *y -= 0.5 * (new_height - *height); + break; + case SP_ASPECT_XMIN_YMAX: + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMID_YMAX: + *x -= 0.5 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + case SP_ASPECT_XMAX_YMAX: + *x -= 1.0 * (new_width - *width); + *y -= 1.0 * (new_height - *height); + break; + default: + break; + } + *width = new_width; + *height = new_height; +} + +#include "clear-n_.h" + +} /* namespace Internal */ +} /* namespace Extension */ +} /* namespace Inkscape */ + +#undef TRACE + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 : -- cgit v1.2.3