/src/libreoffice/vcl/source/outdev/bitmapex.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ |
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 <config_features.h> |
21 | | |
22 | | #include <rtl/math.hxx> |
23 | | #include <comphelper/lok.hxx> |
24 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
25 | | #include <tools/mapunit.hxx> |
26 | | |
27 | | #include <vcl/alpha.hxx> |
28 | | #include <vcl/canvastools.hxx> |
29 | | #include <vcl/metaact.hxx> |
30 | | #include <vcl/virdev.hxx> |
31 | | |
32 | | #include <CoordinateMapper.hxx> |
33 | | #include <drawmode.hxx> |
34 | | #include <salgdi.hxx> |
35 | | |
36 | | bool OutputDevice::TransformAndReduceBitmapExToTargetRange( |
37 | | const basegfx::B2DHomMatrix& aFullTransform, |
38 | | basegfx::B2DRange &aVisibleRange, |
39 | | double &fMaximumArea) |
40 | 0 | { |
41 | | // limit TargetRange to existing pixels (if pixel device) |
42 | | // first get discrete range of object |
43 | 0 | basegfx::B2DRange aFullPixelRange(aVisibleRange); |
44 | |
|
45 | 0 | aFullPixelRange.transform(aFullTransform); |
46 | |
|
47 | 0 | if(basegfx::fTools::equalZero(aFullPixelRange.getWidth()) || basegfx::fTools::equalZero(aFullPixelRange.getHeight())) |
48 | 0 | { |
49 | | // object is outside of visible area |
50 | 0 | return false; |
51 | 0 | } |
52 | | |
53 | | // now get discrete target pixels; start with OutDev pixel size and possibly |
54 | | // intersect with active clipping area |
55 | 0 | basegfx::B2DRange aOutPixel( |
56 | 0 | 0.0, |
57 | 0 | 0.0, |
58 | 0 | GetOutputSizePixel().Width(), |
59 | 0 | GetOutputSizePixel().Height()); |
60 | |
|
61 | 0 | if(IsClipRegion()) |
62 | 0 | { |
63 | 0 | tools::Rectangle aRegionRectangle(GetActiveClipRegion().GetBoundRect()); |
64 | | |
65 | | // caution! Range from rectangle, one too much (!) |
66 | 0 | aRegionRectangle.AdjustRight(-1); |
67 | 0 | aRegionRectangle.AdjustBottom(-1); |
68 | 0 | aOutPixel.intersect( vcl::unotools::b2DRectangleFromRectangle(aRegionRectangle) ); |
69 | 0 | } |
70 | |
|
71 | 0 | if(aOutPixel.isEmpty()) |
72 | 0 | { |
73 | | // no active output area |
74 | 0 | return false; |
75 | 0 | } |
76 | | |
77 | | // if aFullPixelRange is not completely inside of aOutPixel, |
78 | | // reduction of target pixels is possible |
79 | 0 | basegfx::B2DRange aVisiblePixelRange(aFullPixelRange); |
80 | |
|
81 | 0 | if(!aOutPixel.isInside(aFullPixelRange)) |
82 | 0 | { |
83 | 0 | aVisiblePixelRange.intersect(aOutPixel); |
84 | |
|
85 | 0 | if(aVisiblePixelRange.isEmpty()) |
86 | 0 | { |
87 | | // nothing in visible part, reduces to nothing |
88 | 0 | return false; |
89 | 0 | } |
90 | | |
91 | | // aVisiblePixelRange contains the reduced output area in |
92 | | // discrete coordinates. To make it useful everywhere, make it relative to |
93 | | // the object range |
94 | 0 | basegfx::B2DHomMatrix aMakeVisibleRangeRelative; |
95 | |
|
96 | 0 | aVisibleRange = aVisiblePixelRange; |
97 | 0 | aMakeVisibleRangeRelative.translate( |
98 | 0 | -aFullPixelRange.getMinX(), |
99 | 0 | -aFullPixelRange.getMinY()); |
100 | 0 | aMakeVisibleRangeRelative.scale( |
101 | 0 | 1.0 / aFullPixelRange.getWidth(), |
102 | 0 | 1.0 / aFullPixelRange.getHeight()); |
103 | 0 | aVisibleRange.transform(aMakeVisibleRangeRelative); |
104 | 0 | } |
105 | | |
106 | | // for pixel devices, do *not* limit size, else OutputDevice::DrawDeviceAlphaBitmap |
107 | | // will create another, badly scaled bitmap to do the job. Nonetheless, do a |
108 | | // maximum clipping of something big (1600x1280x2). Add 1.0 to avoid rounding |
109 | | // errors in rough estimations |
110 | 0 | const double fNewMaxArea(aVisiblePixelRange.getWidth() * aVisiblePixelRange.getHeight()); |
111 | |
|
112 | 0 | fMaximumArea = std::min(4096000.0, fNewMaxArea + 1.0); |
113 | |
|
114 | 0 | return true; |
115 | 0 | } |
116 | | |
117 | | // MM02 add some test class to get a simple timer-based output to be able |
118 | | // to check if it gets faster - and how much. Uncomment next line or set |
119 | | // DO_TIME_TEST for compile time if you want to use it |
120 | | // #define DO_TIME_TEST |
121 | | #ifdef DO_TIME_TEST |
122 | | #include <tools/time.hxx> |
123 | | struct LocalTimeTest |
124 | | { |
125 | | const sal_uInt64 nStartTime; |
126 | | LocalTimeTest() : nStartTime(tools::Time::GetSystemTicks()) {} |
127 | | ~LocalTimeTest() |
128 | | { |
129 | | const sal_uInt64 nEndTime(tools::Time::GetSystemTicks()); |
130 | | const sal_uInt64 nDiffTime(nEndTime - nStartTime); |
131 | | |
132 | | if(nDiffTime > 0) |
133 | | { |
134 | | OStringBuffer aOutput("Time: "); |
135 | | OString aNumber(OString::number(nDiffTime)); |
136 | | aOutput.append(aNumber); |
137 | | OSL_FAIL(aOutput.getStr()); |
138 | | } |
139 | | } |
140 | | }; |
141 | | #endif |
142 | | |
143 | | void OutputDevice::DrawScaledAndTranslatedBitmap( |
144 | | const basegfx::B2DVector& rScale, const basegfx::B2DVector& rTranslate, |
145 | | const Bitmap& rBitmap) |
146 | 0 | { |
147 | | // with no rotation, shear or mirroring it can be mapped to DrawBitmap |
148 | | // do *not* execute the mirroring here, it's done in the fallback |
149 | | // #i124580# the correct DestSize needs to be calculated based on MaxXY values |
150 | 0 | Point aDestPt(basegfx::fround<tools::Long>(rTranslate.getX()), basegfx::fround<tools::Long>(rTranslate.getY())); |
151 | |
|
152 | 0 | const Size aDestSize( |
153 | 0 | basegfx::fround<tools::Long>(rScale.getX() + rTranslate.getX()) - aDestPt.X(), |
154 | 0 | basegfx::fround<tools::Long>(rScale.getY() + rTranslate.getY()) - aDestPt.Y()); |
155 | |
|
156 | 0 | const Point aOrigin = GetMapMode().GetOrigin(); |
157 | |
|
158 | 0 | if (!mpMetaFile && comphelper::LibreOfficeKit::isActive() && GetMapMode().GetMapUnit() != MapUnit::MapPixel) |
159 | 0 | { |
160 | 0 | aDestPt.Move(aOrigin.getX(), aOrigin.getY()); |
161 | 0 | EnableMapMode(false); |
162 | 0 | } |
163 | |
|
164 | 0 | DrawBitmap(aDestPt, aDestSize, rBitmap); |
165 | 0 | if (!mpMetaFile && comphelper::LibreOfficeKit::isActive() && GetMapMode().GetMapUnit() != MapUnit::MapPixel) |
166 | 0 | { |
167 | 0 | EnableMapMode(); |
168 | 0 | aDestPt.Move(-aOrigin.getX(), -aOrigin.getY()); |
169 | 0 | } |
170 | 0 | return; |
171 | 0 | } |
172 | | |
173 | | void OutputDevice::DrawMirroredBitmap( |
174 | | const basegfx::B2DVector& rScale, const basegfx::B2DVector& rTranslate, |
175 | | const Bitmap& rBitmap) |
176 | 0 | { |
177 | | // with no rotation or shear it can be mapped to DrawBitmap |
178 | | // do *not* execute the mirroring here, it's done in the fallback |
179 | | // #i124580# the correct DestSize needs to be calculated based on MaxXY values |
180 | 0 | const Point aDestPt(basegfx::fround<tools::Long>(rTranslate.getX()), basegfx::fround<tools::Long>(rTranslate.getY())); |
181 | 0 | const Size aDestSize( |
182 | 0 | basegfx::fround<tools::Long>(rScale.getX() + rTranslate.getX()) - aDestPt.X(), |
183 | 0 | basegfx::fround<tools::Long>(rScale.getY() + rTranslate.getY()) - aDestPt.Y()); |
184 | |
|
185 | 0 | DrawBitmap(aDestPt, aDestSize, rBitmap); |
186 | 0 | } |
187 | | |
188 | | void OutputDevice::DrawTransformedBitmapEx( |
189 | | const basegfx::B2DHomMatrix& rTransformation, |
190 | | const Bitmap& rBitmap, |
191 | | double fAlpha) |
192 | 0 | { |
193 | 0 | assert(!is_double_buffered_window()); |
194 | |
|
195 | 0 | if( ImplIsRecordLayout() ) |
196 | 0 | return; |
197 | | |
198 | 0 | if(rBitmap.IsEmpty()) |
199 | 0 | return; |
200 | | |
201 | 0 | if( fAlpha == 0.0 ) |
202 | 0 | return; |
203 | | |
204 | | // MM02 compared to other public methods of OutputDevice |
205 | | // this test was missing and led to zero-ptr-accesses |
206 | 0 | if ( !mpGraphics && !AcquireGraphics() ) |
207 | 0 | return; |
208 | | |
209 | 0 | if ( mbInitClipRegion ) |
210 | 0 | InitClipRegion(); |
211 | | |
212 | | /* |
213 | | tdf#135325 typically in these OutputDevice methods, for the in |
214 | | record-to-metafile case the MetaFile is already written to before the |
215 | | test against mbOutputClipped to determine that output to the current |
216 | | device would result in no visual output. In this case the metafile is |
217 | | written after the test, so we must continue past mbOutputClipped if |
218 | | recording to a metafile. It's typical to record with a device of nominal |
219 | | size and play back later against something of a totally different size. |
220 | | */ |
221 | 0 | if (mbOutputClipped && !mpMetaFile) |
222 | 0 | return; |
223 | | |
224 | | #ifdef DO_TIME_TEST |
225 | | // MM02 start time test when some data (not for trivial stuff). Will |
226 | | // trigger and show data when leaving this method by destructing helper |
227 | | static const char* pEnableBitmapDrawTimerTimer(getenv("SAL_ENABLE_TIMER_BITMAPDRAW")); |
228 | | static bool bUseTimer(nullptr != pEnableBitmapDrawTimerTimer); |
229 | | std::unique_ptr<LocalTimeTest> aTimeTest( |
230 | | bUseTimer && rBitmapEx.GetSizeBytes() > 10000 |
231 | | ? new LocalTimeTest() |
232 | | : nullptr); |
233 | | #endif |
234 | | |
235 | 0 | Bitmap bitmap = rBitmap; |
236 | |
|
237 | 0 | const bool bInvert(RasterOp::Invert == meRasterOp); |
238 | 0 | const bool bBitmapChangedColor(mnDrawMode & (DrawModeFlags::BlackBitmap | DrawModeFlags::WhiteBitmap | DrawModeFlags::GrayBitmap )); |
239 | 0 | const bool bTryDirectPaint(!bInvert && !bBitmapChangedColor && !mpMetaFile); |
240 | | // tdf#130768 CAUTION(!) using GetViewTransformation() is *not* enough here, it may |
241 | | // be that mnOutOffX/mnOutOffY is used - see AOO bug 75163, mentioned at |
242 | | // ImplGetDeviceTransformation declaration |
243 | 0 | basegfx::B2DHomMatrix aFullTransform(mpMapper->GetDeviceTransformation() * rTransformation); |
244 | | |
245 | | // First try to handle additional alpha blending, either directly, or modify the bitmap. |
246 | 0 | if(!rtl::math::approxEqual( fAlpha, 1.0 )) |
247 | 0 | { |
248 | 0 | if(bTryDirectPaint) |
249 | 0 | { |
250 | 0 | if (DrawTransformedBitmap(aFullTransform, bitmap, fAlpha)) |
251 | 0 | { |
252 | | // we are done |
253 | 0 | return; |
254 | 0 | } |
255 | 0 | } |
256 | | // Apply the alpha manually. |
257 | 0 | sal_uInt8 nTransparency( static_cast<sal_uInt8>( ::basegfx::fround( 255.0*(1.0 - fAlpha) + .5) ) ); |
258 | 0 | AlphaMask aAlpha( bitmap.GetSizePixel(), &nTransparency ); |
259 | 0 | if( bitmap.HasAlpha()) |
260 | 0 | aAlpha.BlendWith( bitmap.CreateAlphaMask()); |
261 | 0 | bitmap = Bitmap(bitmap, aAlpha); |
262 | 0 | } |
263 | | |
264 | | // If the backend's implementation is known to not need any optimizations here, pass to it directly. |
265 | | // With most backends it's more performant to try to simplify to DrawBitmap() first. |
266 | 0 | if(bTryDirectPaint && mpGraphics->HasFastDrawTransformedBitmap() && DrawTransformedBitmap(aFullTransform, bitmap)) |
267 | 0 | return; |
268 | | |
269 | | // decompose matrix to check rotation and shear |
270 | 0 | basegfx::B2DVector aScale, aTranslate; |
271 | 0 | double fRotate, fShearX; |
272 | 0 | rTransformation.decompose(aScale, aTranslate, fRotate, fShearX); |
273 | 0 | const bool bRotated(!basegfx::fTools::equalZero(fRotate)); |
274 | 0 | const bool bSheared(!basegfx::fTools::equalZero(fShearX)); |
275 | 0 | const bool bMirroredX(aScale.getX() < 0.0); |
276 | 0 | const bool bMirroredY(aScale.getY() < 0.0); |
277 | |
|
278 | 0 | if (!bRotated && !bSheared && !bMirroredX && !bMirroredY) |
279 | 0 | { |
280 | 0 | DrawScaledAndTranslatedBitmap(aScale, aTranslate, bitmap); |
281 | 0 | return; |
282 | 0 | } |
283 | | |
284 | | // Try the backend's implementation before resorting to the slower fallback here. |
285 | 0 | if (bTryDirectPaint && DrawTransformedBitmap(aFullTransform, bitmap)) |
286 | 0 | return; |
287 | | |
288 | | // take the fallback when no rotate and shear, but mirror (else we would have done this above) |
289 | 0 | if(!bRotated && !bSheared) |
290 | 0 | { |
291 | 0 | DrawMirroredBitmap(aScale, aTranslate, bitmap); |
292 | 0 | return; |
293 | 0 | } |
294 | | |
295 | | // at this point we are either sheared or rotated or both |
296 | 0 | assert(bSheared || bRotated); |
297 | | |
298 | | // fallback; create transformed bitmap the hard way (back-transform |
299 | | // the pixels) and paint |
300 | 0 | basegfx::B2DRange aVisibleRange(0.0, 0.0, 1.0, 1.0); |
301 | | |
302 | | // limit maximum area to something looking good for non-pixel-based targets (metafile, printer) |
303 | | // by using a fixed minimum (allow at least, but no need to utilize) for good smoothing and an area |
304 | | // dependent of original size for good quality when e.g. rotated/sheared. Still, limit to a maximum |
305 | | // to avoid crashes/resource problems (ca. 1500x3000 here) |
306 | 0 | const Size aOriginalSizePixel(bitmap.GetSizePixel()); |
307 | 0 | const double fOrigArea(aOriginalSizePixel.Width() * aOriginalSizePixel.Height() * 0.5); |
308 | 0 | const double fOrigAreaScaled(fOrigArea * 1.44); |
309 | 0 | double fMaximumArea(std::clamp(fOrigAreaScaled, 1000000.0, 4500000.0)); |
310 | |
|
311 | 0 | if(!mpMetaFile) |
312 | 0 | { |
313 | 0 | if ( !TransformAndReduceBitmapExToTargetRange( aFullTransform, aVisibleRange, fMaximumArea ) ) |
314 | 0 | return; |
315 | 0 | } |
316 | | |
317 | 0 | if(aVisibleRange.isEmpty()) |
318 | 0 | return; |
319 | | |
320 | 0 | Bitmap aTransformed(bitmap); |
321 | | |
322 | | // #122923# when the result needs an alpha channel due to being rotated or sheared |
323 | | // and thus uncovering areas, add these channels so that the own transformer (used |
324 | | // in getTransformed) also creates a transformed alpha channel |
325 | 0 | if(!aTransformed.HasAlpha() && (bSheared || bRotated)) |
326 | 0 | { |
327 | | // parts will be uncovered, extend aTransformed with a mask bitmap |
328 | 0 | const Bitmap aContent(aTransformed.CreateColorBitmap()); |
329 | |
|
330 | 0 | AlphaMask aMaskBmp(aContent.GetSizePixel()); |
331 | 0 | aMaskBmp.Erase(0); |
332 | |
|
333 | 0 | aTransformed = Bitmap(aContent, aMaskBmp); |
334 | 0 | } |
335 | |
|
336 | 0 | basegfx::B2DVector aFullScale, aFullTranslate; |
337 | 0 | double fFullRotate, fFullShearX; |
338 | 0 | aFullTransform.decompose(aFullScale, aFullTranslate, fFullRotate, fFullShearX); |
339 | 0 | if (aFullScale.getX() > 0 && aFullScale.getY() > 0 |
340 | 0 | && aOriginalSizePixel.getWidth() > aFullScale.getX() |
341 | 0 | && aOriginalSizePixel.getHeight() > aFullScale.getY()) |
342 | 0 | { |
343 | | // aFullTransform would downscale the bitmap: avoid this, so the recorded metafile can be |
344 | | // better upscaled later. |
345 | 0 | basegfx::B2DHomMatrix aTransform = basegfx::utils::createScaleB2DHomMatrix( |
346 | 0 | aOriginalSizePixel.getWidth() / aFullScale.getX(), |
347 | 0 | aOriginalSizePixel.getHeight() / aFullScale.getY()); |
348 | 0 | aFullTransform *= aTransform; |
349 | 0 | } |
350 | |
|
351 | 0 | double fSourceRatio = 1.0; |
352 | 0 | if (aOriginalSizePixel.getHeight() != 0) |
353 | 0 | { |
354 | 0 | fSourceRatio = aOriginalSizePixel.getWidth() / aOriginalSizePixel.getHeight(); |
355 | 0 | } |
356 | 0 | double fTargetRatio = 1.0; |
357 | 0 | if (aFullScale.getY() != 0) |
358 | 0 | { |
359 | 0 | fTargetRatio = aFullScale.getX() / aFullScale.getY(); |
360 | 0 | } |
361 | 0 | bool bAspectRatioKept = rtl::math::approxEqual(fSourceRatio, fTargetRatio); |
362 | 0 | if (bSheared || !bAspectRatioKept) |
363 | 0 | { |
364 | | // Not only rotation, or scaling does not keep aspect ratio. |
365 | 0 | aTransformed = aTransformed.getTransformed( |
366 | 0 | aFullTransform, |
367 | 0 | aVisibleRange, |
368 | 0 | fMaximumArea); |
369 | 0 | } |
370 | 0 | else |
371 | 0 | { |
372 | | // Just rotation, can do that directly. |
373 | 0 | fFullRotate = fmod(fFullRotate * -1, 2 * M_PI); |
374 | 0 | if (fFullRotate < 0) |
375 | 0 | { |
376 | 0 | fFullRotate += 2 * M_PI; |
377 | 0 | } |
378 | 0 | Degree10 nAngle10(basegfx::fround(basegfx::rad2deg<10>(fFullRotate))); |
379 | 0 | aTransformed.Rotate(nAngle10, COL_TRANSPARENT); |
380 | 0 | } |
381 | 0 | basegfx::B2DRange aTargetRange(0.0, 0.0, 1.0, 1.0); |
382 | | |
383 | | // get logic object target range |
384 | 0 | aTargetRange.transform(rTransformation); |
385 | | |
386 | | // get from unified/relative VisibleRange to logic one |
387 | 0 | aVisibleRange.transform( |
388 | 0 | basegfx::utils::createScaleTranslateB2DHomMatrix( |
389 | 0 | aTargetRange.getRange(), |
390 | 0 | aTargetRange.getMinimum())); |
391 | | |
392 | | // extract point and size; do not remove size, the bitmap may have been prepared reduced on purpose |
393 | | // #i124580# the correct DestSize needs to be calculated based on MaxXY values |
394 | 0 | const Point aDestPt(basegfx::fround<tools::Long>(aVisibleRange.getMinX()), basegfx::fround<tools::Long>(aVisibleRange.getMinY())); |
395 | 0 | const Size aDestSize( |
396 | 0 | basegfx::fround<tools::Long>(aVisibleRange.getMaxX()) - aDestPt.X(), |
397 | 0 | basegfx::fround<tools::Long>(aVisibleRange.getMaxY()) - aDestPt.Y()); |
398 | |
|
399 | 0 | DrawBitmap(aDestPt, aDestSize, aTransformed); |
400 | 0 | } |
401 | | |
402 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ |