Coverage Report

Created: 2026-08-14 10:22

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