1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3  * @file
4  * Cairo surface that remembers its origin.
5  *//*
6  * Authors:
7  *   Krzysztof Kosiński <tweenk.pl@gmail.com>
8  *
9  * Copyright (C) 2011 Authors
10  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
11  */
12 
13 //#include <iostream>
14 #include "display/drawing-surface.h"
15 #include "display/drawing-context.h"
16 #include "display/cairo-utils.h"
17 
18 namespace Inkscape {
19 
20 using Geom::X;
21 using Geom::Y;
22 
23 
24 /**
25  * @class DrawingSurface
26  * Drawing surface that remembers its origin.
27  *
28  * This is a very minimalistic wrapper over cairo_surface_t. The main
29  * extra functionality provided by this class is that it automates
30  * the mapping from "logical space" (coordinates in the rendering)
31  * and the "physical space" (surface pixels). For example, patterns
32  * have to be rendered on tiles which have possibly non-integer
33  * widths and heights.
34  *
35  * This class has delayed allocation functionality - it creates
36  * the Cairo surface it wraps on the first call to createRawContext()
37  * of when a DrawingContext is constructed.
38  */
39 
40 /**
41  * Creates a surface with the given physical extents.
42  * When a drawing context is created for this surface, its pixels
43  * will cover the area under the given rectangle.
44  */
DrawingSurface(Geom::IntRect const & area,int device_scale)45 DrawingSurface::DrawingSurface(Geom::IntRect const &area, int device_scale)
46     : _surface(nullptr)
47     , _origin(area.min())
48     , _scale(1, 1)
49     , _pixels(area.dimensions())
50     , _device_scale(device_scale)
51 {
52     assert(_device_scale > 0);
53 }
54 
55 /**
56  * Creates a surface with the given logical and physical extents.
57  * When a drawing context is created for this surface, its pixels
58  * will cover the area under the given rectangle. IT will contain
59  * the number of pixels specified by the second argument.
60  * @param logbox Logical extents of the surface
61  * @param pixdims Pixel dimensions of the surface.
62  */
DrawingSurface(Geom::Rect const & logbox,Geom::IntPoint const & pixdims,int device_scale)63 DrawingSurface::DrawingSurface(Geom::Rect const &logbox, Geom::IntPoint const &pixdims, int device_scale)
64     : _surface(nullptr)
65     , _origin(logbox.min())
66     , _scale(pixdims[X] / logbox.width(), pixdims[Y] / logbox.height())
67     , _pixels(pixdims)
68     , _device_scale(device_scale)
69 {
70     assert(_device_scale > 0);
71 }
72 
73 /**
74  * Wrap a cairo_surface_t.
75  * This constructor will take an extra reference on @a surface, which will
76  * be released on destruction.
77  */
DrawingSurface(cairo_surface_t * surface,Geom::Point const & origin)78 DrawingSurface::DrawingSurface(cairo_surface_t *surface, Geom::Point const &origin)
79     : _surface(surface)
80     , _origin(origin)
81     , _scale(1, 1)
82 {
83     cairo_surface_reference(surface);
84 
85     double x_scale = 0;
86     double y_scale = 0;
87     cairo_surface_get_device_scale( surface, &x_scale, &y_scale);
88     if (x_scale != y_scale) {
89         std::cerr << "DrawingSurface::DrawingSurface: non-uniform device scale!" << std::endl;
90     }
91     _device_scale = x_scale;
92     assert(_device_scale > 0);
93 
94     _pixels[X] = cairo_image_surface_get_width(surface)/_device_scale;
95     _pixels[Y] = cairo_image_surface_get_height(surface)/_device_scale;
96 }
97 
~DrawingSurface()98 DrawingSurface::~DrawingSurface()
99 {
100     if (_surface)
101         cairo_surface_destroy(_surface);
102 }
103 
104 /// Get the logical extents of the surface.
105 Geom::Rect
area() const106 DrawingSurface::area() const
107 {
108     Geom::Rect r = Geom::Rect::from_xywh(_origin, dimensions());
109     return r;
110 }
111 
112 /// Get the pixel dimensions of the surface
113 Geom::IntPoint
pixels() const114 DrawingSurface::pixels() const
115 {
116     return _pixels;
117 }
118 
119 /// Get the logical width and weight of the surface as a point.
120 Geom::Point
dimensions() const121 DrawingSurface::dimensions() const
122 {
123     Geom::Point logical_dims(_pixels[X] / _scale[X], _pixels[Y] / _scale[Y]);
124     return logical_dims;
125 }
126 
127 Geom::Point
origin() const128 DrawingSurface::origin() const
129 {
130     return _origin;
131 }
132 
133 Geom::Scale
scale() const134 DrawingSurface::scale() const
135 {
136     return _scale;
137 }
138 
139 /// Device scale for HiDPI screens
140 int
device_scale() const141 DrawingSurface::device_scale() const
142 {
143     return _device_scale;
144 }
145 
146 /// Get the transformation applied to the drawing context on construction.
147 Geom::Affine
drawingTransform() const148 DrawingSurface::drawingTransform() const
149 {
150     Geom::Affine ret = Geom::Translate(-_origin) * _scale;
151     return ret;
152 }
153 
154 cairo_surface_type_t
type() const155 DrawingSurface::type() const
156 {
157     // currently hardcoded
158     return CAIRO_SURFACE_TYPE_IMAGE;
159 }
160 
161 /// Drop contents of the surface and release the underlying Cairo object.
162 void
dropContents()163 DrawingSurface::dropContents()
164 {
165     if (_surface) {
166         cairo_surface_destroy(_surface);
167         _surface = nullptr;
168     }
169 }
170 
171 /**
172  * Create a drawing context for this surface.
173  * It's better to use the surface constructor of DrawingContext.
174  */
175 cairo_t *
createRawContext()176 DrawingSurface::createRawContext()
177 {
178     // deferred allocation
179     if (!_surface) {
180         _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
181                                               _pixels[X] * _device_scale,
182                                               _pixels[Y] * _device_scale);
183         cairo_surface_set_device_scale(_surface, _device_scale, _device_scale);
184     }
185     cairo_t *ct = cairo_create(_surface);
186     if (_scale != Geom::Scale::identity()) {
187         cairo_scale(ct, _scale[X], _scale[Y]);
188     }
189     cairo_translate(ct, -_origin[X], -_origin[Y]);
190     return ct;
191 }
192 
193 Geom::IntRect
pixelArea() const194 DrawingSurface::pixelArea() const
195 {
196     Geom::IntRect ret = Geom::IntRect::from_xywh(_origin.round(), _pixels);
197     return ret;
198 }
199 
200 //////////////////////////////////////////////////////////////////////////////
201 
DrawingCache(Geom::IntRect const & area,int device_scale)202 DrawingCache::DrawingCache(Geom::IntRect const &area, int device_scale)
203     : DrawingSurface(area, device_scale)
204     , _clean_region(cairo_region_create())
205     , _pending_area(area)
206 {}
207 
~DrawingCache()208 DrawingCache::~DrawingCache()
209 {
210     cairo_region_destroy(_clean_region);
211 }
212 
213 void
markDirty(Geom::IntRect const & area)214 DrawingCache::markDirty(Geom::IntRect const &area)
215 {
216     cairo_rectangle_int_t dirty = _convertRect(area);
217     cairo_region_subtract_rectangle(_clean_region, &dirty);
218 }
219 void
markClean(Geom::IntRect const & area)220 DrawingCache::markClean(Geom::IntRect const &area)
221 {
222     Geom::OptIntRect r = Geom::intersect(area, pixelArea());
223     if (!r) return;
224     cairo_rectangle_int_t clean = _convertRect(*r);
225     cairo_region_union_rectangle(_clean_region, &clean);
226 }
227 
228 /// Call this during the update phase to schedule a transformation of the cache.
229 void
scheduleTransform(Geom::IntRect const & new_area,Geom::Affine const & trans)230 DrawingCache::scheduleTransform(Geom::IntRect const &new_area, Geom::Affine const &trans)
231 {
232     _pending_area = new_area;
233     _pending_transform *= trans;
234 }
235 
236 /// Transforms the cache according to the transform specified during the update phase.
237 /// Call this during render phase, before painting.
238 void
prepare()239 DrawingCache::prepare()
240 {
241     Geom::IntRect old_area = pixelArea();
242     bool is_identity = _pending_transform.isIdentity();
243     if (is_identity && _pending_area == old_area) return; // no change
244 
245     bool is_integer_translation = is_identity;
246     if (!is_identity && _pending_transform.isTranslation()) {
247         Geom::IntPoint t = _pending_transform.translation().round();
248         if (Geom::are_near(Geom::Point(t), _pending_transform.translation())) {
249             is_integer_translation = true;
250             cairo_region_translate(_clean_region, t[X], t[Y]);
251             if (old_area + t == _pending_area) {
252                 // if the areas match, the only thing to do
253                 // is to ensure that the clean area is not too large
254                 // we can exit early
255                 cairo_rectangle_int_t limit = _convertRect(_pending_area);
256                 cairo_region_intersect_rectangle(_clean_region, &limit);
257                 _origin += t;
258                 _pending_transform.setIdentity();
259                 return;
260             }
261         }
262     }
263 
264     // the area has changed, so the cache content needs to be copied
265     Geom::IntPoint old_origin = old_area.min();
266     cairo_surface_t *old_surface = _surface;
267     _surface = nullptr;
268     _pixels = _pending_area.dimensions();
269     _origin = _pending_area.min();
270 
271     if (is_integer_translation) {
272         // transform the cache only for integer translations and identities
273         cairo_t *ct = createRawContext();
274         if (!is_identity) {
275             ink_cairo_transform(ct, _pending_transform);
276         }
277         cairo_set_source_surface(ct, old_surface, old_origin[X], old_origin[Y]);
278         cairo_set_operator(ct, CAIRO_OPERATOR_SOURCE);
279         cairo_pattern_set_filter(cairo_get_source(ct), CAIRO_FILTER_NEAREST);
280         cairo_paint(ct);
281         cairo_destroy(ct);
282 
283         cairo_rectangle_int_t limit = _convertRect(_pending_area);
284         cairo_region_intersect_rectangle(_clean_region, &limit);
285     } else {
286         // dirty everything
287         cairo_region_destroy(_clean_region);
288         _clean_region = cairo_region_create();
289     }
290 
291     //std::cout << _pending_transform << old_area << _pending_area << std::endl;
292     cairo_surface_destroy(old_surface);
293     _pending_transform.setIdentity();
294 }
295 
296 /**
297  * Paints the clean area from cache and modifies the @a area
298  * parameter to the bounds of the region that must be repainted.
299  */
paintFromCache(DrawingContext & dc,Geom::OptIntRect & area,bool is_filter)300 void DrawingCache::paintFromCache(DrawingContext &dc, Geom::OptIntRect &area, bool is_filter)
301 {
302     if (!area) return;
303 
304     // We subtract the clean region from the area, then get the bounds
305     // of the resulting region. This is the area that needs to be repainted
306     // by the item.
307     // Then we subtract the area that needs to be repainted from the
308     // original area and paint the resulting region from cache.
309     cairo_rectangle_int_t area_c = _convertRect(*area);
310     cairo_region_t *dirty_region = cairo_region_create_rectangle(&area_c);
311     cairo_region_t *cache_region = cairo_region_copy(dirty_region);
312     cairo_region_subtract(dirty_region, _clean_region);
313 
314     if (is_filter && !cairo_region_is_empty(dirty_region)) { // To allow fast panning on high zoom on filters
315         return;
316     }
317     if (cairo_region_is_empty(dirty_region)) {
318         area = Geom::OptIntRect();
319     } else {
320         cairo_rectangle_int_t to_repaint;
321         cairo_region_get_extents(dirty_region, &to_repaint);
322         area = _convertRect(to_repaint);
323         markDirty(*area);
324         cairo_region_subtract_rectangle(cache_region, &to_repaint);
325     }
326     cairo_region_destroy(dirty_region);
327 
328     if (!cairo_region_is_empty(cache_region)) {
329         int nr = cairo_region_num_rectangles(cache_region);
330         cairo_rectangle_int_t tmp;
331         for (int i = 0; i < nr; ++i) {
332             cairo_region_get_rectangle(cache_region, i, &tmp);
333             dc.rectangle(_convertRect(tmp));
334         }
335         dc.setSource(this);
336         dc.fill();
337     }
338     cairo_region_destroy(cache_region);
339 }
340 
341 // debugging utility
342 void
_dumpCache(Geom::OptIntRect const & area)343 DrawingCache::_dumpCache(Geom::OptIntRect const &area)
344 {
345     static int dumpnr = 0;
346     cairo_surface_t *surface = ink_cairo_surface_copy(_surface);
347     DrawingContext dc(surface, _origin);
348     if (!cairo_region_is_empty(_clean_region)) {
349         Inkscape::DrawingContext::Save save(dc);
350         int nr = cairo_region_num_rectangles(_clean_region);
351         cairo_rectangle_int_t tmp;
352         for (int i = 0; i < nr; ++i) {
353             cairo_region_get_rectangle(_clean_region, i, &tmp);
354             dc.rectangle(_convertRect(tmp));
355         }
356         dc.setSource(0,1,0,0.1);
357         dc.fill();
358     }
359     dc.rectangle(*area);
360     dc.setSource(1,0,0,0.1);
361     dc.fill();
362     char *fn = g_strdup_printf("dump%d.png", dumpnr++);
363     cairo_surface_write_to_png(surface, fn);
364     cairo_surface_destroy(surface);
365     g_free(fn);
366 }
367 
368 cairo_rectangle_int_t
_convertRect(Geom::IntRect const & area)369 DrawingCache::_convertRect(Geom::IntRect const &area)
370 {
371     cairo_rectangle_int_t ret;
372     ret.x = area.left();
373     ret.y = area.top();
374     ret.width = area.width();
375     ret.height = area.height();
376     return ret;
377 }
378 
379 Geom::IntRect
_convertRect(cairo_rectangle_int_t const & r)380 DrawingCache::_convertRect(cairo_rectangle_int_t const &r)
381 {
382     Geom::IntRect ret = Geom::IntRect::from_xywh(
383         r.x, r.y,
384         r.width, r.height);
385     return ret;
386 }
387 
388 } // end namespace Inkscape
389 
390 /*
391   Local Variables:
392   mode:c++
393   c-file-style:"stroustrup"
394   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
395   indent-tabs-mode:nil
396   fill-column:99
397   End:
398 */
399 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
400