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/drawinglayer_primitivetypes2d.hxx>
21 #include <drawinglayer/primitive2d/softedgeprimitive2d.hxx>
22 #include <drawinglayer/primitive2d/transformprimitive2d.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 namespace drawinglayer::primitive2d
36 SoftEdgePrimitive2D::SoftEdgePrimitive2D(double fRadius
, Primitive2DContainer
&& aChildren
)
37 : BufferedDecompositionGroupPrimitive2D(std::move(aChildren
))
39 , mfLastDiscreteSoftRadius(0.0)
40 , maLastClippedRange()
44 bool SoftEdgePrimitive2D::operator==(const BasePrimitive2D
& rPrimitive
) const
46 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive
))
48 auto& rCompare
= static_cast<const SoftEdgePrimitive2D
&>(rPrimitive
);
49 return getRadius() == rCompare
.getRadius();
55 bool SoftEdgePrimitive2D::prepareValuesAndcheckValidity(
56 basegfx::B2DRange
& rSoftRange
, basegfx::B2DRange
& rClippedRange
,
57 basegfx::B2DVector
& rDiscreteSoftSize
, double& rfDiscreteSoftRadius
,
58 const geometry::ViewInformation2D
& rViewInformation
) const
60 // no SoftRadius defined, done
61 if (getRadius() <= 0.0)
65 if (getChildren().empty())
68 // no pixel target, done
69 if (rViewInformation
.getObjectToViewTransformation().isIdentity())
72 // get geometry range that defines area that needs to be pixelated
73 rSoftRange
= getChildren().getB2DRange(rViewInformation
);
75 // no range of geometry, done
76 if (rSoftRange
.isEmpty())
79 // initialize ClippedRange to full SoftRange -> all is visible
80 rClippedRange
= rSoftRange
;
82 // get Viewport and check if used. If empty, all is visible (see
83 // ViewInformation2D definition in viewinformation2d.hxx)
84 if (!rViewInformation
.getViewport().isEmpty())
86 // if used, extend by SoftRadius to ensure needed parts are included
87 // that are not visible, but influence the visible parts
88 basegfx::B2DRange
aVisibleArea(rViewInformation
.getViewport());
89 aVisibleArea
.grow(getRadius() * 2);
91 // calculate ClippedRange
92 rClippedRange
.intersect(aVisibleArea
);
94 // if SoftRange is completely outside of VisibleArea, ClippedRange
95 // will be empty and we are done
96 if (rClippedRange
.isEmpty())
100 // calculate discrete pixel size of SoftRange. If it's too small to visualize, we are done
101 rDiscreteSoftSize
= rViewInformation
.getObjectToViewTransformation() * rSoftRange
.getRange();
102 if (ceil(rDiscreteSoftSize
.getX()) < 2.0 || ceil(rDiscreteSoftSize
.getY()) < 2.0)
105 // calculate discrete pixel size of SoftRadius. If it's too small to visualize, we are done
106 rfDiscreteSoftRadius
= ceil(
107 (rViewInformation
.getObjectToViewTransformation() * basegfx::B2DVector(getRadius(), 0))
109 if (rfDiscreteSoftRadius
< 1.0)
115 void SoftEdgePrimitive2D::create2DDecomposition(
116 Primitive2DContainer
& rContainer
, const geometry::ViewInformation2D
& rViewInformation
) const
118 // Use endless while-loop-and-break mechanism due to having multiple
119 // exit scenarios that all have to do the same thing when exiting
122 basegfx::B2DRange aSoftRange
;
123 basegfx::B2DRange aClippedRange
;
124 basegfx::B2DVector aDiscreteSoftSize
;
125 double fDiscreteSoftRadius(0.0);
127 // Check various validity details and calculate/prepare values. If false, we are done
128 if (!prepareValuesAndcheckValidity(aSoftRange
, aClippedRange
, aDiscreteSoftSize
,
129 fDiscreteSoftRadius
, rViewInformation
))
132 // Create embedding transformation from object to top-left zero-aligned
133 // target pixel geometry (discrete form of ClippedRange)
134 // First, move to top-left of SoftRange
135 const sal_uInt32
nDiscreteSoftWidth(ceil(aDiscreteSoftSize
.getX()));
136 const sal_uInt32
nDiscreteSoftHeight(ceil(aDiscreteSoftSize
.getY()));
137 basegfx::B2DHomMatrix
aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
138 -aClippedRange
.getMinX(), -aClippedRange
.getMinY()));
139 // Second, scale to discrete bitmap size
140 // Even when using the offset from ClippedRange, we need to use the
141 // scaling from the full representation, thus from SoftRange
142 aEmbedding
.scale(nDiscreteSoftWidth
/ aSoftRange
.getWidth(),
143 nDiscreteSoftHeight
/ aSoftRange
.getHeight());
145 // Embed content graphics to TransformPrimitive2D
146 const primitive2d::Primitive2DReference
xEmbedRef(
147 new primitive2d::TransformPrimitive2D(aEmbedding
, Primitive2DContainer(getChildren())));
148 primitive2d::Primitive2DContainer xEmbedSeq
{ xEmbedRef
};
150 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
151 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
152 // limit due to this is softEdge functionality and will look good with bitmap scaling
153 // anyways. The value of 250.000 square pixels below maybe adapted as needed.
154 const basegfx::B2DVector
aDiscreteClippedSize(
155 rViewInformation
.getObjectToViewTransformation() * aClippedRange
.getRange());
156 const sal_uInt32
nDiscreteClippedWidth(ceil(aDiscreteClippedSize
.getX()));
157 const sal_uInt32
nDiscreteClippedHeight(ceil(aDiscreteClippedSize
.getY()));
158 const geometry::ViewInformation2D aViewInformation2D
;
159 const sal_uInt32
nMaximumQuadraticPixels(250000);
160 const BitmapEx
aBitmapEx(::drawinglayer::convertToBitmapEx(
161 std::move(xEmbedSeq
), aViewInformation2D
, nDiscreteClippedWidth
, nDiscreteClippedHeight
,
162 nMaximumQuadraticPixels
));
164 if (aBitmapEx
.IsEmpty())
167 // Get BitmapEx and check size. If no content, we are done
168 const Size
& rBitmapExSizePixel(aBitmapEx
.GetSizePixel());
169 if (!(rBitmapExSizePixel
.Width() > 0 && rBitmapExSizePixel
.Height() > 0))
172 // We may have to take a corrective scaling into account when the
173 // MaximumQuadraticPixel limit was used/triggered
176 if (static_cast<sal_uInt32
>(rBitmapExSizePixel
.Width()) != nDiscreteClippedWidth
177 || static_cast<sal_uInt32
>(rBitmapExSizePixel
.Height()) != nDiscreteClippedHeight
)
179 // scale in X and Y should be the same (see fReduceFactor in convertToBitmapEx),
180 // so adapt numerically to a single scale value, they are integer rounded values
181 const double fScaleX(static_cast<double>(rBitmapExSizePixel
.Width())
182 / static_cast<double>(nDiscreteClippedWidth
));
183 const double fScaleY(static_cast<double>(rBitmapExSizePixel
.Height())
184 / static_cast<double>(nDiscreteClippedHeight
));
186 fScale
= (fScaleX
+ fScaleY
) * 0.5;
189 // Get the Alpha and use as base to blur and apply the effect
190 AlphaMask
aMask(aBitmapEx
.GetAlpha());
191 const AlphaMask
blurMask(drawinglayer::primitive2d::ProcessAndBlurAlphaMask(
192 aMask
, -fDiscreteSoftRadius
* fScale
, fDiscreteSoftRadius
* fScale
, 0));
193 aMask
.BlendWith(blurMask
);
195 // The end result is the original bitmap with blurred 8-bit alpha mask
196 BitmapEx
result(aBitmapEx
.GetBitmap(), aMask
);
199 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
200 if (bDoSaveForVisualControl
)
202 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
203 static const OUString
sDumpPath(
204 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
205 if (!sDumpPath
.isEmpty())
207 SvFileStream
aNew(sDumpPath
+ "test_softedge.png",
208 StreamMode::WRITE
| StreamMode::TRUNC
);
209 vcl::PngImageWriter
aPNGWriter(aNew
);
210 aPNGWriter
.write(result
);
215 // Independent from discrete sizes of soft alpha creation, always
216 // map and project soft result to geometry range extended by soft
217 // radius, but to the eventually clipped instance (ClippedRange)
218 const primitive2d::Primitive2DReference
xEmbedRefBitmap(
219 new BitmapPrimitive2D(result
, basegfx::utils::createScaleTranslateB2DHomMatrix(
220 aClippedRange
.getWidth(), aClippedRange
.getHeight(),
221 aClippedRange
.getMinX(), aClippedRange
.getMinY())));
223 rContainer
= primitive2d::Primitive2DContainer
{ xEmbedRefBitmap
};
225 // we made it, return
229 // creation failed for some of many possible reasons, use original
230 // content, so the unmodified original geometry will be the result,
231 // just without any softEdge effect
232 rContainer
= getChildren();
235 void SoftEdgePrimitive2D::get2DDecomposition(
236 Primitive2DDecompositionVisitor
& rVisitor
,
237 const geometry::ViewInformation2D
& rViewInformation
) const
239 // Use endless while-loop-and-break mechanism due to having multiple
240 // exit scenarios that all have to do the same thing when exiting
243 basegfx::B2DRange aSoftRange
;
244 basegfx::B2DRange aClippedRange
;
245 basegfx::B2DVector aDiscreteSoftSize
;
246 double fDiscreteSoftRadius(0.0);
248 // Check various validity details and calculate/prepare values. If false, we are done
249 if (!prepareValuesAndcheckValidity(aSoftRange
, aClippedRange
, aDiscreteSoftSize
,
250 fDiscreteSoftRadius
, rViewInformation
))
253 if (!getBuffered2DDecomposition().empty())
255 // First check is to detect if the last created decompose is capable
256 // to represent the now requested visualization (see similar
257 // implementation at GlowPrimitive2D).
258 if (!maLastClippedRange
.isEmpty() && !maLastClippedRange
.isInside(aClippedRange
))
260 basegfx::B2DRange
aLastClippedRangeAndHairline(maLastClippedRange
);
262 if (!rViewInformation
.getObjectToViewTransformation().isIdentity())
264 // Grow by view-dependent size of 1/2 pixel
265 const double fHalfPixel((rViewInformation
.getInverseObjectToViewTransformation()
266 * basegfx::B2DVector(0.5, 0))
268 aLastClippedRangeAndHairline
.grow(fHalfPixel
);
271 if (!aLastClippedRangeAndHairline
.isInside(aClippedRange
))
273 // Conditions of last local decomposition have changed, delete
274 const_cast<SoftEdgePrimitive2D
*>(this)->setBuffered2DDecomposition(
275 Primitive2DContainer());
280 if (!getBuffered2DDecomposition().empty())
282 // Second check is to react on changes of the DiscreteSoftRadius when
283 // zooming in/out (see similar implementation at GlowPrimitive2D).
284 bool bFree(mfLastDiscreteSoftRadius
<= 0.0 || fDiscreteSoftRadius
<= 0.0);
288 const double fDiff(fabs(mfLastDiscreteSoftRadius
- fDiscreteSoftRadius
));
289 const double fLen(fabs(mfLastDiscreteSoftRadius
) + fabs(fDiscreteSoftRadius
));
290 const double fRelativeChange(fDiff
/ fLen
);
292 // Use a lower value here, soft edge keeps it's content so avoid that it gets too
293 // unsharp in the pixel visualization
294 // Value is in the range of ]0.0 .. 1.0]
295 bFree
= fRelativeChange
>= 0.075;
300 // Conditions of last local decomposition have changed, delete
301 const_cast<SoftEdgePrimitive2D
*>(this)->setBuffered2DDecomposition(
302 Primitive2DContainer());
306 if (getBuffered2DDecomposition().empty())
308 // refresh last used DiscreteSoftRadius and ClippedRange to new remembered values
309 const_cast<SoftEdgePrimitive2D
*>(this)->mfLastDiscreteSoftRadius
= fDiscreteSoftRadius
;
310 const_cast<SoftEdgePrimitive2D
*>(this)->maLastClippedRange
= aClippedRange
;
313 // call parent, that will check for empty, call create2DDecomposition and
314 // set as decomposition
315 BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor
, rViewInformation
);
317 // we made it, return
321 // No soft edge needed for some of many possible reasons, use original content
322 rVisitor
.visit(getChildren());
326 SoftEdgePrimitive2D::getB2DRange(const geometry::ViewInformation2D
& rViewInformation
) const
328 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
329 // use the decompose - what works, but is not needed here.
330 // We know the to-be-visualized geometry and the radius it needs to be extended,
331 // so simply calculate the exact needed range.
332 return getChildren().getB2DRange(rViewInformation
);
335 sal_uInt32
SoftEdgePrimitive2D::getPrimitive2DID() const
337 return PRIMITIVE2D_ID_SOFTEDGEPRIMITIVE2D
;
340 } // end of namespace
342 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */