LibreOffice Module drawinglayer (master) 1
softedgeprimitive2d.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
28
29#ifdef DBG_UTIL
30#include <tools/stream.hxx>
32#endif
33
35{
38 , mfRadius(fRadius)
39 , mfLastDiscreteSoftRadius(0.0)
40 , maLastClippedRange()
41{
42}
43
45{
46 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
47 {
48 auto& rCompare = static_cast<const SoftEdgePrimitive2D&>(rPrimitive);
49 return getRadius() == rCompare.getRadius();
50 }
51
52 return false;
53}
54
56 basegfx::B2DRange& rSoftRange, basegfx::B2DRange& rClippedRange,
57 basegfx::B2DVector& rDiscreteSoftSize, double& rfDiscreteSoftRadius,
58 const geometry::ViewInformation2D& rViewInformation) const
59{
60 // no SoftRadius defined, done
61 if (getRadius() <= 0.0)
62 return false;
63
64 // no geometry, done
65 if (getChildren().empty())
66 return false;
67
68 // no pixel target, done
69 if (rViewInformation.getObjectToViewTransformation().isIdentity())
70 return false;
71
72 // get geometry range that defines area that needs to be pixelated
73 rSoftRange = getChildren().getB2DRange(rViewInformation);
74
75 // no range of geometry, done
76 if (rSoftRange.isEmpty())
77 return false;
78
79 // initialize ClippedRange to full SoftRange -> all is visible
80 rClippedRange = rSoftRange;
81
82 // get Viewport and check if used. If empty, all is visible (see
83 // ViewInformation2D definition in viewinformation2d.hxx)
84 if (!rViewInformation.getViewport().isEmpty())
85 {
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);
90
91 // To do this correctly, it needs to be done in discrete coordinates.
92 // The object may be transformed relative to the original#
93 // ObjectTransformation, e.g. when re-used in shadow
94 aVisibleArea.transform(rViewInformation.getViewTransformation());
95 rClippedRange.transform(rViewInformation.getObjectToViewTransformation());
96
97 // calculate ClippedRange
98 rClippedRange.intersect(aVisibleArea);
99
100 // if SoftRange is completely outside of VisibleArea, ClippedRange
101 // will be empty and we are done
102 if (rClippedRange.isEmpty())
103 return false;
104
105 // convert result back to object coordinates
106 rClippedRange.transform(rViewInformation.getInverseObjectToViewTransformation());
107 }
108
109 // calculate discrete pixel size of SoftRange. If it's too small to visualize, we are done
110 rDiscreteSoftSize = rViewInformation.getObjectToViewTransformation() * rSoftRange.getRange();
111 if (ceil(rDiscreteSoftSize.getX()) < 2.0 || ceil(rDiscreteSoftSize.getY()) < 2.0)
112 return false;
113
114 // calculate discrete pixel size of SoftRadius. If it's too small to visualize, we are done
115 rfDiscreteSoftRadius = ceil(
117 .getLength());
118 if (rfDiscreteSoftRadius < 1.0)
119 return false;
120
121 return true;
122}
123
125 Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
126{
127 // Use endless while-loop-and-break mechanism due to having multiple
128 // exit scenarios that all have to do the same thing when exiting
129 while (true)
130 {
131 basegfx::B2DRange aSoftRange;
132 basegfx::B2DRange aClippedRange;
133 basegfx::B2DVector aDiscreteSoftSize;
134 double fDiscreteSoftRadius(0.0);
135
136 // Check various validity details and calculate/prepare values. If false, we are done
137 if (!prepareValuesAndcheckValidity(aSoftRange, aClippedRange, aDiscreteSoftSize,
138 fDiscreteSoftRadius, rViewInformation))
139 break;
140
141 // Create embedding transformation from object to top-left zero-aligned
142 // target pixel geometry (discrete form of ClippedRange)
143 // First, move to top-left of SoftRange
144 const sal_uInt32 nDiscreteSoftWidth(ceil(aDiscreteSoftSize.getX()));
145 const sal_uInt32 nDiscreteSoftHeight(ceil(aDiscreteSoftSize.getY()));
147 -aClippedRange.getMinX(), -aClippedRange.getMinY()));
148 // Second, scale to discrete bitmap size
149 // Even when using the offset from ClippedRange, we need to use the
150 // scaling from the full representation, thus from SoftRange
151 aEmbedding.scale(nDiscreteSoftWidth / aSoftRange.getWidth(),
152 nDiscreteSoftHeight / aSoftRange.getHeight());
153
154 // Embed content graphics to TransformPrimitive2D
155 const primitive2d::Primitive2DReference xEmbedRef(
157 primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
158
159 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
160 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
161 // limit due to this is softEdge functionality and will look good with bitmap scaling
162 // anyways. The value of 250.000 square pixels below maybe adapted as needed.
163 const basegfx::B2DVector aDiscreteClippedSize(
164 rViewInformation.getObjectToViewTransformation() * aClippedRange.getRange());
165 const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
166 const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
167 const geometry::ViewInformation2D aViewInformation2D;
168 const sal_uInt32 nMaximumQuadraticPixels(250000);
170 std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
171 nMaximumQuadraticPixels));
172
173 if (aBitmapEx.IsEmpty())
174 break;
175
176 // Get BitmapEx and check size. If no content, we are done
177 const Size& rBitmapExSizePixel(aBitmapEx.GetSizePixel());
178 if (!(rBitmapExSizePixel.Width() > 0 && rBitmapExSizePixel.Height() > 0))
179 break;
180
181 // We may have to take a corrective scaling into account when the
182 // MaximumQuadraticPixel limit was used/triggered
183 double fScale(1.0);
184
185 if (static_cast<sal_uInt32>(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth
186 || static_cast<sal_uInt32>(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
187 {
188 // scale in X and Y should be the same (see fReduceFactor in convertToBitmapEx),
189 // so adapt numerically to a single scale value, they are integer rounded values
190 const double fScaleX(static_cast<double>(rBitmapExSizePixel.Width())
191 / static_cast<double>(nDiscreteClippedWidth));
192 const double fScaleY(static_cast<double>(rBitmapExSizePixel.Height())
193 / static_cast<double>(nDiscreteClippedHeight));
194
195 fScale = (fScaleX + fScaleY) * 0.5;
196 }
197
198 // Get the Alpha and use as base to blur and apply the effect
199 AlphaMask aMask(aBitmapEx.GetAlphaMask());
200 if (aMask.IsEmpty()) // There is no mask, fully opaque
201 break;
203 aMask, -fDiscreteSoftRadius * fScale, fDiscreteSoftRadius * fScale, 0));
204 aMask.BlendWith(blurMask);
205
206 // The end result is the original bitmap with blurred 8-bit alpha mask
207 BitmapEx result(aBitmapEx.GetBitmap(), aMask);
208
209#ifdef DBG_UTIL
210 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
211 if (bDoSaveForVisualControl)
212 {
213 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
214 static const OUString sDumpPath(
215 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
216 if (!sDumpPath.isEmpty())
217 {
218 SvFileStream aNew(sDumpPath + "test_softedge.png",
219 StreamMode::WRITE | StreamMode::TRUNC);
220 vcl::PngImageWriter aPNGWriter(aNew);
221 aPNGWriter.write(result);
222 }
223 }
224#endif
225
226 // Independent from discrete sizes of soft alpha creation, always
227 // map and project soft result to geometry range extended by soft
228 // radius, but to the eventually clipped instance (ClippedRange)
229 const primitive2d::Primitive2DReference xEmbedRefBitmap(
231 aClippedRange.getWidth(), aClippedRange.getHeight(),
232 aClippedRange.getMinX(), aClippedRange.getMinY())));
233
234 rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
235
236 // we made it, return
237 return;
238 }
239
240 // creation failed for some of many possible reasons, use original
241 // content, so the unmodified original geometry will be the result,
242 // just without any softEdge effect
243 rContainer = getChildren();
244}
245
248 const geometry::ViewInformation2D& rViewInformation) const
249{
250 // Use endless while-loop-and-break mechanism due to having multiple
251 // exit scenarios that all have to do the same thing when exiting
252 while (true)
253 {
254 basegfx::B2DRange aSoftRange;
255 basegfx::B2DRange aClippedRange;
256 basegfx::B2DVector aDiscreteSoftSize;
257 double fDiscreteSoftRadius(0.0);
258
259 // Check various validity details and calculate/prepare values. If false, we are done
260 if (!prepareValuesAndcheckValidity(aSoftRange, aClippedRange, aDiscreteSoftSize,
261 fDiscreteSoftRadius, rViewInformation))
262 break;
263
264 if (!getBuffered2DDecomposition().empty())
265 {
266 // First check is to detect if the last created decompose is capable
267 // to represent the now requested visualization (see similar
268 // implementation at GlowPrimitive2D).
269 if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
270 {
271 basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
272
273 if (!rViewInformation.getObjectToViewTransformation().isIdentity())
274 {
275 // Grow by view-dependent size of 1/2 pixel
276 const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
277 * basegfx::B2DVector(0.5, 0))
278 .getLength());
279 aLastClippedRangeAndHairline.grow(fHalfPixel);
280 }
281
282 if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
283 {
284 // Conditions of last local decomposition have changed, delete
287 }
288 }
289 }
290
291 if (!getBuffered2DDecomposition().empty())
292 {
293 // Second check is to react on changes of the DiscreteSoftRadius when
294 // zooming in/out (see similar implementation at GlowPrimitive2D).
295 bool bFree(mfLastDiscreteSoftRadius <= 0.0 || fDiscreteSoftRadius <= 0.0);
296
297 if (!bFree)
298 {
299 const double fDiff(fabs(mfLastDiscreteSoftRadius - fDiscreteSoftRadius));
300 const double fLen(fabs(mfLastDiscreteSoftRadius) + fabs(fDiscreteSoftRadius));
301 const double fRelativeChange(fDiff / fLen);
302
303 // Use a lower value here, soft edge keeps it's content so avoid that it gets too
304 // unsharp in the pixel visualization
305 // Value is in the range of ]0.0 .. 1.0]
306 bFree = fRelativeChange >= 0.075;
307 }
308
309 if (bFree)
310 {
311 // Conditions of last local decomposition have changed, delete
314 }
315 }
316
317 if (getBuffered2DDecomposition().empty())
318 {
319 // refresh last used DiscreteSoftRadius and ClippedRange to new remembered values
320 const_cast<SoftEdgePrimitive2D*>(this)->mfLastDiscreteSoftRadius = fDiscreteSoftRadius;
321 const_cast<SoftEdgePrimitive2D*>(this)->maLastClippedRange = aClippedRange;
322 }
323
324 // call parent, that will check for empty, call create2DDecomposition and
325 // set as decomposition
327
328 // we made it, return
329 return;
330 }
331
332 // No soft edge needed for some of many possible reasons, use original content
333 rVisitor.visit(getChildren());
334}
335
338{
339 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
340 // use the decompose - what works, but is not needed here.
341 // We know the to-be-visualized geometry and the radius it needs to be extended,
342 // so simply calculate the exact needed range.
343 return getChildren().getB2DRange(rViewInformation);
344}
345
347{
349}
350
351} // end of namespace
352
353/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
bool IsEmpty() const
void BlendWith(const AlphaMask &rOther)
const AlphaMask & GetAlphaMask() const
bool IsEmpty() const
Bitmap GetBitmap(Color aTransparentReplaceColor) const
const Size & GetSizePixel() const
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 & getViewTransformation() 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
basegfx::B2DRange getB2DRange(const geometry::ViewInformation2D &aViewInformation) const
virtual void visit(const Primitive2DReference &)=0
virtual sal_uInt32 getPrimitive2DID() const override
provide unique ID
bool prepareValuesAndcheckValidity(basegfx::B2DRange &rSoftRange, basegfx::B2DRange &rClippedRange, basegfx::B2DVector &rDiscreteSoftSize, double &rfDiscreteSoftRadius, const geometry::ViewInformation2D &rViewInformation) const
helpers
virtual bool operator==(const BasePrimitive2D &rPrimitive) const override
compare operator
double mfLastDiscreteSoftRadius
last used DiscreteSoftRadius and ClippedRange
virtual void get2DDecomposition(Primitive2DDecompositionVisitor &rVisitor, const geometry::ViewInformation2D &rViewInformation) const override
The default implementation will return an empty sequence.
virtual basegfx::B2DRange getB2DRange(const geometry::ViewInformation2D &rViewInformation) const override
get range
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.
SoftEdgePrimitive2D(double fRadius, Primitive2DContainer &&aChildren)
constructor
bool write(const BitmapEx &rBitmap)
#define PRIMITIVE2D_ID_SOFTEDGEPRIMITIVE2D
double getLength(const B2DPolygon &rCandidate)
B2DHomMatrix createScaleTranslateB2DHomMatrix(double fScaleX, double fScaleY, double fTranslateX, double fTranslateY)
B2DHomMatrix createTranslateB2DHomMatrix(double fTranslateX, double fTranslateY)
AlphaMask ProcessAndBlurAlphaMask(const Bitmap &rMask, double fErodeDilateRadius, double fBlurRadius, sal_uInt8 nTransparency, bool bConvertTo1Bit)
BitmapEx convertToBitmapEx(drawinglayer::primitive2d::Primitive2DContainer &&rSeq, const geometry::ViewInformation2D &rViewInformation2D, sal_uInt32 nDiscreteWidth, sal_uInt32 nDiscreteHeight, sal_uInt32 nMaxSquarePixels)
Definition: converters.cxx:156
Any result