tdf#130857 qt weld: Support mail merge "Server Auth" dialog
[LibreOffice.git] / drawinglayer / source / primitive2d / shadowprimitive2d.cxx
blobc6a7a2e667073a716f81f590c06183ba3886f555
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <drawinglayer/primitive2d/shadowprimitive2d.hxx>
21 #include <basegfx/color/bcolormodifier.hxx>
22 #include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx>
23 #include <drawinglayer/primitive2d/transformprimitive2d.hxx>
24 #include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
25 #include <basegfx/matrix/b2dhommatrixtools.hxx>
26 #include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
27 #include <toolkit/helper/vclunohelper.hxx>
28 #include <drawinglayer/converters.hxx>
29 #include "GlowSoftEgdeShadowTools.hxx"
31 #ifdef DBG_UTIL
32 #include <tools/stream.hxx>
33 #include <vcl/filter/PngImageWriter.hxx>
34 #endif
36 #include <memory>
37 #include <utility>
39 using namespace com::sun::star;
41 namespace drawinglayer::primitive2d
43 ShadowPrimitive2D::ShadowPrimitive2D(basegfx::B2DHomMatrix aShadowTransform,
44 const basegfx::BColor& rShadowColor, double fShadowBlur,
45 Primitive2DContainer&& aChildren)
46 : BufferedDecompositionGroupPrimitive2D(std::move(aChildren))
47 , maShadowTransform(std::move(aShadowTransform))
48 , maShadowColor(rShadowColor)
49 , mfShadowBlur(fShadowBlur)
50 , mfLastDiscreteBlurRadius(0.0)
51 , maLastClippedRange()
53 // activate callback to flush buffered decomposition content
54 setCallbackSeconds(15);
57 bool ShadowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
59 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
61 const ShadowPrimitive2D& rCompare = static_cast<const ShadowPrimitive2D&>(rPrimitive);
63 return (getShadowTransform() == rCompare.getShadowTransform()
64 && getShadowColor() == rCompare.getShadowColor()
65 && getShadowBlur() == rCompare.getShadowBlur());
68 return false;
71 // Helper to get the to-be-shadowed geometry completely embedded to
72 // a ModifiedColorPrimitive2D (change to ShadowColor) and TransformPrimitive2D
73 // (direction/offset/transformation of shadow). Since this is used pretty
74 // often, pack into a helper
75 void ShadowPrimitive2D::getFullyEmbeddedShadowPrimitives(Primitive2DContainer& rContainer) const
77 if (getChildren().empty())
78 return;
80 // create a modifiedColorPrimitive containing the shadow color and the content
81 const basegfx::BColorModifierSharedPtr aBColorModifier
82 = std::make_shared<basegfx::BColorModifier_replace>(getShadowColor());
83 const Primitive2DReference xRefA(
84 new ModifiedColorPrimitive2D(Primitive2DContainer(getChildren()), aBColorModifier));
85 Primitive2DContainer aSequenceB{ xRefA };
87 // build transformed primitiveVector with shadow offset and add to target
88 rContainer.visit(new TransformPrimitive2D(getShadowTransform(), std::move(aSequenceB)));
91 bool ShadowPrimitive2D::prepareValuesAndcheckValidity(
92 basegfx::B2DRange& rBlurRange, basegfx::B2DRange& rClippedRange,
93 basegfx::B2DVector& rDiscreteBlurSize, double& rfDiscreteBlurRadius,
94 const geometry::ViewInformation2D& rViewInformation) const
96 // no BlurRadius defined, done
97 if (getShadowBlur() <= 0.0)
98 return false;
100 // no geometry, done
101 if (getChildren().empty())
102 return false;
104 // no pixel target, done
105 if (rViewInformation.getObjectToViewTransformation().isIdentity())
106 return false;
108 // get fully embedded ShadowPrimitive
109 Primitive2DContainer aEmbedded;
110 getFullyEmbeddedShadowPrimitives(aEmbedded);
112 // get geometry range that defines area that needs to be pixelated
113 rBlurRange = aEmbedded.getB2DRange(rViewInformation);
115 // no range of geometry, done
116 if (rBlurRange.isEmpty())
117 return false;
119 // extend range by BlurRadius in all directions
120 rBlurRange.grow(getShadowBlur());
122 // initialize ClippedRange to full BlurRange -> all is visible
123 rClippedRange = rBlurRange;
125 // get Viewport and check if used. If empty, all is visible (see
126 // ViewInformation2D definition in viewinformation2d.hxx)
127 if (!rViewInformation.getViewport().isEmpty())
129 // if used, extend by BlurRadius to ensure needed parts are included
130 basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
131 aVisibleArea.grow(getShadowBlur());
133 // calculate ClippedRange
134 rClippedRange.intersect(aVisibleArea);
136 // if BlurRange is completely outside of VisibleArea, ClippedRange
137 // will be empty and we are done
138 if (rClippedRange.isEmpty())
139 return false;
142 // calculate discrete pixel size of BlurRange. If it's too small to visualize, we are done
143 rDiscreteBlurSize = rViewInformation.getObjectToViewTransformation() * rBlurRange.getRange();
144 if (ceil(rDiscreteBlurSize.getX()) < 2.0 || ceil(rDiscreteBlurSize.getY()) < 2.0)
145 return false;
147 // calculate discrete pixel size of BlurRadius. If it's too small to visualize, we are done
148 rfDiscreteBlurRadius = ceil(
149 (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getShadowBlur(), 0))
150 .getLength());
151 if (rfDiscreteBlurRadius < 1.0)
152 return false;
154 return true;
157 void ShadowPrimitive2D::create2DDecomposition(
158 Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
160 if (getShadowBlur() <= 0.0)
162 // Normal (non-blurred) shadow is already completely
163 // handled by get2DDecomposition and not buffered. It
164 // does not need to be since it's a simple embedding
165 // to a ModifiedColorPrimitive2D and TransformPrimitive2D
166 return;
169 // from here on we process a blurred shadow
170 basegfx::B2DRange aBlurRange;
171 basegfx::B2DRange aClippedRange;
172 basegfx::B2DVector aDiscreteBlurSize;
173 double fDiscreteBlurRadius(0.0);
175 // Check various validity details and calculate/prepare values. If false, we are done
176 if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
177 fDiscreteBlurRadius, rViewInformation))
178 return;
180 // Create embedding transformation from object to top-left zero-aligned
181 // target pixel geometry (discrete form of ClippedRange)
182 // First, move to top-left of BlurRange
183 const sal_uInt32 nDiscreteBlurWidth(ceil(aDiscreteBlurSize.getX()));
184 const sal_uInt32 nDiscreteBlurHeight(ceil(aDiscreteBlurSize.getY()));
185 basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
186 -aClippedRange.getMinX(), -aClippedRange.getMinY()));
187 // Second, scale to discrete bitmap size
188 // Even when using the offset from ClippedRange, we need to use the
189 // scaling from the full representation, thus from BlurRange
190 aEmbedding.scale(nDiscreteBlurWidth / aBlurRange.getWidth(),
191 nDiscreteBlurHeight / aBlurRange.getHeight());
193 // Get fully embedded ShadowPrimitives. This will also embed to
194 // ModifiedColorPrimitive2D (what is not urgently needed) to create
195 // the alpha channel, but a paint with all colors set to a single
196 // one (like shadowColor here) is often less expensive due to possible
197 // simplifications painting the primitives (e.g. gradient)
198 Primitive2DContainer aEmbedded;
199 getFullyEmbeddedShadowPrimitives(aEmbedded);
201 // Embed content graphics to TransformPrimitive2D
202 const primitive2d::Primitive2DReference xEmbedRef(
203 new primitive2d::TransformPrimitive2D(aEmbedding, std::move(aEmbedded)));
204 primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
206 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
207 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
208 // limit due to this is Blurred Shadow functionality and will look good with bitmap
209 // scaling anyways. The value of 250.000 square pixels below maybe adapted as needed.
210 const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
211 * aClippedRange.getRange());
212 const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
213 const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
214 const geometry::ViewInformation2D aViewInformation2D;
215 const sal_uInt32 nMaximumQuadraticPixels(250000);
217 // I have now added a helper that just creates the mask without having
218 // to render the content, use it, it's faster
219 const AlphaMask aAlpha(::drawinglayer::createAlphaMask(
220 std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
221 nMaximumQuadraticPixels));
223 // if we have no shadow, we are done
224 if (aAlpha.IsEmpty())
225 return;
227 const Size aBitmapExSizePixel(aAlpha.GetSizePixel());
228 if (!(aBitmapExSizePixel.Width() > 0 && aBitmapExSizePixel.Height() > 0))
229 return;
231 // We may have to take a corrective scaling into account when the
232 // MaximumQuadraticPixel limit was used/triggered
233 double fScale(1.0);
235 if (static_cast<sal_uInt32>(aBitmapExSizePixel.Width()) != nDiscreteClippedWidth
236 || static_cast<sal_uInt32>(aBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
238 // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
239 // so adapt numerically to a single scale value, they are integer rounded values
240 const double fScaleX(static_cast<double>(aBitmapExSizePixel.Width())
241 / static_cast<double>(nDiscreteClippedWidth));
242 const double fScaleY(static_cast<double>(aBitmapExSizePixel.Height())
243 / static_cast<double>(nDiscreteClippedHeight));
245 fScale = (fScaleX + fScaleY) * 0.5;
248 // Use the Alpha as base to blur and apply the effect
249 const AlphaMask mask(drawinglayer::primitive2d::ProcessAndBlurAlphaMask(
250 aAlpha, 0, fDiscreteBlurRadius * fScale, 0, false));
252 // The end result is the bitmap filled with blur color and blurred 8-bit alpha mask
253 Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP);
254 bmp.Erase(Color(getShadowColor()));
255 BitmapEx result(bmp, mask);
257 #ifdef DBG_UTIL
258 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
259 if (bDoSaveForVisualControl)
261 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
262 static const OUString sDumpPath(
263 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
264 if (!sDumpPath.isEmpty())
266 SvFileStream aNew(sDumpPath + "test_shadowblur.png",
267 StreamMode::WRITE | StreamMode::TRUNC);
268 vcl::PngImageWriter aPNGWriter(aNew);
269 aPNGWriter.write(result);
272 #endif
274 // Independent from discrete sizes of blur alpha creation, always
275 // map and project blur result to geometry range extended by blur
276 // radius, but to the eventually clipped instance (ClippedRange)
277 const primitive2d::Primitive2DReference xEmbedRefBitmap(
278 new BitmapPrimitive2D(result, basegfx::utils::createScaleTranslateB2DHomMatrix(
279 aClippedRange.getWidth(), aClippedRange.getHeight(),
280 aClippedRange.getMinX(), aClippedRange.getMinY())));
282 rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
285 void ShadowPrimitive2D::get2DDecomposition(
286 Primitive2DDecompositionVisitor& rVisitor,
287 const geometry::ViewInformation2D& rViewInformation) const
289 if (getShadowBlur() <= 0.0)
291 // normal (non-blurred) shadow
292 if (getChildren().empty())
293 return;
295 // get fully embedded ShadowPrimitives
296 Primitive2DContainer aEmbedded;
297 getFullyEmbeddedShadowPrimitives(aEmbedded);
299 rVisitor.visit(aEmbedded);
300 return;
303 // here we have a blurred shadow, check conditions of last
304 // buffered decompose and decide re-use or re-create by using
305 // setBuffered2DDecomposition to reset local buffered version
306 basegfx::B2DRange aBlurRange;
307 basegfx::B2DRange aClippedRange;
308 basegfx::B2DVector aDiscreteBlurSize;
309 double fDiscreteBlurRadius(0.0);
311 // Check various validity details and calculate/prepare values. If false, we are done
312 if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
313 fDiscreteBlurRadius, rViewInformation))
314 return;
316 if (!getBuffered2DDecomposition().empty())
318 // First check is to detect if the last created decompose is capable
319 // to represent the now requested visualization (see similar
320 // implementation at GlowPrimitive2D).
321 if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
323 basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
325 if (!rViewInformation.getObjectToViewTransformation().isIdentity())
327 // Grow by view-dependent size of 1/2 pixel
328 const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
329 * basegfx::B2DVector(0.5, 0))
330 .getLength());
331 aLastClippedRangeAndHairline.grow(fHalfPixel);
334 if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
336 // Conditions of last local decomposition have changed, delete
337 const_cast<ShadowPrimitive2D*>(this)->setBuffered2DDecomposition(
338 Primitive2DContainer());
343 if (!getBuffered2DDecomposition().empty())
345 // Second check is to react on changes of the DiscreteSoftRadius when
346 // zooming in/out (see similar implementation at ShadowPrimitive2D).
347 bool bFree(mfLastDiscreteBlurRadius <= 0.0 || fDiscreteBlurRadius <= 0.0);
349 if (!bFree)
351 const double fDiff(fabs(mfLastDiscreteBlurRadius - fDiscreteBlurRadius));
352 const double fLen(fabs(mfLastDiscreteBlurRadius) + fabs(fDiscreteBlurRadius));
353 const double fRelativeChange(fDiff / fLen);
355 // Use lower fixed values here to change more often, higher to change less often.
356 // Value is in the range of ]0.0 .. 1.0]
357 bFree = fRelativeChange >= 0.15;
360 if (bFree)
362 // Conditions of last local decomposition have changed, delete
363 const_cast<ShadowPrimitive2D*>(this)->setBuffered2DDecomposition(
364 Primitive2DContainer());
368 if (getBuffered2DDecomposition().empty())
370 // refresh last used DiscreteBlurRadius and ClippedRange to new remembered values
371 const_cast<ShadowPrimitive2D*>(this)->mfLastDiscreteBlurRadius = fDiscreteBlurRadius;
372 const_cast<ShadowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
375 // call parent, that will check for empty, call create2DDecomposition and
376 // set as decomposition
377 BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
380 basegfx::B2DRange
381 ShadowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
383 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
384 // use the decompose - what works, but is not needed here.
385 // We know the to-be-visualized geometry and the radius it needs to be extended,
386 // so simply calculate the exact needed range.
387 basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
389 if (getShadowBlur() > 0.0)
391 // blurred shadow, that extends the geometry
392 aRetval.grow(getShadowBlur());
395 aRetval.transform(getShadowTransform());
396 return aRetval;
399 // provide unique ID
400 sal_uInt32 ShadowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_SHADOWPRIMITIVE2D; }
402 } // end of namespace
404 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */