Coverage Report

Created: 2026-10-14 09:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/drawinglayer/source/primitive2d/glowprimitive2d.cxx
Line
Count
Source
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
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 <drawinglayer/converters.hxx>
26
#include <vcl/graph.hxx>
27
#include "GlowSoftEgdeShadowTools.hxx"
28
29
#ifdef DBG_UTIL
30
#include <o3tl/environment.hxx>
31
#include <tools/stream.hxx>
32
#include <vcl/filter/PngImageWriter.hxx>
33
#endif
34
35
using namespace com::sun::star;
36
37
namespace drawinglayer::primitive2d
38
{
39
namespace
40
{
41
// Grows the range by the glow radius, plus two discrete pixels for the rounding of the range and
42
// of the blur radius. Each axis is measured on its own, since the view can scale them differently.
43
void growToGlowRange(basegfx::B2DRange& rRange, double fGlowRadius,
44
                     const geometry::ViewInformation2D& rViewInformation)
45
0
{
46
0
    rRange.grow(fGlowRadius);
47
48
0
    if (rViewInformation.getObjectToViewTransformation().isIdentity())
49
0
        return;
50
51
0
    const basegfx::B2DHomMatrix& rInverse(rViewInformation.getInverseObjectToViewTransformation());
52
0
    const double fMarginX = 2.0 * (rInverse * basegfx::B2DVector(1.0, 0.0)).getLength();
53
0
    const double fMarginY = 2.0 * (rInverse * basegfx::B2DVector(0.0, 1.0)).getLength();
54
55
0
    rRange = basegfx::B2DRange(rRange.getMinX() - fMarginX, rRange.getMinY() - fMarginY,
56
0
                               rRange.getMaxX() + fMarginX, rRange.getMaxY() + fMarginY);
57
0
}
58
}
59
60
GlowPrimitive2D::GlowPrimitive2D(const Color& rGlowColor, double fRadius,
61
                                 Primitive2DContainer&& rChildren)
62
0
    : BufferedDecompositionGroupPrimitive2D(std::move(rChildren))
63
0
    , maGlowColor(rGlowColor)
64
0
    , mfGlowRadius(fRadius)
65
0
    , mfLastDiscreteGlowRadius(0.0)
66
0
    , maLastGlowRange()
67
0
{
68
    // activate callback to flush buffered decomposition content
69
0
    activateFlushOnTimer();
70
0
}
71
72
bool GlowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
73
0
{
74
0
    if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
75
0
    {
76
0
        const GlowPrimitive2D& rCompare = static_cast<const GlowPrimitive2D&>(rPrimitive);
77
78
0
        return (getGlowRadius() == rCompare.getGlowRadius()
79
0
                && getGlowColor() == rCompare.getGlowColor());
80
0
    }
81
82
0
    return false;
83
0
}
84
85
bool GlowPrimitive2D::prepareValuesAndcheckValidity(
86
    basegfx::B2DRange& rGlowRange, basegfx::B2DVector& rDiscreteGlowSize,
87
    double& rfDiscreteGlowRadius, const geometry::ViewInformation2D& rViewInformation) const
88
0
{
89
    // no GlowRadius defined, done
90
0
    if (getGlowRadius() <= 0.0)
91
0
        return false;
92
93
    // no geometry, done
94
0
    if (getChildren().empty())
95
0
        return false;
96
97
    // no pixel target, done
98
0
    if (rViewInformation.getObjectToViewTransformation().isIdentity())
99
0
        return false;
100
101
    // get geometry range that defines area that needs to be pixelated
102
0
    rGlowRange = getChildren().getB2DRange(rViewInformation);
103
104
    // no range of geometry, done
105
0
    if (rGlowRange.isEmpty())
106
0
        return false;
107
108
    // The viewport below only decides whether to draw at all, so one bitmap covers the whole glow
109
    // and scrolling does not rasterize it again.
110
0
    growToGlowRange(rGlowRange, getGlowRadius(), rViewInformation);
111
112
    // get Viewport and check if used. If empty, all is visible (see
113
    // ViewInformation2D definition in viewinformation2d.hxx)
114
0
    if (!rViewInformation.getViewport().isEmpty())
115
0
    {
116
        // if used, extend by GlowRadius to ensure needed parts are included
117
0
        basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
118
0
        aVisibleArea.grow(getGlowRadius());
119
120
        // To do this correctly, it needs to be done in discrete coordinates.
121
        // The object may be transformed relative to the original
122
        // ObjectTransformation, e.g. when re-used in shadow
123
0
        aVisibleArea.transform(rViewInformation.getViewTransformation());
124
125
0
        basegfx::B2DRange aGlowRangeInView(rGlowRange);
126
0
        aGlowRangeInView.transform(rViewInformation.getObjectToViewTransformation());
127
128
        // none of the glow reaches the VisibleArea, we are done
129
0
        if (!aVisibleArea.overlaps(aGlowRangeInView))
130
0
            return false;
131
0
    }
132
133
    // calculate discrete pixel size of GlowRange. If it's too small to visualize, we are done
134
0
    rDiscreteGlowSize = rViewInformation.getObjectToViewTransformation() * rGlowRange.getRange();
135
0
    if (ceil(rDiscreteGlowSize.getX()) < 2.0 || ceil(rDiscreteGlowSize.getY()) < 2.0)
136
0
        return false;
137
138
    // calculate discrete pixel size of GlowRadius. If it's too small to visualize, we are done
139
0
    rfDiscreteGlowRadius = ceil(
140
0
        (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getGlowRadius(), 0))
141
0
            .getLength());
142
0
    if (rfDiscreteGlowRadius < 1.0)
143
0
        return false;
144
145
0
    return true;
146
0
}
147
148
void GlowPrimitive2D::create2DDecomposition(
149
    Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
150
0
{
151
0
    basegfx::B2DRange aGlowRange;
152
0
    basegfx::B2DVector aDiscreteGlowSize;
153
0
    double fDiscreteGlowRadius(0.0);
154
155
    // Check various validity details and calculate/prepare values. If false, we are done
156
0
    if (!prepareValuesAndcheckValidity(aGlowRange, aDiscreteGlowSize, fDiscreteGlowRadius,
157
0
                                       rViewInformation))
158
0
        return;
159
160
    // Create embedding transformation from object to top-left zero-aligned target pixel geometry.
161
    // First, move to top-left of GlowRange
162
0
    const sal_uInt32 nDiscreteGlowWidth(ceil(aDiscreteGlowSize.getX()));
163
0
    const sal_uInt32 nDiscreteGlowHeight(ceil(aDiscreteGlowSize.getY()));
164
0
    basegfx::B2DHomMatrix aEmbedding(
165
0
        basegfx::utils::createTranslateB2DHomMatrix(-aGlowRange.getMinX(), -aGlowRange.getMinY()));
166
    // Second, scale to discrete bitmap size
167
0
    aEmbedding.scale(nDiscreteGlowWidth / aGlowRange.getWidth(),
168
0
                     nDiscreteGlowHeight / aGlowRange.getHeight());
169
170
    // Embed content graphics to TransformPrimitive2D
171
0
    const primitive2d::Primitive2DReference xEmbedRef(
172
0
        new primitive2d::TransformPrimitive2D(aEmbedding, Primitive2DContainer(getChildren())));
173
0
    primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
174
175
    // Create Bitmap using drawinglayer tooling, including a MaximumQuadraticPixel
176
    // limitation to be safe and not go runtime/memory havoc. This is the only place the glow gives
177
    // up resolution, and one budget covers every radius, so a wide halo keeps as much of the
178
    // object's outline as a narrow one.
179
0
    const geometry::ViewInformation2D aViewInformation2D;
180
0
    const sal_uInt32 nMaximumQuadraticPixels = 500000;
181
182
    // I have now added a helper that just creates the mask without having
183
    // to render the content, use it, it's faster
184
0
    const AlphaMask aAlpha(::drawinglayer::createAlphaMask(std::move(xEmbedSeq), aViewInformation2D,
185
0
                                                           nDiscreteGlowWidth, nDiscreteGlowHeight,
186
0
                                                           nMaximumQuadraticPixels));
187
188
0
    if (aAlpha.IsEmpty())
189
0
        return;
190
191
0
    const Size aBitmapExSizePixel(aAlpha.GetSizePixel());
192
193
0
    if (aBitmapExSizePixel.Width() <= 0 || aBitmapExSizePixel.Height() <= 0)
194
0
        return;
195
196
    // We may have to take a corrective scaling into account when the
197
    // MaximumQuadraticPixel limit was used/triggered
198
0
    double fScale(1.0);
199
200
0
    if (sal_uInt32(aBitmapExSizePixel.Width()) != nDiscreteGlowWidth
201
0
        || sal_uInt32(aBitmapExSizePixel.Height()) != nDiscreteGlowHeight)
202
0
    {
203
        // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
204
        // so adapt numerically to a single scale value, they are integer rounded values
205
0
        const double fScaleX = double(aBitmapExSizePixel.Width()) / double(nDiscreteGlowWidth);
206
0
        const double fScaleY = double(aBitmapExSizePixel.Height()) / double(nDiscreteGlowHeight);
207
208
0
        fScale = (fScaleX + fScaleY) * 0.5;
209
0
    }
210
211
    // fDiscreteGlowRadius is the size of the halo from each side of the object. The halo is the
212
    // border of glow color that fades from glow transparency level to fully transparent over the
213
    // whole radius.
214
0
    AlphaMask mask(
215
0
        CreateGlowAlphaMask(aAlpha, fDiscreteGlowRadius * fScale, 255 - getGlowColor().GetAlpha()));
216
217
    // The end result is the bitmap filled with glow color and blurred 8-bit alpha mask
218
0
    Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP);
219
0
    bmp.Erase(getGlowColor());
220
0
    Bitmap result(bmp, mask);
221
222
#ifdef DBG_UTIL
223
    static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
224
    if (bDoSaveForVisualControl)
225
    {
226
        // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
227
        static const OUString sDumpPath(o3tl::getEnvironment(u"VCL_DUMP_BMP_PATH"_ustr));
228
        if (!sDumpPath.isEmpty())
229
        {
230
            SvFileStream aNew(sDumpPath + "test_glow.png", StreamMode::WRITE | StreamMode::TRUNC);
231
            vcl::PngImageWriter aPNGWriter(aNew);
232
            aPNGWriter.write(result);
233
        }
234
    }
235
#endif
236
237
    // Independent from discrete sizes of glow alpha creation, always
238
    // map and project glow result to geometry range extended by glow radius
239
0
    const primitive2d::Primitive2DReference xEmbedRefBitmap(
240
0
        new BitmapPrimitive2D(result, basegfx::utils::createScaleTranslateB2DHomMatrix(
241
0
                                          aGlowRange.getWidth(), aGlowRange.getHeight(),
242
0
                                          aGlowRange.getMinX(), aGlowRange.getMinY())));
243
244
0
    rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
245
0
}
246
247
// Using tooling class BufferedDecompositionGroupPrimitive2D now, so
248
// no more need to locally do the buffered get2DDecomposition here,
249
// see BufferedDecompositionGroupPrimitive2D::get2DDecomposition
250
void GlowPrimitive2D::get2DDecomposition(Primitive2DDecompositionVisitor& rVisitor,
251
                                         const geometry::ViewInformation2D& rViewInformation) const
252
0
{
253
0
    basegfx::B2DRange aGlowRange;
254
0
    basegfx::B2DVector aDiscreteGlowSize;
255
0
    double fDiscreteGlowRadius(0.0);
256
257
    // Check various validity details and calculate/prepare values. If false, we are done
258
0
    if (!prepareValuesAndcheckValidity(aGlowRange, aDiscreteGlowSize, fDiscreteGlowRadius,
259
0
                                       rViewInformation))
260
0
        return;
261
262
0
    if (hasBuffered2DDecomposition())
263
0
    {
264
        // First check is to detect if the last created decompose is capable
265
        // to represent the now requested visualization.
266
        // GlowRange is the needed visualizationArea for the current glow
267
        // effect, LastGlowRange is the one from the existing/last rendering.
268
        // Check if last created area is sufficient and can be re-used
269
0
        if (!maLastGlowRange.isEmpty() && !maLastGlowRange.isInside(aGlowRange))
270
0
        {
271
            // To avoid unnecessary invalidations due to being *very* correct
272
            // with HairLines (which are view-dependent and thus change the
273
            // result(s) here slightly when changing zoom), add a slight unsharp
274
            // component if we have a ViewTransform. The derivation is inside
275
            // the range of half a pixel (due to one pixel hairline)
276
0
            basegfx::B2DRange aLastGlowRangeAndHairline(maLastGlowRange);
277
278
0
            if (!rViewInformation.getObjectToViewTransformation().isIdentity())
279
0
            {
280
                // Grow by view-dependent size of 1/2 pixel
281
0
                const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
282
0
                                         * basegfx::B2DVector(0.5, 0))
283
0
                                            .getLength());
284
0
                aLastGlowRangeAndHairline.grow(fHalfPixel);
285
0
            }
286
287
0
            if (!aLastGlowRangeAndHairline.isInside(aGlowRange))
288
0
            {
289
                // Conditions of last local decomposition have changed, delete
290
0
                const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(
291
0
                    Primitive2DContainer());
292
0
            }
293
0
        }
294
0
    }
295
296
0
    if (hasBuffered2DDecomposition())
297
0
    {
298
        // Second check is to react on changes of the DiscreteGlowRadius when
299
        // zooming in/out.
300
        // Use the known last and current DiscreteGlowRadius to decide
301
        // if the visualization can be re-used. Be a little 'creative' here
302
        // and make it dependent on a *relative* change - it is not necessary
303
        // to re-create everytime if the exact value is missed since zooming
304
        // pixel-based glow effect is pretty good due to it's smooth nature
305
0
        bool bFree(mfLastDiscreteGlowRadius <= 0.0 || fDiscreteGlowRadius <= 0.0);
306
307
0
        if (!bFree)
308
0
        {
309
0
            const double fDiff(fabs(mfLastDiscreteGlowRadius - fDiscreteGlowRadius));
310
0
            const double fLen(fabs(mfLastDiscreteGlowRadius) + fabs(fDiscreteGlowRadius));
311
0
            const double fRelativeChange(fDiff / fLen);
312
313
            // Use lower fixed values here to change more often, higher to change less often.
314
            // Value is in the range of ]0.0 .. 1.0]
315
0
            bFree = fRelativeChange >= 0.15;
316
0
        }
317
318
0
        if (bFree)
319
0
        {
320
            // Conditions of last local decomposition have changed, delete
321
0
            const_cast<GlowPrimitive2D*>(this)->setBuffered2DDecomposition(Primitive2DContainer());
322
0
        }
323
0
    }
324
325
0
    if (!hasBuffered2DDecomposition())
326
0
    {
327
        // refresh last used DiscreteGlowRadius and GlowRange to new remembered values
328
0
        const_cast<GlowPrimitive2D*>(this)->mfLastDiscreteGlowRadius = fDiscreteGlowRadius;
329
0
        const_cast<GlowPrimitive2D*>(this)->maLastGlowRange = aGlowRange;
330
0
    }
331
332
    // call parent, that will check for empty, call create2DDecomposition and
333
    // set as decomposition
334
0
    BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
335
0
}
336
337
basegfx::B2DRange
338
GlowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
339
0
{
340
    // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
341
    // use the decompose - what works, but is not needed here.
342
    // We know the to-be-visualized geometry and the radius it needs to be extended,
343
    // so simply calculate the exact needed range.
344
0
    basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
345
346
    // We need additional space for the glow from all sides, and for the margin around it
347
0
    growToGlowRange(aRetval, getGlowRadius(), rViewInformation);
348
349
0
    return aRetval;
350
0
}
351
352
// provide unique ID
353
0
sal_uInt32 GlowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_GLOWPRIMITIVE2D; }
354
355
} // end of namespace
356
357
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */