Coverage Report

Created: 2026-09-28 10:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/drawinglayer/source/primitive2d/shadowprimitive2d.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/shadowprimitive2d.hxx>
21
#include <basegfx/color/bcolormodifier.hxx>
22
#include <drawinglayer/primitive2d/modifiedcolorprimitive2d.hxx>
23
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
24
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
25
#include <basegfx/matrix/b2dhommatrixtools.hxx>
26
#include <drawinglayer/primitive2d/bitmapprimitive2d.hxx>
27
#include <drawinglayer/converters.hxx>
28
#include <tools/color.hxx>
29
#include <vcl/graph.hxx>
30
#include "GlowSoftEgdeShadowTools.hxx"
31
32
#ifdef DBG_UTIL
33
#include <o3tl/environment.hxx>
34
#include <tools/stream.hxx>
35
#include <vcl/filter/PngImageWriter.hxx>
36
#endif
37
38
#include <memory>
39
#include <utility>
40
41
using namespace com::sun::star;
42
43
namespace drawinglayer::primitive2d
44
{
45
ShadowPrimitive2D::ShadowPrimitive2D(basegfx::B2DHomMatrix aShadowTransform,
46
                                     const basegfx::BColor& rShadowColor, double fShadowBlur,
47
                                     Primitive2DContainer&& aChildren)
48
0
    : BufferedDecompositionGroupPrimitive2D(std::move(aChildren))
49
0
    , maShadowTransform(std::move(aShadowTransform))
50
0
    , maShadowColor(rShadowColor)
51
0
    , mfShadowBlur(fShadowBlur)
52
0
    , mfLastDiscreteBlurRadius(0.0)
53
0
    , maLastClippedRange()
54
0
{
55
    // activate callback to flush buffered decomposition content
56
0
    activateFlushOnTimer();
57
0
}
58
59
bool ShadowPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
60
0
{
61
0
    if (BufferedDecompositionGroupPrimitive2D::operator==(rPrimitive))
62
0
    {
63
0
        const ShadowPrimitive2D& rCompare = static_cast<const ShadowPrimitive2D&>(rPrimitive);
64
65
0
        return (getShadowTransform() == rCompare.getShadowTransform()
66
0
                && getShadowColor() == rCompare.getShadowColor()
67
0
                && getShadowBlur() == rCompare.getShadowBlur());
68
0
    }
69
70
0
    return false;
71
0
}
72
73
// Helper to get the to-be-shadowed geometry completely embedded to
74
// a ModifiedColorPrimitive2D (change to ShadowColor) and TransformPrimitive2D
75
// (direction/offset/transformation of shadow). Since this is used pretty
76
// often, pack into a helper
77
void ShadowPrimitive2D::getFullyEmbeddedShadowPrimitives(Primitive2DContainer& rContainer) const
78
0
{
79
0
    if (getChildren().empty())
80
0
        return;
81
82
    // create a modifiedColorPrimitive containing the shadow color and the content
83
0
    basegfx::BColorModifierSharedPtr aBColorModifier
84
0
        = std::make_shared<basegfx::BColorModifier_replace>(getShadowColor());
85
0
    Primitive2DReference xRefA(new ModifiedColorPrimitive2D(Primitive2DContainer(getChildren()),
86
0
                                                            std::move(aBColorModifier)));
87
0
    Primitive2DContainer aSequenceB{ xRefA };
88
89
    // build transformed primitiveVector with shadow offset and add to target
90
0
    rContainer.visit(new TransformPrimitive2D(getShadowTransform(), std::move(aSequenceB)));
91
0
}
92
93
bool ShadowPrimitive2D::prepareValuesAndcheckValidity(
94
    basegfx::B2DRange& rBlurRange, basegfx::B2DRange& rClippedRange,
95
    basegfx::B2DVector& rDiscreteBlurSize, double& rfDiscreteBlurRadius,
96
    const geometry::ViewInformation2D& rViewInformation) const
97
0
{
98
    // no BlurRadius defined, done
99
0
    if (getShadowBlur() <= 0.0)
100
0
        return false;
101
102
    // no geometry, done
103
0
    if (getChildren().empty())
104
0
        return false;
105
106
    // no pixel target, done
107
0
    if (rViewInformation.getObjectToViewTransformation().isIdentity())
108
0
        return false;
109
110
    // get fully embedded ShadowPrimitive
111
0
    Primitive2DContainer aEmbedded;
112
0
    getFullyEmbeddedShadowPrimitives(aEmbedded);
113
114
    // get geometry range that defines area that needs to be pixelated
115
0
    rBlurRange = aEmbedded.getB2DRange(rViewInformation);
116
117
    // no range of geometry, done
118
0
    if (rBlurRange.isEmpty())
119
0
        return false;
120
121
    // extend range by BlurRadius in all directions
122
0
    rBlurRange.grow(getShadowBlur());
123
124
    // initialize ClippedRange to full BlurRange -> all is visible
125
0
    rClippedRange = rBlurRange;
126
127
    // get Viewport and check if used. If empty, all is visible (see
128
    // ViewInformation2D definition in viewinformation2d.hxx)
129
0
    if (!rViewInformation.getViewport().isEmpty())
130
0
    {
131
        // if used, extend by BlurRadius to ensure needed parts are included
132
0
        basegfx::B2DRange aVisibleArea(rViewInformation.getViewport());
133
0
        aVisibleArea.grow(getShadowBlur());
134
135
        // calculate ClippedRange
136
0
        rClippedRange.intersect(aVisibleArea);
137
138
        // if BlurRange is completely outside of VisibleArea, ClippedRange
139
        // will be empty and we are done
140
0
        if (rClippedRange.isEmpty())
141
0
            return false;
142
0
    }
143
144
    // calculate discrete pixel size of BlurRange. If it's too small to visualize, we are done
145
0
    rDiscreteBlurSize = rViewInformation.getObjectToViewTransformation() * rBlurRange.getRange();
146
0
    if (ceil(rDiscreteBlurSize.getX()) < 2.0 || ceil(rDiscreteBlurSize.getY()) < 2.0)
147
0
        return false;
148
149
    // calculate discrete pixel size of BlurRadius. If it's too small to visualize, we are done
150
0
    rfDiscreteBlurRadius = ceil(
151
0
        (rViewInformation.getObjectToViewTransformation() * basegfx::B2DVector(getShadowBlur(), 0))
152
0
            .getLength());
153
0
    if (rfDiscreteBlurRadius < 1.0)
154
0
        return false;
155
156
0
    return true;
157
0
}
158
159
void ShadowPrimitive2D::create2DDecomposition(
160
    Primitive2DContainer& rContainer, const geometry::ViewInformation2D& rViewInformation) const
161
0
{
162
0
    if (getShadowBlur() <= 0.0)
163
0
    {
164
        // Normal (non-blurred) shadow is already completely
165
        // handled by get2DDecomposition and not buffered. It
166
        // does not need to be since it's a simple embedding
167
        // to a ModifiedColorPrimitive2D and TransformPrimitive2D
168
0
        return;
169
0
    }
170
171
    // from here on we process a blurred shadow
172
0
    basegfx::B2DRange aBlurRange;
173
0
    basegfx::B2DRange aClippedRange;
174
0
    basegfx::B2DVector aDiscreteBlurSize;
175
0
    double fDiscreteBlurRadius(0.0);
176
177
    // Check various validity details and calculate/prepare values. If false, we are done
178
0
    if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
179
0
                                       fDiscreteBlurRadius, rViewInformation))
180
0
        return;
181
182
    // Create embedding transformation from object to top-left zero-aligned
183
    // target pixel geometry (discrete form of ClippedRange)
184
    // First, move to top-left of BlurRange
185
0
    const sal_uInt32 nDiscreteBlurWidth(ceil(aDiscreteBlurSize.getX()));
186
0
    const sal_uInt32 nDiscreteBlurHeight(ceil(aDiscreteBlurSize.getY()));
187
0
    basegfx::B2DHomMatrix aEmbedding(basegfx::utils::createTranslateB2DHomMatrix(
188
0
        -aClippedRange.getMinX(), -aClippedRange.getMinY()));
189
    // Second, scale to discrete bitmap size
190
    // Even when using the offset from ClippedRange, we need to use the
191
    // scaling from the full representation, thus from BlurRange
192
0
    aEmbedding.scale(nDiscreteBlurWidth / aBlurRange.getWidth(),
193
0
                     nDiscreteBlurHeight / aBlurRange.getHeight());
194
195
    // Get fully embedded ShadowPrimitives. This will also embed to
196
    // ModifiedColorPrimitive2D (what is not urgently needed) to create
197
    // the alpha channel, but a paint with all colors set to a single
198
    // one (like shadowColor here) is often less expensive due to possible
199
    // simplifications painting the primitives (e.g. gradient)
200
0
    Primitive2DContainer aEmbedded;
201
0
    getFullyEmbeddedShadowPrimitives(aEmbedded);
202
203
    // Embed content graphics to TransformPrimitive2D
204
0
    const primitive2d::Primitive2DReference xEmbedRef(
205
0
        new primitive2d::TransformPrimitive2D(aEmbedding, std::move(aEmbedded)));
206
0
    primitive2d::Primitive2DContainer xEmbedSeq{ xEmbedRef };
207
208
    // Create Bitmap using drawinglayer tooling, including a MaximumQuadraticPixel
209
    // limitation to be safe and not go runtime/memory havoc. Use a pretty small
210
    // limit due to this is Blurred Shadow functionality and will look good with bitmap
211
    // scaling anyways. The value of 250.000 square pixels below maybe adapted as needed.
212
0
    const basegfx::B2DVector aDiscreteClippedSize(rViewInformation.getObjectToViewTransformation()
213
0
                                                  * aClippedRange.getRange());
214
0
    const sal_uInt32 nDiscreteClippedWidth(ceil(aDiscreteClippedSize.getX()));
215
0
    const sal_uInt32 nDiscreteClippedHeight(ceil(aDiscreteClippedSize.getY()));
216
0
    const geometry::ViewInformation2D aViewInformation2D;
217
0
    const sal_uInt32 nMaximumQuadraticPixels(250000);
218
219
    // I have now added a helper that just creates the mask without having
220
    // to render the content, use it, it's faster
221
0
    const AlphaMask aAlpha(::drawinglayer::createAlphaMask(
222
0
        std::move(xEmbedSeq), aViewInformation2D, nDiscreteClippedWidth, nDiscreteClippedHeight,
223
0
        nMaximumQuadraticPixels));
224
225
    // if we have no shadow, we are done
226
0
    if (aAlpha.IsEmpty())
227
0
        return;
228
229
0
    const Size aBitmapExSizePixel(aAlpha.GetSizePixel());
230
0
    if (!(aBitmapExSizePixel.Width() > 0 && aBitmapExSizePixel.Height() > 0))
231
0
        return;
232
233
    // We may have to take a corrective scaling into account when the
234
    // MaximumQuadraticPixel limit was used/triggered
235
0
    double fScale(1.0);
236
237
0
    if (static_cast<sal_uInt32>(aBitmapExSizePixel.Width()) != nDiscreteClippedWidth
238
0
        || static_cast<sal_uInt32>(aBitmapExSizePixel.Height()) != nDiscreteClippedHeight)
239
0
    {
240
        // scale in X and Y should be the same (see fReduceFactor in createAlphaMask),
241
        // so adapt numerically to a single scale value, they are integer rounded values
242
0
        const double fScaleX(static_cast<double>(aBitmapExSizePixel.Width())
243
0
                             / static_cast<double>(nDiscreteClippedWidth));
244
0
        const double fScaleY(static_cast<double>(aBitmapExSizePixel.Height())
245
0
                             / static_cast<double>(nDiscreteClippedHeight));
246
247
0
        fScale = (fScaleX + fScaleY) * 0.5;
248
0
    }
249
250
    // Use the Alpha as base to blur and apply the effect
251
0
    const AlphaMask mask(drawinglayer::primitive2d::ProcessAndBlurAlphaMask(
252
0
        aAlpha, 0, fDiscreteBlurRadius * fScale, 0, false));
253
254
    // The end result is the bitmap filled with blur color and blurred 8-bit alpha mask
255
0
    Bitmap bmp(aAlpha.GetSizePixel(), vcl::PixelFormat::N24_BPP);
256
0
    bmp.Erase(Color(getShadowColor()));
257
0
    Bitmap result(bmp, mask);
258
259
#ifdef DBG_UTIL
260
    static bool bDoSaveForVisualControl(false); // loplugin:constvars:ignore
261
    if (bDoSaveForVisualControl)
262
    {
263
        // VCL_DUMP_BMP_PATH should be like C:/path/ or ~/path/
264
        static const OUString sDumpPath(o3tl::getEnvironment(u"VCL_DUMP_BMP_PATH"_ustr));
265
        if (!sDumpPath.isEmpty())
266
        {
267
            SvFileStream aNew(sDumpPath + "test_shadowblur.png",
268
                              StreamMode::WRITE | StreamMode::TRUNC);
269
            vcl::PngImageWriter aPNGWriter(aNew);
270
            aPNGWriter.write(result);
271
        }
272
    }
273
#endif
274
275
    // Independent from discrete sizes of blur alpha creation, always
276
    // map and project blur result to geometry range extended by blur
277
    // radius, but to the eventually clipped instance (ClippedRange)
278
0
    const primitive2d::Primitive2DReference xEmbedRefBitmap(
279
0
        new BitmapPrimitive2D(result, basegfx::utils::createScaleTranslateB2DHomMatrix(
280
0
                                          aClippedRange.getWidth(), aClippedRange.getHeight(),
281
0
                                          aClippedRange.getMinX(), aClippedRange.getMinY())));
282
283
0
    rContainer = primitive2d::Primitive2DContainer{ xEmbedRefBitmap };
284
0
}
285
286
void ShadowPrimitive2D::get2DDecomposition(
287
    Primitive2DDecompositionVisitor& rVisitor,
288
    const geometry::ViewInformation2D& rViewInformation) const
289
0
{
290
0
    if (getShadowBlur() <= 0.0)
291
0
    {
292
        // normal (non-blurred) shadow
293
0
        if (getChildren().empty())
294
0
            return;
295
296
        // get fully embedded ShadowPrimitives
297
0
        Primitive2DContainer aEmbedded;
298
0
        getFullyEmbeddedShadowPrimitives(aEmbedded);
299
300
0
        rVisitor.visit(aEmbedded);
301
0
        return;
302
0
    }
303
304
    // here we have a blurred shadow, check conditions of last
305
    // buffered decompose and decide re-use or re-create by using
306
    // setBuffered2DDecomposition to reset local buffered version
307
0
    basegfx::B2DRange aBlurRange;
308
0
    basegfx::B2DRange aClippedRange;
309
0
    basegfx::B2DVector aDiscreteBlurSize;
310
0
    double fDiscreteBlurRadius(0.0);
311
312
    // Check various validity details and calculate/prepare values. If false, we are done
313
0
    if (!prepareValuesAndcheckValidity(aBlurRange, aClippedRange, aDiscreteBlurSize,
314
0
                                       fDiscreteBlurRadius, rViewInformation))
315
0
        return;
316
317
0
    if (hasBuffered2DDecomposition())
318
0
    {
319
        // First check is to detect if the last created decompose is capable
320
        // to represent the now requested visualization (see similar
321
        // implementation at GlowPrimitive2D).
322
0
        if (!maLastClippedRange.isEmpty() && !maLastClippedRange.isInside(aClippedRange))
323
0
        {
324
0
            basegfx::B2DRange aLastClippedRangeAndHairline(maLastClippedRange);
325
326
0
            if (!rViewInformation.getObjectToViewTransformation().isIdentity())
327
0
            {
328
                // Grow by view-dependent size of 1/2 pixel
329
0
                const double fHalfPixel((rViewInformation.getInverseObjectToViewTransformation()
330
0
                                         * basegfx::B2DVector(0.5, 0))
331
0
                                            .getLength());
332
0
                aLastClippedRangeAndHairline.grow(fHalfPixel);
333
0
            }
334
335
0
            if (!aLastClippedRangeAndHairline.isInside(aClippedRange))
336
0
            {
337
                // Conditions of last local decomposition have changed, delete
338
0
                const_cast<ShadowPrimitive2D*>(this)->setBuffered2DDecomposition(
339
0
                    Primitive2DContainer());
340
0
            }
341
0
        }
342
0
    }
343
344
0
    if (hasBuffered2DDecomposition())
345
0
    {
346
        // Second check is to react on changes of the DiscreteSoftRadius when
347
        // zooming in/out (see similar implementation at ShadowPrimitive2D).
348
0
        bool bFree(mfLastDiscreteBlurRadius <= 0.0 || fDiscreteBlurRadius <= 0.0);
349
350
0
        if (!bFree)
351
0
        {
352
0
            const double fDiff(fabs(mfLastDiscreteBlurRadius - fDiscreteBlurRadius));
353
0
            const double fLen(fabs(mfLastDiscreteBlurRadius) + fabs(fDiscreteBlurRadius));
354
0
            const double fRelativeChange(fDiff / fLen);
355
356
            // Use lower fixed values here to change more often, higher to change less often.
357
            // Value is in the range of ]0.0 .. 1.0]
358
0
            bFree = fRelativeChange >= 0.15;
359
0
        }
360
361
0
        if (bFree)
362
0
        {
363
            // Conditions of last local decomposition have changed, delete
364
0
            const_cast<ShadowPrimitive2D*>(this)->setBuffered2DDecomposition(
365
0
                Primitive2DContainer());
366
0
        }
367
0
    }
368
369
0
    if (!hasBuffered2DDecomposition())
370
0
    {
371
        // refresh last used DiscreteBlurRadius and ClippedRange to new remembered values
372
0
        const_cast<ShadowPrimitive2D*>(this)->mfLastDiscreteBlurRadius = fDiscreteBlurRadius;
373
0
        const_cast<ShadowPrimitive2D*>(this)->maLastClippedRange = aClippedRange;
374
0
    }
375
376
    // call parent, that will check for empty, call create2DDecomposition and
377
    // set as decomposition
378
0
    BufferedDecompositionGroupPrimitive2D::get2DDecomposition(rVisitor, rViewInformation);
379
0
}
380
381
basegfx::B2DRange
382
ShadowPrimitive2D::getB2DRange(const geometry::ViewInformation2D& rViewInformation) const
383
0
{
384
    // Hint: Do *not* use GroupPrimitive2D::getB2DRange, that will (unnecessarily)
385
    // use the decompose - what works, but is not needed here.
386
    // We know the to-be-visualized geometry and the radius it needs to be extended,
387
    // so simply calculate the exact needed range.
388
0
    basegfx::B2DRange aRetval(getChildren().getB2DRange(rViewInformation));
389
390
0
    if (getShadowBlur() > 0.0)
391
0
    {
392
        // blurred shadow, that extends the geometry
393
0
        aRetval.grow(getShadowBlur());
394
0
    }
395
396
0
    aRetval.transform(getShadowTransform());
397
0
    return aRetval;
398
0
}
399
400
// provide unique ID
401
0
sal_uInt32 ShadowPrimitive2D::getPrimitive2DID() const { return PRIMITIVE2D_ID_SHADOWPRIMITIVE2D; }
402
403
} // end of namespace
404
405
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */