LibreOffice Module drawinglayer (master) 1
shadowprimitive2d.cxx
Go to the documentation of this file.
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
30
31#ifdef DBG_UTIL
32#include <tools/stream.hxx>
34#endif
35
36#include <memory>
37#include <utility>
38
39using namespace com::sun::star;
40
42{
44 const basegfx::BColor& rShadowColor, double fShadowBlur,
45 Primitive2DContainer&& aChildren)
47 , maShadowTransform(std::move(aShadowTransform))
48 , maShadowColor(rShadowColor)
49 , mfShadowBlur(fShadowBlur)
50 , mfLastDiscreteBlurRadius(0.0)
51 , maLastClippedRange()
52{
53}
54
56{
57 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
58 {
59 const ShadowPrimitive2D& rCompare = static_cast<const ShadowPrimitive2D&>(rPrimitive);
60
61 return (getShadowTransform() == rCompare.getShadowTransform()
62 && getShadowColor() == rCompare.getShadowColor()
63 && getShadowBlur() == rCompare.getShadowBlur());
64 }
65
66 return false;
67}
68
69// Helper to get the to-be-shadowed geometry completely embedded to
70// a ModifiedColorPrimitive2D (change to ShadowColor) and TransformPrimitive2D
71// (direction/offset/transformation of shadow). Since this is used pretty
72// often, pack into a helper
74{
75 if (getChildren().empty())
76 return;
77
78 // create a modifiedColorPrimitive containing the shadow color and the content
79 const basegfx::BColorModifierSharedPtr aBColorModifier
80 = std::make_shared<basegfx::BColorModifier_replace>(getShadowColor());
81 const Primitive2DReference xRefA(
83 Primitive2DContainer aSequenceB{ xRefA };
84
85 // build transformed primitiveVector with shadow offset and add to target
86 rContainer.visit(new TransformPrimitive2D(getShadowTransform(), std::move(aSequenceB)));
87}
88
90 basegfx::B2DRange& rBlurRange, basegfx::B2DRange& rClippedRange,
91 basegfx::B2DVector& rDiscreteBlurSize, double& rfDiscreteBlurRadius,
92 const geometry::ViewInformation2D& rViewInformation) const
93{
94 // no BlurRadius defined, done
95 if (getShadowBlur() <= 0.0)
96 return false;
97
98 // no geometry, done
99 if (getChildren().empty())
100 return false;
101
102 // no pixel target, done
103 if (rViewInformation.getObjectToViewTransformation().isIdentity())
104 return false;
105
106 // get fully embedded ShadowPrimitive
107 Primitive2DContainer aEmbedded;
109
110 // get geometry range that defines area that needs to be pixelated
111 rBlurRange = aEmbedded.getB2DRange(rViewInformation);
112
113 // no range of geometry, done
114 if (rBlurRange.isEmpty())
115 return false;
116
117 // extend range by BlurRadius in all directions
118 rBlurRange.grow(getShadowBlur());
119
120 // initialize ClippedRange to full BlurRange -> all is visible
121 rClippedRange = rBlurRange;
122
123 // get Viewport and check if used. If empty, all is visible (see
124 // ViewInformation2D definition in viewinformation2d.hxx)
125 if (!rViewInformation.getViewport().isEmpty())
126 {
127 // if used, extend by BlurRadius to ensure needed parts are included
128 basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
129 aVisibleArea.grow(getShadowBlur());
130
131 // calculate ClippedRange
132 rClippedRange.intersect(aVisibleArea);
133
134 // if BlurRange is completely outside of VisibleArea, ClippedRange
135 // will be empty and we are done
136 if (rClippedRange.isEmpty())
137 return false;
138 }
139
140 // calculate discrete pixel size of BlurRange. If it's too small to visualize, we are done
141 rDiscreteBlurSize = rViewInformation.getObjectToViewTransformation() * rBlurRange.getRange();
142 if (ceil(rDiscreteBlurSize.getX()) < 2.0 || ceil(rDiscreteBlurSize.getY()) < 2.0)
143 return false;
144
145 // calculate discrete pixel size of BlurRadius. If it's too small to visualize, we are done
146 rfDiscreteBlurRadius = ceil(
148 .getLength());
149 if (rfDiscreteBlurRadius < 1.0)
150 return false;
151
152 return true;
153}
154
156 Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
157{
158 if (getShadowBlur() <= 0.0)
159 {
160 // Normal (non-blurred) shadow is already completely
161 // handled by get2DDecomposition and not buffered. It
162 // does not need to be since it's a simple embedding
163 // to a ModifiedColorPrimitive2D and TransformPrimitive2D
164 return;
165 }
166
167 // from here on we process a blurred shadow
168 basegfx::B2DRange aBlurRange;
169 basegfx::B2DRange aClippedRange;
170 basegfx::B2DVector aDiscreteBlurSize;
171 double fDiscreteBlurRadius(0.0);
172
173 // Check various validity details and calculate/prepare values. If false, we are done
174 if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
175 fDiscreteBlurRadius, rViewInformation))
176 return;
177
178 // Create embedding transformation from object to top-left zero-aligned
179 // target pixel geometry (discrete form of ClippedRange)
180 // First, move to top-left of BlurRange
181 const sal_uInt32 nDiscreteBlurWidth(ceil(aDiscreteBlurSize.getX()));
182 const sal_uInt32 nDiscreteBlurHeight(ceil(aDiscreteBlurSize.getY()));
184 -aClippedRange.getMinX(), -aClippedRange.getMinY()));
185 // Second, scale to discrete bitmap size
186 // Even when using the offset from ClippedRange, we need to use the
187 // scaling from the full representation, thus from BlurRange
188 aEmbedding.scale(nDiscreteBlurWidth / aBlurRange.getWidth(),
189 nDiscreteBlurHeight / aBlurRange.getHeight());
190
191 // Get fully embedded ShadowPrimitives. This will also embed to
192 // ModifiedColorPrimitive2D (what is not urgently needed) to create
193 // the alpha channel, but a paint with all colors set to a single
194 // one (like shadowColor here) is often less expensive due to possible
195 // simplifications painting the primitives (e.g. gradient)
196 Primitive2DContainer aEmbedded;
198
199 // Embed content graphics to TransformPrimitive2D
200 const primitive2d::Primitive2DReference xEmbedRef(
201 new primitive2d::TransformPrimitive2D(aEmbedding, std::move(aEmbedded)));
202 primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
203
204 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
205 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
206 // limit due to this is Blurred Shadow functionality and will look good with bitmap
207 // scaling anyways. The value of 250.000 square pixels below maybe adapted as needed.
208 const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
209 * aClippedRange.getRange());
210 const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
211 const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
212 const geometry::ViewInformation2D aViewInformation2D;
213 const sal_uInt32 nMaximumQuadraticPixels(250000);
214
215 // I have now added a helper that just creates the mask without having
216 // to render the content, use it, it's faster
218 std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
219 nMaximumQuadraticPixels));
220
221 // if we have no shadow, we are done
222 if (aAlpha.IsEmpty())
223 return;
224
225 const Size& rBitmapExSizePixel(aAlpha.GetSizePixel());
226 if (!(rBitmapExSizePixel.Width() > 0 && rBitmapExSizePixel.Height() > 0))
227 return;
228
229 // We may have to take a corrective scaling into account when the
230 // MaximumQuadraticPixel limit was used/triggered
231 double fScale(1.0);
232
233 if (static_cast<sal_uInt32>(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth
234 || static_cast<sal_uInt32>(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
235 {
236 // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
237 // so adapt numerically to a single scale value, they are integer rounded values
238 const double fScaleX(static_cast<double>(rBitmapExSizePixel.Width())
239 / static_cast<double>(nDiscreteClippedWidth));
240 const double fScaleY(static_cast<double>(rBitmapExSizePixel.Height())
241 / static_cast<double>(nDiscreteClippedHeight));
242
243 fScale = (fScaleX + fScaleY) * 0.5;
244 }
245
246 // Use the Alpha as base to blur and apply the effect
248 aAlpha, 0, fDiscreteBlurRadius * fScale, 0, false));
249
250 // The end result is the bitmap filled with blur color and blurred 8-bit alpha mask
252 bmp.Erase(Color(getShadowColor()));
253 BitmapEx result(bmp, mask);
254
255#ifdef DBG_UTIL
256 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
257 if (bDoSaveForVisualControl)
258 {
259 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
260 static const OUString sDumpPath(
261 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
262 if (!sDumpPath.isEmpty())
263 {
264 SvFileStream aNew(sDumpPath + "test_shadowblur.png",
265 StreamMode::WRITE | StreamMode::TRUNC);
266 vcl::PngImageWriter aPNGWriter(aNew);
267 aPNGWriter.write(result);
268 }
269 }
270#endif
271
272 // Independent from discrete sizes of blur alpha creation, always
273 // map and project blur result to geometry range extended by blur
274 // radius, but to the eventually clipped instance (ClippedRange)
275 const primitive2d::Primitive2DReference xEmbedRefBitmap(
277 aClippedRange.getWidth(), aClippedRange.getHeight(),
278 aClippedRange.getMinX(), aClippedRange.getMinY())));
279
280 rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
281}
282
285 const geometry::ViewInformation2D& rViewInformation) const
286{
287 if (getShadowBlur() <= 0.0)
288 {
289 // normal (non-blurred) shadow
290 if (getChildren().empty())
291 return;
292
293 // get fully embedded ShadowPrimitives
294 Primitive2DContainer aEmbedded;
296
297 rVisitor.visit(aEmbedded);
298 return;
299 }
300
301 // here we have a blurred shadow, check conditions of last
302 // buffered decompose and decide re-use or re-create by using
303 // setBuffered2DDecomposition to reset local buffered version
304 basegfx::B2DRange aBlurRange;
305 basegfx::B2DRange aClippedRange;
306 basegfx::B2DVector aDiscreteBlurSize;
307 double fDiscreteBlurRadius(0.0);
308
309 // Check various validity details and calculate/prepare values. If false, we are done
310 if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
311 fDiscreteBlurRadius, rViewInformation))
312 return;
313
314 if (!getBuffered2DDecomposition().empty())
315 {
316 // First check is to detect if the last created decompose is capable
317 // to represent the now requested visualization (see similar
318 // implementation at GlowPrimitive2D).
319 if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
320 {
321 basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
322
323 if (!rViewInformation.getObjectToViewTransformation().isIdentity())
324 {
325 // Grow by view-dependent size of 1/2 pixel
326 const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
327 * basegfx::B2DVector(0.5, 0))
328 .getLength());
329 aLastClippedRangeAndHairline.grow(fHalfPixel);
330 }
331
332 if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
333 {
334 // Conditions of last local decomposition have changed, delete
337 }
338 }
339 }
340
341 if (!getBuffered2DDecomposition().empty())
342 {
343 // Second check is to react on changes of the DiscreteSoftRadius when
344 // zooming in/out (see similar implementation at ShadowPrimitive2D).
345 bool bFree(mfLastDiscreteBlurRadius <= 0.0 || fDiscreteBlurRadius <= 0.0);
346
347 if (!bFree)
348 {
349 const double fDiff(fabs(mfLastDiscreteBlurRadius - fDiscreteBlurRadius));
350 const double fLen(fabs(mfLastDiscreteBlurRadius) + fabs(fDiscreteBlurRadius));
351 const double fRelativeChange(fDiff / fLen);
352
353 // Use lower fixed values here to change more often, higher to change less often.
354 // Value is in the range of ]0.0 .. 1.0]
355 bFree = fRelativeChange >= 0.15;
356 }
357
358 if (bFree)
359 {
360 // Conditions of last local decomposition have changed, delete
363 }
364 }
365
366 if (getBuffered2DDecomposition().empty())
367 {
368 // refresh last used DiscreteBlurRadius and ClippedRange to new remembered values
369 const_cast<ShadowPrimitive2D*>(this)->mfLastDiscreteBlurRadius = fDiscreteBlurRadius;
370 const_cast<ShadowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
371 }
372
373 // call parent, that will check for empty, call create2DDecomposition and
374 // set as decomposition
376}
377
380{
381 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
382 // use the decompose - what works, but is not needed here.
383 // We know the to-be-visualized geometry and the radius it needs to be extended,
384 // so simply calculate the exact needed range.
385 basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
386
387 if (getShadowBlur() > 0.0)
388 {
389 // blurred shadow, that extends the geometry
390 aRetval.grow(getShadowBlur());
391 }
392
393 aRetval.transform(getShadowTransform());
394 return aRetval;
395}
396
397// provide unique ID
399
400} // end of namespace
401
402/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
bool IsEmpty() const
Size GetSizePixel() const
bool Erase(const Color &rFillColor)
constexpr tools::Long Height() const
constexpr tools::Long Width() const
void scale(double fX, double fY)
bool isIdentity() const
BASEGFX_DLLPUBLIC void transform(const B2DHomMatrix &rMatrix)
B2DVector getRange() const
void grow(TYPE fValue)
TYPE getWidth() const
TYPE getMinX() const
TYPE getMinY() const
void intersect(const Range2D &rRange)
bool isInside(const Tuple2D< TYPE > &rTuple) const
bool isEmpty() const
TYPE getHeight() const
TYPE getX() const
TYPE getY() const
const basegfx::B2DRange & getViewport() const
Empty viewport means everything is visible.
const basegfx::B2DHomMatrix & getInverseObjectToViewTransformation() const
const basegfx::B2DHomMatrix & getObjectToViewTransformation() const
On-demand prepared Object to View transformation and its inverse for convenience.
virtual void get2DDecomposition(Primitive2DDecompositionVisitor &rVisitor, const geometry::ViewInformation2D &rViewInformation) const override
identical to BufferedDecompositionPrimitive2D, see there please
const Primitive2DContainer & getBuffered2DDecomposition() const
identical to BufferedDecompositionPrimitive2D, see there please
const Primitive2DContainer & getChildren() const
data read access
virtual void visit(const Primitive2DReference &rSource) override
basegfx::B2DRange getB2DRange(const geometry::ViewInformation2D &aViewInformation) const
virtual void visit(const Primitive2DReference &)=0
virtual basegfx::B2DRange getB2DRange(const geometry::ViewInformation2D &rViewInformation) const override
get range
ShadowPrimitive2D(basegfx::B2DHomMatrix aShadowTransform, const basegfx::BColor &rShadowColor, double fShadowBlur, Primitive2DContainer &&aChildren)
constructor
virtual bool operator==(const BasePrimitive2D &rPrimitive) const override
compare operator
void getFullyEmbeddedShadowPrimitives(Primitive2DContainer &rContainer) const
helpers
const basegfx::BColor & getShadowColor() const
virtual void create2DDecomposition(Primitive2DContainer &rContainer, const geometry::ViewInformation2D &rViewInformation) const override
method which is to be used to implement the local decomposition of a 2D primitive.
virtual sal_uInt32 getPrimitive2DID() const override
provide unique ID
virtual void get2DDecomposition(Primitive2DDecompositionVisitor &rVisitor, const geometry::ViewInformation2D &rViewInformation) const override
create decomposition
const basegfx::B2DHomMatrix & getShadowTransform() const
data read access
double mfLastDiscreteBlurRadius
last used DiscreteBlurRadius and ClippedRange
bool prepareValuesAndcheckValidity(basegfx::B2DRange &rRange, basegfx::B2DRange &rClippedRange, basegfx::B2DVector &rDiscreteSize, double &rfDiscreteBlurRadius, const geometry::ViewInformation2D &rViewInformation) const
bool write(const BitmapEx &rBitmap)
#define PRIMITIVE2D_ID_SHADOWPRIMITIVE2D
double getLength(const B2DPolygon &rCandidate)
B2DHomMatrix createScaleTranslateB2DHomMatrix(double fScaleX, double fScaleY, double fTranslateX, double fTranslateY)
B2DHomMatrix createTranslateB2DHomMatrix(double fTranslateX, double fTranslateY)
std::shared_ptr< BColorModifier > BColorModifierSharedPtr
AlphaMask ProcessAndBlurAlphaMask(const Bitmap &rMask, double fErodeDilateRadius, double fBlurRadius, sal_uInt8 nTransparency, bool bConvertTo1Bit)
AlphaMask createAlphaMask(drawinglayer::primitive2d::Primitive2DContainer &&rSeq, const geometry::ViewInformation2D &rViewInformation2D, sal_uInt32 nDiscreteWidth, sal_uInt32 nDiscreteHeight, sal_uInt32 nMaxSquarePixels, bool bUseLuminance)
Definition: converters.cxx:139
const ::Color maShadowColor
Any result