summaryrefslogtreecommitdiffstats
path: root/src/display/drawing-surface.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/display/drawing-surface.cpp414
1 files changed, 414 insertions, 0 deletions
diff --git a/src/display/drawing-surface.cpp b/src/display/drawing-surface.cpp
new file mode 100644
index 0000000..4cf4f50
--- /dev/null
+++ b/src/display/drawing-surface.cpp
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/**
+ * @file
+ * Cairo surface that remembers its origin.
+ *//*
+ * Authors:
+ * Krzysztof KosiƄski <tweenk.pl@gmail.com>
+ *
+ * Copyright (C) 2011 Authors
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+//#include <iostream>
+#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 :