// 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 :