// SPDX-License-Identifier: GPL-2.0-or-later /** * @file * Cairo surface that remembers its origin. *//* * Authors: * Krzysztof KosiƄski * * Copyright (C) 2011 Authors * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ //#include #ifdef WITH_PATCHED_CAIRO #include "3rdparty/cairo/src/cairo.h" #endif #include "preferences.h" #include "display/drawing-surface.h" #include "display/drawing-context.h" #include "display/cairo-utils.h" namespace Inkscape { using Geom::X; using Geom::Y; /** * @class DrawingSurface * Drawing surface that remembers its origin. * * This is a very minimalistic wrapper over cairo_surface_t. The main * extra functionality provided by this class is that it automates * the mapping from "logical space" (coordinates in the rendering) * and the "physical space" (surface pixels). For example, patterns * have to be rendered on tiles which have possibly non-integer * widths and heights. * * This class has delayed allocation functionality - it creates * the Cairo surface it wraps on the first call to createRawContext() * of when a DrawingContext is constructed. */ /** * Creates a surface with the given physical extents. * When a drawing context is created for this surface, its pixels * will cover the area under the given rectangle. */ DrawingSurface::DrawingSurface(Geom::IntRect const &area, int device_scale) : _surface(nullptr) , _origin(area.min()) , _scale(1, 1) , _pixels(area.dimensions()) , _device_scale(device_scale) { assert(_device_scale > 0); } /** * Creates a surface with the given logical and physical extents. * When a drawing context is created for this surface, its pixels * will cover the area under the given rectangle. IT will contain * the number of pixels specified by the second argument. * @param logbox Logical extents of the surface * @param pixdims Pixel dimensions of the surface. */ DrawingSurface::DrawingSurface(Geom::Rect const &logbox, Geom::IntPoint const &pixdims, int device_scale) : _surface(nullptr) , _origin(logbox.min()) , _scale(pixdims[X] / logbox.width(), pixdims[Y] / logbox.height()) , _pixels(pixdims) , _device_scale(device_scale) { assert(_device_scale > 0); } /** * Wrap a cairo_surface_t. * This constructor will take an extra reference on @a surface, which will * be released on destruction. */ DrawingSurface::DrawingSurface(cairo_surface_t *surface, Geom::Point const &origin) : _surface(surface) , _origin(origin) , _scale(1, 1) { cairo_surface_reference(surface); double x_scale = 0; double y_scale = 0; cairo_surface_get_device_scale( surface, &x_scale, &y_scale); if (x_scale != y_scale) { std::cerr << "DrawingSurface::DrawingSurface: non-uniform device scale!" << std::endl; } _device_scale = x_scale; assert(_device_scale > 0); _pixels[X] = cairo_image_surface_get_width(surface)/_device_scale; _pixels[Y] = cairo_image_surface_get_height(surface)/_device_scale; } DrawingSurface::~DrawingSurface() { if (_surface) cairo_surface_destroy(_surface); } /// Get the logical extents of the surface. Geom::Rect DrawingSurface::area() const { Geom::Rect r = Geom::Rect::from_xywh(_origin, dimensions()); return r; } /// Get the pixel dimensions of the surface Geom::IntPoint DrawingSurface::pixels() const { return _pixels; } /// Get the logical width and weight of the surface as a point. Geom::Point DrawingSurface::dimensions() const { Geom::Point logical_dims(_pixels[X] / _scale[X], _pixels[Y] / _scale[Y]); return logical_dims; } Geom::Point DrawingSurface::origin() const { return _origin; } Geom::Scale DrawingSurface::scale() const { return _scale; } /// Device scale for HiDPI screens int DrawingSurface::device_scale() const { return _device_scale; } /// Get the transformation applied to the drawing context on construction. Geom::Affine DrawingSurface::drawingTransform() const { Geom::Affine ret = Geom::Translate(-_origin) * _scale; return ret; } cairo_surface_type_t DrawingSurface::type() const { // currently hardcoded return CAIRO_SURFACE_TYPE_IMAGE; } /// Drop contents of the surface and release the underlying Cairo object. void DrawingSurface::dropContents() { if (_surface) { cairo_surface_destroy(_surface); _surface = nullptr; } } /** * Create a drawing context for this surface. * It's better to use the surface constructor of DrawingContext. */ cairo_t * DrawingSurface::createRawContext() { // deferred allocation if (!_surface) { _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _pixels[X] * _device_scale, _pixels[Y] * _device_scale); cairo_surface_set_device_scale(_surface, _device_scale, _device_scale); #ifdef CAIRO_HAS_DITHER Inkscape::Preferences *prefs = Inkscape::Preferences::get(); if(prefs->getBool("/options/dithering/render", false)) cairo_image_surface_set_dither(_surface, CAIRO_DITHER_BEST); #endif } cairo_t *ct = cairo_create(_surface); if (_scale != Geom::Scale::identity()) { cairo_scale(ct, _scale[X], _scale[Y]); } cairo_translate(ct, -_origin[X], -_origin[Y]); return ct; } Geom::IntRect DrawingSurface::pixelArea() const { Geom::IntRect ret = Geom::IntRect::from_xywh(_origin.round(), _pixels); return ret; } ////////////////////////////////////////////////////////////////////////////// DrawingCache::DrawingCache(Geom::IntRect const &area, int device_scale) : DrawingSurface(area, device_scale) , _clean_region(cairo_region_create()) , _pending_area(area) {} DrawingCache::~DrawingCache() { cairo_region_destroy(_clean_region); } void DrawingCache::markDirty(Geom::IntRect const &area) { cairo_rectangle_int_t dirty = _convertRect(area); cairo_region_subtract_rectangle(_clean_region, &dirty); } void DrawingCache::markClean(Geom::IntRect const &area) { Geom::OptIntRect r = Geom::intersect(area, pixelArea()); if (!r) return; cairo_rectangle_int_t clean = _convertRect(*r); cairo_region_union_rectangle(_clean_region, &clean); } /// Call this during the update phase to schedule a transformation of the cache. void DrawingCache::scheduleTransform(Geom::IntRect const &new_area, Geom::Affine const &trans) { _pending_area = new_area; _pending_transform *= trans; } /// Transforms the cache according to the transform specified during the update phase. /// Call this during render phase, before painting. void DrawingCache::prepare() { Geom::IntRect old_area = pixelArea(); bool is_identity = _pending_transform.isIdentity(); if (is_identity && _pending_area == old_area) return; // no change bool is_integer_translation = is_identity; if (!is_identity && _pending_transform.isTranslation()) { Geom::IntPoint t = _pending_transform.translation().round(); if (Geom::are_near(Geom::Point(t), _pending_transform.translation())) { is_integer_translation = true; cairo_region_translate(_clean_region, t[X], t[Y]); if (old_area + t == _pending_area) { // if the areas match, the only thing to do // is to ensure that the clean area is not too large // we can exit early cairo_rectangle_int_t limit = _convertRect(_pending_area); cairo_region_intersect_rectangle(_clean_region, &limit); _origin += t; _pending_transform.setIdentity(); return; } } } // the area has changed, so the cache content needs to be copied Geom::IntPoint old_origin = old_area.min(); cairo_surface_t *old_surface = _surface; _surface = nullptr; _pixels = _pending_area.dimensions(); _origin = _pending_area.min(); if (is_integer_translation) { // transform the cache only for integer translations and identities cairo_t *ct = createRawContext(); if (!is_identity) { ink_cairo_transform(ct, _pending_transform); } cairo_set_source_surface(ct, old_surface, old_origin[X], old_origin[Y]); cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE); cairo_pattern_set_filter(cairo_get_source(ct), CAIRO_FILTER_NEAREST); cairo_paint(ct); cairo_destroy(ct); cairo_rectangle_int_t limit = _convertRect(_pending_area); cairo_region_intersect_rectangle(_clean_region, &limit); } else { // dirty everything cairo_region_destroy(_clean_region); _clean_region = cairo_region_create(); } //std::cout << _pending_transform << old_area << _pending_area << std::endl; cairo_surface_destroy(old_surface); _pending_transform.setIdentity(); } /** * Paints the clean area from cache and modifies the @a area * parameter to the bounds of the region that must be repainted. */ void DrawingCache::paintFromCache(DrawingContext &dc, Geom::OptIntRect &area, bool is_filter) { if (!area) return; // We subtract the clean region from the area, then get the bounds // of the resulting region. This is the area that needs to be repainted // by the item. // Then we subtract the area that needs to be repainted from the // original area and paint the resulting region from cache. cairo_rectangle_int_t area_c = _convertRect(*area); cairo_region_t *dirty_region = cairo_region_create_rectangle(&area_c); cairo_region_t *cache_region = cairo_region_copy(dirty_region); cairo_region_subtract(dirty_region, _clean_region); if (is_filter && !cairo_region_is_empty(dirty_region)) { // To allow fast panning on high zoom on filters cairo_region_destroy(cache_region); cairo_region_destroy(dirty_region); cairo_region_destroy(_clean_region); _clean_region = cairo_region_create(); return; } if (cairo_region_is_empty(dirty_region)) { area = Geom::OptIntRect(); } else { cairo_rectangle_int_t to_repaint; cairo_region_get_extents(dirty_region, &to_repaint); area = _convertRect(to_repaint); markDirty(*area); cairo_region_subtract_rectangle(cache_region, &to_repaint); } cairo_region_destroy(dirty_region); if (!cairo_region_is_empty(cache_region)) { int nr = cairo_region_num_rectangles(cache_region); cairo_rectangle_int_t tmp; for (int i = 0; i < nr; ++i) { cairo_region_get_rectangle(cache_region, i, &tmp); dc.rectangle(_convertRect(tmp)); } dc.setSource(this); dc.fill(); } cairo_region_destroy(cache_region); } // debugging utility void DrawingCache::_dumpCache(Geom::OptIntRect const &area) { static int dumpnr = 0; cairo_surface_t *surface = ink_cairo_surface_copy(_surface); DrawingContext dc(surface, _origin); if (!cairo_region_is_empty(_clean_region)) { Inkscape::DrawingContext::Save save(dc); int nr = cairo_region_num_rectangles(_clean_region); cairo_rectangle_int_t tmp; for (int i = 0; i < nr; ++i) { cairo_region_get_rectangle(_clean_region, i, &tmp); dc.rectangle(_convertRect(tmp)); } dc.setSource(0,1,0,0.1); dc.fill(); } dc.rectangle(*area); dc.setSource(1,0,0,0.1); dc.fill(); char *fn = g_strdup_printf("dump%d.png", dumpnr++); cairo_surface_write_to_png(surface, fn); cairo_surface_destroy(surface); g_free(fn); } cairo_rectangle_int_t DrawingCache::_convertRect(Geom::IntRect const &area) { cairo_rectangle_int_t ret; ret.x = area.left(); ret.y = area.top(); ret.width = area.width(); ret.height = area.height(); return ret; } Geom::IntRect DrawingCache::_convertRect(cairo_rectangle_int_t const &r) { Geom::IntRect ret = Geom::IntRect::from_xywh( r.x, r.y, r.width, r.height); return ret; } } // end 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 :