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/glowprimitive2d.hxx>
21 #include <drawinglayer/primitive2d/transformprimitive2d.hxx>
22 #include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
23 #include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
24 #include <basegfx/matrix/b2dhommatrixtools.hxx>
25 #include <toolkit/helper/vclunohelper.hxx>
26 #include <drawinglayer/converters.hxx>
27 #include "GlowSoftEgdeShadowTools.hxx"
30 #include <tools/stream.hxx>
31 #include <vcl/filter/PngImageWriter.hxx>
34 using namespace com::sun::star
;
36 namespace drawinglayer::primitive2d
38 GlowPrimitive2D::GlowPrimitive2D(const Color
& rGlowColor
, double fRadius
,
39 Primitive2DContainer
&& rChildren
)
40 : BufferedDecompositionGroupPrimitive2D(std::move(rChildren
))
41 , maGlowColor(rGlowColor
)
42 , mfGlowRadius(fRadius
)
43 , mfLastDiscreteGlowRadius(0.0)
44 , maLastClippedRange()
48 bool GlowPrimitive2D::operator==(const BasePrimitive2D
& rPrimitive
) const
50 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive
))
52 const GlowPrimitive2D
& rCompare
= static_cast<const GlowPrimitive2D
&>(rPrimitive
);
54 return (getGlowRadius() == rCompare
.getGlowRadius()
55 && getGlowColor() == rCompare
.getGlowColor());
61 bool GlowPrimitive2D::prepareValuesAndcheckValidity(
62 basegfx::B2DRange
& rGlowRange
, basegfx::B2DRange
& rClippedRange
,
63 basegfx::B2DVector
& rDiscreteGlowSize
, double& rfDiscreteGlowRadius
,
64 const geometry::ViewInformation2D
& rViewInformation
) const
66 // no GlowRadius defined, done
67 if (getGlowRadius() <= 0.0)
71 if (getChildren().empty())
74 // no pixel target, done
75 if (rViewInformation
.getObjectToViewTransformation().isIdentity())
78 // get geometry range that defines area that needs to be pixelated
79 rGlowRange
= getChildren().getB2DRange(rViewInformation
);
81 // no range of geometry, done
82 if (rGlowRange
.isEmpty())
85 // extend range by GlowRadius in all directions
86 rGlowRange
.grow(getGlowRadius());
88 // initialize ClippedRange to full GlowRange -> all is visible
89 rClippedRange
= rGlowRange
;
91 // get Viewport and check if used. If empty, all is visible (see
92 // ViewInformation2D definition in viewinformation2d.hxx)
93 if (!rViewInformation
.getViewport().isEmpty())
95 // if used, extend by GlowRadius to ensure needed parts are included
96 basegfx::B2DRange
aVisibleArea(rViewInformation
.getViewport());
97 aVisibleArea
.grow(getGlowRadius());
99 // calculate ClippedRange
100 rClippedRange
.intersect(aVisibleArea
);
102 // if GlowRange is completely outside of VisibleArea, ClippedRange
103 // will be empty and we are done
104 if (rClippedRange
.isEmpty())
108 // calculate discrete pixel size of GlowRange. If it's too small to visualize, we are done
109 rDiscreteGlowSize
= rViewInformation
.getObjectToViewTransformation() * rGlowRange
.getRange();
110 if (ceil(rDiscreteGlowSize
.getX()) < 2.0 || ceil(rDiscreteGlowSize
.getY()) < 2.0)
113 // calculate discrete pixel size of GlowRadius. If it's too small to visualize, we are done
114 rfDiscreteGlowRadius
= ceil(
115 (rViewInformation
.getObjectToViewTransformation() * basegfx::B2DVector(getGlowRadius(), 0))
117 if (rfDiscreteGlowRadius
< 1.0)
123 void GlowPrimitive2D::create2DDecomposition(
124 Primitive2DContainer
& rContainer
, const geometry::ViewInformation2D
& rViewInformation
) const
126 basegfx::B2DRange aGlowRange
;
127 basegfx::B2DRange aClippedRange
;
128 basegfx::B2DVector aDiscreteGlowSize
;
129 double fDiscreteGlowRadius(0.0);
131 // Check various validity details and calculate/prepare values. If false, we are done
132 if (!prepareValuesAndcheckValidity(aGlowRange
, aClippedRange
, aDiscreteGlowSize
,
133 fDiscreteGlowRadius
, rViewInformation
))
136 // Create embedding transformation from object to top-left zero-aligned
137 // target pixel geometry (discrete form of ClippedRange)
138 // First, move to top-left of GlowRange
139 const sal_uInt32
nDiscreteGlowWidth(ceil(aDiscreteGlowSize
.getX()));
140 const sal_uInt32
nDiscreteGlowHeight(ceil(aDiscreteGlowSize
.getY()));
141 basegfx::B2DHomMatrix
aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
142 -aClippedRange
.getMinX(), -aClippedRange
.getMinY()));
143 // Second, scale to discrete bitmap size
144 // Even when using the offset from ClippedRange, we need to use the
145 // scaling from the full representation, thus from GlowRange
146 aEmbedding
.scale(nDiscreteGlowWidth
/ aGlowRange
.getWidth(),
147 nDiscreteGlowHeight
/ aGlowRange
.getHeight());
149 // Embed content graphics to TransformPrimitive2D
150 const primitive2d::Primitive2DReference
xEmbedRef(
151 new primitive2d::TransformPrimitive2D(aEmbedding
, Primitive2DContainer(getChildren())));
152 primitive2d::Primitive2DContainer xEmbedSeq
{ xEmbedRef
};
154 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
155 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
156 // limit due to this is glow functionality and will look good with bitmap scaling
157 // anyways. The value of 250.000 square pixels below maybe adapted as needed.
158 const basegfx::B2DVector
aDiscreteClippedSize(rViewInformation
.getObjectToViewTransformation()
159 * aClippedRange
.getRange());
160 const sal_uInt32
nDiscreteClippedWidth(ceil(aDiscreteClippedSize
.getX()));
161 const sal_uInt32
nDiscreteClippedHeight(ceil(aDiscreteClippedSize
.getY()));
162 const geometry::ViewInformation2D aViewInformation2D
;
163 const sal_uInt32
nMaximumQuadraticPixels(250000);
165 // I have now added a helper that just creates the mask without having
166 // to render the content, use it, it's faster
167 const AlphaMask
aAlpha(::drawinglayer::createAlphaMask(
168 std::move(xEmbedSeq
), aViewInformation2D
, nDiscreteClippedWidth
, nDiscreteClippedHeight
,
169 nMaximumQuadraticPixels
));
171 if (!aAlpha
.IsEmpty())
173 const Size
& rBitmapExSizePixel(aAlpha
.GetSizePixel());
175 if (rBitmapExSizePixel
.Width() > 0 && rBitmapExSizePixel
.Height() > 0)
177 // We may have to take a corrective scaling into account when the
178 // MaximumQuadraticPixel limit was used/triggered
181 if (static_cast<sal_uInt32
>(rBitmapExSizePixel
.Width()) != nDiscreteClippedWidth
182 || static_cast<sal_uInt32
>(rBitmapExSizePixel
.Height()) != nDiscreteClippedHeight
)
184 // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
185 // so adapt numerically to a single scale value, they are integer rounded values
186 const double fScaleX(static_cast<double>(rBitmapExSizePixel
.Width())
187 / static_cast<double>(nDiscreteClippedWidth
));
188 const double fScaleY(static_cast<double>(rBitmapExSizePixel
.Height())
189 / static_cast<double>(nDiscreteClippedHeight
));
191 fScale
= (fScaleX
+ fScaleY
) * 0.5;
194 // fDiscreteGlowRadius is the size of the halo from each side of the object. The halo is the
195 // border of glow color that fades from glow transparency level to fully transparent
196 // When blurring a sharp boundary (our case), it gets 50% of original intensity, and
197 // fades to both sides by the blur radius; thus blur radius is half of glow radius.
198 // Consider glow transparency (initial transparency near the object edge)
199 const AlphaMask
mask(ProcessAndBlurAlphaMask(aAlpha
, fDiscreteGlowRadius
* fScale
/ 2.0,
200 fDiscreteGlowRadius
* fScale
/ 2.0,
201 255 - getGlowColor().GetAlpha()));
203 // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
204 Bitmap
bmp(aAlpha
.GetSizePixel(), vcl::PixelFormat::N24_BPP
);
205 bmp
.Erase(getGlowColor());
206 BitmapEx
result(bmp
, mask
);
209 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
210 if (bDoSaveForVisualControl
)
212 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
213 static const OUString
sDumpPath(
214 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
215 if (!sDumpPath
.isEmpty())
217 SvFileStream
aNew(sDumpPath
+ "test_glow.png",
218 StreamMode::WRITE
| StreamMode::TRUNC
);
219 vcl::PngImageWriter
aPNGWriter(aNew
);
220 aPNGWriter
.write(result
);
225 // Independent from discrete sizes of glow alpha creation, always
226 // map and project glow result to geometry range extended by glow
227 // radius, but to the eventually clipped instance (ClippedRange)
228 const primitive2d::Primitive2DReference
xEmbedRefBitmap(new BitmapPrimitive2D(
229 result
, basegfx::utils::createScaleTranslateB2DHomMatrix(
230 aClippedRange
.getWidth(), aClippedRange
.getHeight(),
231 aClippedRange
.getMinX(), aClippedRange
.getMinY())));
233 rContainer
= primitive2d::Primitive2DContainer
{ xEmbedRefBitmap
};
238 // Using tooling class BufferedDecompositionGroupPrimitive2D now, so
239 // no more need to locally do the buffered get2DDecomposition here,
240 // see BufferedDecompositionGroupPrimitive2D::get2DDecomposition
241 void GlowPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor
& rVisitor
,
242 const geometry::ViewInformation2D
& rViewInformation
) const
244 basegfx::B2DRange aGlowRange
;
245 basegfx::B2DRange aClippedRange
;
246 basegfx::B2DVector aDiscreteGlowSize
;
247 double fDiscreteGlowRadius(0.0);
249 // Check various validity details and calculate/prepare values. If false, we are done
250 if (!prepareValuesAndcheckValidity(aGlowRange
, aClippedRange
, aDiscreteGlowSize
,
251 fDiscreteGlowRadius
, rViewInformation
))
254 if (!getBuffered2DDecomposition().empty())
256 // First check is to detect if the last created decompose is capable
257 // to represent the now requested visualization.
258 // ClippedRange is the needed visualizationArea for the current glow
259 // effect, LastClippedRange is the one from the existing/last rendering.
260 // Check if last created area is sufficient and can be re-used
261 if (!maLastClippedRange
.isEmpty() && !maLastClippedRange
.isInside(aClippedRange
))
263 // To avoid unnecessary invalidations due to being *very* correct
264 // with HairLines (which are view-dependent and thus change the
265 // result(s) here slightly when changing zoom), add a slight unsharp
266 // component if we have a ViewTransform. The derivation is inside
267 // the range of half a pixel (due to one pixel hairline)
268 basegfx::B2DRange
aLastClippedRangeAndHairline(maLastClippedRange
);
270 if (!rViewInformation
.getObjectToViewTransformation().isIdentity())
272 // Grow by view-dependent size of 1/2 pixel
273 const double fHalfPixel((rViewInformation
.getInverseObjectToViewTransformation()
274 * basegfx::B2DVector(0.5, 0))
276 aLastClippedRangeAndHairline
.grow(fHalfPixel
);
279 if (!aLastClippedRangeAndHairline
.isInside(aClippedRange
))
281 // Conditions of last local decomposition have changed, delete
282 const_cast<GlowPrimitive2D
*>(this)->setBuffered2DDecomposition(
283 Primitive2DContainer());
288 if (!getBuffered2DDecomposition().empty())
290 // Second check is to react on changes of the DiscreteGlowRadius when
292 // Use the known last and current DiscreteGlowRadius to decide
293 // if the visualization can be re-used. Be a little 'creative' here
294 // and make it dependent on a *relative* change - it is not necessary
295 // to re-create everytime if the exact value is missed since zooming
296 // pixel-based glow effect is pretty good due to it's smooth nature
297 bool bFree(mfLastDiscreteGlowRadius
<= 0.0 || fDiscreteGlowRadius
<= 0.0);
301 const double fDiff(fabs(mfLastDiscreteGlowRadius
- fDiscreteGlowRadius
));
302 const double fLen(fabs(mfLastDiscreteGlowRadius
) + fabs(fDiscreteGlowRadius
));
303 const double fRelativeChange(fDiff
/ fLen
);
305 // Use lower fixed values here to change more often, higher to change less often.
306 // Value is in the range of ]0.0 .. 1.0]
307 bFree
= fRelativeChange
>= 0.15;
312 // Conditions of last local decomposition have changed, delete
313 const_cast<GlowPrimitive2D
*>(this)->setBuffered2DDecomposition(Primitive2DContainer());
317 if (getBuffered2DDecomposition().empty())
319 // refresh last used DiscreteGlowRadius and ClippedRange to new remembered values
320 const_cast<GlowPrimitive2D
*>(this)->mfLastDiscreteGlowRadius
= fDiscreteGlowRadius
;
321 const_cast<GlowPrimitive2D
*>(this)->maLastClippedRange
= aClippedRange
;
324 // call parent, that will check for empty, call create2DDecomposition and
325 // set as decomposition
326 BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor
, rViewInformation
);
330 GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D
& rViewInformation
) const
332 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
333 // use the decompose - what works, but is not needed here.
334 // We know the to-be-visualized geometry and the radius it needs to be extended,
335 // so simply calculate the exact needed range.
336 basegfx::B2DRange
aRetval(getChildren().getB2DRange(rViewInformation
));
338 // We need additional space for the glow from all sides
339 aRetval
.grow(getGlowRadius());
345 sal_uInt32
GlowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_GLOWPRIMITIVE2D
; }
347 } // end of namespace
349 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */