/src/libreoffice/drawinglayer/source/tools/converters.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 <drawinglayer/geometry/viewinformation2d.hxx> |
21 | | #include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx> |
22 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
23 | | #include <drawinglayer/primitive2d/transformprimitive2d.hxx> |
24 | | #include <drawinglayer/processor2d/baseprocessor2d.hxx> |
25 | | #include <drawinglayer/processor2d/processor2dtools.hxx> |
26 | | #include <tools/mapunit.hxx> |
27 | | #include <vcl/alpha.hxx> |
28 | | #include <vcl/svapp.hxx> |
29 | | #include <vcl/virdev.hxx> |
30 | | #include <com/sun/star/geometry/RealRectangle2D.hpp> |
31 | | #include <comphelper/diagnose_ex.hxx> |
32 | | |
33 | | #include <drawinglayer/converters.hxx> |
34 | | #include <config_vclplug.h> |
35 | | |
36 | | #ifdef DBG_UTIL |
37 | | #include <o3tl/environment.hxx> |
38 | | #include <tools/stream.hxx> |
39 | | #include <vcl/dibtools.hxx> |
40 | | #endif |
41 | | |
42 | | namespace |
43 | | { |
44 | | bool implPrepareConversion(drawinglayer::primitive2d::Primitive2DContainer& rSequence, |
45 | | sal_uInt32& rnDiscreteWidth, sal_uInt32& rnDiscreteHeight, |
46 | | const sal_uInt32 nMaxSquarePixels) |
47 | 102 | { |
48 | 102 | if (rSequence.empty()) |
49 | 0 | return false; |
50 | | |
51 | 102 | if (rnDiscreteWidth <= 0 || rnDiscreteHeight <= 0) |
52 | 0 | return false; |
53 | | |
54 | 102 | const sal_uInt32 nViewVisibleArea(rnDiscreteWidth * rnDiscreteHeight); |
55 | | |
56 | 102 | if (nViewVisibleArea > nMaxSquarePixels) |
57 | 0 | { |
58 | | // reduce render size |
59 | 0 | double fReduceFactor |
60 | 0 | = sqrt(static_cast<double>(nMaxSquarePixels) / static_cast<double>(nViewVisibleArea)); |
61 | 0 | rnDiscreteWidth = basegfx::fround(static_cast<double>(rnDiscreteWidth) * fReduceFactor); |
62 | 0 | rnDiscreteHeight = basegfx::fround(static_cast<double>(rnDiscreteHeight) * fReduceFactor); |
63 | |
|
64 | 0 | const drawinglayer::primitive2d::Primitive2DReference aEmbed( |
65 | 0 | new drawinglayer::primitive2d::TransformPrimitive2D( |
66 | 0 | basegfx::utils::createScaleB2DHomMatrix(fReduceFactor, fReduceFactor), |
67 | 0 | std::move(rSequence))); |
68 | |
|
69 | 0 | rSequence = drawinglayer::primitive2d::Primitive2DContainer{ aEmbed }; |
70 | 0 | } |
71 | | |
72 | 102 | return true; |
73 | 102 | } |
74 | | |
75 | | AlphaMask implcreateAlphaMask(drawinglayer::primitive2d::Primitive2DContainer& rSequence, |
76 | | const drawinglayer::geometry::ViewInformation2D& rViewInformation2D, |
77 | | const Size& rSizePixel, bool bUseLuminance) |
78 | 102 | { |
79 | 102 | ScopedVclPtrInstance<VirtualDevice> pContent; |
80 | | |
81 | | // prepare vdev |
82 | 102 | if (!pContent->SetOutputSizePixel(rSizePixel, false)) |
83 | 0 | { |
84 | 0 | SAL_WARN("vcl", "Cannot set VirtualDevice to size : " << rSizePixel.Width() << "x" |
85 | 0 | << rSizePixel.Height()); |
86 | 0 | return AlphaMask(); |
87 | 0 | } |
88 | | |
89 | | // create pixel processor, also already takes care of AAing and |
90 | | // checking the getOptionsDrawinglayer().IsAntiAliasing() switch. If |
91 | | // not wanted, change after this call as needed |
92 | 102 | std::unique_ptr<drawinglayer::processor2d::BaseProcessor2D> pContentProcessor |
93 | 102 | = drawinglayer::processor2d::createPixelProcessor2DFromOutputDevice(*pContent, |
94 | 102 | rViewInformation2D); |
95 | | |
96 | | // prepare for mask creation |
97 | 102 | pContent->SetMapMode(MapMode(MapUnit::MapPixel)); |
98 | | |
99 | | // set transparency to all white (fully transparent) |
100 | 102 | pContent->Erase(); |
101 | | |
102 | 102 | basegfx::BColorModifierSharedPtr aBColorModifier; |
103 | 102 | if (bUseLuminance) |
104 | 0 | { |
105 | | // new mode: bUseLuminance allows simple creation of alpha channels |
106 | | // for any content (e.g. gradients) |
107 | 0 | aBColorModifier = std::make_shared<basegfx::BColorModifier_luminance_to_alpha>(); |
108 | 0 | } |
109 | 102 | else |
110 | 102 | { |
111 | | // Embed primitives to paint them black (fully opaque) |
112 | 102 | aBColorModifier |
113 | 102 | = std::make_shared<basegfx::BColorModifier_replace>(basegfx::BColor(0.0, 0.0, 0.0)); |
114 | 102 | } |
115 | 102 | const drawinglayer::primitive2d::Primitive2DReference xRef( |
116 | 102 | new drawinglayer::primitive2d::ModifiedColorPrimitive2D(std::move(rSequence), |
117 | 102 | std::move(aBColorModifier))); |
118 | 102 | const drawinglayer::primitive2d::Primitive2DContainer xSeq{ xRef }; |
119 | | |
120 | | // render |
121 | 102 | pContentProcessor->process(xSeq); |
122 | 102 | pContentProcessor.reset(); |
123 | | |
124 | | // get alpha channel from vdev |
125 | 102 | pContent->EnableMapMode(false); |
126 | 102 | const Point aEmptyPoint; |
127 | | |
128 | | // Convert from transparency->alpha. |
129 | | // FIXME in theory I should be able to directly construct alpha by using black as background |
130 | | // and white as foreground, but that doesn't work for some reason. |
131 | 102 | Bitmap aContentBitmap = pContent->GetBitmap(aEmptyPoint, rSizePixel); |
132 | 102 | aContentBitmap.Invert(); |
133 | | |
134 | 102 | return AlphaMask(aContentBitmap); |
135 | 102 | } |
136 | | } |
137 | | |
138 | | namespace drawinglayer |
139 | | { |
140 | | AlphaMask createAlphaMask(drawinglayer::primitive2d::Primitive2DContainer&& rSeq, |
141 | | const geometry::ViewInformation2D& rViewInformation2D, |
142 | | sal_uInt32 nDiscreteWidth, sal_uInt32 nDiscreteHeight, |
143 | | sal_uInt32 nMaxSquarePixels, bool bUseLuminance) |
144 | 0 | { |
145 | 0 | drawinglayer::primitive2d::Primitive2DContainer aSequence(std::move(rSeq)); |
146 | |
|
147 | 0 | if (!implPrepareConversion(aSequence, nDiscreteWidth, nDiscreteHeight, nMaxSquarePixels)) |
148 | 0 | { |
149 | 0 | return AlphaMask(); |
150 | 0 | } |
151 | | |
152 | 0 | const Size aSizePixel(nDiscreteWidth, nDiscreteHeight); |
153 | |
|
154 | 0 | return implcreateAlphaMask(aSequence, rViewInformation2D, aSizePixel, bUseLuminance); |
155 | 0 | } |
156 | | |
157 | | Bitmap convertToBitmap(drawinglayer::primitive2d::Primitive2DContainer&& rSeq, |
158 | | const geometry::ViewInformation2D& rViewInformation2D, |
159 | | sal_uInt32 nDiscreteWidth, sal_uInt32 nDiscreteHeight, |
160 | | sal_uInt32 nMaxSquarePixels, bool bForceAlphaMaskCreation) |
161 | 102 | { |
162 | 102 | drawinglayer::primitive2d::Primitive2DContainer aSequence(std::move(rSeq)); |
163 | | |
164 | 102 | if (!implPrepareConversion(aSequence, nDiscreteWidth, nDiscreteHeight, nMaxSquarePixels)) |
165 | 0 | { |
166 | 0 | return Bitmap(); |
167 | 0 | } |
168 | | |
169 | 102 | #if USE_HEADLESS_CODE |
170 | | // shortcut: try to directly create a PixelProcessor2D with |
171 | | // RGBA support - that's what we need |
172 | | // Currently only implemented for CairoSDPR, so add code only |
173 | | // for USE_HEADLESS_CODE, but is designed as a general functionality |
174 | 102 | std::unique_ptr<processor2d::BaseProcessor2D> pRGBAProcessor |
175 | 102 | = processor2d::createPixelProcessor2DFromScratch(rViewInformation2D, nDiscreteWidth, nDiscreteHeight, true); |
176 | 102 | if (pRGBAProcessor) |
177 | 0 | { |
178 | | // render content |
179 | 0 | pRGBAProcessor->process(aSequence); |
180 | | |
181 | | // create final Bitmap result (content) |
182 | 0 | const Bitmap aRetval(processor2d::extractBitmapFromBaseProcessor2D(pRGBAProcessor)); |
183 | | |
184 | | // check if we have a result and return if so |
185 | 0 | if (!aRetval.IsEmpty()) |
186 | 0 | return aRetval; |
187 | 0 | } |
188 | 102 | #endif |
189 | | |
190 | 102 | const Point aEmptyPoint; |
191 | 102 | const Size aSizePixel(nDiscreteWidth, nDiscreteHeight); |
192 | | |
193 | | // Create target VirtualDevice. Go back to using a simple RGB |
194 | | // target version (compared with former version, see history). |
195 | | // Reasons are manyfold: |
196 | | // - Avoid the RGBA mode for VirtualDevice (two VDevs) |
197 | | // - It's not suggested to be used outside presentation engine |
198 | | // - It only works *by chance* with VCLPrimitiveRenderer |
199 | | // - Usage of two-VDev alpha-VDev avoided alpha blending against |
200 | | // COL_WHITE in the 1st layer of targets (not in buffers below) |
201 | | // but is kind of a 'hack' doing so |
202 | | // - Other renderers (system-dependent PrimitiveRenderers, other |
203 | | // than the VCL-based ones) will probably not support split |
204 | | // VDevs for content/alpha, so require a method that works with |
205 | | // RGB targeting (for now) |
206 | | // - Less resource usage, better speed (no 2 VDevs, no merge of |
207 | | // AlphaChannels) |
208 | | // As long as not all our mechanisms are changed to RGBA completely, |
209 | | // mixing these is just too dangerous and expensive and may to wrong |
210 | | // or deliver bad quality results. |
211 | | // Nonetheless we need a RGBA result here. Luckily we are able to |
212 | | // create a complete and valid AlphaChannel using 'createAlphaMask' |
213 | | // above. |
214 | | // When we know the content (RGB result from renderer), alpha |
215 | | // (result from createAlphaMask) and the start condition (content |
216 | | // rendered against COL_WHITE), it is possible to calculate back |
217 | | // the content, quasi 'remove' that initial blending against |
218 | | // COL_WHITE. |
219 | | // That is what the helper Bitmap::RemoveBlendedStartColor does. |
220 | | // Luckily we only need it for this 'convertToBitmapEx', not in |
221 | | // any other rendering. It could be further optimized, too. |
222 | | // This gives good results, it is in principle comparable with |
223 | | // the results using pre-multiplied alpha tooling, also reducing |
224 | | // the range of values where high alpha values are used. |
225 | 102 | ScopedVclPtrInstance<VirtualDevice> pContent(*Application::GetDefaultDevice()); |
226 | | |
227 | | // prepare vdev |
228 | 102 | if (!pContent->SetOutputSizePixel(aSizePixel, false)) |
229 | 0 | { |
230 | 0 | SAL_WARN("vcl", "Cannot set VirtualDevice to size : " << aSizePixel.Width() << "x" |
231 | 0 | << aSizePixel.Height()); |
232 | 0 | return Bitmap(); |
233 | 0 | } |
234 | | |
235 | | // We map to pixel, use that MapMode. Init by erasing. |
236 | 102 | pContent->SetMapMode(MapMode(MapUnit::MapPixel)); |
237 | 102 | pContent->Erase(); |
238 | | |
239 | | // create pixel processor, also already takes care of AAing and |
240 | | // checking the getOptionsDrawinglayer().IsAntiAliasing() switch. If |
241 | | // not wanted, change after this call as needed |
242 | 102 | std::unique_ptr<processor2d::BaseProcessor2D> pContentProcessor |
243 | 102 | = processor2d::createPixelProcessor2DFromOutputDevice(*pContent, rViewInformation2D); |
244 | | |
245 | | // render content |
246 | 102 | pContentProcessor->process(aSequence); |
247 | | |
248 | | // create final Bitmap result (content) |
249 | 102 | Bitmap aRetval(pContent->GetBitmap(aEmptyPoint, aSizePixel)); |
250 | | |
251 | | #ifdef DBG_UTIL |
252 | | static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore |
253 | | if (bDoSaveForVisualControl) |
254 | | { |
255 | | // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/ |
256 | | static const OUString sDumpPath(o3tl::getEnvironment(u"VCL_DUMP_BMP_PATH"_ustr)); |
257 | | if (!sDumpPath.isEmpty()) |
258 | | { |
259 | | SvFileStream aNew(sDumpPath + "test_content.bmp", |
260 | | StreamMode::WRITE | StreamMode::TRUNC); |
261 | | WriteDIB(aRetval, aNew, false, true); |
262 | | } |
263 | | } |
264 | | #endif |
265 | | |
266 | | // Create the AlphaMask using a method that does this always correct (also used |
267 | | // now in GlowPrimitive2D and ShadowPrimitive2D which both only need the |
268 | | // AlphaMask to do their job, so speeding that up, too). |
269 | 102 | AlphaMask aAlpha(implcreateAlphaMask(aSequence, rViewInformation2D, aSizePixel, false)); |
270 | | |
271 | | #ifdef DBG_UTIL |
272 | | if (bDoSaveForVisualControl) |
273 | | { |
274 | | // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/ |
275 | | static const OUString sDumpPath(o3tl::getEnvironment(u"VCL_DUMP_BMP_PATH"_ustr)); |
276 | | if (!sDumpPath.isEmpty()) |
277 | | { |
278 | | SvFileStream aNew(sDumpPath + "test_alpha.bmp", StreamMode::WRITE | StreamMode::TRUNC); |
279 | | WriteDIB(aAlpha.GetBitmap(), aNew, false, true); |
280 | | } |
281 | | } |
282 | | #endif |
283 | | |
284 | 102 | if (bForceAlphaMaskCreation || aAlpha.hasAlpha()) |
285 | 102 | { |
286 | | // Need to correct content using known alpha to get to background-free |
287 | | // RGBA result, usable e.g. in PNG export(s) or convert-to-bitmap. |
288 | | // Now that vcl supports bitmaps with an alpha channel, only apply |
289 | | // this correction to bitmaps without an alpha channel. |
290 | 102 | if (pContent->GetBitCount() < 32) |
291 | 0 | { |
292 | 0 | aRetval.RemoveBlendedStartColor(COL_BLACK, aAlpha); |
293 | 0 | } |
294 | 102 | else |
295 | 102 | { |
296 | | // tdf#157558 invert and remove blended white color |
297 | | // Before commit 81994cb2b8b32453a92bcb011830fcb884f22ff3, |
298 | | // RemoveBlendedStartColor(COL_BLACK, aAlpha) would darken |
299 | | // the bitmap when running a slideshow, printing, or exporting |
300 | | // to PDF. To get the same effect, the alpha mask must be |
301 | | // inverted, RemoveBlendedStartColor(COL_WHITE, aAlpha) |
302 | | // called, and the alpha mask uninverted. |
303 | 102 | aAlpha.Invert(); |
304 | 102 | aRetval.RemoveBlendedStartColor(COL_WHITE, aAlpha); |
305 | 102 | aAlpha.Invert(); |
306 | 102 | } |
307 | | // return combined result |
308 | 102 | return Bitmap(aRetval, aAlpha); |
309 | 102 | } |
310 | 0 | else |
311 | 0 | return aRetval; |
312 | 102 | } |
313 | | |
314 | | Bitmap convertPrimitive2DContainerToBitmap(primitive2d::Primitive2DContainer&& rSequence, |
315 | | const basegfx::B2DRange& rTargetRange, |
316 | | sal_uInt32 nMaximumQuadraticPixels, |
317 | | const o3tl::Length eTargetUnit, |
318 | | const std::optional<Size>& rTargetDPI) |
319 | 99 | { |
320 | 99 | if (rSequence.empty()) |
321 | 0 | return Bitmap(); |
322 | | |
323 | 99 | try |
324 | 99 | { |
325 | 99 | css::geometry::RealRectangle2D aRealRect; |
326 | 99 | aRealRect.X1 = rTargetRange.getMinX(); |
327 | 99 | aRealRect.Y1 = rTargetRange.getMinY(); |
328 | 99 | aRealRect.X2 = rTargetRange.getMaxX(); |
329 | 99 | aRealRect.Y2 = rTargetRange.getMaxY(); |
330 | | |
331 | | // get system DPI |
332 | 99 | Size aDPI( |
333 | 99 | Application::GetDefaultDevice()->LogicToPixel(Size(1, 1), MapMode(MapUnit::MapInch))); |
334 | 99 | if (rTargetDPI.has_value()) |
335 | 0 | { |
336 | 0 | aDPI = *rTargetDPI; |
337 | 0 | } |
338 | | |
339 | 99 | ::sal_uInt32 DPI_X = aDPI.getWidth(); |
340 | 99 | ::sal_uInt32 DPI_Y = aDPI.getHeight(); |
341 | 99 | const basegfx::B2DRange aRange(aRealRect.X1, aRealRect.Y1, aRealRect.X2, aRealRect.Y2); |
342 | 99 | const double fWidth(aRange.getWidth()); |
343 | 99 | const double fHeight(aRange.getHeight()); |
344 | | |
345 | 99 | if (fWidth <= 0.0 || fHeight <= 0.0 || basegfx::fTools::equalZero(fWidth) || basegfx::fTools::equalZero(fHeight)) |
346 | 0 | return Bitmap(); |
347 | | |
348 | 99 | if (0 == DPI_X) |
349 | 0 | { |
350 | 0 | DPI_X = 75; |
351 | 0 | } |
352 | | |
353 | 99 | if (0 == DPI_Y) |
354 | 0 | { |
355 | 0 | DPI_Y = 75; |
356 | 0 | } |
357 | | |
358 | 99 | if (0 == nMaximumQuadraticPixels) |
359 | 0 | { |
360 | 0 | nMaximumQuadraticPixels = 500000; |
361 | 0 | } |
362 | | |
363 | 99 | const auto aViewInformation2D = geometry::createViewInformation2D({}); |
364 | 99 | const sal_uInt32 nDiscreteWidth( |
365 | 99 | basegfx::fround(o3tl::convert(fWidth, eTargetUnit, o3tl::Length::in) * DPI_X)); |
366 | 99 | const sal_uInt32 nDiscreteHeight( |
367 | 99 | basegfx::fround(o3tl::convert(fHeight, eTargetUnit, o3tl::Length::in) * DPI_Y)); |
368 | | |
369 | 99 | basegfx::B2DHomMatrix aEmbedding( |
370 | 99 | basegfx::utils::createTranslateB2DHomMatrix(-aRange.getMinX(), -aRange.getMinY())); |
371 | | |
372 | 99 | aEmbedding.scale(nDiscreteWidth / fWidth, nDiscreteHeight / fHeight); |
373 | | |
374 | 99 | const primitive2d::Primitive2DReference xEmbedRef( |
375 | 99 | new primitive2d::TransformPrimitive2D(aEmbedding, std::move(rSequence))); |
376 | 99 | primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef }; |
377 | | |
378 | 99 | Bitmap aBitmap(convertToBitmap(std::move(xEmbedSeq), aViewInformation2D, |
379 | 99 | nDiscreteWidth, nDiscreteHeight, |
380 | 99 | nMaximumQuadraticPixels)); |
381 | | |
382 | 99 | if (aBitmap.IsEmpty()) |
383 | 0 | return Bitmap(); |
384 | 99 | aBitmap.SetPrefMapMode(MapMode(MapUnit::Map100thMM)); |
385 | 99 | aBitmap.SetPrefSize(Size(basegfx::fround<tools::Long>(fWidth), basegfx::fround<tools::Long>(fHeight))); |
386 | | |
387 | 99 | return aBitmap; |
388 | 99 | } |
389 | 99 | catch (const css::uno::Exception&) |
390 | 99 | { |
391 | 0 | TOOLS_WARN_EXCEPTION("vcl", "Got no graphic::XPrimitive2DRenderer!"); |
392 | 0 | } |
393 | 99 | catch (const std::exception& e) |
394 | 99 | { |
395 | 0 | SAL_WARN("vcl", "Got no graphic::XPrimitive2DRenderer! : " << e.what()); |
396 | 0 | } |
397 | | |
398 | 0 | return Bitmap(); |
399 | 99 | } |
400 | | |
401 | | } // end of namespace drawinglayer |
402 | | |
403 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |