/src/libreoffice/vcl/headless/CairoCommon.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | /* |
3 | | * This file is part of the LibreOffice project. |
4 | | * |
5 | | * This Source Code Form is subject to the terms of the Mozilla Public |
6 | | * License, v. 2.0. If a copy of the MPL was not distributed with this |
7 | | * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
8 | | * |
9 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <vcl/gradient.hxx> |
21 | | #include <vcl/svapp.hxx> |
22 | | #include <headless/BitmapHelper.hxx> |
23 | | #include <headless/CairoCommon.hxx> |
24 | | #include <vcl/cairo.hxx> |
25 | | #include <vcl/CairoFormats.hxx> |
26 | | #include <vcl/BitmapTools.hxx> |
27 | | #include <SalGradient.hxx> |
28 | | #include <tools/helpers.hxx> |
29 | | #include <basegfx/utils/canvastools.hxx> |
30 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
31 | | #include <basegfx/polygon/b2dpolygontools.hxx> |
32 | | #include <basegfx/range/b2irange.hxx> |
33 | | #include <comphelper/configuration.hxx> |
34 | | #include <sal/log.hxx> |
35 | | #include <osl/module.h> |
36 | | |
37 | | #include <com/sun/star/awt/GradientStyle.hpp> |
38 | | |
39 | | #if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 12, 0) |
40 | | #error "require at least cairo 1.12.0" |
41 | | #endif |
42 | | |
43 | | void dl_cairo_surface_set_device_scale(cairo_surface_t* surface, double x_scale, double y_scale) |
44 | 966k | { |
45 | 966k | #if !HAVE_DLAPI || !defined(SYSTEM_CAIRO) |
46 | 966k | cairo_surface_set_device_scale(surface, x_scale, y_scale); |
47 | | #else |
48 | | static auto func = reinterpret_cast<void (*)(cairo_surface_t*, double, double)>( |
49 | | osl_getAsciiFunctionSymbol(nullptr, "cairo_surface_set_device_scale")); |
50 | | if (func) |
51 | | func(surface, x_scale, y_scale); |
52 | | #endif |
53 | 966k | } |
54 | | |
55 | | void dl_cairo_surface_get_device_scale(cairo_surface_t* surface, double* x_scale, double* y_scale) |
56 | 1.43M | { |
57 | 1.43M | #if !HAVE_DLAPI || !defined(SYSTEM_CAIRO) |
58 | 1.43M | cairo_surface_get_device_scale(surface, x_scale, y_scale); |
59 | | #else |
60 | | static auto func = reinterpret_cast<void (*)(cairo_surface_t*, double*, double*)>( |
61 | | osl_getAsciiFunctionSymbol(nullptr, "cairo_surface_get_device_scale")); |
62 | | if (func) |
63 | | func(surface, x_scale, y_scale); |
64 | | else |
65 | | { |
66 | | if (x_scale) |
67 | | *x_scale = 1.0; |
68 | | if (y_scale) |
69 | | *y_scale = 1.0; |
70 | | } |
71 | | #endif |
72 | 1.43M | } |
73 | | |
74 | | basegfx::B2DRange getFillDamage(cairo_t* cr) |
75 | 440k | { |
76 | 440k | double x1, y1, x2, y2; |
77 | | |
78 | | // this is faster than cairo_fill_extents, at the cost of some overdraw |
79 | 440k | cairo_path_extents(cr, &x1, &y1, &x2, &y2); |
80 | | |
81 | | // support B2DRange::isEmpty() |
82 | 440k | if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) |
83 | 25.8k | { |
84 | 25.8k | return basegfx::B2DRange(x1, y1, x2, y2); |
85 | 25.8k | } |
86 | | |
87 | 414k | return basegfx::B2DRange(); |
88 | 440k | } |
89 | | |
90 | | basegfx::B2DRange getClipBox(cairo_t* cr) |
91 | 440k | { |
92 | 440k | double x1, y1, x2, y2; |
93 | | |
94 | 440k | cairo_clip_extents(cr, &x1, &y1, &x2, &y2); |
95 | | |
96 | | // support B2DRange::isEmpty() |
97 | 440k | if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) |
98 | 440k | { |
99 | 440k | return basegfx::B2DRange(x1, y1, x2, y2); |
100 | 440k | } |
101 | | |
102 | 0 | return basegfx::B2DRange(); |
103 | 440k | } |
104 | | |
105 | | basegfx::B2DRange getClippedFillDamage(cairo_t* cr) |
106 | 440k | { |
107 | 440k | basegfx::B2DRange aDamageRect(getFillDamage(cr)); |
108 | 440k | aDamageRect.intersect(getClipBox(cr)); |
109 | 440k | return aDamageRect; |
110 | 440k | } |
111 | | |
112 | | basegfx::B2DRange getStrokeDamage(cairo_t* cr) |
113 | 0 | { |
114 | 0 | double x1, y1, x2, y2; |
115 | | |
116 | | // less accurate, but much faster |
117 | 0 | cairo_path_extents(cr, &x1, &y1, &x2, &y2); |
118 | | |
119 | | // support B2DRange::isEmpty() |
120 | 0 | if (0.0 != x1 || 0.0 != y1 || 0.0 != x2 || 0.0 != y2) |
121 | 0 | { |
122 | 0 | return basegfx::B2DRange(x1, y1, x2, y2); |
123 | 0 | } |
124 | | |
125 | 0 | return basegfx::B2DRange(); |
126 | 0 | } |
127 | | |
128 | | basegfx::B2DRange getClippedStrokeDamage(cairo_t* cr) |
129 | 0 | { |
130 | 0 | basegfx::B2DRange aDamageRect(getStrokeDamage(cr)); |
131 | 0 | aDamageRect.intersect(getClipBox(cr)); |
132 | 0 | return aDamageRect; |
133 | 0 | } |
134 | | |
135 | | // Remove bClosePath: Checked that the already used mechanism for Win using |
136 | | // Gdiplus already relies on rPolygon.isClosed(), so should be safe to replace |
137 | | // this. |
138 | | // For PixelSnap we need the ObjectToDevice transformation here now. This is a |
139 | | // special case relative to the also executed LineDraw-Offset of (0.5, 0.5) in |
140 | | // DeviceCoordinates: The LineDraw-Offset is applied *after* the snap, so we |
141 | | // need the ObjectToDevice transformation *without* that offset here to do the |
142 | | // same. The LineDraw-Offset will be applied by the callers using a linear |
143 | | // transformation for Cairo now |
144 | | // For support of PixelSnapHairline we also need the ObjectToDevice transformation |
145 | | // and a method (same as in gdiimpl.cxx for Win and Gdiplus). This is needed e.g. |
146 | | // for Chart-content visualization. CAUTION: It's not the same as PixelSnap (!) |
147 | | // tdf#129845 add reply value to allow counting a point/byte/size measurement to |
148 | | // be included |
149 | | static size_t AddPolygonToPath(cairo_t* cr, const basegfx::B2DPolygon& rPolygon, |
150 | | const basegfx::B2DHomMatrix& rObjectToDevice, bool bPixelSnap, |
151 | | bool bPixelSnapHairline) |
152 | 0 | { |
153 | | // short circuit if there is nothing to do |
154 | 0 | const sal_uInt32 nPointCount(rPolygon.count()); |
155 | 0 | size_t nSizeMeasure(0); |
156 | |
|
157 | 0 | if (0 == nPointCount) |
158 | 0 | { |
159 | 0 | return nSizeMeasure; |
160 | 0 | } |
161 | | |
162 | 0 | const bool bHasCurves(rPolygon.areControlPointsUsed()); |
163 | 0 | const bool bClosePath(rPolygon.isClosed()); |
164 | 0 | const bool bObjectToDeviceUsed(!rObjectToDevice.isIdentity()); |
165 | 0 | basegfx::B2DHomMatrix aObjectToDeviceInv; |
166 | 0 | basegfx::B2DPoint aLast; |
167 | 0 | PixelSnapper aSnapper; |
168 | |
|
169 | 0 | for (sal_uInt32 nPointIdx = 0, nPrevIdx = 0;; nPrevIdx = nPointIdx++) |
170 | 0 | { |
171 | 0 | int nClosedIdx = nPointIdx; |
172 | 0 | if (nPointIdx >= nPointCount) |
173 | 0 | { |
174 | | // prepare to close last curve segment if needed |
175 | 0 | if (bClosePath && (nPointIdx == nPointCount)) |
176 | 0 | { |
177 | 0 | nClosedIdx = 0; |
178 | 0 | } |
179 | 0 | else |
180 | 0 | { |
181 | 0 | break; |
182 | 0 | } |
183 | 0 | } |
184 | | |
185 | 0 | basegfx::B2DPoint aPoint(rPolygon.getB2DPoint(nClosedIdx)); |
186 | |
|
187 | 0 | if (bPixelSnap) |
188 | 0 | { |
189 | | // snap device coordinates to full pixels |
190 | 0 | if (bObjectToDeviceUsed) |
191 | 0 | { |
192 | | // go to DeviceCoordinates |
193 | 0 | aPoint *= rObjectToDevice; |
194 | 0 | } |
195 | | |
196 | | // snap by rounding |
197 | 0 | aPoint.setX(basegfx::fround(aPoint.getX())); |
198 | 0 | aPoint.setY(basegfx::fround(aPoint.getY())); |
199 | |
|
200 | 0 | if (bObjectToDeviceUsed) |
201 | 0 | { |
202 | 0 | if (aObjectToDeviceInv.isIdentity()) |
203 | 0 | { |
204 | 0 | aObjectToDeviceInv = rObjectToDevice; |
205 | 0 | aObjectToDeviceInv.invert(); |
206 | 0 | } |
207 | | |
208 | | // go back to ObjectCoordinates |
209 | 0 | aPoint *= aObjectToDeviceInv; |
210 | 0 | } |
211 | 0 | } |
212 | |
|
213 | 0 | if (bPixelSnapHairline) |
214 | 0 | { |
215 | | // snap horizontal and vertical lines (mainly used in Chart for |
216 | | // 'nicer' AAing) |
217 | 0 | aPoint = aSnapper.snap(rPolygon, rObjectToDevice, aObjectToDeviceInv, nClosedIdx); |
218 | 0 | } |
219 | |
|
220 | 0 | if (!nPointIdx) |
221 | 0 | { |
222 | | // first point => just move there |
223 | 0 | cairo_move_to(cr, aPoint.getX(), aPoint.getY()); |
224 | 0 | aLast = aPoint; |
225 | 0 | continue; |
226 | 0 | } |
227 | | |
228 | 0 | bool bPendingCurve(false); |
229 | |
|
230 | 0 | if (bHasCurves) |
231 | 0 | { |
232 | 0 | bPendingCurve = rPolygon.isNextControlPointUsed(nPrevIdx); |
233 | 0 | bPendingCurve |= rPolygon.isPrevControlPointUsed(nClosedIdx); |
234 | 0 | } |
235 | |
|
236 | 0 | if (!bPendingCurve) // line segment |
237 | 0 | { |
238 | 0 | cairo_line_to(cr, aPoint.getX(), aPoint.getY()); |
239 | 0 | nSizeMeasure++; |
240 | 0 | } |
241 | 0 | else // cubic bezier segment |
242 | 0 | { |
243 | 0 | basegfx::B2DPoint aCP1 = rPolygon.getNextControlPoint(nPrevIdx); |
244 | 0 | basegfx::B2DPoint aCP2 = rPolygon.getPrevControlPoint(nClosedIdx); |
245 | | |
246 | | // tdf#99165 if the control points are 'empty', create the mathematical |
247 | | // correct replacement ones to avoid problems with the graphical sub-system |
248 | | // tdf#101026 The 1st attempt to create a mathematically correct replacement control |
249 | | // vector was wrong. Best alternative is one as close as possible which means short. |
250 | 0 | if (aCP1.equal(aLast)) |
251 | 0 | { |
252 | 0 | aCP1 = aLast + ((aCP2 - aLast) * 0.0005); |
253 | 0 | } |
254 | |
|
255 | 0 | if (aCP2.equal(aPoint)) |
256 | 0 | { |
257 | 0 | aCP2 = aPoint + ((aCP1 - aPoint) * 0.0005); |
258 | 0 | } |
259 | |
|
260 | 0 | cairo_curve_to(cr, aCP1.getX(), aCP1.getY(), aCP2.getX(), aCP2.getY(), aPoint.getX(), |
261 | 0 | aPoint.getY()); |
262 | | // take some bigger measure for curve segments - too expensive to subdivide |
263 | | // here and that precision not needed, but four (2 points, 2 control-points) |
264 | | // would be a too low weight |
265 | 0 | nSizeMeasure += 10; |
266 | 0 | } |
267 | |
|
268 | 0 | aLast = aPoint; |
269 | 0 | } |
270 | |
|
271 | 0 | if (bClosePath) |
272 | 0 | { |
273 | 0 | cairo_close_path(cr); |
274 | 0 | } |
275 | |
|
276 | 0 | return nSizeMeasure; |
277 | 0 | } |
278 | | |
279 | | basegfx::B2DPoint PixelSnapper::snap(const basegfx::B2DPolygon& rPolygon, |
280 | | const basegfx::B2DHomMatrix& rObjectToDevice, |
281 | | basegfx::B2DHomMatrix& rObjectToDeviceInv, sal_uInt32 nIndex) |
282 | 0 | { |
283 | 0 | const sal_uInt32 nCount(rPolygon.count()); |
284 | | |
285 | | // get the data |
286 | 0 | if (nIndex == 0) |
287 | 0 | { |
288 | | // if it's the first time, we need to calculate everything |
289 | 0 | maPrevPoint = rObjectToDevice * rPolygon.getB2DPoint((nIndex + nCount - 1) % nCount); |
290 | 0 | maCurrPoint = rObjectToDevice * rPolygon.getB2DPoint(nIndex); |
291 | 0 | maPrevTuple = basegfx::fround(maPrevPoint); |
292 | 0 | maCurrTuple = basegfx::fround(maCurrPoint); |
293 | 0 | } |
294 | 0 | else |
295 | 0 | { |
296 | | // but for all other times, we can re-use the previous iteration computations |
297 | 0 | maPrevPoint = maCurrPoint; |
298 | 0 | maPrevTuple = maCurrTuple; |
299 | 0 | maCurrPoint = maNextPoint; |
300 | 0 | maCurrTuple = maNextTuple; |
301 | 0 | } |
302 | 0 | maNextPoint = rObjectToDevice * rPolygon.getB2DPoint((nIndex + 1) % nCount); |
303 | 0 | maNextTuple = basegfx::fround(maNextPoint); |
304 | | |
305 | | // get the states |
306 | 0 | const bool bPrevVertical(maPrevTuple.getX() == maCurrTuple.getX()); |
307 | 0 | const bool bNextVertical(maNextTuple.getX() == maCurrTuple.getX()); |
308 | 0 | const bool bPrevHorizontal(maPrevTuple.getY() == maCurrTuple.getY()); |
309 | 0 | const bool bNextHorizontal(maNextTuple.getY() == maCurrTuple.getY()); |
310 | 0 | const bool bSnapX(bPrevVertical || bNextVertical); |
311 | 0 | const bool bSnapY(bPrevHorizontal || bNextHorizontal); |
312 | |
|
313 | 0 | if (bSnapX || bSnapY) |
314 | 0 | { |
315 | 0 | basegfx::B2DPoint aSnappedPoint(bSnapX ? maCurrTuple.getX() : maCurrPoint.getX(), |
316 | 0 | bSnapY ? maCurrTuple.getY() : maCurrPoint.getY()); |
317 | |
|
318 | 0 | if (rObjectToDeviceInv.isIdentity()) |
319 | 0 | { |
320 | 0 | rObjectToDeviceInv = rObjectToDevice; |
321 | 0 | rObjectToDeviceInv.invert(); |
322 | 0 | } |
323 | |
|
324 | 0 | aSnappedPoint *= rObjectToDeviceInv; |
325 | |
|
326 | 0 | return aSnappedPoint; |
327 | 0 | } |
328 | | |
329 | 0 | return rPolygon.getB2DPoint(nIndex); |
330 | 0 | } |
331 | | |
332 | | SystemDependentData_CairoPath::SystemDependentData_CairoPath(size_t nSizeMeasure, cairo_t* cr, |
333 | | bool bNoJoin, bool bAntiAlias, |
334 | | const std::vector<double>* pStroke) |
335 | 0 | : basegfx::SystemDependentData(Application::GetSystemDependentDataManager(), |
336 | 0 | basegfx::SDD_Type::SDDType_CairoPath) |
337 | 0 | , mpCairoPath(nullptr) |
338 | 0 | , mbNoJoin(bNoJoin) |
339 | 0 | , mbAntiAlias(bAntiAlias) |
340 | 0 | { |
341 | 0 | static const bool bFuzzing = comphelper::IsFuzzing(); |
342 | | |
343 | | // tdf#129845 only create a copy of the path when nSizeMeasure is |
344 | | // bigger than some decent threshold |
345 | 0 | if (!bFuzzing && nSizeMeasure > 50) |
346 | 0 | { |
347 | 0 | mpCairoPath = cairo_copy_path(cr); |
348 | |
|
349 | 0 | if (nullptr != pStroke) |
350 | 0 | { |
351 | 0 | maStroke = *pStroke; |
352 | 0 | } |
353 | 0 | } |
354 | 0 | } |
355 | | |
356 | | SystemDependentData_CairoPath::~SystemDependentData_CairoPath() |
357 | 0 | { |
358 | 0 | if (nullptr != mpCairoPath) |
359 | 0 | { |
360 | 0 | cairo_path_destroy(mpCairoPath); |
361 | 0 | mpCairoPath = nullptr; |
362 | 0 | } |
363 | 0 | } |
364 | | |
365 | | sal_Int64 SystemDependentData_CairoPath::estimateUsageInBytes() const |
366 | 0 | { |
367 | | // tdf#129845 by using the default return value of zero when no path |
368 | | // was created, SystemDependentData::calculateCombinedHoldCyclesInSeconds |
369 | | // will do the right thing and not buffer this entry at all |
370 | 0 | sal_Int64 nRetval(0); |
371 | |
|
372 | 0 | if (nullptr != mpCairoPath) |
373 | 0 | { |
374 | | // per node |
375 | | // - num_data incarnations of |
376 | | // - sizeof(cairo_path_data_t) which is a union of defines and point data |
377 | | // thus may 2 x sizeof(double) |
378 | 0 | nRetval = mpCairoPath->num_data * sizeof(cairo_path_data_t); |
379 | 0 | } |
380 | |
|
381 | 0 | return nRetval; |
382 | 0 | } |
383 | | |
384 | | void add_polygon_path(cairo_t* cr, const basegfx::B2DPolyPolygon& rPolyPolygon, |
385 | | const basegfx::B2DHomMatrix& rObjectToDevice, bool bPixelSnap) |
386 | 0 | { |
387 | | // try to access buffered data |
388 | 0 | std::shared_ptr<SystemDependentData_CairoPath> pSystemDependentData_CairoPath( |
389 | 0 | rPolyPolygon.getSystemDependentData<SystemDependentData_CairoPath>( |
390 | 0 | basegfx::SDD_Type::SDDType_CairoPath)); |
391 | |
|
392 | 0 | if (pSystemDependentData_CairoPath) |
393 | 0 | { |
394 | | // re-use data |
395 | 0 | cairo_append_path(cr, pSystemDependentData_CairoPath->getCairoPath()); |
396 | 0 | } |
397 | 0 | else |
398 | 0 | { |
399 | | // create data |
400 | 0 | size_t nSizeMeasure(0); |
401 | |
|
402 | 0 | for (const auto& rPoly : rPolyPolygon) |
403 | 0 | { |
404 | | // PixelOffset used: Was dependent of 'm_aLineColor != SALCOLOR_NONE' |
405 | | // Adapt setupPolyPolygon-users to set a linear transformation to achieve PixelOffset |
406 | 0 | nSizeMeasure += AddPolygonToPath(cr, rPoly, rObjectToDevice, bPixelSnap, false); |
407 | 0 | } |
408 | | |
409 | | // copy and add to buffering mechanism |
410 | | // for decisions how/what to buffer, see Note in WinSalGraphicsImpl::drawPolyPolygon |
411 | 0 | pSystemDependentData_CairoPath |
412 | 0 | = rPolyPolygon.addOrReplaceSystemDependentData<SystemDependentData_CairoPath>( |
413 | 0 | nSizeMeasure, cr, false, false, nullptr); |
414 | 0 | } |
415 | 0 | } |
416 | | |
417 | | cairo_user_data_key_t* CairoCommon::getDamageKey() |
418 | 19.4k | { |
419 | 19.4k | static cairo_user_data_key_t aDamageKey; |
420 | 19.4k | return &aDamageKey; |
421 | 19.4k | } |
422 | | |
423 | | sal_uInt16 CairoCommon::GetBitCount() const |
424 | 987k | { |
425 | 987k | if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_ALPHA) |
426 | 0 | return 1; |
427 | 987k | return 32; |
428 | 987k | } |
429 | | |
430 | | cairo_t* CairoCommon::getCairoContext(bool bXorModeAllowed, bool bAntiAlias) const |
431 | 433k | { |
432 | 433k | cairo_t* cr; |
433 | 433k | if (m_ePaintMode == PaintMode::Xor && bXorModeAllowed) |
434 | 0 | cr = createTmpCompatibleCairoContext(); |
435 | 433k | else |
436 | 433k | cr = cairo_create(m_pSurface); |
437 | 433k | cairo_set_line_width(cr, 1); |
438 | 433k | cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); |
439 | 433k | cairo_set_antialias(cr, bAntiAlias ? CAIRO_ANTIALIAS_DEFAULT : CAIRO_ANTIALIAS_NONE); |
440 | 433k | cairo_set_operator(cr, CAIRO_OPERATOR_OVER); |
441 | | |
442 | | // ensure no linear transformation and no PathInfo in local cairo_path_t |
443 | 433k | cairo_identity_matrix(cr); |
444 | 433k | cairo_new_path(cr); |
445 | | |
446 | 433k | return cr; |
447 | 433k | } |
448 | | |
449 | | void CairoCommon::releaseCairoContext(cairo_t* cr, bool bXorModeAllowed, |
450 | | const basegfx::B2DRange& rExtents) const |
451 | 433k | { |
452 | 433k | const bool bXoring = (m_ePaintMode == PaintMode::Xor && bXorModeAllowed); |
453 | | |
454 | 433k | if (rExtents.isEmpty()) |
455 | 414k | { |
456 | | //nothing changed, return early |
457 | 414k | if (bXoring) |
458 | 0 | { |
459 | 0 | cairo_surface_t* surface = cairo_get_target(cr); |
460 | 0 | cairo_surface_destroy(surface); |
461 | 0 | } |
462 | 414k | cairo_destroy(cr); |
463 | 414k | return; |
464 | 414k | } |
465 | | |
466 | 19.4k | basegfx::B2IRange aIntExtents(basegfx::unotools::b2ISurroundingRangeFromB2DRange(rExtents)); |
467 | 19.4k | sal_Int32 nExtentsLeft(aIntExtents.getMinX()), nExtentsTop(aIntExtents.getMinY()); |
468 | 19.4k | sal_Int32 nExtentsRight(aIntExtents.getMaxX()), nExtentsBottom(aIntExtents.getMaxY()); |
469 | 19.4k | sal_Int32 nWidth = m_aFrameSize.getX(); |
470 | 19.4k | sal_Int32 nHeight = m_aFrameSize.getY(); |
471 | 19.4k | nExtentsLeft = std::max<sal_Int32>(nExtentsLeft, 0); |
472 | 19.4k | nExtentsTop = std::max<sal_Int32>(nExtentsTop, 0); |
473 | 19.4k | nExtentsRight = std::min<sal_Int32>(nExtentsRight, nWidth); |
474 | 19.4k | nExtentsBottom = std::min<sal_Int32>(nExtentsBottom, nHeight); |
475 | | |
476 | 19.4k | cairo_surface_t* surface = cairo_get_target(cr); |
477 | 19.4k | cairo_surface_flush(surface); |
478 | | |
479 | | //For the most part we avoid the use of XOR these days, but there |
480 | | //are some edge cases where legacy stuff still supports it, so |
481 | | //emulate it (slowly) here. |
482 | 19.4k | if (bXoring) |
483 | 0 | doXorOnRelease(nExtentsLeft, nExtentsTop, nExtentsRight, nExtentsBottom, surface, nWidth); |
484 | | |
485 | 19.4k | cairo_destroy(cr); // unref |
486 | | |
487 | 19.4k | DamageHandler* pDamage |
488 | 19.4k | = static_cast<DamageHandler*>(cairo_surface_get_user_data(m_pSurface, getDamageKey())); |
489 | | |
490 | 19.4k | if (pDamage) |
491 | 0 | { |
492 | 0 | pDamage->damaged(pDamage->handle, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, |
493 | 0 | nExtentsBottom - nExtentsTop); |
494 | 0 | } |
495 | 19.4k | } |
496 | | |
497 | | void CairoCommon::applyFullDamage() const |
498 | 4 | { |
499 | 4 | if (nullptr == m_pSurface) |
500 | 0 | return; |
501 | 4 | DamageHandler* pDamage |
502 | 4 | = static_cast<DamageHandler*>(cairo_surface_get_user_data(m_pSurface, getDamageKey())); |
503 | 4 | if (nullptr == pDamage) |
504 | 4 | return; |
505 | 0 | pDamage->damaged(pDamage->handle, 0, 0, m_aFrameSize.getX(), m_aFrameSize.getY()); |
506 | 0 | } |
507 | | |
508 | | void CairoCommon::doXorOnRelease(sal_Int32 nExtentsLeft, sal_Int32 nExtentsTop, |
509 | | sal_Int32 nExtentsRight, sal_Int32 nExtentsBottom, |
510 | | cairo_surface_t* const surface, sal_Int32 nWidth) const |
511 | 0 | { |
512 | | //For the most part we avoid the use of XOR these days, but there |
513 | | //are some edge cases where legacy stuff still supports it, so |
514 | | //emulate it (slowly) here. |
515 | 0 | cairo_surface_t* target_surface = m_pSurface; |
516 | 0 | if (cairo_surface_get_type(target_surface) != CAIRO_SURFACE_TYPE_IMAGE) |
517 | 0 | { |
518 | | //in the unlikely case we can't use m_pSurface directly, copy contents |
519 | | //to another temp image surface |
520 | 0 | if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_COLOR_ALPHA) |
521 | 0 | target_surface = cairo_surface_map_to_image(target_surface, nullptr); |
522 | 0 | else |
523 | 0 | { |
524 | | // for gen, which is CAIRO_FORMAT_RGB24/CAIRO_CONTENT_COLOR I'm getting |
525 | | // visual corruption in vcldemo with cairo_surface_map_to_image |
526 | 0 | cairo_t* copycr = createTmpCompatibleCairoContext(); |
527 | 0 | cairo_rectangle(copycr, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, |
528 | 0 | nExtentsBottom - nExtentsTop); |
529 | 0 | cairo_set_source_surface(copycr, m_pSurface, 0, 0); |
530 | 0 | cairo_fill(copycr); |
531 | 0 | target_surface = cairo_get_target(copycr); |
532 | 0 | cairo_destroy(copycr); |
533 | 0 | } |
534 | 0 | } |
535 | |
|
536 | 0 | cairo_surface_flush(target_surface); |
537 | 0 | unsigned char* target_surface_data = cairo_image_surface_get_data(target_surface); |
538 | 0 | unsigned char* xor_surface_data = cairo_image_surface_get_data(surface); |
539 | |
|
540 | 0 | cairo_format_t nFormat = cairo_image_surface_get_format(target_surface); |
541 | 0 | assert(nFormat == CAIRO_FORMAT_ARGB32 && "need to implement CAIRO_FORMAT_A1 after all here"); |
542 | 0 | sal_Int32 nStride = cairo_format_stride_for_width(nFormat, nWidth * m_fScale); |
543 | 0 | sal_Int32 nUnscaledExtentsLeft = nExtentsLeft * m_fScale; |
544 | 0 | sal_Int32 nUnscaledExtentsRight = nExtentsRight * m_fScale; |
545 | 0 | sal_Int32 nUnscaledExtentsTop = nExtentsTop * m_fScale; |
546 | 0 | sal_Int32 nUnscaledExtentsBottom = nExtentsBottom * m_fScale; |
547 | | |
548 | | // Handle headless size forced to (1,1) by SvpSalFrame::GetSurfaceFrameSize(). |
549 | 0 | int target_surface_width = cairo_image_surface_get_width(target_surface); |
550 | 0 | if (nUnscaledExtentsLeft > target_surface_width) |
551 | 0 | nUnscaledExtentsLeft = target_surface_width; |
552 | 0 | if (nUnscaledExtentsRight > target_surface_width) |
553 | 0 | nUnscaledExtentsRight = target_surface_width; |
554 | 0 | int target_surface_height = cairo_image_surface_get_height(target_surface); |
555 | 0 | if (nUnscaledExtentsTop > target_surface_height) |
556 | 0 | nUnscaledExtentsTop = target_surface_height; |
557 | 0 | if (nUnscaledExtentsBottom > target_surface_height) |
558 | 0 | nUnscaledExtentsBottom = target_surface_height; |
559 | |
|
560 | 0 | #if !ENABLE_WASM_STRIP_PREMULTIPLY |
561 | 0 | vcl::bitmap::lookup_table const& unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); |
562 | 0 | vcl::bitmap::lookup_table const& premultiply_table = vcl::bitmap::get_premultiply_table(); |
563 | 0 | #endif |
564 | 0 | for (sal_Int32 y = nUnscaledExtentsTop; y < nUnscaledExtentsBottom; ++y) |
565 | 0 | { |
566 | 0 | unsigned char* true_row = target_surface_data + (nStride * y); |
567 | 0 | unsigned char* xor_row = xor_surface_data + (nStride * y); |
568 | 0 | unsigned char* true_data = true_row + (nUnscaledExtentsLeft * 4); |
569 | 0 | unsigned char* xor_data = xor_row + (nUnscaledExtentsLeft * 4); |
570 | 0 | for (sal_Int32 x = nUnscaledExtentsLeft; x < nUnscaledExtentsRight; ++x) |
571 | 0 | { |
572 | 0 | sal_uInt8 a = true_data[SVP_CAIRO_ALPHA]; |
573 | 0 | sal_uInt8 xor_a = xor_data[SVP_CAIRO_ALPHA]; |
574 | | #if ENABLE_WASM_STRIP_PREMULTIPLY |
575 | | sal_uInt8 b = vcl::bitmap::unpremultiply(true_data[SVP_CAIRO_BLUE], a) |
576 | | ^ vcl::bitmap::unpremultiply(xor_data[SVP_CAIRO_BLUE], xor_a); |
577 | | sal_uInt8 g = vcl::bitmap::unpremultiply(true_data[SVP_CAIRO_GREEN], a) |
578 | | ^ vcl::bitmap::unpremultiply(xor_data[SVP_CAIRO_GREEN], xor_a); |
579 | | sal_uInt8 r = vcl::bitmap::unpremultiply(true_data[SVP_CAIRO_RED], a) |
580 | | ^ vcl::bitmap::unpremultiply(xor_data[SVP_CAIRO_RED], xor_a); |
581 | | true_data[SVP_CAIRO_BLUE] = vcl::bitmap::premultiply(b, a); |
582 | | true_data[SVP_CAIRO_GREEN] = vcl::bitmap::premultiply(g, a); |
583 | | true_data[SVP_CAIRO_RED] = vcl::bitmap::premultiply(r, a); |
584 | | #else |
585 | 0 | sal_uInt8 b = unpremultiply_table[a][true_data[SVP_CAIRO_BLUE]] |
586 | 0 | ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_BLUE]]; |
587 | 0 | sal_uInt8 g = unpremultiply_table[a][true_data[SVP_CAIRO_GREEN]] |
588 | 0 | ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_GREEN]]; |
589 | 0 | sal_uInt8 r = unpremultiply_table[a][true_data[SVP_CAIRO_RED]] |
590 | 0 | ^ unpremultiply_table[xor_a][xor_data[SVP_CAIRO_RED]]; |
591 | 0 | true_data[SVP_CAIRO_BLUE] = premultiply_table[a][b]; |
592 | 0 | true_data[SVP_CAIRO_GREEN] = premultiply_table[a][g]; |
593 | 0 | true_data[SVP_CAIRO_RED] = premultiply_table[a][r]; |
594 | 0 | #endif |
595 | 0 | true_data += 4; |
596 | 0 | xor_data += 4; |
597 | 0 | } |
598 | 0 | } |
599 | 0 | cairo_surface_mark_dirty(target_surface); |
600 | |
|
601 | 0 | if (target_surface != m_pSurface) |
602 | 0 | { |
603 | 0 | if (cairo_surface_get_content(m_pSurface) == CAIRO_CONTENT_COLOR_ALPHA) |
604 | 0 | cairo_surface_unmap_image(m_pSurface, target_surface); |
605 | 0 | else |
606 | 0 | { |
607 | 0 | cairo_t* copycr = cairo_create(m_pSurface); |
608 | | //copy contents back from image surface |
609 | 0 | cairo_rectangle(copycr, nExtentsLeft, nExtentsTop, nExtentsRight - nExtentsLeft, |
610 | 0 | nExtentsBottom - nExtentsTop); |
611 | 0 | cairo_set_source_surface(copycr, target_surface, 0, 0); |
612 | 0 | cairo_fill(copycr); |
613 | 0 | cairo_destroy(copycr); |
614 | 0 | cairo_surface_destroy(target_surface); |
615 | 0 | } |
616 | 0 | } |
617 | |
|
618 | 0 | cairo_surface_destroy(surface); |
619 | 0 | } |
620 | | |
621 | | cairo_t* CairoCommon::createTmpCompatibleCairoContext() const |
622 | 0 | { |
623 | 0 | cairo_surface_t* target = cairo_surface_create_similar_image(m_pSurface, CAIRO_FORMAT_ARGB32, |
624 | 0 | m_aFrameSize.getX() * m_fScale, |
625 | 0 | m_aFrameSize.getY() * m_fScale); |
626 | |
|
627 | 0 | dl_cairo_surface_set_device_scale(target, m_fScale, m_fScale); |
628 | |
|
629 | 0 | return cairo_create(target); |
630 | 0 | } |
631 | | |
632 | | void CairoCommon::applyColor(cairo_t* cr, Color aColor, double fTransparency) |
633 | 0 | { |
634 | 0 | if (cairo_surface_get_content(cairo_get_target(cr)) != CAIRO_CONTENT_ALPHA) |
635 | 0 | { |
636 | 0 | cairo_set_source_rgba(cr, aColor.GetRed() / 255.0, aColor.GetGreen() / 255.0, |
637 | 0 | aColor.GetBlue() / 255.0, 1.0 - fTransparency); |
638 | 0 | } |
639 | 0 | else |
640 | 0 | { |
641 | 0 | double fSet = aColor == COL_BLACK ? 1.0 : 0.0; |
642 | 0 | cairo_set_source_rgba(cr, 1, 1, 1, fSet); |
643 | 0 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); |
644 | 0 | } |
645 | 0 | } |
646 | | |
647 | | void CairoCommon::applyColor2(cairo_t* cr, Color aColor) |
648 | 12.9k | { |
649 | 12.9k | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); |
650 | 12.9k | cairo_content_t eContentType = cairo_surface_get_content(cairo_get_target(cr)); |
651 | 12.9k | if (eContentType == CAIRO_CONTENT_COLOR_ALPHA) |
652 | 12.9k | { |
653 | 12.9k | cairo_set_source_rgba(cr, aColor.GetRed() / 255.0, aColor.GetGreen() / 255.0, |
654 | 12.9k | aColor.GetBlue() / 255.0, aColor.GetAlpha() / 255.0); |
655 | 12.9k | } |
656 | 0 | else if (eContentType == CAIRO_CONTENT_COLOR) |
657 | 0 | { |
658 | 0 | cairo_set_source_rgba(cr, aColor.GetRed() / 255.0, aColor.GetGreen() / 255.0, |
659 | 0 | aColor.GetBlue() / 255.0, 1.0); |
660 | 0 | } |
661 | 0 | else // CAIRO_CONTENT_ALPHA |
662 | 0 | { |
663 | 0 | double fSet = aColor == COL_BLACK ? 1.0 : 0.0; |
664 | 0 | cairo_set_source_rgba(cr, 1, 1, 1, fSet); |
665 | 0 | } |
666 | 12.9k | } |
667 | | |
668 | | void CairoCommon::clipRegion(cairo_t* cr, const vcl::Region& rClipRegion) |
669 | 433k | { |
670 | 433k | RectangleVector aRectangles; |
671 | 433k | if (!rClipRegion.IsEmpty()) |
672 | 433k | { |
673 | 433k | rClipRegion.GetRegionRectangles(aRectangles); |
674 | 433k | } |
675 | 433k | if (!aRectangles.empty()) |
676 | 0 | { |
677 | 0 | bool bEmpty = true; |
678 | 0 | for (auto const& rectangle : aRectangles) |
679 | 0 | { |
680 | 0 | if (rectangle.GetWidth() <= 0 || rectangle.GetHeight() <= 0) |
681 | 0 | { |
682 | 0 | SAL_WARN("vcl.gdi", "bad clip rect of: " << rectangle); |
683 | 0 | continue; |
684 | 0 | } |
685 | 0 | cairo_rectangle(cr, rectangle.Left(), rectangle.Top(), rectangle.GetWidth(), |
686 | 0 | rectangle.GetHeight()); |
687 | 0 | bEmpty = false; |
688 | 0 | } |
689 | 0 | if (!bEmpty) |
690 | 0 | cairo_clip(cr); |
691 | 0 | } |
692 | 433k | } |
693 | | |
694 | 433k | void CairoCommon::clipRegion(cairo_t* cr) { CairoCommon::clipRegion(cr, m_aClipRegion); } |
695 | | |
696 | | void CairoCommon::SetXORMode(bool bSet, bool /*bInvertOnly*/) |
697 | 471k | { |
698 | 471k | m_ePaintMode = bSet ? PaintMode::Xor : PaintMode::Over; |
699 | 471k | } |
700 | | |
701 | | void CairoCommon::SetROPLineColor(SalROPColor nROPColor) |
702 | 0 | { |
703 | 0 | switch (nROPColor) |
704 | 0 | { |
705 | 0 | case SalROPColor::N0: |
706 | 0 | m_oLineColor = Color(0, 0, 0); |
707 | 0 | break; |
708 | 0 | case SalROPColor::N1: |
709 | 0 | case SalROPColor::Invert: |
710 | 0 | m_oLineColor = Color(0xff, 0xff, 0xff); |
711 | 0 | break; |
712 | 0 | } |
713 | 0 | } |
714 | | |
715 | | void CairoCommon::SetROPFillColor(SalROPColor nROPColor) |
716 | 0 | { |
717 | 0 | switch (nROPColor) |
718 | 0 | { |
719 | 0 | case SalROPColor::N0: |
720 | 0 | m_oFillColor = Color(0, 0, 0); |
721 | 0 | break; |
722 | 0 | case SalROPColor::N1: |
723 | 0 | case SalROPColor::Invert: |
724 | 0 | m_oFillColor = Color(0xff, 0xff, 0xff); |
725 | 0 | break; |
726 | 0 | } |
727 | 0 | } |
728 | | |
729 | | void CairoCommon::drawPixel(const std::optional<Color>& rLineColor, tools::Long nX, tools::Long nY, |
730 | | bool bAntiAlias) |
731 | 414k | { |
732 | 414k | if (!rLineColor) |
733 | 0 | return; |
734 | | |
735 | 414k | cairo_t* cr = getCairoContext(true, bAntiAlias); |
736 | 414k | clipRegion(cr); |
737 | | |
738 | 414k | cairo_rectangle(cr, nX, nY, 1, 1); |
739 | | |
740 | 414k | cairo_content_t eContentType = cairo_surface_get_content(cairo_get_target(cr)); |
741 | 414k | if (eContentType == CAIRO_CONTENT_COLOR_ALPHA) |
742 | 414k | { |
743 | 414k | cairo_set_source_rgba(cr, rLineColor->GetRed() / 255.0, rLineColor->GetGreen() / 255.0, |
744 | 414k | rLineColor->GetBlue() / 255.0, rLineColor->GetAlpha() / 255.0); |
745 | 414k | } |
746 | 0 | else if (eContentType == CAIRO_CONTENT_COLOR) |
747 | 0 | { |
748 | 0 | cairo_set_source_rgba(cr, rLineColor->GetRed() / 255.0, rLineColor->GetGreen() / 255.0, |
749 | 0 | rLineColor->GetBlue() / 255.0, 1.0); |
750 | 0 | } |
751 | 0 | else // eContentType == CAIRO_CONTENT_ALPHA |
752 | 0 | { |
753 | 0 | double fSet = *rLineColor == COL_BLACK ? 1.0 : 0.0; |
754 | 0 | cairo_set_source_rgba(cr, 1, 1, 1, fSet); |
755 | 0 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); |
756 | 0 | } |
757 | | |
758 | 414k | cairo_fill(cr); |
759 | | |
760 | 414k | basegfx::B2DRange extents = getClippedFillDamage(cr); |
761 | 414k | releaseCairoContext(cr, true, extents); |
762 | 414k | } |
763 | | |
764 | | Color CairoCommon::getPixel(cairo_surface_t* pSurface, tools::Long nX, tools::Long nY) |
765 | 427k | { |
766 | 427k | cairo_surface_t* target |
767 | 427k | = cairo_surface_create_similar_image(pSurface, CAIRO_FORMAT_ARGB32, 1, 1); |
768 | | |
769 | 427k | cairo_t* cr = cairo_create(target); |
770 | | |
771 | 427k | cairo_rectangle(cr, 0, 0, 1, 1); |
772 | 427k | cairo_set_source_surface(cr, pSurface, -nX, -nY); |
773 | 427k | cairo_paint(cr); |
774 | 427k | cairo_destroy(cr); |
775 | | |
776 | 427k | cairo_surface_flush(target); |
777 | 427k | #if !ENABLE_WASM_STRIP_PREMULTIPLY |
778 | 427k | vcl::bitmap::lookup_table const& unpremultiply_table = vcl::bitmap::get_unpremultiply_table(); |
779 | 427k | #endif |
780 | 427k | unsigned char* data = cairo_image_surface_get_data(target); |
781 | 427k | sal_uInt8 a = data[SVP_CAIRO_ALPHA]; |
782 | | #if ENABLE_WASM_STRIP_PREMULTIPLY |
783 | | sal_uInt8 b = vcl::bitmap::unpremultiply(data[SVP_CAIRO_BLUE], a); |
784 | | sal_uInt8 g = vcl::bitmap::unpremultiply(data[SVP_CAIRO_GREEN], a); |
785 | | sal_uInt8 r = vcl::bitmap::unpremultiply(data[SVP_CAIRO_RED], a); |
786 | | #else |
787 | 427k | sal_uInt8 b = unpremultiply_table[a][data[SVP_CAIRO_BLUE]]; |
788 | 427k | sal_uInt8 g = unpremultiply_table[a][data[SVP_CAIRO_GREEN]]; |
789 | 427k | sal_uInt8 r = unpremultiply_table[a][data[SVP_CAIRO_RED]]; |
790 | 427k | #endif |
791 | 427k | Color aColor(ColorAlpha, a, r, g, b); |
792 | 427k | cairo_surface_destroy(target); |
793 | | |
794 | 427k | return aColor; |
795 | 427k | } |
796 | | |
797 | | void CairoCommon::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2, tools::Long nY2, |
798 | | bool bAntiAlias) |
799 | 0 | { |
800 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
801 | 0 | clipRegion(cr); |
802 | |
|
803 | 0 | basegfx::B2DPolygon aPoly; |
804 | | |
805 | | // PixelOffset used: To not mix with possible PixelSnap, cannot do |
806 | | // directly on coordinates as tried before - despite being already 'snapped' |
807 | | // due to being integer. If it would be directly added here, it would be |
808 | | // 'snapped' again when !getAntiAlias(), losing the (0.5, 0.5) offset |
809 | 0 | aPoly.append(basegfx::B2DPoint(nX1, nY1)); |
810 | 0 | aPoly.append(basegfx::B2DPoint(nX2, nY2)); |
811 | | |
812 | | // PixelOffset used: Set PixelOffset as linear transformation |
813 | 0 | cairo_matrix_t aMatrix; |
814 | 0 | cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); |
815 | 0 | cairo_set_matrix(cr, &aMatrix); |
816 | |
|
817 | 0 | AddPolygonToPath(cr, aPoly, basegfx::B2DHomMatrix(), !bAntiAlias, false); |
818 | |
|
819 | 0 | CairoCommon::applyColor(cr, *m_oLineColor); |
820 | |
|
821 | 0 | basegfx::B2DRange extents = getClippedStrokeDamage(cr); |
822 | 0 | extents.translate(0.5, 0.5); |
823 | |
|
824 | 0 | cairo_stroke(cr); |
825 | |
|
826 | 0 | releaseCairoContext(cr, false, extents); |
827 | 0 | } |
828 | | |
829 | | // true if we have a fill color and the line color is the same or non-existent |
830 | | static bool onlyFillRect(const std::optional<Color>& rFillColor, |
831 | | const std::optional<Color>& rLineColor) |
832 | 12.9k | { |
833 | 12.9k | if (!rFillColor) |
834 | 0 | return false; |
835 | 12.9k | if (!rLineColor) |
836 | 12.9k | return true; |
837 | 0 | return *rFillColor == *rLineColor; |
838 | 12.9k | } |
839 | | |
840 | | void CairoCommon::drawRect(double nX, double nY, double nWidth, double nHeight, bool bAntiAlias) |
841 | 12.9k | { |
842 | | // fast path for the common case of simply creating a solid block of color |
843 | 12.9k | if (onlyFillRect(m_oFillColor, m_oLineColor)) |
844 | 12.9k | { |
845 | | // don't bother trying to draw stuff which is effectively invisible |
846 | 12.9k | if (nWidth < 0.1 || nHeight < 0.1) |
847 | 0 | return; |
848 | | |
849 | 12.9k | cairo_t* cr = getCairoContext(true, bAntiAlias); |
850 | 12.9k | clipRegion(cr); |
851 | | |
852 | 12.9k | bool bPixelSnap = !bAntiAlias; |
853 | 12.9k | if (bPixelSnap) |
854 | 12.9k | { |
855 | | // snap by rounding |
856 | 12.9k | nX = basegfx::fround(nX); |
857 | 12.9k | nY = basegfx::fround(nY); |
858 | 12.9k | nWidth = basegfx::fround(nWidth); |
859 | 12.9k | nHeight = basegfx::fround(nHeight); |
860 | 12.9k | } |
861 | 12.9k | cairo_rectangle(cr, nX, nY, nWidth, nHeight); |
862 | | |
863 | 12.9k | CairoCommon::applyColor2(cr, *m_oFillColor); |
864 | | // Get FillDamage |
865 | 12.9k | basegfx::B2DRange extents = getClippedFillDamage(cr); |
866 | | |
867 | 12.9k | cairo_fill(cr); |
868 | | |
869 | 12.9k | releaseCairoContext(cr, true, extents); |
870 | | |
871 | 12.9k | return; |
872 | 12.9k | } |
873 | | // because of the -1 hack we have to do fill and draw separately |
874 | 0 | std::optional<Color> aOrigFillColor = m_oFillColor; |
875 | 0 | std::optional<Color> aOrigLineColor = m_oLineColor; |
876 | 0 | m_oFillColor = std::nullopt; |
877 | 0 | m_oLineColor = std::nullopt; |
878 | |
|
879 | 0 | if (aOrigFillColor) |
880 | 0 | { |
881 | 0 | basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( |
882 | 0 | basegfx::B2DRectangle(nX, nY, nX + nWidth, nY + nHeight)); |
883 | |
|
884 | 0 | m_oFillColor = aOrigFillColor; |
885 | 0 | drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aRect), |
886 | 0 | (255 - aOrigFillColor->GetAlpha()) / 255.0, bAntiAlias); |
887 | 0 | m_oFillColor = std::nullopt; |
888 | 0 | } |
889 | |
|
890 | 0 | if (aOrigLineColor) |
891 | 0 | { |
892 | | // need -1 hack to exclude the bottom and right edges to act like wingdi "Rectangle" |
893 | | // function which is what was probably the ultimate origin of this behavior |
894 | 0 | basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( |
895 | 0 | basegfx::B2DRectangle(nX, nY, nX + nWidth - 1, nY + nHeight - 1)); |
896 | |
|
897 | 0 | m_oLineColor = aOrigLineColor; |
898 | 0 | drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aRect), |
899 | 0 | (255 - aOrigLineColor->GetAlpha()) / 255.0, bAntiAlias); |
900 | 0 | m_oLineColor = std::nullopt; |
901 | 0 | } |
902 | |
|
903 | 0 | m_oFillColor = aOrigFillColor; |
904 | 0 | m_oLineColor = aOrigLineColor; |
905 | 0 | } |
906 | | |
907 | | void CairoCommon::drawPolygon(sal_uInt32 nPoints, const Point* pPtAry, bool bAntiAlias) |
908 | 0 | { |
909 | 0 | basegfx::B2DPolygon aPoly; |
910 | 0 | aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); |
911 | 0 | for (sal_uInt32 i = 1; i < nPoints; ++i) |
912 | 0 | aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); |
913 | |
|
914 | 0 | drawPolyPolygon(basegfx::B2DHomMatrix(), basegfx::B2DPolyPolygon(aPoly), 0.0, bAntiAlias); |
915 | 0 | } |
916 | | |
917 | | void CairoCommon::drawPolyPolygon(sal_uInt32 nPoly, const sal_uInt32* pPointCounts, |
918 | | const Point** pPtAry, bool bAntiAlias) |
919 | 0 | { |
920 | 0 | basegfx::B2DPolyPolygon aPolyPoly; |
921 | 0 | for (sal_uInt32 nPolygon = 0; nPolygon < nPoly; ++nPolygon) |
922 | 0 | { |
923 | 0 | sal_uInt32 nPoints = pPointCounts[nPolygon]; |
924 | 0 | if (nPoints) |
925 | 0 | { |
926 | 0 | const Point* pPoints = pPtAry[nPolygon]; |
927 | 0 | basegfx::B2DPolygon aPoly; |
928 | 0 | aPoly.append(basegfx::B2DPoint(pPoints->getX(), pPoints->getY()), nPoints); |
929 | 0 | for (sal_uInt32 i = 1; i < nPoints; ++i) |
930 | 0 | aPoly.setB2DPoint(i, basegfx::B2DPoint(pPoints[i].getX(), pPoints[i].getY())); |
931 | |
|
932 | 0 | aPolyPoly.append(aPoly); |
933 | 0 | } |
934 | 0 | } |
935 | |
|
936 | 0 | drawPolyPolygon(basegfx::B2DHomMatrix(), aPolyPoly, 0.0, bAntiAlias); |
937 | 0 | } |
938 | | |
939 | | void CairoCommon::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice, |
940 | | const basegfx::B2DPolyPolygon& rPolyPolygon, double fTransparency, |
941 | | bool bAntiAlias) |
942 | 0 | { |
943 | 0 | const bool bHasFill(m_oFillColor.has_value()); |
944 | 0 | const bool bHasLine(m_oLineColor.has_value()); |
945 | |
|
946 | 0 | if (0 == rPolyPolygon.count() || !(bHasFill || bHasLine) || fTransparency < 0.0 |
947 | 0 | || fTransparency >= 1.0) |
948 | 0 | { |
949 | 0 | return; |
950 | 0 | } |
951 | | |
952 | 0 | static const bool bFuzzing = comphelper::IsFuzzing(); |
953 | 0 | if (bFuzzing) |
954 | 0 | { |
955 | 0 | const basegfx::B2DRange aRange(rPolyPolygon.getB2DRange()); |
956 | 0 | if (aRange.getMinX() < -nCairoCoordinateMax || aRange.getMinY() < -nCairoCoordinateMax |
957 | 0 | || aRange.getMaxX() > nCairoCoordinateMax || aRange.getMaxY() > nCairoCoordinateMax) |
958 | 0 | { |
959 | 0 | SAL_WARN("vcl.gdi", "drawPolyPolygon, skipping suspicious range of: " |
960 | 0 | << aRange << " for fuzzing performance"); |
961 | 0 | return; |
962 | 0 | } |
963 | 0 | } |
964 | | |
965 | 0 | if (!bHasLine) |
966 | 0 | { |
967 | | // don't bother trying to draw stuff which is effectively invisible, speeds up |
968 | | // drawing some complex drawings. This optimisation is not valid when we do |
969 | | // the pixel offset thing (i.e. bHasLine) |
970 | 0 | basegfx::B2DRange aPolygonRange = rPolyPolygon.getB2DRange(); |
971 | 0 | aPolygonRange.transform(rObjectToDevice); |
972 | 0 | if (aPolygonRange.getWidth() < 0.1 || aPolygonRange.getHeight() < 0.1) |
973 | 0 | return; |
974 | 0 | } |
975 | | |
976 | 0 | cairo_t* cr = getCairoContext(true, bAntiAlias); |
977 | 0 | if (cairo_status(cr) != CAIRO_STATUS_SUCCESS) |
978 | 0 | { |
979 | 0 | SAL_WARN("vcl.gdi", |
980 | 0 | "cannot render to surface: " << cairo_status_to_string(cairo_status(cr))); |
981 | 0 | releaseCairoContext(cr, true, basegfx::B2DRange()); |
982 | 0 | return; |
983 | 0 | } |
984 | 0 | clipRegion(cr); |
985 | | |
986 | | // Set full (Object-to-Device) transformation - if used |
987 | 0 | if (!rObjectToDevice.isIdentity()) |
988 | 0 | { |
989 | 0 | cairo_matrix_t aMatrix; |
990 | |
|
991 | 0 | cairo_matrix_init(&aMatrix, rObjectToDevice.get(0, 0), rObjectToDevice.get(1, 0), |
992 | 0 | rObjectToDevice.get(0, 1), rObjectToDevice.get(1, 1), |
993 | 0 | rObjectToDevice.get(0, 2), rObjectToDevice.get(1, 2)); |
994 | 0 | cairo_set_matrix(cr, &aMatrix); |
995 | 0 | } |
996 | | |
997 | | // To make releaseCairoContext work, use empty extents |
998 | 0 | basegfx::B2DRange extents; |
999 | |
|
1000 | 0 | if (bHasFill) |
1001 | 0 | { |
1002 | 0 | add_polygon_path(cr, rPolyPolygon, rObjectToDevice, !bAntiAlias); |
1003 | |
|
1004 | 0 | CairoCommon::applyColor(cr, *m_oFillColor, fTransparency); |
1005 | | // Get FillDamage (will be extended for LineDamage below) |
1006 | 0 | extents = getClippedFillDamage(cr); |
1007 | |
|
1008 | 0 | cairo_fill(cr); |
1009 | 0 | } |
1010 | |
|
1011 | 0 | if (bHasLine) |
1012 | 0 | { |
1013 | | // PixelOffset used: Set PixelOffset as linear transformation |
1014 | 0 | cairo_matrix_t aMatrix; |
1015 | 0 | cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); |
1016 | 0 | cairo_set_matrix(cr, &aMatrix); |
1017 | |
|
1018 | 0 | add_polygon_path(cr, rPolyPolygon, rObjectToDevice, !bAntiAlias); |
1019 | |
|
1020 | 0 | CairoCommon::applyColor(cr, *m_oLineColor, fTransparency); |
1021 | | |
1022 | | // expand with possible StrokeDamage |
1023 | 0 | basegfx::B2DRange stroke_extents = getClippedStrokeDamage(cr); |
1024 | 0 | stroke_extents.translate(0.5, 0.5); |
1025 | 0 | extents.expand(stroke_extents); |
1026 | |
|
1027 | 0 | cairo_stroke(cr); |
1028 | 0 | } |
1029 | | |
1030 | | // if transformation has been applied, transform also extents (ranges) |
1031 | | // of damage so they can be correctly redrawn |
1032 | 0 | extents.transform(rObjectToDevice); |
1033 | 0 | releaseCairoContext(cr, true, extents); |
1034 | 0 | } |
1035 | | |
1036 | | void CairoCommon::drawPolyLine(sal_uInt32 nPoints, const Point* pPtAry, bool bAntiAlias) |
1037 | 0 | { |
1038 | 0 | basegfx::B2DPolygon aPoly; |
1039 | 0 | aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); |
1040 | 0 | for (sal_uInt32 i = 1; i < nPoints; ++i) |
1041 | 0 | aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); |
1042 | 0 | aPoly.setClosed(false); |
1043 | |
|
1044 | 0 | drawPolyLine(basegfx::B2DHomMatrix(), aPoly, 0.0, 1.0, nullptr, basegfx::B2DLineJoin::Miter, |
1045 | 0 | css::drawing::LineCap_BUTT, basegfx::deg2rad(15.0) /*default*/, false, bAntiAlias); |
1046 | 0 | } |
1047 | | |
1048 | | bool CairoCommon::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice, |
1049 | | const basegfx::B2DPolygon& rPolyLine, double fTransparency, |
1050 | | double fLineWidth, const std::vector<double>* pStroke, |
1051 | | basegfx::B2DLineJoin eLineJoin, css::drawing::LineCap eLineCap, |
1052 | | double fMiterMinimumAngle, bool bPixelSnapHairline, bool bAntiAlias) |
1053 | 0 | { |
1054 | | // short circuit if there is nothing to do |
1055 | 0 | if (0 == rPolyLine.count() || fTransparency < 0.0 || fTransparency >= 1.0) |
1056 | 0 | { |
1057 | 0 | return true; |
1058 | 0 | } |
1059 | | |
1060 | 0 | static const bool bFuzzing = comphelper::IsFuzzing(); |
1061 | 0 | if (bFuzzing) |
1062 | 0 | { |
1063 | 0 | const basegfx::B2DRange aRange(rPolyLine.getB2DRange()); |
1064 | 0 | if (aRange.getMinX() < -nCairoCoordinateMax || aRange.getMinY() < -nCairoCoordinateMax |
1065 | 0 | || aRange.getMaxX() > nCairoCoordinateMax || aRange.getMaxY() > nCairoCoordinateMax) |
1066 | 0 | { |
1067 | 0 | SAL_WARN("vcl.gdi", "drawPolyLine, skipping suspicious range of: " |
1068 | 0 | << aRange << " for fuzzing performance"); |
1069 | 0 | return true; |
1070 | 0 | } |
1071 | 0 | } |
1072 | | |
1073 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1074 | 0 | clipRegion(cr); |
1075 | | |
1076 | | // need to check/handle LineWidth when ObjectToDevice transformation is used |
1077 | 0 | const bool bObjectToDeviceIsIdentity(rObjectToDevice.isIdentity()); |
1078 | | |
1079 | | // tdf#124848 calculate-back logical LineWidth for a hairline |
1080 | | // since this implementation hands over the transformation to |
1081 | | // the graphic sub-system |
1082 | 0 | if (fLineWidth == 0) |
1083 | 0 | { |
1084 | 0 | fLineWidth = 1.0; |
1085 | |
|
1086 | 0 | if (!bObjectToDeviceIsIdentity) |
1087 | 0 | { |
1088 | 0 | basegfx::B2DHomMatrix aObjectToDeviceInv(rObjectToDevice); |
1089 | 0 | aObjectToDeviceInv.invert(); |
1090 | 0 | fLineWidth = (aObjectToDeviceInv * basegfx::B2DVector(fLineWidth, 0)).getLength(); |
1091 | 0 | } |
1092 | 0 | } |
1093 | | |
1094 | | // PixelOffset used: Need to reflect in linear transformation |
1095 | 0 | cairo_matrix_t aMatrix; |
1096 | 0 | basegfx::B2DHomMatrix aDamageMatrix(basegfx::utils::createTranslateB2DHomMatrix(0.5, 0.5)); |
1097 | |
|
1098 | 0 | if (bObjectToDeviceIsIdentity) |
1099 | 0 | { |
1100 | | // Set PixelOffset as requested |
1101 | 0 | cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); |
1102 | 0 | } |
1103 | 0 | else |
1104 | 0 | { |
1105 | | // Prepare ObjectToDevice transformation. Take PixelOffset for Lines into |
1106 | | // account: Multiply from left to act in DeviceCoordinates |
1107 | 0 | aDamageMatrix = aDamageMatrix * rObjectToDevice; |
1108 | 0 | cairo_matrix_init(&aMatrix, aDamageMatrix.get(0, 0), aDamageMatrix.get(1, 0), |
1109 | 0 | aDamageMatrix.get(0, 1), aDamageMatrix.get(1, 1), aDamageMatrix.get(0, 2), |
1110 | 0 | aDamageMatrix.get(1, 2)); |
1111 | 0 | } |
1112 | | |
1113 | | // set linear transformation |
1114 | 0 | cairo_set_matrix(cr, &aMatrix); |
1115 | | |
1116 | | // setup line attributes |
1117 | 0 | cairo_line_join_t eCairoLineJoin = CAIRO_LINE_JOIN_MITER; |
1118 | 0 | switch (eLineJoin) |
1119 | 0 | { |
1120 | 0 | case basegfx::B2DLineJoin::Bevel: |
1121 | 0 | eCairoLineJoin = CAIRO_LINE_JOIN_BEVEL; |
1122 | 0 | break; |
1123 | 0 | case basegfx::B2DLineJoin::Round: |
1124 | 0 | eCairoLineJoin = CAIRO_LINE_JOIN_ROUND; |
1125 | 0 | break; |
1126 | 0 | case basegfx::B2DLineJoin::NONE: |
1127 | 0 | case basegfx::B2DLineJoin::Miter: |
1128 | 0 | eCairoLineJoin = CAIRO_LINE_JOIN_MITER; |
1129 | 0 | break; |
1130 | 0 | } |
1131 | | |
1132 | | // convert miter minimum angle to miter limit |
1133 | 0 | double fMiterLimit = 1.0 / sin(std::max(fMiterMinimumAngle, 0.01 * M_PI) / 2.0); |
1134 | | |
1135 | | // setup cap attribute |
1136 | 0 | cairo_line_cap_t eCairoLineCap(CAIRO_LINE_CAP_BUTT); |
1137 | |
|
1138 | 0 | switch (eLineCap) |
1139 | 0 | { |
1140 | 0 | default: // css::drawing::LineCap_BUTT: |
1141 | 0 | { |
1142 | 0 | eCairoLineCap = CAIRO_LINE_CAP_BUTT; |
1143 | 0 | break; |
1144 | 0 | } |
1145 | 0 | case css::drawing::LineCap_ROUND: |
1146 | 0 | { |
1147 | 0 | eCairoLineCap = CAIRO_LINE_CAP_ROUND; |
1148 | 0 | break; |
1149 | 0 | } |
1150 | 0 | case css::drawing::LineCap_SQUARE: |
1151 | 0 | { |
1152 | 0 | eCairoLineCap = CAIRO_LINE_CAP_SQUARE; |
1153 | 0 | break; |
1154 | 0 | } |
1155 | 0 | } |
1156 | | |
1157 | 0 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); |
1158 | 0 | cairo_set_source_rgba(cr, m_oLineColor->GetRed() / 255.0, m_oLineColor->GetGreen() / 255.0, |
1159 | 0 | m_oLineColor->GetBlue() / 255.0, 1.0 - fTransparency); |
1160 | |
|
1161 | 0 | cairo_set_line_join(cr, eCairoLineJoin); |
1162 | 0 | cairo_set_line_cap(cr, eCairoLineCap); |
1163 | |
|
1164 | 0 | constexpr int MaxNormalLineWidthPx = 64; |
1165 | 0 | if (fLineWidth > MaxNormalLineWidthPx) |
1166 | 0 | { |
1167 | 0 | const double fLineWidthPixel |
1168 | 0 | = bObjectToDeviceIsIdentity |
1169 | 0 | ? fLineWidth |
1170 | 0 | : (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength(); |
1171 | 0 | constexpr double MaxLineWidth = 0x20000000; |
1172 | | // if the width is pixels is excessive, or if the actual number is huge, then |
1173 | | // when fuzzing drop it to something small |
1174 | 0 | if (fLineWidthPixel > MaxNormalLineWidthPx || fLineWidth > MaxLineWidth) |
1175 | 0 | { |
1176 | 0 | SAL_WARN("vcl.gdi", "drawPolyLine, suspicious input line width of: " |
1177 | 0 | << fLineWidth << ", will be " << fLineWidthPixel |
1178 | 0 | << " pixels thick"); |
1179 | 0 | if (bFuzzing) |
1180 | 0 | { |
1181 | 0 | basegfx::B2DHomMatrix aObjectToDeviceInv(rObjectToDevice); |
1182 | 0 | aObjectToDeviceInv.invert(); |
1183 | 0 | fLineWidth = (aObjectToDeviceInv * basegfx::B2DVector(MaxNormalLineWidthPx, 0)) |
1184 | 0 | .getLength(); |
1185 | 0 | fLineWidth = std::min(fLineWidth, 2048.0); |
1186 | 0 | } |
1187 | 0 | } |
1188 | 0 | } |
1189 | 0 | cairo_set_line_width(cr, fLineWidth); |
1190 | 0 | cairo_set_miter_limit(cr, fMiterLimit); |
1191 | | |
1192 | | // try to access buffered data |
1193 | 0 | std::shared_ptr<SystemDependentData_CairoPath> pSystemDependentData_CairoPath( |
1194 | 0 | rPolyLine.getSystemDependentData<SystemDependentData_CairoPath>( |
1195 | 0 | basegfx::SDD_Type::SDDType_CairoPath)); |
1196 | | |
1197 | | // MM01 need to do line dashing as fallback stuff here now |
1198 | 0 | const double fDotDashLength( |
1199 | 0 | nullptr != pStroke ? std::accumulate(pStroke->begin(), pStroke->end(), 0.0) : 0.0); |
1200 | 0 | const bool bStrokeUsed(0.0 != fDotDashLength); |
1201 | 0 | assert(!bStrokeUsed || (bStrokeUsed && pStroke)); |
1202 | | |
1203 | | // MM01 activate to stroke directly |
1204 | 0 | if (bStrokeUsed) |
1205 | 0 | { |
1206 | 0 | cairo_set_dash(cr, pStroke->data(), pStroke->size(), 0.0); |
1207 | 0 | } |
1208 | | |
1209 | | // check for basegfx::B2DLineJoin::NONE to react accordingly |
1210 | 0 | const bool bNoJoin(basegfx::B2DLineJoin::NONE == eLineJoin && fLineWidth > 0.0 |
1211 | 0 | && !basegfx::fTools::equalZero(fLineWidth)); |
1212 | |
|
1213 | 0 | if (pSystemDependentData_CairoPath) |
1214 | 0 | { |
1215 | 0 | auto strokeEquals |
1216 | 0 | = [](const std::vector<double>& rStroke1, const std::vector<double>* pStroke2) -> bool { |
1217 | 0 | if (!pStroke2) |
1218 | 0 | return rStroke1.empty(); |
1219 | 0 | return rStroke1 == *pStroke2; |
1220 | 0 | }; |
1221 | | // check data validity |
1222 | 0 | if (nullptr == pSystemDependentData_CairoPath->getCairoPath() |
1223 | 0 | || pSystemDependentData_CairoPath->getNoJoin() != bNoJoin |
1224 | 0 | || pSystemDependentData_CairoPath->getAntiAlias() != bAntiAlias |
1225 | 0 | || !strokeEquals(pSystemDependentData_CairoPath->getStroke(), pStroke) |
1226 | 0 | || bPixelSnapHairline /*tdf#124700*/) |
1227 | 0 | { |
1228 | | // data invalid, forget |
1229 | 0 | pSystemDependentData_CairoPath.reset(); |
1230 | 0 | } |
1231 | 0 | } |
1232 | |
|
1233 | 0 | if (pSystemDependentData_CairoPath) |
1234 | 0 | { |
1235 | | // re-use data |
1236 | 0 | cairo_append_path(cr, pSystemDependentData_CairoPath->getCairoPath()); |
1237 | 0 | } |
1238 | 0 | else |
1239 | 0 | { |
1240 | | // create data |
1241 | 0 | size_t nSizeMeasure(0); |
1242 | | |
1243 | | // MM01 need to do line dashing as fallback stuff here now |
1244 | 0 | basegfx::B2DPolyPolygon aPolyPolygonLine; |
1245 | | |
1246 | | // no line dashing or direct stroke, just copy |
1247 | 0 | aPolyPolygonLine.append(rPolyLine); |
1248 | | |
1249 | | // MM01 checked/verified for Cairo |
1250 | 0 | for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++) |
1251 | 0 | { |
1252 | 0 | const basegfx::B2DPolygon& aPolyLine(aPolyPolygonLine.getB2DPolygon(a)); |
1253 | |
|
1254 | 0 | if (!bNoJoin) |
1255 | 0 | { |
1256 | | // PixelOffset now reflected in linear transformation used |
1257 | 0 | nSizeMeasure |
1258 | 0 | += AddPolygonToPath(cr, aPolyLine, |
1259 | 0 | rObjectToDevice, // ObjectToDevice *without* LineDraw-Offset |
1260 | 0 | !bAntiAlias, bPixelSnapHairline); |
1261 | 0 | } |
1262 | 0 | else |
1263 | 0 | { |
1264 | 0 | const sal_uInt32 nPointCount(aPolyLine.count()); |
1265 | 0 | const sal_uInt32 nEdgeCount(aPolyLine.isClosed() ? nPointCount : nPointCount - 1); |
1266 | 0 | basegfx::B2DPolygon aEdge; |
1267 | |
|
1268 | 0 | aEdge.append(aPolyLine.getB2DPoint(0)); |
1269 | 0 | aEdge.append(basegfx::B2DPoint(0.0, 0.0)); |
1270 | |
|
1271 | 0 | for (sal_uInt32 i(0); i < nEdgeCount; i++) |
1272 | 0 | { |
1273 | 0 | const sal_uInt32 nNextIndex((i + 1) % nPointCount); |
1274 | 0 | aEdge.setB2DPoint(1, aPolyLine.getB2DPoint(nNextIndex)); |
1275 | 0 | aEdge.setNextControlPoint(0, aPolyLine.getNextControlPoint(i)); |
1276 | 0 | aEdge.setPrevControlPoint(1, aPolyLine.getPrevControlPoint(nNextIndex)); |
1277 | | |
1278 | | // PixelOffset now reflected in linear transformation used |
1279 | 0 | nSizeMeasure += AddPolygonToPath( |
1280 | 0 | cr, aEdge, |
1281 | 0 | rObjectToDevice, // ObjectToDevice *without* LineDraw-Offset |
1282 | 0 | !bAntiAlias, bPixelSnapHairline); |
1283 | | |
1284 | | // prepare next step |
1285 | 0 | aEdge.setB2DPoint(0, aEdge.getB2DPoint(1)); |
1286 | 0 | } |
1287 | 0 | } |
1288 | 0 | } |
1289 | | |
1290 | | // copy and add to buffering mechanism |
1291 | 0 | if (!bPixelSnapHairline /*tdf#124700*/) |
1292 | 0 | { |
1293 | 0 | pSystemDependentData_CairoPath |
1294 | 0 | = rPolyLine.addOrReplaceSystemDependentData<SystemDependentData_CairoPath>( |
1295 | 0 | nSizeMeasure, cr, bNoJoin, bAntiAlias, pStroke); |
1296 | 0 | } |
1297 | 0 | } |
1298 | | |
1299 | | // extract extents |
1300 | 0 | basegfx::B2DRange extents = getClippedStrokeDamage(cr); |
1301 | | // transform also extents (ranges) of damage so they can be correctly redrawn |
1302 | 0 | extents.transform(aDamageMatrix); |
1303 | | |
1304 | | // draw and consume |
1305 | 0 | cairo_stroke(cr); |
1306 | |
|
1307 | 0 | releaseCairoContext(cr, false, extents); |
1308 | |
|
1309 | 0 | return true; |
1310 | 0 | } |
1311 | | |
1312 | | bool CairoCommon::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth, |
1313 | | tools::Long nHeight, sal_uInt8 nTransparency, bool bAntiAlias) |
1314 | 0 | { |
1315 | 0 | const bool bHasFill(m_oFillColor.has_value()); |
1316 | 0 | const bool bHasLine(m_oLineColor.has_value()); |
1317 | |
|
1318 | 0 | if (!bHasFill && !bHasLine) |
1319 | 0 | return true; |
1320 | | |
1321 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1322 | 0 | clipRegion(cr); |
1323 | |
|
1324 | 0 | const double fTransparency = nTransparency * (1.0 / 100); |
1325 | | |
1326 | | // To make releaseCairoContext work, use empty extents |
1327 | 0 | basegfx::B2DRange extents; |
1328 | |
|
1329 | 0 | if (bHasFill) |
1330 | 0 | { |
1331 | 0 | cairo_rectangle(cr, nX, nY, nWidth, nHeight); |
1332 | |
|
1333 | 0 | applyColor(cr, *m_oFillColor, fTransparency); |
1334 | | |
1335 | | // set FillDamage |
1336 | 0 | extents = getClippedFillDamage(cr); |
1337 | |
|
1338 | 0 | cairo_fill(cr); |
1339 | 0 | } |
1340 | |
|
1341 | 0 | if (bHasLine) |
1342 | 0 | { |
1343 | | // PixelOffset used: Set PixelOffset as linear transformation |
1344 | | // Note: Was missing here - probably not by purpose (?) |
1345 | 0 | cairo_matrix_t aMatrix; |
1346 | 0 | cairo_matrix_init_translate(&aMatrix, 0.5, 0.5); |
1347 | 0 | cairo_set_matrix(cr, &aMatrix); |
1348 | |
|
1349 | 0 | cairo_rectangle(cr, nX, nY, nWidth, nHeight); |
1350 | |
|
1351 | 0 | applyColor(cr, *m_oLineColor, fTransparency); |
1352 | | |
1353 | | // expand with possible StrokeDamage |
1354 | 0 | basegfx::B2DRange stroke_extents = getClippedStrokeDamage(cr); |
1355 | 0 | stroke_extents.translate(0.5, 0.5); |
1356 | 0 | extents.expand(stroke_extents); |
1357 | |
|
1358 | 0 | cairo_stroke(cr); |
1359 | 0 | } |
1360 | |
|
1361 | 0 | releaseCairoContext(cr, false, extents); |
1362 | |
|
1363 | 0 | return true; |
1364 | 0 | } |
1365 | | |
1366 | | bool CairoCommon::drawGradient(const tools::PolyPolygon& rPolyPolygon, const Gradient& rGradient, |
1367 | | bool bAntiAlias) |
1368 | 0 | { |
1369 | 0 | if (rGradient.GetStyle() != css::awt::GradientStyle_LINEAR |
1370 | 0 | && rGradient.GetStyle() != css::awt::GradientStyle_RADIAL) |
1371 | 0 | return false; // unsupported |
1372 | 0 | if (rGradient.GetSteps() != 0) |
1373 | 0 | return false; // We can't tell cairo how many colors to use in the gradient. |
1374 | | |
1375 | 0 | cairo_t* cr = getCairoContext(true, bAntiAlias); |
1376 | 0 | clipRegion(cr); |
1377 | |
|
1378 | 0 | tools::Rectangle aInputRect(rPolyPolygon.GetBoundRect()); |
1379 | 0 | if (rPolyPolygon.IsRect()) |
1380 | 0 | { |
1381 | | // Rect->Polygon conversion loses the right and bottom edge, fix that. |
1382 | 0 | aInputRect.AdjustRight(1); |
1383 | 0 | aInputRect.AdjustBottom(1); |
1384 | 0 | basegfx::B2DHomMatrix rObjectToDevice; |
1385 | 0 | AddPolygonToPath(cr, tools::Polygon(aInputRect).getB2DPolygon(), rObjectToDevice, |
1386 | 0 | !bAntiAlias, false); |
1387 | 0 | } |
1388 | 0 | else |
1389 | 0 | { |
1390 | 0 | basegfx::B2DPolyPolygon aB2DPolyPolygon(rPolyPolygon.getB2DPolyPolygon()); |
1391 | 0 | for (auto const& rPolygon : std::as_const(aB2DPolyPolygon)) |
1392 | 0 | { |
1393 | 0 | basegfx::B2DHomMatrix rObjectToDevice; |
1394 | 0 | AddPolygonToPath(cr, rPolygon, rObjectToDevice, !bAntiAlias, false); |
1395 | 0 | } |
1396 | 0 | } |
1397 | |
|
1398 | 0 | Gradient aGradient(rGradient); |
1399 | |
|
1400 | 0 | tools::Rectangle aBoundRect; |
1401 | 0 | Point aCenter; |
1402 | |
|
1403 | 0 | aGradient.SetAngle(aGradient.GetAngle() + 2700_deg10); |
1404 | 0 | aGradient.GetBoundRect(aInputRect, aBoundRect, aCenter); |
1405 | 0 | if (aBoundRect.IsEmpty()) |
1406 | 0 | SAL_WARN("vcl.gdi", "empty gradient bounding rectangle"); |
1407 | 0 | else |
1408 | 0 | { |
1409 | 0 | Color aStartColor = aGradient.GetStartColor(); |
1410 | 0 | Color aEndColor = aGradient.GetEndColor(); |
1411 | |
|
1412 | 0 | cairo_pattern_t* pattern; |
1413 | 0 | if (rGradient.GetStyle() == css::awt::GradientStyle_LINEAR) |
1414 | 0 | { |
1415 | 0 | tools::Polygon aPoly(aBoundRect); |
1416 | 0 | aPoly.Rotate(aCenter, aGradient.GetAngle() % 3600_deg10); |
1417 | 0 | pattern = cairo_pattern_create_linear(aPoly[0].X(), aPoly[0].Y(), aPoly[1].X(), |
1418 | 0 | aPoly[1].Y()); |
1419 | 0 | } |
1420 | 0 | else |
1421 | 0 | { |
1422 | 0 | double radius = std::max(aBoundRect.GetWidth() / 2.0, aBoundRect.GetHeight() / 2.0); |
1423 | | // Move the center a bit to the top-left (the default VCL algorithm is a bit off-center that way, |
1424 | | // cairo is the opposite way). |
1425 | 0 | pattern = cairo_pattern_create_radial(aCenter.X() - 0.5, aCenter.Y() - 0.5, 0, |
1426 | 0 | aCenter.X() - 0.5, aCenter.Y() - 0.5, radius); |
1427 | 0 | std::swap(aStartColor, aEndColor); |
1428 | 0 | } |
1429 | |
|
1430 | 0 | cairo_pattern_add_color_stop_rgba( |
1431 | 0 | pattern, aGradient.GetBorder() / 100.0, |
1432 | 0 | aStartColor.GetRed() * aGradient.GetStartIntensity() / 25500.0, |
1433 | 0 | aStartColor.GetGreen() * aGradient.GetStartIntensity() / 25500.0, |
1434 | 0 | aStartColor.GetBlue() * aGradient.GetStartIntensity() / 25500.0, 1.0); |
1435 | |
|
1436 | 0 | cairo_pattern_add_color_stop_rgba( |
1437 | 0 | pattern, 1.0, aEndColor.GetRed() * aGradient.GetEndIntensity() / 25500.0, |
1438 | 0 | aEndColor.GetGreen() * aGradient.GetEndIntensity() / 25500.0, |
1439 | 0 | aEndColor.GetBlue() * aGradient.GetEndIntensity() / 25500.0, 1.0); |
1440 | |
|
1441 | 0 | cairo_set_source(cr, pattern); |
1442 | 0 | cairo_pattern_destroy(pattern); |
1443 | 0 | } |
1444 | | |
1445 | 0 | basegfx::B2DRange extents = getClippedFillDamage(cr); |
1446 | 0 | cairo_fill_preserve(cr); |
1447 | |
|
1448 | 0 | releaseCairoContext(cr, true, extents); |
1449 | |
|
1450 | 0 | return true; |
1451 | 0 | } |
1452 | | |
1453 | | bool CairoCommon::implDrawGradient(basegfx::B2DPolyPolygon const& rPolyPolygon, |
1454 | | SalGradient const& rGradient, bool bAntiAlias) |
1455 | 0 | { |
1456 | 0 | cairo_t* cr = getCairoContext(true, bAntiAlias); |
1457 | |
|
1458 | 0 | basegfx::B2DHomMatrix rObjectToDevice; |
1459 | |
|
1460 | 0 | for (auto const& rPolygon : rPolyPolygon) |
1461 | 0 | AddPolygonToPath(cr, rPolygon, rObjectToDevice, !bAntiAlias, false); |
1462 | |
|
1463 | 0 | cairo_pattern_t* pattern |
1464 | 0 | = cairo_pattern_create_linear(rGradient.maPoint1.getX(), rGradient.maPoint1.getY(), |
1465 | 0 | rGradient.maPoint2.getX(), rGradient.maPoint2.getY()); |
1466 | |
|
1467 | 0 | for (SalGradientStop const& rStop : rGradient.maStops) |
1468 | 0 | { |
1469 | 0 | double r = rStop.maColor.GetRed() / 255.0; |
1470 | 0 | double g = rStop.maColor.GetGreen() / 255.0; |
1471 | 0 | double b = rStop.maColor.GetBlue() / 255.0; |
1472 | 0 | double a = rStop.maColor.GetAlpha() / 255.0; |
1473 | 0 | double offset = rStop.mfOffset; |
1474 | |
|
1475 | 0 | cairo_pattern_add_color_stop_rgba(pattern, offset, r, g, b, a); |
1476 | 0 | } |
1477 | 0 | cairo_set_source(cr, pattern); |
1478 | 0 | cairo_pattern_destroy(pattern); |
1479 | |
|
1480 | 0 | basegfx::B2DRange extents = getClippedFillDamage(cr); |
1481 | |
|
1482 | 0 | cairo_fill_preserve(cr); |
1483 | |
|
1484 | 0 | releaseCairoContext(cr, true, extents); |
1485 | |
|
1486 | 0 | return true; |
1487 | 0 | } |
1488 | | |
1489 | | namespace |
1490 | | { |
1491 | | basegfx::B2DRange renderWithOperator(cairo_t* cr, const SalTwoRect& rTR, cairo_surface_t* source, |
1492 | | cairo_operator_t eOperator = CAIRO_OPERATOR_SOURCE) |
1493 | 12.9k | { |
1494 | 12.9k | cairo_rectangle(cr, rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); |
1495 | | |
1496 | 12.9k | basegfx::B2DRange extents = getClippedFillDamage(cr); |
1497 | | |
1498 | 12.9k | cairo_clip(cr); |
1499 | | |
1500 | 12.9k | cairo_translate(cr, rTR.mnDestX, rTR.mnDestY); |
1501 | 12.9k | if (rTR.mnSrcWidth != 0 && rTR.mnSrcHeight != 0) |
1502 | 12.9k | { |
1503 | 12.9k | double fXScale = static_cast<double>(rTR.mnDestWidth) / rTR.mnSrcWidth; |
1504 | 12.9k | double fYScale = static_cast<double>(rTR.mnDestHeight) / rTR.mnSrcHeight; |
1505 | 12.9k | cairo_scale(cr, fXScale, fYScale); |
1506 | 12.9k | } |
1507 | | |
1508 | 12.9k | cairo_save(cr); |
1509 | 12.9k | cairo_set_source_surface(cr, source, -rTR.mnSrcX, -rTR.mnSrcY); |
1510 | | |
1511 | 12.9k | if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) |
1512 | 12.9k | { |
1513 | | //tdf#133716 borders of upscaled images should not be blurred |
1514 | 12.9k | cairo_pattern_t* sourcepattern = cairo_get_source(cr); |
1515 | 12.9k | cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD); |
1516 | 12.9k | } |
1517 | | |
1518 | 12.9k | cairo_set_operator(cr, eOperator); |
1519 | 12.9k | cairo_paint(cr); |
1520 | 12.9k | cairo_restore(cr); |
1521 | | |
1522 | 12.9k | return extents; |
1523 | 12.9k | } |
1524 | | |
1525 | | } // end anonymous ns |
1526 | | |
1527 | | basegfx::B2DRange CairoCommon::renderSource(cairo_t* cr, const SalTwoRect& rTR, |
1528 | | cairo_surface_t* source) |
1529 | 6.47k | { |
1530 | 6.47k | return renderWithOperator(cr, rTR, source, CAIRO_OPERATOR_SOURCE); |
1531 | 6.47k | } |
1532 | | |
1533 | | void CairoCommon::copyWithOperator(const SalTwoRect& rTR, cairo_surface_t* source, |
1534 | | cairo_operator_t eOp, bool bAntiAlias) |
1535 | 6.47k | { |
1536 | 6.47k | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1537 | 6.47k | clipRegion(cr); |
1538 | | |
1539 | 6.47k | basegfx::B2DRange extents = renderWithOperator(cr, rTR, source, eOp); |
1540 | | |
1541 | 6.47k | releaseCairoContext(cr, false, extents); |
1542 | 6.47k | } |
1543 | | |
1544 | | void CairoCommon::copySource(const SalTwoRect& rTR, cairo_surface_t* source, bool bAntiAlias) |
1545 | 0 | { |
1546 | 0 | copyWithOperator(rTR, source, CAIRO_OPERATOR_SOURCE, bAntiAlias); |
1547 | 0 | } |
1548 | | |
1549 | | void CairoCommon::copyBitsCairo(const SalTwoRect& rTR, cairo_surface_t* pSourceSurface, |
1550 | | bool bAntiAlias) |
1551 | 0 | { |
1552 | 0 | SalTwoRect aTR(rTR); |
1553 | |
|
1554 | 0 | cairo_surface_t* pCopy = nullptr; |
1555 | |
|
1556 | 0 | if (pSourceSurface == getSurface()) |
1557 | 0 | { |
1558 | | //self copy is a problem, so dup source in that case |
1559 | 0 | pCopy |
1560 | 0 | = cairo_surface_create_similar(pSourceSurface, cairo_surface_get_content(getSurface()), |
1561 | 0 | aTR.mnSrcWidth * m_fScale, aTR.mnSrcHeight * m_fScale); |
1562 | 0 | dl_cairo_surface_set_device_scale(pCopy, m_fScale, m_fScale); |
1563 | 0 | cairo_t* cr = cairo_create(pCopy); |
1564 | 0 | cairo_set_source_surface(cr, pSourceSurface, -aTR.mnSrcX, -aTR.mnSrcY); |
1565 | 0 | cairo_rectangle(cr, 0, 0, aTR.mnSrcWidth, aTR.mnSrcHeight); |
1566 | 0 | cairo_fill(cr); |
1567 | 0 | cairo_destroy(cr); |
1568 | |
|
1569 | 0 | pSourceSurface = pCopy; |
1570 | |
|
1571 | 0 | aTR.mnSrcX = 0; |
1572 | 0 | aTR.mnSrcY = 0; |
1573 | 0 | } |
1574 | |
|
1575 | 0 | copySource(aTR, pSourceSurface, bAntiAlias); |
1576 | |
|
1577 | 0 | if (pCopy) |
1578 | 0 | cairo_surface_destroy(pCopy); |
1579 | 0 | } |
1580 | | |
1581 | | namespace |
1582 | | { |
1583 | | cairo_pattern_t* create_stipple() |
1584 | 0 | { |
1585 | 0 | static const unsigned char data[16] = { 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, |
1586 | 0 | 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF }; |
1587 | 0 | cairo_surface_t* surface = cairo_image_surface_create_for_data(const_cast<unsigned char*>(data), |
1588 | 0 | CAIRO_FORMAT_A8, 4, 4, 4); |
1589 | 0 | cairo_pattern_t* pattern = cairo_pattern_create_for_surface(surface); |
1590 | 0 | cairo_surface_destroy(surface); |
1591 | 0 | cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT); |
1592 | 0 | cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST); |
1593 | 0 | return pattern; |
1594 | 0 | } |
1595 | | } // end anonymous ns |
1596 | | |
1597 | | void CairoCommon::invert(const basegfx::B2DPolygon& rPoly, SalInvert nFlags, bool bAntiAlias) |
1598 | 0 | { |
1599 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1600 | 0 | clipRegion(cr); |
1601 | | |
1602 | | // To make releaseCairoContext work, use empty extents |
1603 | 0 | basegfx::B2DRange extents; |
1604 | |
|
1605 | 0 | AddPolygonToPath(cr, rPoly, basegfx::B2DHomMatrix(), !bAntiAlias, false); |
1606 | |
|
1607 | 0 | cairo_set_source_rgb(cr, 1.0, 1.0, 1.0); |
1608 | |
|
1609 | 0 | cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE); |
1610 | |
|
1611 | 0 | if (nFlags & SalInvert::TrackFrame) |
1612 | 0 | { |
1613 | 0 | cairo_set_line_width(cr, 2.0); |
1614 | 0 | const double dashLengths[2] = { 4.0, 4.0 }; |
1615 | 0 | cairo_set_dash(cr, dashLengths, 2, 0); |
1616 | |
|
1617 | 0 | extents = getClippedStrokeDamage(cr); |
1618 | | //see tdf#106577 under wayland, some pixel droppings seen, maybe we're |
1619 | | //out by one somewhere, or cairo_stroke_extents is confused by |
1620 | | //dashes/line width |
1621 | 0 | if (!extents.isEmpty()) |
1622 | 0 | { |
1623 | 0 | extents.grow(1); |
1624 | 0 | } |
1625 | |
|
1626 | 0 | cairo_stroke(cr); |
1627 | 0 | } |
1628 | 0 | else |
1629 | 0 | { |
1630 | 0 | extents = getClippedFillDamage(cr); |
1631 | |
|
1632 | 0 | cairo_clip(cr); |
1633 | |
|
1634 | 0 | if (nFlags & SalInvert::N50) |
1635 | 0 | { |
1636 | 0 | cairo_pattern_t* pattern = create_stipple(); |
1637 | 0 | cairo_surface_t* surface = cairo_surface_create_similar( |
1638 | 0 | m_pSurface, cairo_surface_get_content(m_pSurface), extents.getWidth() * m_fScale, |
1639 | 0 | extents.getHeight() * m_fScale); |
1640 | |
|
1641 | 0 | dl_cairo_surface_set_device_scale(surface, m_fScale, m_fScale); |
1642 | 0 | cairo_t* stipple_cr = cairo_create(surface); |
1643 | 0 | cairo_set_source_rgb(stipple_cr, 1.0, 1.0, 1.0); |
1644 | 0 | cairo_mask(stipple_cr, pattern); |
1645 | 0 | cairo_pattern_destroy(pattern); |
1646 | 0 | cairo_destroy(stipple_cr); |
1647 | 0 | cairo_mask_surface(cr, surface, extents.getMinX(), extents.getMinY()); |
1648 | 0 | cairo_surface_destroy(surface); |
1649 | 0 | } |
1650 | 0 | else |
1651 | 0 | { |
1652 | 0 | cairo_paint(cr); |
1653 | 0 | } |
1654 | 0 | } |
1655 | |
|
1656 | 0 | releaseCairoContext(cr, false, extents); |
1657 | 0 | } |
1658 | | |
1659 | | void CairoCommon::invert(tools::Long nX, tools::Long nY, tools::Long nWidth, tools::Long nHeight, |
1660 | | SalInvert nFlags, bool bAntiAlias) |
1661 | 0 | { |
1662 | 0 | basegfx::B2DPolygon aRect = basegfx::utils::createPolygonFromRect( |
1663 | 0 | basegfx::B2DRectangle(nX, nY, nX + nWidth, nY + nHeight)); |
1664 | |
|
1665 | 0 | invert(aRect, nFlags, bAntiAlias); |
1666 | 0 | } |
1667 | | |
1668 | | void CairoCommon::invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nFlags, bool bAntiAlias) |
1669 | 0 | { |
1670 | 0 | basegfx::B2DPolygon aPoly; |
1671 | 0 | aPoly.append(basegfx::B2DPoint(pPtAry->getX(), pPtAry->getY()), nPoints); |
1672 | 0 | for (sal_uInt32 i = 1; i < nPoints; ++i) |
1673 | 0 | aPoly.setB2DPoint(i, basegfx::B2DPoint(pPtAry[i].getX(), pPtAry[i].getY())); |
1674 | 0 | aPoly.setClosed(true); |
1675 | |
|
1676 | 0 | invert(aPoly, nFlags, bAntiAlias); |
1677 | 0 | } |
1678 | | |
1679 | | void CairoCommon::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, |
1680 | | bool bAntiAlias) |
1681 | 6.47k | { |
1682 | | // MM02 try to access buffered BitmapHelper |
1683 | 6.47k | std::shared_ptr<BitmapHelper> aSurface; |
1684 | 6.47k | tryToUseSourceBuffer(rSalBitmap, aSurface); |
1685 | 6.47k | cairo_surface_t* source = aSurface->getSurface(rPosAry.mnDestWidth, rPosAry.mnDestHeight); |
1686 | | |
1687 | 6.47k | if (!source) |
1688 | 0 | { |
1689 | 0 | SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap case"); |
1690 | 0 | return; |
1691 | 0 | } |
1692 | | |
1693 | | #if 0 // LO code is not yet bitmap32-ready. |
1694 | | // if m_bSupportsBitmap32 becomes true for Svp revisit this |
1695 | | copyWithOperator(rPosAry, source, CAIRO_OPERATOR_OVER, bAntiAlias); |
1696 | | #else |
1697 | 6.47k | copyWithOperator(rPosAry, source, CAIRO_OPERATOR_SOURCE, bAntiAlias); |
1698 | 6.47k | #endif |
1699 | 6.47k | } |
1700 | | |
1701 | | void CairoCommon::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap, |
1702 | | bool bAntiAlias) |
1703 | 0 | { |
1704 | | // MM02 try to access buffered BitmapHelper |
1705 | 0 | std::shared_ptr<BitmapHelper> aSurface; |
1706 | 0 | tryToUseSourceBuffer(rSalBitmap, aSurface); |
1707 | 0 | cairo_surface_t* source = aSurface->getSurface(rPosAry.mnDestWidth, rPosAry.mnDestHeight); |
1708 | |
|
1709 | 0 | if (!source) |
1710 | 0 | { |
1711 | 0 | SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawAlphaBitmap case"); |
1712 | 0 | return; |
1713 | 0 | } |
1714 | | |
1715 | 0 | copyWithOperator(rPosAry, source, CAIRO_OPERATOR_OVER, bAntiAlias); |
1716 | 0 | } |
1717 | | |
1718 | | bool CairoCommon::drawTransformedBitmap(const basegfx::B2DPoint& rNull, const basegfx::B2DPoint& rX, |
1719 | | const basegfx::B2DPoint& rY, const SalBitmap& rSourceBitmap, |
1720 | | double fAlpha, bool bAntiAlias) |
1721 | 0 | { |
1722 | 0 | if (fAlpha != 1.0) |
1723 | 0 | return false; |
1724 | | |
1725 | | // MM02 try to access buffered BitmapHelper |
1726 | 0 | std::shared_ptr<BitmapHelper> aSurface; |
1727 | 0 | tryToUseSourceBuffer(rSourceBitmap, aSurface); |
1728 | 0 | const tools::Long nDestWidth(basegfx::fround(basegfx::B2DVector(rX - rNull).getLength())); |
1729 | 0 | const tools::Long nDestHeight(basegfx::fround(basegfx::B2DVector(rY - rNull).getLength())); |
1730 | 0 | cairo_surface_t* source(aSurface->getSurface(nDestWidth, nDestHeight)); |
1731 | |
|
1732 | 0 | if (!source) |
1733 | 0 | { |
1734 | 0 | SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawTransformedBitmap case"); |
1735 | 0 | return false; |
1736 | 0 | } |
1737 | | |
1738 | 0 | const Size aSize = rSourceBitmap.GetSize(); |
1739 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1740 | 0 | clipRegion(cr); |
1741 | | |
1742 | | // setup the image transformation |
1743 | | // using the rNull,rX,rY points as destinations for the (0,0),(0,Width),(Height,0) source points |
1744 | 0 | const basegfx::B2DVector aXRel = rX - rNull; |
1745 | 0 | const basegfx::B2DVector aYRel = rY - rNull; |
1746 | 0 | cairo_matrix_t matrix; |
1747 | 0 | cairo_matrix_init(&matrix, aXRel.getX() / aSize.Width(), aXRel.getY() / aSize.Width(), |
1748 | 0 | aYRel.getX() / aSize.Height(), aYRel.getY() / aSize.Height(), rNull.getX(), |
1749 | 0 | rNull.getY()); |
1750 | |
|
1751 | 0 | cairo_transform(cr, &matrix); |
1752 | |
|
1753 | 0 | cairo_rectangle(cr, 0, 0, aSize.Width(), aSize.Height()); |
1754 | 0 | basegfx::B2DRange extents = getClippedFillDamage(cr); |
1755 | 0 | cairo_clip(cr); |
1756 | |
|
1757 | 0 | cairo_set_source_surface(cr, source, 0, 0); |
1758 | 0 | cairo_paint(cr); |
1759 | |
|
1760 | 0 | releaseCairoContext(cr, false, extents); |
1761 | |
|
1762 | 0 | return true; |
1763 | 0 | } |
1764 | | |
1765 | | void CairoCommon::drawMask(const SalTwoRect& rTR, const SalBitmap& rSalBitmap, Color nMaskColor, |
1766 | | bool bAntiAlias) |
1767 | 0 | { |
1768 | | /** creates an image from the given rectangle, replacing all black pixels |
1769 | | * with nMaskColor and making all others fully transparent */ |
1770 | | // MM02 here decided *against* using buffered BitmapHelper |
1771 | | // because the data gets somehow 'unmultiplied'. This may also be |
1772 | | // done just once, but I am not sure if this is safe to do. |
1773 | | // So for now dispense re-using data here. |
1774 | 0 | BitmapHelper aSurface(rSalBitmap, true); // The mask is argb32 |
1775 | 0 | if (!aSurface.getSurface()) |
1776 | 0 | { |
1777 | 0 | SAL_WARN("vcl.gdi", "unsupported SvpSalGraphics::drawMask case"); |
1778 | 0 | return; |
1779 | 0 | } |
1780 | 0 | sal_Int32 nStride; |
1781 | 0 | unsigned char* mask_data = aSurface.getBits(nStride); |
1782 | 0 | for (tools::Long y = rTR.mnSrcY; y < rTR.mnSrcY + rTR.mnSrcHeight; ++y) |
1783 | 0 | { |
1784 | 0 | unsigned char* row = mask_data + (nStride * y); |
1785 | 0 | unsigned char* data = row + (rTR.mnSrcX * 4); |
1786 | 0 | for (tools::Long x = rTR.mnSrcX; x < rTR.mnSrcX + rTR.mnSrcWidth; ++x) |
1787 | 0 | { |
1788 | | // Don't need to unpremultiply the data here, because we are checking for zero, |
1789 | | // and that is the same multiplied or un-multiplied. |
1790 | 0 | if (data[SVP_CAIRO_RED] == 0 && data[SVP_CAIRO_GREEN] == 0 && data[SVP_CAIRO_BLUE] == 0) |
1791 | 0 | { |
1792 | 0 | data[SVP_CAIRO_RED] = nMaskColor.GetRed(); |
1793 | 0 | data[SVP_CAIRO_GREEN] = nMaskColor.GetGreen(); |
1794 | 0 | data[SVP_CAIRO_BLUE] = nMaskColor.GetBlue(); |
1795 | 0 | data[SVP_CAIRO_ALPHA] = 0xff; |
1796 | 0 | } |
1797 | 0 | else |
1798 | 0 | { |
1799 | 0 | data[0] = 0; |
1800 | 0 | data[1] = 0; |
1801 | 0 | data[2] = 0; |
1802 | 0 | data[3] = 0; |
1803 | 0 | } |
1804 | 0 | data += 4; |
1805 | 0 | } |
1806 | 0 | } |
1807 | 0 | aSurface.mark_dirty(); |
1808 | |
|
1809 | 0 | cairo_t* cr = getCairoContext(false, bAntiAlias); |
1810 | 0 | clipRegion(cr); |
1811 | |
|
1812 | 0 | cairo_rectangle(cr, rTR.mnDestX, rTR.mnDestY, rTR.mnDestWidth, rTR.mnDestHeight); |
1813 | |
|
1814 | 0 | basegfx::B2DRange extents = getClippedFillDamage(cr); |
1815 | |
|
1816 | 0 | cairo_clip(cr); |
1817 | |
|
1818 | 0 | cairo_translate(cr, rTR.mnDestX, rTR.mnDestY); |
1819 | 0 | double fXScale = static_cast<double>(rTR.mnDestWidth) / rTR.mnSrcWidth; |
1820 | 0 | double fYScale = static_cast<double>(rTR.mnDestHeight) / rTR.mnSrcHeight; |
1821 | 0 | cairo_scale(cr, fXScale, fYScale); |
1822 | 0 | cairo_set_source_surface(cr, aSurface.getSurface(), -rTR.mnSrcX, -rTR.mnSrcY); |
1823 | |
|
1824 | 0 | if (cairo_status(cr) == CAIRO_STATUS_SUCCESS) |
1825 | 0 | { |
1826 | | //tdf#133716 borders of upscaled images should not be blurred |
1827 | 0 | cairo_pattern_t* sourcepattern = cairo_get_source(cr); |
1828 | 0 | cairo_pattern_set_extend(sourcepattern, CAIRO_EXTEND_PAD); |
1829 | 0 | } |
1830 | |
|
1831 | 0 | cairo_paint(cr); |
1832 | |
|
1833 | 0 | releaseCairoContext(cr, false, extents); |
1834 | 0 | } |
1835 | | |
1836 | | std::shared_ptr<SalBitmap> CairoCommon::getBitmap(tools::Long nX, tools::Long nY, |
1837 | | tools::Long nWidth, tools::Long nHeight, |
1838 | | bool bWithoutAlpha) |
1839 | 6.47k | { |
1840 | 6.47k | std::shared_ptr<SvpSalBitmap> pBitmap = std::make_shared<SvpSalBitmap>(); |
1841 | 6.47k | BitmapPalette aPal; |
1842 | 6.47k | assert(GetBitCount() != 1 && "not supported anymore"); |
1843 | 6.47k | vcl::PixelFormat ePixelFormat = vcl::PixelFormat::N32_BPP; |
1844 | | |
1845 | 6.47k | if (!pBitmap->ImplCreate(Size(nWidth, nHeight), ePixelFormat, aPal, false, bWithoutAlpha)) |
1846 | 0 | { |
1847 | 0 | SAL_WARN("vcl.gdi", "SvpSalGraphics::getBitmap, cannot create bitmap"); |
1848 | 0 | return nullptr; |
1849 | 0 | } |
1850 | | |
1851 | 6.47k | cairo_surface_t* target = CairoCommon::createCairoSurface(pBitmap->GetBuffer()); |
1852 | 6.47k | if (!target) |
1853 | 0 | { |
1854 | 0 | SAL_WARN("vcl.gdi", "SvpSalGraphics::getBitmap, cannot create cairo surface"); |
1855 | 0 | return nullptr; |
1856 | 0 | } |
1857 | 6.47k | cairo_t* cr = cairo_create(target); |
1858 | | |
1859 | 6.47k | SalTwoRect aTR(nX, nY, nWidth, nHeight, 0, 0, nWidth, nHeight); |
1860 | 6.47k | CairoCommon::renderSource(cr, aTR, m_pSurface); |
1861 | | |
1862 | 6.47k | cairo_destroy(cr); |
1863 | 6.47k | cairo_surface_destroy(target); |
1864 | | |
1865 | 6.47k | return pBitmap; |
1866 | 6.47k | } |
1867 | | |
1868 | | cairo_format_t getCairoFormat(const BitmapBuffer& rBuffer) |
1869 | 25.8k | { |
1870 | 25.8k | cairo_format_t nFormat; |
1871 | 25.8k | #ifdef HAVE_CAIRO_FORMAT_RGB24_888 |
1872 | 25.8k | assert(rBuffer.mnBitCount == 32 || rBuffer.mnBitCount == 24); |
1873 | | #else |
1874 | | assert(rBuffer.mnBitCount == 32); |
1875 | | #endif |
1876 | | |
1877 | 25.8k | if (rBuffer.mnBitCount == 32) |
1878 | 25.8k | nFormat = CAIRO_FORMAT_ARGB32; |
1879 | 0 | #ifdef HAVE_CAIRO_FORMAT_RGB24_888 |
1880 | 0 | else if (rBuffer.mnBitCount == 24) |
1881 | 0 | nFormat = CAIRO_FORMAT_RGB24_888; |
1882 | 0 | #endif |
1883 | 0 | else |
1884 | 0 | nFormat = CAIRO_FORMAT_A1; |
1885 | 25.8k | return nFormat; |
1886 | 25.8k | } |
1887 | | |
1888 | | namespace |
1889 | | { |
1890 | | bool isCairoCompatible(const BitmapBuffer* pBuffer) |
1891 | 12.9k | { |
1892 | 12.9k | if (!pBuffer) |
1893 | 0 | return false; |
1894 | | |
1895 | | // We use Cairo that supports 24-bit RGB. |
1896 | 12.9k | #ifdef HAVE_CAIRO_FORMAT_RGB24_888 |
1897 | 12.9k | if (pBuffer->mnBitCount != 32 && pBuffer->mnBitCount != 24) |
1898 | | #else |
1899 | | if (pBuffer->mnBitCount != 32) |
1900 | | #endif |
1901 | 0 | return false; |
1902 | | |
1903 | 12.9k | cairo_format_t nFormat = getCairoFormat(*pBuffer); |
1904 | 12.9k | return (cairo_format_stride_for_width(nFormat, pBuffer->mnWidth) == pBuffer->mnScanlineSize); |
1905 | 12.9k | } |
1906 | | } |
1907 | | |
1908 | | cairo_surface_t* CairoCommon::createCairoSurface(const BitmapBuffer* pBuffer) |
1909 | 12.9k | { |
1910 | 12.9k | if (!isCairoCompatible(pBuffer)) |
1911 | 0 | return nullptr; |
1912 | | |
1913 | 12.9k | cairo_format_t nFormat = getCairoFormat(*pBuffer); |
1914 | 12.9k | cairo_surface_t* target = cairo_image_surface_create_for_data( |
1915 | 12.9k | pBuffer->mpBits, nFormat, pBuffer->mnWidth, pBuffer->mnHeight, pBuffer->mnScanlineSize); |
1916 | 12.9k | if (cairo_surface_status(target) != CAIRO_STATUS_SUCCESS) |
1917 | 0 | { |
1918 | 0 | cairo_surface_destroy(target); |
1919 | 0 | return nullptr; |
1920 | 0 | } |
1921 | 12.9k | return target; |
1922 | 12.9k | } |
1923 | | |
1924 | | bool CairoCommon::supportsOperation(OutDevSupportType eType) |
1925 | 0 | { |
1926 | 0 | switch (eType) |
1927 | 0 | { |
1928 | 0 | case OutDevSupportType::TransparentText: |
1929 | 0 | return true; |
1930 | 0 | } |
1931 | 0 | return false; |
1932 | 0 | } |
1933 | | |
1934 | | std::optional<BitmapBuffer> FastConvert24BitRgbTo32BitCairo(const BitmapBuffer* pSrc) |
1935 | 0 | { |
1936 | 0 | if (pSrc == nullptr) |
1937 | 0 | return std::nullopt; |
1938 | | |
1939 | 0 | assert(pSrc->meFormat == SVP_24BIT_FORMAT); |
1940 | 0 | const tools::Long nWidth = pSrc->mnWidth; |
1941 | 0 | const tools::Long nHeight = pSrc->mnHeight; |
1942 | 0 | std::optional<BitmapBuffer> pDst(std::in_place); |
1943 | 0 | pDst->meFormat = ScanlineFormat::N32BitTcArgb; |
1944 | 0 | pDst->meDirection = ScanlineDirection::TopDown; |
1945 | 0 | pDst->mnWidth = nWidth; |
1946 | 0 | pDst->mnHeight = nHeight; |
1947 | 0 | pDst->mnBitCount = 32; |
1948 | 0 | pDst->maPalette = pSrc->maPalette; |
1949 | |
|
1950 | 0 | tools::Long nScanlineBase; |
1951 | 0 | const bool bFail = o3tl::checked_multiply<tools::Long>(pDst->mnBitCount, nWidth, nScanlineBase); |
1952 | 0 | if (bFail) |
1953 | 0 | { |
1954 | 0 | SAL_WARN("vcl.gdi", "checked multiply failed"); |
1955 | 0 | pDst->mpBits = nullptr; |
1956 | 0 | return std::nullopt; |
1957 | 0 | } |
1958 | | |
1959 | 0 | pDst->mnScanlineSize = AlignedWidth4Bytes(nScanlineBase); |
1960 | 0 | if (pDst->mnScanlineSize < nScanlineBase / 8) |
1961 | 0 | { |
1962 | 0 | SAL_WARN("vcl.gdi", "scanline calculation wraparound"); |
1963 | 0 | pDst->mpBits = nullptr; |
1964 | 0 | return std::nullopt; |
1965 | 0 | } |
1966 | | |
1967 | 0 | try |
1968 | 0 | { |
1969 | 0 | pDst->mpBits = new sal_uInt8[pDst->mnScanlineSize * nHeight]; |
1970 | 0 | } |
1971 | 0 | catch (const std::bad_alloc&) |
1972 | 0 | { |
1973 | | // memory exception, clean up |
1974 | 0 | pDst->mpBits = nullptr; |
1975 | 0 | return std::nullopt; |
1976 | 0 | } |
1977 | | |
1978 | 0 | for (tools::Long y = 0; y < nHeight; ++y) |
1979 | 0 | { |
1980 | 0 | sal_uInt8* pS = pSrc->mpBits + y * pSrc->mnScanlineSize; |
1981 | 0 | sal_uInt8* pD = pDst->mpBits + y * pDst->mnScanlineSize; |
1982 | 0 | for (tools::Long x = 0; x < nWidth; ++x) |
1983 | 0 | { |
1984 | | #if ENABLE_CAIRO_RGBA |
1985 | | static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcRgba, |
1986 | | "Expected SVP_CAIRO_FORMAT set to N32BitTcRgba"); |
1987 | | static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcRgb, |
1988 | | "Expected SVP_24BIT_FORMAT set to N24BitTcRgb"); |
1989 | | pD[0] = pS[0]; |
1990 | | pD[1] = pS[1]; |
1991 | | pD[2] = pS[2]; |
1992 | | pD[3] = 0xff; // Alpha |
1993 | | #elif defined OSL_BIGENDIAN |
1994 | | static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcArgb, |
1995 | | "Expected SVP_CAIRO_FORMAT set to N32BitTcArgb"); |
1996 | | static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcRgb, |
1997 | | "Expected SVP_24BIT_FORMAT set to N24BitTcRgb"); |
1998 | | pD[0] = 0xff; // Alpha |
1999 | | pD[1] = pS[0]; |
2000 | | pD[2] = pS[1]; |
2001 | | pD[3] = pS[2]; |
2002 | | #else |
2003 | 0 | static_assert(SVP_CAIRO_FORMAT == ScanlineFormat::N32BitTcBgra, |
2004 | 0 | "Expected SVP_CAIRO_FORMAT set to N32BitTcAgrx"); |
2005 | 0 | static_assert(SVP_24BIT_FORMAT == ScanlineFormat::N24BitTcBgr, |
2006 | 0 | "Expected SVP_24BIT_FORMAT set to N24BitTcBgr"); |
2007 | 0 | pD[0] = pS[0]; |
2008 | 0 | pD[1] = pS[1]; |
2009 | 0 | pD[2] = pS[2]; |
2010 | 0 | pD[3] = 0xff; // Alpha |
2011 | 0 | #endif |
2012 | |
|
2013 | 0 | pS += 3; |
2014 | 0 | pD += 4; |
2015 | 0 | } |
2016 | 0 | } |
2017 | |
|
2018 | 0 | return pDst; |
2019 | 0 | } |
2020 | | |
2021 | | namespace |
2022 | | { |
2023 | | // check for env var that decides for using downscale pattern |
2024 | | const char* pDisableDownScale(getenv("SAL_DISABLE_CAIRO_DOWNSCALE")); |
2025 | | bool bDisableDownScale(nullptr != pDisableDownScale); |
2026 | | } |
2027 | | |
2028 | | cairo_surface_t* SurfaceHelper::implCreateOrReuseDownscale(unsigned long nTargetWidth, |
2029 | | unsigned long nTargetHeight) |
2030 | 0 | { |
2031 | 0 | const unsigned long nSourceWidth(cairo_image_surface_get_width(pSurface)); |
2032 | 0 | const unsigned long nSourceHeight(cairo_image_surface_get_height(pSurface)); |
2033 | | |
2034 | | // zoomed in, need to stretch at paint, no pre-scale useful |
2035 | 0 | if (nTargetWidth >= nSourceWidth || nTargetHeight >= nSourceHeight) |
2036 | 0 | { |
2037 | 0 | return pSurface; |
2038 | 0 | } |
2039 | | |
2040 | | // calculate downscale factor |
2041 | 0 | unsigned long nWFactor(1); |
2042 | 0 | unsigned long nW((nSourceWidth + 1) / 2); |
2043 | 0 | unsigned long nHFactor(1); |
2044 | 0 | unsigned long nH((nSourceHeight + 1) / 2); |
2045 | |
|
2046 | 0 | while (nW > nTargetWidth && nW > 1) |
2047 | 0 | { |
2048 | 0 | nW = (nW + 1) / 2; |
2049 | 0 | nWFactor *= 2; |
2050 | 0 | } |
2051 | |
|
2052 | 0 | while (nH > nTargetHeight && nH > 1) |
2053 | 0 | { |
2054 | 0 | nH = (nH + 1) / 2; |
2055 | 0 | nHFactor *= 2; |
2056 | 0 | } |
2057 | |
|
2058 | 0 | if (1 == nWFactor && 1 == nHFactor) |
2059 | 0 | { |
2060 | | // original size *is* best binary size, use it |
2061 | 0 | return pSurface; |
2062 | 0 | } |
2063 | | |
2064 | | // go up one scale again - look for no change |
2065 | 0 | nW = (1 == nWFactor) ? nTargetWidth : nW * 2; |
2066 | 0 | nH = (1 == nHFactor) ? nTargetHeight : nH * 2; |
2067 | | |
2068 | | // check if we have a downscaled version of required size |
2069 | | // bail out if the multiplication for the key would overflow |
2070 | 0 | if (nW >= SAL_MAX_UINT32 || nH >= SAL_MAX_UINT32) |
2071 | 0 | return pSurface; |
2072 | 0 | const sal_uInt64 key((nW * static_cast<sal_uInt64>(SAL_MAX_UINT32)) + nH); |
2073 | 0 | auto isHit(maDownscaled.find(key)); |
2074 | |
|
2075 | 0 | if (isHit != maDownscaled.end()) |
2076 | 0 | { |
2077 | 0 | return isHit->second; |
2078 | 0 | } |
2079 | | |
2080 | | // create new surface in the targeted size |
2081 | 0 | cairo_surface_t* pSurfaceTarget |
2082 | 0 | = cairo_surface_create_similar(pSurface, cairo_surface_get_content(pSurface), nW, nH); |
2083 | | |
2084 | | // made a version to scale self first that worked well, but would've |
2085 | | // been hard to support CAIRO_FORMAT_A1 including bit shifting, so |
2086 | | // I decided to go with cairo itself - use CAIRO_FILTER_FAST or |
2087 | | // CAIRO_FILTER_GOOD though. Please modify as needed for |
2088 | | // performance/quality |
2089 | 0 | cairo_t* cr = cairo_create(pSurfaceTarget); |
2090 | 0 | const double fScaleX(static_cast<double>(nW) / static_cast<double>(nSourceWidth)); |
2091 | 0 | const double fScaleY(static_cast<double>(nH) / static_cast<double>(nSourceHeight)); |
2092 | 0 | cairo_scale(cr, fScaleX, fScaleY); |
2093 | 0 | cairo_set_source_surface(cr, pSurface, 0.0, 0.0); |
2094 | 0 | cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_GOOD); |
2095 | 0 | cairo_paint(cr); |
2096 | 0 | cairo_destroy(cr); |
2097 | | |
2098 | | // need to set device_scale for downscale surfaces to get |
2099 | | // them handled correctly |
2100 | 0 | cairo_surface_set_device_scale(pSurfaceTarget, fScaleX, fScaleY); |
2101 | | |
2102 | | // add entry to cached entries |
2103 | 0 | maDownscaled[key] = pSurfaceTarget; |
2104 | |
|
2105 | 0 | return pSurfaceTarget; |
2106 | 0 | } |
2107 | | |
2108 | | bool SurfaceHelper::isTrivial() const |
2109 | 6.47k | { |
2110 | 6.47k | constexpr unsigned long nMinimalSquareSizeToBuffer(64 * 64); |
2111 | 6.47k | const unsigned long nSourceWidth(cairo_image_surface_get_width(pSurface)); |
2112 | 6.47k | const unsigned long nSourceHeight(cairo_image_surface_get_height(pSurface)); |
2113 | | |
2114 | 6.47k | return nSourceWidth * nSourceHeight < nMinimalSquareSizeToBuffer; |
2115 | 6.47k | } |
2116 | | |
2117 | | SurfaceHelper::SurfaceHelper() |
2118 | 6.47k | : pSurface(nullptr) |
2119 | 6.47k | { |
2120 | 6.47k | } |
2121 | | |
2122 | | SurfaceHelper::~SurfaceHelper() |
2123 | 6.47k | { |
2124 | 6.47k | cairo_surface_destroy(pSurface); |
2125 | 6.47k | for (auto& candidate : maDownscaled) |
2126 | 0 | { |
2127 | 0 | cairo_surface_destroy(candidate.second); |
2128 | 0 | } |
2129 | 6.47k | } |
2130 | | |
2131 | | cairo_surface_t* SurfaceHelper::getSurface(unsigned long nTargetWidth, |
2132 | | unsigned long nTargetHeight) const |
2133 | 6.47k | { |
2134 | 6.47k | if (bDisableDownScale || 0 == nTargetWidth || 0 == nTargetHeight || !pSurface || isTrivial()) |
2135 | 6.47k | { |
2136 | | // caller asks for original or disabled or trivial (smaller than a minimal square size) |
2137 | | // also excludes zero cases for width/height after this point if need to prescale |
2138 | 6.47k | return pSurface; |
2139 | 6.47k | } |
2140 | | |
2141 | 0 | return const_cast<SurfaceHelper*>(this)->implCreateOrReuseDownscale(nTargetWidth, |
2142 | 0 | nTargetHeight); |
2143 | 6.47k | } |
2144 | | |
2145 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |