1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
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"
32 #include <tools/stream.hxx>
33 #include <vcl/filter/PngImageWriter.hxx>
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()
55 bool ShadowPrimitive2D::operator==(const BasePrimitive2D
& rPrimitive
) const
57 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive
))
59 const ShadowPrimitive2D
& rCompare
= static_cast<const ShadowPrimitive2D
&>(rPrimitive
);
61 return (getShadowTransform() == rCompare
.getShadowTransform()
62 && getShadowColor() == rCompare
.getShadowColor()
63 && getShadowBlur() == rCompare
.getShadowBlur());
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
73 void ShadowPrimitive2D::getFullyEmbeddedShadowPrimitives(Primitive2DContainer
& rContainer
) const
75 if (getChildren().empty())
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(
82 new ModifiedColorPrimitive2D(Primitive2DContainer(getChildren()), aBColorModifier
));
83 Primitive2DContainer aSequenceB
{ xRefA
};
85 // build transformed primitiveVector with shadow offset and add to target
86 rContainer
.visit(new TransformPrimitive2D(getShadowTransform(), std::move(aSequenceB
)));
89 bool ShadowPrimitive2D::prepareValuesAndcheckValidity(
90 basegfx::B2DRange
& rBlurRange
, basegfx::B2DRange
& rClippedRange
,
91 basegfx::B2DVector
& rDiscreteBlurSize
, double& rfDiscreteBlurRadius
,
92 const geometry::ViewInformation2D
& rViewInformation
) const
94 // no BlurRadius defined, done
95 if (getShadowBlur() <= 0.0)
99 if (getChildren().empty())
102 // no pixel target, done
103 if (rViewInformation
.getObjectToViewTransformation().isIdentity())
106 // get fully embedded ShadowPrimitive
107 Primitive2DContainer aEmbedded
;
108 getFullyEmbeddedShadowPrimitives(aEmbedded
);
110 // get geometry range that defines area that needs to be pixelated
111 rBlurRange
= aEmbedded
.getB2DRange(rViewInformation
);
113 // no range of geometry, done
114 if (rBlurRange
.isEmpty())
117 // extend range by BlurRadius in all directions
118 rBlurRange
.grow(getShadowBlur());
120 // initialize ClippedRange to full BlurRange -> all is visible
121 rClippedRange
= rBlurRange
;
123 // get Viewport and check if used. If empty, all is visible (see
124 // ViewInformation2D definition in viewinformation2d.hxx)
125 if (!rViewInformation
.getViewport().isEmpty())
127 // if used, extend by BlurRadius to ensure needed parts are included
128 basegfx::B2DRange
aVisibleArea(rViewInformation
.getViewport());
129 aVisibleArea
.grow(getShadowBlur());
131 // calculate ClippedRange
132 rClippedRange
.intersect(aVisibleArea
);
134 // if BlurRange is completely outside of VisibleArea, ClippedRange
135 // will be empty and we are done
136 if (rClippedRange
.isEmpty())
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)
145 // calculate discrete pixel size of BlurRadius. If it's too small to visualize, we are done
146 rfDiscreteBlurRadius
= ceil(
147 (rViewInformation
.getObjectToViewTransformation() * basegfx::B2DVector(getShadowBlur(), 0))
149 if (rfDiscreteBlurRadius
< 1.0)
155 void ShadowPrimitive2D::create2DDecomposition(
156 Primitive2DContainer
& rContainer
, const geometry::ViewInformation2D
& rViewInformation
) const
158 if (getShadowBlur() <= 0.0)
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
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);
173 // Check various validity details and calculate/prepare values. If false, we are done
174 if (!prepareValuesAndcheckValidity(aBlurRange
, aClippedRange
, aDiscreteBlurSize
,
175 fDiscreteBlurRadius
, rViewInformation
))
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()));
183 basegfx::B2DHomMatrix
aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
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());
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
;
197 getFullyEmbeddedShadowPrimitives(aEmbedded
);
199 // Embed content graphics to TransformPrimitive2D
200 const primitive2d::Primitive2DReference
xEmbedRef(
201 new primitive2d::TransformPrimitive2D(aEmbedding
, std::move(aEmbedded
)));
202 primitive2d::Primitive2DContainer xEmbedSeq
{ xEmbedRef
};
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);
215 // I have now added a helper that just creates the mask without having
216 // to render the content, use it, it's faster
217 const AlphaMask
aAlpha(::drawinglayer::createAlphaMask(
218 std::move(xEmbedSeq
), aViewInformation2D
, nDiscreteClippedWidth
, nDiscreteClippedHeight
,
219 nMaximumQuadraticPixels
));
221 // if we have no shadow, we are done
222 if (aAlpha
.IsEmpty())
225 const Size
& rBitmapExSizePixel(aAlpha
.GetSizePixel());
226 if (!(rBitmapExSizePixel
.Width() > 0 && rBitmapExSizePixel
.Height() > 0))
229 // We may have to take a corrective scaling into account when the
230 // MaximumQuadraticPixel limit was used/triggered
233 if (static_cast<sal_uInt32
>(rBitmapExSizePixel
.Width()) != nDiscreteClippedWidth
234 || static_cast<sal_uInt32
>(rBitmapExSizePixel
.Height()) != nDiscreteClippedHeight
)
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
));
243 fScale
= (fScaleX
+ fScaleY
) * 0.5;
246 // Use the Alpha as base to blur and apply the effect
247 const AlphaMask
mask(drawinglayer::primitive2d::ProcessAndBlurAlphaMask(
248 aAlpha
, 0, fDiscreteBlurRadius
* fScale
, 0, false));
250 // The end result is the bitmap filled with blur color and blurred 8-bit alpha mask
251 Bitmap
bmp(aAlpha
.GetSizePixel(), vcl::PixelFormat::N24_BPP
);
252 bmp
.Erase(Color(getShadowColor()));
253 BitmapEx
result(bmp
, mask
);
256 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
257 if (bDoSaveForVisualControl
)
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())
264 SvFileStream
aNew(sDumpPath
+ "test_shadowblur.png",
265 StreamMode::WRITE
| StreamMode::TRUNC
);
266 vcl::PngImageWriter
aPNGWriter(aNew
);
267 aPNGWriter
.write(result
);
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(
276 new BitmapPrimitive2D(result
, basegfx::utils::createScaleTranslateB2DHomMatrix(
277 aClippedRange
.getWidth(), aClippedRange
.getHeight(),
278 aClippedRange
.getMinX(), aClippedRange
.getMinY())));
280 rContainer
= primitive2d::Primitive2DContainer
{ xEmbedRefBitmap
};
283 void ShadowPrimitive2D::get2DDecomposition(
284 Primitive2DDecompositionVisitor
& rVisitor
,
285 const geometry::ViewInformation2D
& rViewInformation
) const
287 if (getShadowBlur() <= 0.0)
289 // normal (non-blurred) shadow
290 if (getChildren().empty())
293 // get fully embedded ShadowPrimitives
294 Primitive2DContainer aEmbedded
;
295 getFullyEmbeddedShadowPrimitives(aEmbedded
);
297 rVisitor
.visit(aEmbedded
);
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);
309 // Check various validity details and calculate/prepare values. If false, we are done
310 if (!prepareValuesAndcheckValidity(aBlurRange
, aClippedRange
, aDiscreteBlurSize
,
311 fDiscreteBlurRadius
, rViewInformation
))
314 if (!getBuffered2DDecomposition().empty())
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
))
321 basegfx::B2DRange
aLastClippedRangeAndHairline(maLastClippedRange
);
323 if (!rViewInformation
.getObjectToViewTransformation().isIdentity())
325 // Grow by view-dependent size of 1/2 pixel
326 const double fHalfPixel((rViewInformation
.getInverseObjectToViewTransformation()
327 * basegfx::B2DVector(0.5, 0))
329 aLastClippedRangeAndHairline
.grow(fHalfPixel
);
332 if (!aLastClippedRangeAndHairline
.isInside(aClippedRange
))
334 // Conditions of last local decomposition have changed, delete
335 const_cast<ShadowPrimitive2D
*>(this)->setBuffered2DDecomposition(
336 Primitive2DContainer());
341 if (!getBuffered2DDecomposition().empty())
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);
349 const double fDiff(fabs(mfLastDiscreteBlurRadius
- fDiscreteBlurRadius
));
350 const double fLen(fabs(mfLastDiscreteBlurRadius
) + fabs(fDiscreteBlurRadius
));
351 const double fRelativeChange(fDiff
/ fLen
);
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;
360 // Conditions of last local decomposition have changed, delete
361 const_cast<ShadowPrimitive2D
*>(this)->setBuffered2DDecomposition(
362 Primitive2DContainer());
366 if (getBuffered2DDecomposition().empty())
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
;
373 // call parent, that will check for empty, call create2DDecomposition and
374 // set as decomposition
375 BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor
, rViewInformation
);
379 ShadowPrimitive2D::getB2DRange(const geometry::ViewInformation2D
& rViewInformation
) const
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
));
387 if (getShadowBlur() > 0.0)
389 // blurred shadow, that extends the geometry
390 aRetval
.grow(getShadowBlur());
393 aRetval
.transform(getShadowTransform());
398 sal_uInt32
ShadowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_SHADOWPRIMITIVE2D
; }
400 } // end of namespace
402 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */