// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Canvas item belonging to an SVG drawing element. *//* * Authors: * Krzysztof KosiƄski * * Copyright (C) 2011 Authors * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include #include "display/drawing-context.h" #include "display/drawing-group.h" #include "display/drawing-item.h" #include "display/drawing-pattern.h" #include "display/drawing-surface.h" #include "display/drawing-text.h" #include "display/drawing.h" #include "display/cairo-utils.h" #include "display/cairo-templates.h" #include "display/control/canvas-item-drawing.h" #include "ui/widget/canvas.h" // Mark area for redrawing. #include "nr-filter.h" #include "style.h" #include "object/sp-item.h" static constexpr auto CACHE_SCORE_THRESHOLD = 50000.0; ///< Do not consider objects for caching below this score. namespace Inkscape { struct CacheData { mutable std::mutex mutables; mutable std::optional surface; }; /** * @class DrawingItem * SVG drawing item for display. * * This class represents the renderable portion of the SVG document. Typically this * is created by the SP tree, in particular the invoke_show() virtual function. * * @section ObjectLifetime Object lifetime * Deleting a DrawingItem will cause all of its children to be deleted as well. * This can lead to nasty surprises if you hold references to things * which are children of what is being deleted. Therefore, in the SP tree, * you always need to delete the item views of children before deleting * the view of the parent. Do not call delete on things returned from invoke_show() * - this will cause dangling pointers inside the SPItem and lead to a crash. * Use the corresponding invoke_hide() method. * * Outside of the SP tree, you should not use any references after the root node * has been deleted. */ DrawingItem::DrawingItem(Drawing &drawing) : _drawing(drawing) , _parent(nullptr) , _key(0) , _style(nullptr) , _context_style(nullptr) , _contains_unisolated_blend(false) , style_vector_effect_size(false) , style_vector_effect_rotate(false) , style_vector_effect_fixed(false) , _opacity(1.0) , _clip(nullptr) , _mask(nullptr) , _fill_pattern(nullptr) , _stroke_pattern(nullptr) , _item(nullptr) , _state(0) , _child_type(ChildType::ORPHAN) , _background_new(0) , _background_accumulate(0) , _visible(true) , _sensitive(true) , _cached_persistent(0) , _has_cache_iterator(0) , _propagate_state(0) , _pick_children(0) , _antialias(2) , _isolation(SP_CSS_ISOLATION_AUTO) , _blend_mode(SP_CSS_BLEND_NORMAL) { } DrawingItem::~DrawingItem() { // Unactivate if active. if (auto itemdrawing = _drawing.getCanvasItemDrawing()) { if (itemdrawing->get_active() == this) { itemdrawing->set_active(nullptr); } } else { // Typically happens, e.g. for any non-Canvas Drawing. } // Remove caching candidate entry. if (_has_cache_iterator) { _drawing._candidate_items.erase(_cache_iterator); } // Remove from the set of cached items and delete cache. _setCached(false, true); _children.clear_and_dispose([] (auto c) { delete c; }); delete _clip; delete _mask; delete static_cast(_fill_pattern); delete static_cast(_stroke_pattern); } /// Returns true if item is among the descendants. Will return false if item == this. bool DrawingItem::isAncestorOf(DrawingItem const *item) const { for (auto c = item->_parent; c; c = c->_parent) { if (c == this) return true; } return false; } bool DrawingItem::unisolatedBlend() const { if (_blend_mode != SP_CSS_BLEND_NORMAL) { return true; } else if (_mask || _filter || _opacity < 0.995 || _isolation == SP_CSS_ISOLATION_ISOLATE) { return false; } else { return _contains_unisolated_blend; } } void DrawingItem::appendChild(DrawingItem *item) { // Ok to perform non-deferred modification of child, because not part of rendering tree yet. assert(item->_child_type == ChildType::ORPHAN); item->_parent = this; item->_child_type = ChildType::NORMAL; defer([=] { _children.push_back(*item); // This ensures that _markForUpdate() called on the child will recurse to this item item->_state = STATE_ALL; // Because _markForUpdate recurses through ancestors, we can simply call it // on the just-added child. This has the additional benefit that we do not // rely on the appended child being in the default non-updated state. // We set propagate to true, because the child might have descendants of its own. item->_markForUpdate(STATE_ALL, true); }); } void DrawingItem::prependChild(DrawingItem *item) { // See appendChild for explanations. assert(item->_child_type == ChildType::ORPHAN); item->_parent = this; item->_child_type = ChildType::NORMAL; defer([=] { _children.push_front(*item); item->_state = STATE_ALL; item->_markForUpdate(STATE_ALL, true); }); } // Clear this node's ordinary children, deleting them and their descendants without otherwise changing them in any way. void DrawingItem::clearChildren() { defer([=] { if (_children.empty()) return; _markForRendering(); _children.clear_and_dispose([] (auto c) { delete c; }); _markForUpdate(STATE_ALL, false); }); } void DrawingItem::setTransform(Geom::Affine const &transform) { defer([=] { auto constexpr EPS = 1e-18; auto current = _transform ? *_transform : Geom::identity(); if (Geom::are_near(transform, current, EPS)) return; _markForRendering(); _transform = transform.isIdentity(EPS) ? nullptr : std::make_unique(transform); _markForUpdate(STATE_ALL, true); }); } void DrawingItem::setOpacity(float opacity) { defer([=] { if (opacity == _opacity) return; _opacity = opacity; _markForRendering(); }); } void DrawingItem::setAntialiasing(unsigned antialias) { defer([=] { if (_antialias == antialias) return; _antialias = antialias; _markForRendering(); }); } void DrawingItem::setIsolation(bool isolation) { defer([=] { if (isolation == _isolation) return; _isolation = isolation; _markForRendering(); }); } void DrawingItem::setBlendMode(SPBlendMode blend_mode) { defer([=] { if (blend_mode == _blend_mode) return; _blend_mode = blend_mode; _markForRendering(); }); } void DrawingItem::setVisible(bool visible) { defer([=] { if (visible == _visible) return; _visible = visible; _markForRendering(); }); } void DrawingItem::setSensitive(bool sensitive) { defer([=] { // Must be deferred, since in bitfield. _sensitive = sensitive; }); } /** * Enable / disable storing the rendering in memory. * Calling setCached(false, true) will also remove the persistent status */ void DrawingItem::_setCached(bool cached, bool persistent) { static bool const cache_env = getenv("_INKSCAPE_DISABLE_CACHE"); if (cache_env) { return; } if (persistent) { _cached_persistent = cached && persistent; } else if (_cached_persistent) { return; } if (cached == (bool)_cache) { return; } if (cached) { _cache = std::make_unique(); _drawing._cached_items.insert(this); } else { _cache.reset(); _drawing._cached_items.erase(this); } } /** * Process information related to the new style. * * Note: _style is not used by DrawingGlyphs which uses its parent style. */ void DrawingItem::setStyle(SPStyle const *style, SPStyle const *context_style) { // Ok to not defer setting the style pointer, because the pointer itself is only read by SPObject-side code. _style = style; if (context_style) { _context_style = context_style; } else if (_parent) { _context_style = _parent->_context_style; } // Copy required information out of style. bool background_new = false; bool vector_effect_size = false; bool vector_effect_rotate = false; bool vector_effect_fixed = false; if (style) { background_new = style->enable_background.set && style->enable_background.value == SP_CSS_BACKGROUND_NEW; vector_effect_size = _style->vector_effect.size; vector_effect_rotate = _style->vector_effect.rotate; vector_effect_fixed = _style->vector_effect.fixed; } // Defer setting the style information on the DrawingItem. defer([=] { _markForRendering(); if (background_new != _background_new) { _background_new = background_new; _markForUpdate(STATE_BACKGROUND, true); } style_vector_effect_size = vector_effect_size; style_vector_effect_rotate = vector_effect_rotate; style_vector_effect_fixed = vector_effect_fixed; _markForUpdate(STATE_ALL, false); }); } /** * Recursively update children style. * The purpose of this call is to update fill and stroke for markers that have elements with * fill/stroke property values of 'context-fill' or 'context-stroke'. Marker styling is not * updated like other 'clones' as marker instances are not included the SP object tree. * Note: this is a virtual function. */ void DrawingItem::setChildrenStyle(SPStyle const *context_style) { _context_style = context_style; for (auto &i : _children) { i.setChildrenStyle(context_style); } } void DrawingItem::setClip(DrawingItem *item) { if (item) { assert(item->_child_type == ChildType::ORPHAN); item->_parent = this; item->_child_type = ChildType::CLIP; } defer([=] { _markForRendering(); delete _clip; _clip = item; _markForUpdate(STATE_ALL, true); }); } void DrawingItem::setMask(DrawingItem *item) { if (item) { assert(item->_child_type == ChildType::ORPHAN); item->_parent = this; item->_child_type = ChildType::MASK; } defer([=] { _markForRendering(); delete _mask; _mask = item; _markForUpdate(STATE_ALL, true); }); } void DrawingItem::setFillPattern(DrawingPattern *pattern) { if (pattern) { assert(pattern->_child_type == ChildType::ORPHAN); pattern->_parent = this; pattern->_child_type = ChildType::FILL; } defer([=] { _markForRendering(); delete static_cast(_fill_pattern); _fill_pattern = pattern; _markForUpdate(STATE_ALL, false); }); } void DrawingItem::setStrokePattern(DrawingPattern *pattern) { if (pattern) { assert(pattern->_child_type == ChildType::ORPHAN); pattern->_parent = this; pattern->_child_type = ChildType::STROKE; } defer([=] { _markForRendering(); delete static_cast(_stroke_pattern); _stroke_pattern = pattern; _markForUpdate(STATE_ALL, false); }); } /// Move this item to the given place in the Z order of siblings. Does nothing if the item is not a normal child. void DrawingItem::setZOrder(unsigned zorder) { if (_child_type != ChildType::NORMAL) return; defer([=] { auto it = _parent->_children.iterator_to(*this); _parent->_children.erase(it); auto it2 = _parent->_children.begin(); std::advance(it2, std::min(zorder, _parent->_children.size())); _parent->_children.insert(it2, *this); _markForRendering(); }); } void DrawingItem::setItemBounds(Geom::OptRect const &bounds) { defer([=] { _item_bbox = bounds; }); } void DrawingItem::setFilterRenderer(std::unique_ptr filter) { defer([=, filter = std::move(filter)] () mutable { _filter = std::move(filter); _markForRendering(); }); } /** * Update derived data before operations. * The purpose of this call is to recompute internal data which depends * on the attributes of the object, but is not directly settable by the user. * Precomputing this data speeds up later rendering, because some items * can be omitted. * * Currently this method handles updating the visual and geometric bounding boxes * in pixels, storing the total transformation from item space to the screen * and cache invalidation. * * @param area Area to which the update should be restricted. Only takes effect * if the bounding box is known. * @param ctx A structure to store cascading state. * @param flags Which internal data should be recomputed. This can be any combination * of StateFlags. * @param reset State fields that should be reset before processing them. This is * a means to force a recomputation of internal data even if the item * considers it up to date. Mainly for internal use, such as * propagating bounding box recomputation to children when the item's * transform changes. */ void DrawingItem::update(Geom::IntRect const &area, UpdateContext const &ctx, unsigned flags, unsigned reset) { // We don't need to update what is not visible if (!_visible) { _state = STATE_ALL; // Touch the state for future change to this item return; } bool const outline = _drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay(); bool const filters = _drawing.renderMode() != RenderMode::NO_FILTERS; bool const forcecache = _filter && filters; // Set reset flags according to propagation status reset |= _propagate_state; _propagate_state = 0; _state &= ~reset; // reset state of this item if ((~_state & flags) == 0) return; // nothing to do // TODO this might be wrong if (_state & STATE_BBOX) { // we have up-to-date bbox if (!area.intersects(outline ? _bbox : _drawbox)) return; } // compute which elements need an update unsigned to_update = _state ^ flags; // this needs to be called before we recurse into children if (to_update & STATE_BACKGROUND) { _background_accumulate = _background_new; if (_child_type == ChildType::NORMAL && _parent->_background_accumulate) _background_accumulate = true; } UpdateContext child_ctx(ctx); if (_transform) { child_ctx.ctm = *_transform * ctx.ctm; } // Vector effects if (style_vector_effect_fixed) { child_ctx.ctm.setTranslation(Geom::Point(0, 0)); } if (style_vector_effect_size) { double value = child_ctx.ctm.descrim(); if (value > 0.0) { child_ctx.ctm[0] /= value; child_ctx.ctm[1] /= value; child_ctx.ctm[2] /= value; child_ctx.ctm[3] /= value; } } if (style_vector_effect_rotate) { double value = child_ctx.ctm.descrim(); child_ctx.ctm[0] = value; child_ctx.ctm[1] = 0.0; child_ctx.ctm[2] = 0.0; child_ctx.ctm[3] = value; } // Remember the transformation matrix. Geom::Affine ctm_change; bool affine_changed = false; if (!Geom::are_near(_ctm, child_ctx.ctm)) { ctm_change = _ctm.inverse() * child_ctx.ctm; affine_changed = true; } _ctm = child_ctx.ctm; bool const totally_invalidated = reset & STATE_TOTAL_INV; if (totally_invalidated) { // Perform work that would have been done by our call to _markForRendering(), // had it not been overshadowed by a totally-invalidating node. if (_cache && _cache->surface) { _cache->surface->markDirty(); } _dropPatternCache(); } // Decide whether this node should be a totally-invalidating node. bool const totally_invalidate = _update_complexity >= 20 && affine_changed; if (totally_invalidate) { reset |= STATE_TOTAL_INV; } // Recalculate update complexity; to be recalculated immediately below and by _updateItem(). _update_complexity = 1; auto add_complexity_if = [&] (DrawingItem *c) { if (c) { _update_complexity += c->_update_complexity; } }; add_complexity_if(_clip); add_complexity_if(_mask); add_complexity_if(_fill_pattern); add_complexity_if(_stroke_pattern); // Reset contains_unisolated_blend; to be recalculated by _updateItem(). _contains_unisolated_blend = false; // Moved from code that was previously in render(). if (forcecache) { _setCached((bool)_cacheRect(), true); } // update _bbox and call this function for children _state = _updateItem(area, child_ctx, flags, reset); // update drawingitems contained in filter if (_filter) { _filter->update(); } if (to_update & STATE_BBOX) { // compute drawbox if (_filter && filters) { Geom::OptRect enlarged = _filter->filter_effect_area(_item_bbox); if (enlarged) { *enlarged *= ctm(); _drawbox = enlarged->roundOutwards(); } else { _drawbox = Geom::OptIntRect(); } } else { _drawbox = _bbox; } // Clipping if (_clip) { _clip->update(area, child_ctx, flags, reset); if (outline) { _bbox.unionWith(_clip->_bbox); } else { _drawbox.intersectWith(_clip->_bbox); } } // Masking if (_mask) { _mask->update(area, child_ctx, flags, reset); if (outline) { _bbox.unionWith(_mask->_bbox); } else { // for masking, we need full drawbox of mask _drawbox.intersectWith(_mask->_drawbox); } } // Crude fix for outline overlay bbox issues with filtered objects. // (Real solution is to carefully review all bbox/drawbox uses.) if (_drawing.outlineOverlay()) { _bbox |= _drawbox; } } if (to_update & STATE_CACHE) { // Remove old cache iterator. if (_has_cache_iterator) { _drawing._candidate_items.erase(_cache_iterator); _has_cache_iterator = false; } // Determine whether this item is cachable. bool isolated = _mask || _filter || _opacity < 0.995 || _blend_mode != SP_CSS_BLEND_NORMAL || _isolation == SP_CSS_ISOLATION_ISOLATE || _child_type == ChildType::ROOT; bool cacheable = !_contains_unisolated_blend || isolated; // Determine whether to make this item eligible for caching, by creating a cache iterator. double score = _cacheScore(); if (score >= CACHE_SCORE_THRESHOLD && cacheable) { CacheRecord cr; cr.score = score; // if _cacheRect() is empty, a negative score will be returned from _cacheScore(), // so this will not execute (cache score threshold must be positive) cr.cache_size = _cacheRect()->area() * 4; cr.item = this; auto it = std::lower_bound(_drawing._candidate_items.begin(), _drawing._candidate_items.end(), cr, std::greater()); _cache_iterator = _drawing._candidate_items.insert(it, cr); _has_cache_iterator = true; } /* Update cache if enabled. * General note: here we only tell the cache how it has to transform * during the render phase. The transformation is deferred because * after the update the item can have its caching turned off, * e.g. because its filter was removed. This way we avoid temporarily * using more memory than the cache budget */ if (_cache && _cache->surface) { Geom::OptIntRect cl = _cacheRect(); if (_visible && cl && _has_cache_iterator) { // never create cache for invisible items // this takes care of invalidation on transform _cache->surface->scheduleTransform(*cl, ctm_change); } else { // Destroy cache for this item - outside of canvas or invisible. // The opposite transition (invisible -> visible or object // entering the canvas) is handled during the render phase _setCached(false, true); } } } if (to_update & STATE_RENDER) { // now that we know drawbox, dirty the corresponding rect on canvas // unless filtered, groups do not need to render by themselves, only their members if (_fill_pattern) { _fill_pattern->update(area, child_ctx, flags, reset); } if (_stroke_pattern) { _stroke_pattern->update(area, child_ctx, flags, reset); } if (!totally_invalidated) { if (!is(this) || (_filter && filters) || totally_invalidate) { _markForRendering(); } } } } struct MaskLuminanceToAlpha { guint32 operator()(guint32 in) { guint r = 0, g = 0, b = 0; Display::ExtractRGB32(in, r, g, b); // the operation of unpremul -> luminance-to-alpha -> multiply by alpha // is equivalent to luminance-to-alpha on premultiplied color values // original computation in double: r*0.2125 + g*0.7154 + b*0.0721 guint32 ao = r*109 + g*366 + b*37; // coeffs add up to 512 return ((ao + 256) << 15) & 0xff000000; // equivalent to ((ao + 256) / 512) << 24 } }; /** * Rasterize items. * This method submits the drawing operations required to draw this item * to the supplied DrawingContext, restricting drawing the specified area. * * This method does some common tasks and calls the item-specific rendering * function, _renderItem(), to render e.g. paths or bitmaps. * * @param flags Rendering options. This deals mainly with cache control. */ unsigned DrawingItem::render(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags, DrawingItem const *stop_at) const { bool const outline = flags & RENDER_OUTLINE; bool const render_filters = !(flags & RENDER_NO_FILTERS); bool const forcecache = _filter && render_filters; // stop_at is handled in DrawingGroup, but this check is required to handle the case // where a filtered item with background-accessing filter has enable-background: new if (this == stop_at) { return RENDER_STOP; } // If we are invisible, return immediately if (!_visible) { return RENDER_OK; } if (_ctm.isSingular(1e-18)) { return RENDER_OK; } // TODO convert outline rendering to a separate virtual function if (outline) { _renderOutline(dc, rc, area, flags); return RENDER_OK; } Geom::OptIntRect carea = area & _drawbox; if (!carea) { return RENDER_OK; } Geom::OptIntRect iarea = carea; // expand carea to contain the dependent area of filters. if (forcecache) { iarea = _cacheRect(); if (!iarea) { iarea = carea; _filter->area_enlarge(*iarea, this); iarea.intersectWith(_drawbox); } } // carea is the area to paint carea = iarea & _drawbox; if (!carea) { return RENDER_OK; } // Device scale for HiDPI screens (typically 1 or 2) int const device_scale = dc.surface()->device_scale(); std::unique_lock lock; // Render from cache if possible, unless requested not to (hatches). if (_cache && !(flags & RENDER_BYPASS_CACHE)) { lock = std::unique_lock(_cache->mutables); if (_cache->surface) { if (_cache->surface->device_scale() != device_scale) { _cache->surface->markDirty(); } _cache->surface->prepare(); dc.setOperator(ink_css_blend_to_cairo_operator(_blend_mode)); _cache->surface->paintFromCache(dc, carea, forcecache); if (!carea) { dc.setSource(0, 0, 0, 0); return RENDER_OK; } } else { // There is no cache. This could be because caching of this item // was just turned on after the last update phase, or because // we were previously outside of the canvas. Geom::OptIntRect cl = _cacheRect(); if (!cl) cl = carea; _cache->surface.emplace(*cl, device_scale); } if (!forcecache) { lock.unlock(); // Only hold the lock for the full duration of rendering for filters. } } else { // if our caching was turned off after the last update, it was already deleted in setCached() } // determine whether this shape needs intermediate rendering. bool const greyscale = _drawing.colorMode() == ColorMode::GRAYSCALE && !(flags & RENDER_OUTLINE); bool const isolate_root = _contains_unisolated_blend || greyscale; bool const needs_intermediate_rendering = _clip // 1. it has a clipping path || _mask // 2. it has a mask || (_filter && render_filters) // 3. it has a filter || _opacity < 0.995 // 4. it is non-opaque || _blend_mode != SP_CSS_BLEND_NORMAL // 5. it has blend mode || _isolation == SP_CSS_ISOLATION_ISOLATE // 6. it is isolated || (_child_type == ChildType::ROOT && isolate_root) // 7. it is the root and needs isolation || (bool)_cache; // 8. it is to be cached /* How the rendering is done. * * Clipping, masking and opacity are done by rendering them to a surface * and then compositing the object's rendering onto it with the IN operator. * The object itself is rendered to a group. * * Opacity is done by rendering the clipping path with an alpha * value corresponding to the opacity. If there is no clipping path, * the entire intermediate surface is painted with alpha corresponding * to the opacity value. * */ // Short-circuit the simple case. // We also use this path for filter background rendering, because masking, clipping, // filters and opacity do not apply when rendering the ancestors of the filtered // element if ((flags & RENDER_FILTER_BACKGROUND) || !needs_intermediate_rendering) { dc.setOperator(ink_css_blend_to_cairo_operator(SP_CSS_BLEND_NORMAL)); return _renderItem(dc, rc, *carea, flags & ~RENDER_FILTER_BACKGROUND, stop_at); } DrawingSurface intermediate(*carea, device_scale); DrawingContext ict(intermediate); cairo_set_antialias(ict.raw(), cairo_get_antialias(dc.raw())); // propagate antialias setting // This path fails for patterns/hatches when stepping the pattern to handle overflows. // The offsets are applied to drawing context (dc) but they are not copied to the // intermediate context. Something like this is needed: // Copy cairo matrix from dc to intermediate, needed for patterns/hatches // cairo_matrix_t cairo_matrix; // cairo_get_matrix(dc.raw(), &cairo_matrix); // cairo_set_matrix(ict.raw(), &cairo_matrix); // For the moment we disable caching for patterns, // see https://gitlab.com/inkscape/inkscape/-/issues/309 unsigned render_result = RENDER_OK; // 1. Render clipping path with alpha = opacity. ict.setSource(0,0,0,_opacity); // Since clip can be combined with opacity, the result could be incorrect // for overlapping clip children. To fix this we use the SOURCE operator // instead of the default OVER. ict.setOperator(CAIRO_OPERATOR_SOURCE); ict.paint(); if (_clip) { ict.pushGroup(); _clip->clip(ict, rc, *carea); ict.popGroupToSource(); ict.setOperator(CAIRO_OPERATOR_IN); ict.paint(); } ict.setOperator(CAIRO_OPERATOR_OVER); // reset back to default // 2. Render the mask if present and compose it with the clipping path + opacity. if (_mask) { ict.pushGroup(); _mask->render(ict, rc, *carea, flags); cairo_surface_t *mask_s = ict.rawTarget(); // Convert mask's luminance to alpha ink_cairo_surface_filter(mask_s, mask_s, MaskLuminanceToAlpha()); ict.popGroupToSource(); ict.setOperator(CAIRO_OPERATOR_IN); ict.paint(); ict.setOperator(CAIRO_OPERATOR_OVER); } // 3. Render object itself ict.pushGroup(); render_result = _renderItem(ict, rc, *carea, flags, stop_at); // 4. Apply filter. if (_filter && render_filters) { bool rendered = false; if (_filter->uses_background() && _background_accumulate) { auto bg_root = this; for (; bg_root; bg_root = bg_root->_parent) { if (bg_root->_background_new || bg_root->_filter) break; } if (bg_root) { DrawingSurface bg(*carea, device_scale); DrawingContext bgdc(bg); bg_root->render(bgdc, rc, *carea, flags | RENDER_FILTER_BACKGROUND, this); _filter->render(this, ict, &bgdc, rc); rendered = true; } } if (!rendered) { _filter->render(this, ict, nullptr, rc); } // Note that because the object was rendered to a group, // the internals of the filter need to use cairo_get_group_target() // instead of cairo_get_target(). } // 4b. Apply greyscale rendering mode, if root node. if (greyscale && _child_type == ChildType::ROOT) { ink_cairo_surface_filter(ict.rawTarget(), ict.rawTarget(), _drawing.grayscaleMatrix()); } // 5. Render object inside the composited mask + clip ict.popGroupToSource(); ict.setOperator(CAIRO_OPERATOR_IN); ict.paint(); // 6. Paint the completed rendering onto the base context (or into cache) if (_cache && !(flags & RENDER_BYPASS_CACHE)) { if (!forcecache) { lock.lock(); // Only hold the lock for the full duration of rendering for filters. } assert(lock); assert(_cache->surface); auto cachect = DrawingContext(*_cache->surface); cachect.rectangle(*carea); cachect.setOperator(CAIRO_OPERATOR_SOURCE); cachect.setSource(&intermediate); cachect.fill(); _cache->surface->markClean(*carea); } dc.rectangle(*carea); dc.setSource(&intermediate); // 7. Render blend mode dc.setOperator(ink_css_blend_to_cairo_operator(_blend_mode)); dc.fill(); dc.setSource(0,0,0,0); // Web isolation only works if parent doesn't have transform // the call above is to clear a ref on the intermediate surface held by dc return render_result; } /** * A stand alone render, ignoring all other objects in the document. */ unsigned DrawingItem::render(DrawingContext &dc, Geom::IntRect const &area, unsigned flags) const { auto rc = RenderContext{ 0xff }; // black outlines return render(dc, rc, area, flags); } void DrawingItem::_renderOutline(DrawingContext &dc, RenderContext &rc, Geom::IntRect const &area, unsigned flags) const { // intersect with bbox rather than drawbox, as we want to render things outside // of the clipping path as well auto carea = Geom::intersect(area, _bbox); if (!carea) return; // just render everything: item, clip, mask // First, render the object itself _renderItem(dc, rc, *carea, flags, nullptr); // render clip and mask, if any auto saved_rgba = rc.outline_color; // save current outline color // render clippath as an object, using a different color if (_clip) { rc.outline_color = _drawing.clipOutlineColor(); _clip->render(dc, rc, *carea, flags); } // render mask as an object, using a different color if (_mask) { rc.outline_color = _drawing.maskOutlineColor(); _mask->render(dc, rc, *carea, flags); } rc.outline_color = saved_rgba; // restore outline color } /** * Rasterize the clipping path. * This method submits drawing operations required to draw a basic filled shape * of the item to the supplied drawing context. Rendering is limited to the * given area. The rendering of the clipped object is composited into * the result of this call using the IN operator. See the implementation * of render() for details. */ void DrawingItem::clip(DrawingContext &dc, Inkscape::RenderContext &rc, Geom::IntRect const &area) const { // don't bother if the object does not implement clipping (e.g. DrawingImage) if (!_canClip()) return; if (!_visible) return; if (!area.intersects(_bbox)) return; dc.setSource(0,0,0,1); dc.pushGroup(); // rasterize the clipping path _clipItem(dc, rc, area); if (_clip) { // The item used as the clipping path itself has a clipping path. // Render this item's clipping path onto a temporary surface, then composite it // with the item using the IN operator dc.pushGroup(); _clip->clip(dc, rc, area); dc.popGroupToSource(); dc.setOperator(CAIRO_OPERATOR_IN); dc.paint(); } dc.popGroupToSource(); dc.setOperator(CAIRO_OPERATOR_OVER); dc.paint(); dc.setSource(0,0,0,0); } /** * Get the item under the specified point. * Searches the tree for the first item in the Z-order which is closer than * @a delta to the given point. The pick should be visual - for example * an object with a thick stroke should pick on the entire area of the stroke. * @param p Search point * @param delta Maximum allowed distance from the point * @param sticky Whether the pick should ignore visibility and sensitivity. * When false, only visible and sensitive objects are considered. * When true, invisible and insensitive objects can also be picked. */ DrawingItem *DrawingItem::pick(Geom::Point const &p, double delta, unsigned flags) { // Sometimes there's no BBOX in state, reason unknown (bug 992817) // I made this not an assert to remove the warning if (!(_state & STATE_BBOX) || !(_state & STATE_PICK)) { g_warning("Invalid state when picking: STATE_BBOX = %d, STATE_PICK = %d", _state & STATE_BBOX, _state & STATE_PICK); return nullptr; } // ignore invisible and insensitive items unless sticky if (!(flags & PICK_STICKY) && !(_visible && _sensitive)) { return nullptr; } bool outline = flags & PICK_OUTLINE; if (!outline) { // pick inside clipping path; if NULL, it means the object is clipped away there if (_clip) { DrawingItem *cpick = _clip->pick(p, delta, flags | PICK_AS_CLIP); if (!cpick) { return nullptr; } } // same for mask if (_mask) { DrawingItem *mpick = _mask->pick(p, delta, flags); if (!mpick) { return nullptr; } } } Geom::OptIntRect box = outline || (flags & PICK_AS_CLIP) ? _bbox : _drawbox; if (!box) { return nullptr; } Geom::Rect expanded = *box; expanded.expandBy(delta); auto dglyps = cast(this); if (dglyps && !(flags & PICK_AS_CLIP)) { expanded = dglyps->getPickBox(); } if (expanded.contains(p)) { return _pickItem(p, delta, flags); } return nullptr; } // For debugging Glib::ustring DrawingItem::name() const { if (_item) { if (_item->getId()) return _item->getId(); else return "No object id"; } else { return "No associated object"; } } // For debugging: Print drawing tree structure. void DrawingItem::recursivePrintTree(unsigned level) const { if (level == 0) { std::cout << "Display Item Tree" << std::endl; } std::cout << "DI: "; for (int i = 0; i < level; i++) { std::cout << " "; } std::cout << name() << std::endl; for (auto &i : _children) { i.recursivePrintTree(level + 1); } } /** * Marks the current visual bounding box of the item for redrawing. * This is called whenever the object changes its visible appearance. * For some cases (such as setting opacity) this is enough, but for others * _markForUpdate() also needs to be called. */ void DrawingItem::_markForRendering() { bool outline = _drawing.renderMode() == RenderMode::OUTLINE || _drawing.outlineOverlay(); Geom::OptIntRect dirty = outline ? _bbox : _drawbox; if (!dirty) return; // dirty the caches of all parents DrawingItem *bkg_root = nullptr; for (auto i = this; i; i = i->_parent) { if (i != this && i->_filter) { i->_filter->area_enlarge(*dirty, i); } if (i->_cache && i->_cache->surface) { i->_cache->surface->markDirty(*dirty); } i->_dropPatternCache(); if (i->_background_accumulate) { bkg_root = i; } } if (bkg_root && bkg_root->_parent && bkg_root->_parent->_parent) { bkg_root->_invalidateFilterBackground(*dirty); } if (auto canvasitem = drawing().getCanvasItemDrawing()) { canvasitem->get_canvas()->redraw_area(*dirty); } } void DrawingItem::_invalidateFilterBackground(Geom::IntRect const &area) { if (!_drawbox.intersects(area)) return; if (_cache && _cache->surface && _filter && _filter->uses_background()) { _cache->surface->markDirty(area); } for (auto & i : _children) { i._invalidateFilterBackground(area); } } /** * Marks the item as needing a recomputation of internal data. * * This mechanism avoids traversing the entire rendering tree (which could be vast) * on every trivial state changed in any item. Only items marked as needing * an update (having some bits in their _state unset) will be traversed * during the update call. * * The _propagate variable is another optimization. We use it to specify that * all children should also have the corresponding flags unset before checking * whether they need to be traversed. This way there is one less traversal * of the tree. Without this we would need to unset state bits in all children. * With _propagate we do this during the update call, when we have to recurse * into children anyway. */ void DrawingItem::_markForUpdate(unsigned flags, bool propagate) { if (propagate) { _propagate_state |= flags; } if (_state & flags) { unsigned oldstate = _state; _state &= ~flags; if (oldstate != _state && _parent) { // If we actually reset anything in state, recurse on the parent. _parent->_markForUpdate(flags, false); } else { // If nothing changed, it means our ancestors are already invalidated // up to the root. Do not bother recursing, because it won't change anything. // Also do this if we are the root item, because we have no more ancestors // to invalidate. if (drawing().getCanvasItemDrawing()) { drawing().getCanvasItemDrawing()->request_update(); } else { // Typically happens, e.g. for any non-Canvas Drawing. } } } } /** * Compute the caching score. * * Higher scores mean the item is more aggressively prioritized for automatic * caching by Inkscape::Drawing. */ double DrawingItem::_cacheScore() { Geom::OptIntRect cache_rect = _cacheRect(); if (!cache_rect) return -1.0; // a crude first approximation: // the basic score is the number of pixels in the drawbox double score = cache_rect->area(); // this is multiplied by the filter complexity and its expansion if (_filter && _drawing.renderMode() != RenderMode::NO_FILTERS) { score *= _filter->complexity(_ctm); Geom::IntRect ref_area = Geom::IntRect::from_xywh(0, 0, 16, 16); Geom::IntRect test_area = ref_area; Geom::IntRect limit_area(0, INT_MIN, 16, INT_MAX); _filter->area_enlarge(test_area, this); // area_enlarge never shrinks the rect, so the result of intersection below must be non-empty score *= (double)(test_area & limit_area)->area() / ref_area.area(); } // if the object is clipped, add 1/2 of its bbox pixels if (_clip && _clip->_bbox) { score += _clip->_bbox->area() * 0.5; } // if masked, add mask score if (_mask) { score += _mask->_cacheScore(); } //g_message("caching score: %f", score); return score; } inline void expandByScale(Geom::IntRect &rect, double scale) { double fraction = (scale - 1) / 2; rect.expandBy(rect.width() * fraction, rect.height() * fraction); } Geom::OptIntRect DrawingItem::_cacheRect() const { Geom::OptIntRect r = _drawbox & _drawing.cacheLimit(); if (_filter && _drawing.cacheLimit() && _drawing.renderMode() != RenderMode::NO_FILTERS && r && r != _drawbox) { // we check unfiltered item is enough inside the cache area to render properly Geom::OptIntRect canvas = r; expandByScale(*canvas, 0.5); Geom::OptIntRect valid = Geom::intersect(canvas, _bbox); if (!valid && _bbox) { valid = _bbox; // contract the item _bbox to get reduced size to render. $ seems good enough expandByScale(*valid, 0.5); // now we get the nearest point to cache area Geom::IntPoint center = _drawing.cacheLimit()->midpoint(); Geom::IntPoint nearest = valid->nearestEdgePoint(center); r.expandTo(nearest); } return _drawbox & r; } return r; } void apply_antialias(DrawingContext &dc, int antialias) { switch (antialias) { case 0: cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_NONE); break; case 1: cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_FAST); break; case 2: cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_GOOD); break; case 3: cairo_set_antialias(dc.raw(), CAIRO_ANTIALIAS_BEST); break; default: g_assert_not_reached(); } } // Remove this node from its parent, then delete it. void DrawingItem::unlink() { defer([=] { // This only happens for the top-level deleted item. if (_parent) { _markForRendering(); } switch (_child_type) { case ChildType::NORMAL: { auto it = _parent->_children.iterator_to(*this); _parent->_children.erase(it); break; } case ChildType::CLIP: _parent->_clip = nullptr; break; case ChildType::MASK: _parent->_mask = nullptr; break; case ChildType::FILL: _parent->_fill_pattern = nullptr; break; case ChildType::STROKE: _parent->_stroke_pattern = nullptr; break; case ChildType::ROOT: _drawing._root = nullptr; break; default: break; } if (_parent) { bool propagate = _child_type == ChildType::CLIP || _child_type == ChildType::MASK; _parent->_markForUpdate(STATE_ALL, propagate); } delete this; }); } } // namespace Inkscape /* 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 :