LibreOffice Module drawinglayer (master) 1
glowprimitive2d.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
34using namespace com::sun::star;
35
37{
38GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius,
39 Primitive2DContainer&& rChildren)
41 , maGlowColor(rGlowColor)
42 , mfGlowRadius(fRadius)
43 , mfLastDiscreteGlowRadius(0.0)
44 , maLastClippedRange()
45{
46}
47
48bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
49{
50 if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
51 {
52 const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive);
53
54 return (getGlowRadius() == rCompare.getGlowRadius()
55 && getGlowColor() == rCompare.getGlowColor());
56 }
57
58 return false;
59}
60
62 basegfx::B2DRange& rGlowRange, basegfx::B2DRange& rClippedRange,
63 basegfx::B2DVector& rDiscreteGlowSize, double& rfDiscreteGlowRadius,
64 const geometry::ViewInformation2D& rViewInformation) const
65{
66 // no GlowRadius defined, done
67 if (getGlowRadius() <= 0.0)
68 return false;
69
70 // no geometry, done
71 if (getChildren().empty())
72 return false;
73
74 // no pixel target, done
75 if (rViewInformation.getObjectToViewTransformation().isIdentity())
76 return false;
77
78 // get geometry range that defines area that needs to be pixelated
79 rGlowRange = getChildren().getB2DRange(rViewInformation);
80
81 // no range of geometry, done
82 if (rGlowRange.isEmpty())
83 return false;
84
85 // extend range by GlowRadius in all directions
86 rGlowRange.grow(getGlowRadius());
87
88 // initialize ClippedRange to full GlowRange -> all is visible
89 rClippedRange = rGlowRange;
90
91 // get Viewport and check if used. If empty, all is visible (see
92 // ViewInformation2D definition in viewinformation2d.hxx)
93 if (!rViewInformation.getViewport().isEmpty())
94 {
95 // if used, extend by GlowRadius to ensure needed parts are included
96 basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
97 aVisibleArea.grow(getGlowRadius());
98
99 // To do this correctly, it needs to be done in discrete coordinates.
100 // The object may be transformed relative to the original#
101 // ObjectTransformation, e.g. when re-used in shadow
102 aVisibleArea.transform(rViewInformation.getViewTransformation());
103 rClippedRange.transform(rViewInformation.getObjectToViewTransformation());
104
105 // calculate ClippedRange
106 rClippedRange.intersect(aVisibleArea);
107
108 // if GlowRange is completely outside of VisibleArea, ClippedRange
109 // will be empty and we are done
110 if (rClippedRange.isEmpty())
111 return false;
112
113 // convert result back to object coordinates
114 rClippedRange.transform(rViewInformation.getInverseObjectToViewTransformation());
115 }
116
117 // calculate discrete pixel size of GlowRange. If it's too small to visualize, we are done
118 rDiscreteGlowSize = rViewInformation.getObjectToViewTransformation() * rGlowRange.getRange();
119 if (ceil(rDiscreteGlowSize.getX()) < 2.0 || ceil(rDiscreteGlowSize.getY()) < 2.0)
120 return false;
121
122 // calculate discrete pixel size of GlowRadius. If it's too small to visualize, we are done
123 rfDiscreteGlowRadius = ceil(
125 .getLength());
126 if (rfDiscreteGlowRadius < 1.0)
127 return false;
128
129 return true;
130}
131
133 Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
134{
135 basegfx::B2DRange aGlowRange;
136 basegfx::B2DRange aClippedRange;
137 basegfx::B2DVector aDiscreteGlowSize;
138 double fDiscreteGlowRadius(0.0);
139
140 // Check various validity details and calculate/prepare values. If false, we are done
141 if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
142 fDiscreteGlowRadius, rViewInformation))
143 return;
144
145 // Create embedding transformation from object to top-left zero-aligned
146 // target pixel geometry (discrete form of ClippedRange)
147 // First, move to top-left of GlowRange
148 const sal_uInt32 nDiscreteGlowWidth(ceil(aDiscreteGlowSize.getX()));
149 const sal_uInt32 nDiscreteGlowHeight(ceil(aDiscreteGlowSize.getY()));
151 -aClippedRange.getMinX(), -aClippedRange.getMinY()));
152 // Second, scale to discrete bitmap size
153 // Even when using the offset from ClippedRange, we need to use the
154 // scaling from the full representation, thus from GlowRange
155 aEmbedding.scale(nDiscreteGlowWidth / aGlowRange.getWidth(),
156 nDiscreteGlowHeight / aGlowRange.getHeight());
157
158 // Embed content graphics to TransformPrimitive2D
159 const primitive2d::Primitive2DReference xEmbedRef(
161 primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
162
163 // Create BitmapEx using drawinglayer tooling, including a MaximumQuadraticPixel
164 // limitation to be safe and not go runtime/memory havoc. Use a pretty small
165 // limit due to this is glow functionality and will look good with bitmap scaling
166 // anyways. The value of 250.000 square pixels below maybe adapted as needed.
167 const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
168 * aClippedRange.getRange());
169 const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
170 const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
171 const geometry::ViewInformation2D aViewInformation2D;
172 const sal_uInt32 nMaximumQuadraticPixels(250000);
173
174 // I have now added a helper that just creates the mask without having
175 // to render the content, use it, it's faster
177 std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
178 nMaximumQuadraticPixels));
179
180 if (!aAlpha.IsEmpty())
181 {
182 const Size& rBitmapExSizePixel(aAlpha.GetSizePixel());
183
184 if (rBitmapExSizePixel.Width() > 0 && rBitmapExSizePixel.Height() > 0)
185 {
186 // We may have to take a corrective scaling into account when the
187 // MaximumQuadraticPixel limit was used/triggered
188 double fScale(1.0);
189
190 if (static_cast<sal_uInt32>(rBitmapExSizePixel.Width()) != nDiscreteClippedWidth
191 || static_cast<sal_uInt32>(rBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
192 {
193 // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
194 // so adapt numerically to a single scale value, they are integer rounded values
195 const double fScaleX(static_cast<double>(rBitmapExSizePixel.Width())
196 / static_cast<double>(nDiscreteClippedWidth));
197 const double fScaleY(static_cast<double>(rBitmapExSizePixel.Height())
198 / static_cast<double>(nDiscreteClippedHeight));
199
200 fScale = (fScaleX + fScaleY) * 0.5;
201 }
202
203 // fDiscreteGlowRadius is the size of the halo from each side of the object. The halo is the
204 // border of glow color that fades from glow transparency level to fully transparent
205 // When blurring a sharp boundary (our case), it gets 50% of original intensity, and
206 // fades to both sides by the blur radius; thus blur radius is half of glow radius.
207 // Consider glow transparency (initial transparency near the object edge)
208 const AlphaMask mask(ProcessAndBlurAlphaMask(aAlpha, fDiscreteGlowRadius * fScale / 2.0,
209 fDiscreteGlowRadius * fScale / 2.0,
210 255 - getGlowColor().GetAlpha()));
211
212 // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
214 bmp.Erase(getGlowColor());
215 BitmapEx result(bmp, mask);
216
217#ifdef DBG_UTIL
218 static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
219 if (bDoSaveForVisualControl)
220 {
221 // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
222 static const OUString sDumpPath(
223 OUString::createFromAscii(std::getenv("VCL_DUMP_BMP_PATH")));
224 if (!sDumpPath.isEmpty())
225 {
226 SvFileStream aNew(sDumpPath + "test_glow.png",
227 StreamMode::WRITE | StreamMode::TRUNC);
228 vcl::PngImageWriter aPNGWriter(aNew);
229 aPNGWriter.write(result);
230 }
231 }
232#endif
233
234 // Independent from discrete sizes of glow alpha creation, always
235 // map and project glow result to geometry range extended by glow
236 // radius, but to the eventually clipped instance (ClippedRange)
239 aClippedRange.getWidth(), aClippedRange.getHeight(),
240 aClippedRange.getMinX(), aClippedRange.getMinY())));
241
242 rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
243 }
244 }
245}
246
247// Using tooling class BufferedDecompositionGroupPrimitive2D now, so
248// no more need to locally do the buffered get2DDecomposition here,
249// see BufferedDecompositionGroupPrimitive2D::get2DDecomposition
251 const geometry::ViewInformation2D& rViewInformation) const
252{
253 basegfx::B2DRange aGlowRange;
254 basegfx::B2DRange aClippedRange;
255 basegfx::B2DVector aDiscreteGlowSize;
256 double fDiscreteGlowRadius(0.0);
257
258 // Check various validity details and calculate/prepare values. If false, we are done
259 if (!prepareValuesAndcheckValidity(aGlowRange, aClippedRange, aDiscreteGlowSize,
260 fDiscreteGlowRadius, rViewInformation))
261 return;
262
263 if (!getBuffered2DDecomposition().empty())
264 {
265 // First check is to detect if the last created decompose is capable
266 // to represent the now requested visualization.
267 // ClippedRange is the needed visualizationArea for the current glow
268 // effect, LastClippedRange is the one from the existing/last rendering.
269 // Check if last created area is sufficient and can be re-used
270 if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
271 {
272 // To avoid unnecessary invalidations due to being *very* correct
273 // with HairLines (which are view-dependent and thus change the
274 // result(s) here slightly when changing zoom), add a slight unsharp
275 // component if we have a ViewTransform. The derivation is inside
276 // the range of half a pixel (due to one pixel hairline)
277 basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
278
279 if (!rViewInformation.getObjectToViewTransformation().isIdentity())
280 {
281 // Grow by view-dependent size of 1/2 pixel
282 const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
283 * basegfx::B2DVector(0.5, 0))
284 .getLength());
285 aLastClippedRangeAndHairline.grow(fHalfPixel);
286 }
287
288 if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
289 {
290 // Conditions of last local decomposition have changed, delete
293 }
294 }
295 }
296
297 if (!getBuffered2DDecomposition().empty())
298 {
299 // Second check is to react on changes of the DiscreteGlowRadius when
300 // zooming in/out.
301 // Use the known last and current DiscreteGlowRadius to decide
302 // if the visualization can be re-used. Be a little 'creative' here
303 // and make it dependent on a *relative* change - it is not necessary
304 // to re-create everytime if the exact value is missed since zooming
305 // pixel-based glow effect is pretty good due to it's smooth nature
306 bool bFree(mfLastDiscreteGlowRadius <= 0.0 || fDiscreteGlowRadius <= 0.0);
307
308 if (!bFree)
309 {
310 const double fDiff(fabs(mfLastDiscreteGlowRadius - fDiscreteGlowRadius));
311 const double fLen(fabs(mfLastDiscreteGlowRadius) + fabs(fDiscreteGlowRadius));
312 const double fRelativeChange(fDiff / fLen);
313
314 // Use lower fixed values here to change more often, higher to change less often.
315 // Value is in the range of ]0.0 .. 1.0]
316 bFree = fRelativeChange >= 0.15;
317 }
318
319 if (bFree)
320 {
321 // Conditions of last local decomposition have changed, delete
323 }
324 }
325
326 if (getBuffered2DDecomposition().empty())
327 {
328 // refresh last used DiscreteGlowRadius and ClippedRange to new remembered values
329 const_cast<GlowPrimitive2D*>(this)->mfLastDiscreteGlowRadius = fDiscreteGlowRadius;
330 const_cast<GlowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
331 }
332
333 // call parent, that will check for empty, call create2DDecomposition and
334 // set as decomposition
336}
337
340{
341 // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
342 // use the decompose - what works, but is not needed here.
343 // We know the to-be-visualized geometry and the radius it needs to be extended,
344 // so simply calculate the exact needed range.
345 basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
346
347 // We need additional space for the glow from all sides
348 aRetval.grow(getGlowRadius());
349
350 return aRetval;
351}
352
353// provide unique ID
355
356} // end of namespace
357
358/* 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 & 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
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.
GlowPrimitive2D(const Color &rGlowColor, double fRadius, Primitive2DContainer &&rChildren)
constructor
double mfLastDiscreteGlowRadius
last used DiscreteGlowRadius and ClippedRange
virtual bool operator==(const BasePrimitive2D &rPrimitive) const override
compare operator
const Color & getGlowColor() const
data read access
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
bool prepareValuesAndcheckValidity(basegfx::B2DRange &rRange, basegfx::B2DRange &rClippedRange, basegfx::B2DVector &rDiscreteSize, double &rfDiscreteGlowRadius, const geometry::ViewInformation2D &rViewInformation) const
helpers
virtual sal_uInt32 getPrimitive2DID() const override
provide unique ID
const Primitive2DContainer & getChildren() const
data read access
basegfx::B2DRange getB2DRange(const geometry::ViewInformation2D &aViewInformation) const
bool write(const BitmapEx &rBitmap)
#define PRIMITIVE2D_ID_GLOWPRIMITIVE2D
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)
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
Any result