Coverage Report

Created: 2026-10-14 09:33

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/vcl/source/outdev/bitmapex.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
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 <config_features.h>
21
22
#include <rtl/math.hxx>
23
#include <comphelper/lok.hxx>
24
#include <basegfx/matrix/b2dhommatrixtools.hxx>
25
#include <tools/mapunit.hxx>
26
27
#include <vcl/alpha.hxx>
28
#include <vcl/canvastools.hxx>
29
#include <vcl/metaact.hxx>
30
#include <vcl/virdev.hxx>
31
32
#include <CoordinateMapper.hxx>
33
#include <drawmode.hxx>
34
#include <salgdi.hxx>
35
36
bool OutputDevice::TransformAndReduceBitmapExToTargetRange(
37
        const basegfx::B2DHomMatrix& aFullTransform,
38
        basegfx::B2DRange &aVisibleRange,
39
        double &fMaximumArea)
40
0
{
41
    // limit TargetRange to existing pixels (if pixel device)
42
    // first get discrete range of object
43
0
    basegfx::B2DRange aFullPixelRange(aVisibleRange);
44
45
0
    aFullPixelRange.transform(aFullTransform);
46
47
0
    if(basegfx::fTools::equalZero(aFullPixelRange.getWidth()) || basegfx::fTools::equalZero(aFullPixelRange.getHeight()))
48
0
    {
49
        // object is outside of visible area
50
0
        return false;
51
0
    }
52
53
    // now get discrete target pixels; start with OutDev pixel size and possibly
54
    // intersect with active clipping area
55
0
    basegfx::B2DRange aOutPixel(
56
0
        0.0,
57
0
        0.0,
58
0
        GetOutputSizePixel().Width(),
59
0
        GetOutputSizePixel().Height());
60
61
0
    if(IsClipRegion())
62
0
    {
63
0
        tools::Rectangle aRegionRectangle(GetActiveClipRegion().GetBoundRect());
64
65
        // caution! Range from rectangle, one too much (!)
66
0
        aRegionRectangle.AdjustRight(-1);
67
0
        aRegionRectangle.AdjustBottom(-1);
68
0
        aOutPixel.intersect( vcl::unotools::b2DRectangleFromRectangle(aRegionRectangle) );
69
0
    }
70
71
0
    if(aOutPixel.isEmpty())
72
0
    {
73
        // no active output area
74
0
        return false;
75
0
    }
76
77
    // if aFullPixelRange is not completely inside of aOutPixel,
78
    // reduction of target pixels is possible
79
0
    basegfx::B2DRange aVisiblePixelRange(aFullPixelRange);
80
81
0
    if(!aOutPixel.isInside(aFullPixelRange))
82
0
    {
83
0
        aVisiblePixelRange.intersect(aOutPixel);
84
85
0
        if(aVisiblePixelRange.isEmpty())
86
0
        {
87
            // nothing in visible part, reduces to nothing
88
0
            return false;
89
0
        }
90
91
        // aVisiblePixelRange contains the reduced output area in
92
        // discrete coordinates. To make it useful everywhere, make it relative to
93
        // the object range
94
0
        basegfx::B2DHomMatrix aMakeVisibleRangeRelative;
95
96
0
        aVisibleRange = aVisiblePixelRange;
97
0
        aMakeVisibleRangeRelative.translate(
98
0
            -aFullPixelRange.getMinX(),
99
0
            -aFullPixelRange.getMinY());
100
0
        aMakeVisibleRangeRelative.scale(
101
0
            1.0 / aFullPixelRange.getWidth(),
102
0
            1.0 / aFullPixelRange.getHeight());
103
0
        aVisibleRange.transform(aMakeVisibleRangeRelative);
104
0
    }
105
106
    // for pixel devices, do *not* limit size, else OutputDevice::DrawDeviceAlphaBitmap
107
    // will create another, badly scaled bitmap to do the job. Nonetheless, do a
108
    // maximum clipping of something big (1600x1280x2). Add 1.0 to avoid rounding
109
    // errors in rough estimations
110
0
    const double fNewMaxArea(aVisiblePixelRange.getWidth() * aVisiblePixelRange.getHeight());
111
112
0
    fMaximumArea = std::min(4096000.0, fNewMaxArea + 1.0);
113
114
0
    return true;
115
0
}
116
117
// MM02 add some test class to get a simple timer-based output to be able
118
// to check if it gets faster - and how much. Uncomment next line or set
119
// DO_TIME_TEST for compile time if you want to use it
120
// #define DO_TIME_TEST
121
#ifdef DO_TIME_TEST
122
#include <tools/time.hxx>
123
struct LocalTimeTest
124
{
125
    const sal_uInt64 nStartTime;
126
    LocalTimeTest() : nStartTime(tools::Time::GetSystemTicks()) {}
127
    ~LocalTimeTest()
128
    {
129
        const sal_uInt64 nEndTime(tools::Time::GetSystemTicks());
130
        const sal_uInt64 nDiffTime(nEndTime - nStartTime);
131
132
        if(nDiffTime > 0)
133
        {
134
            OStringBuffer aOutput("Time: ");
135
            OString aNumber(OString::number(nDiffTime));
136
            aOutput.append(aNumber);
137
            OSL_FAIL(aOutput.getStr());
138
        }
139
    }
140
};
141
#endif
142
143
void OutputDevice::DrawScaledAndTranslatedBitmap(
144
        const basegfx::B2DVector& rScale, const basegfx::B2DVector& rTranslate,
145
        const Bitmap& rBitmap)
146
0
{
147
    // with no rotation, shear or mirroring it can be mapped to DrawBitmap
148
    // do *not* execute the mirroring here, it's done in the fallback
149
    // #i124580# the correct DestSize needs to be calculated based on MaxXY values
150
0
    Point aDestPt(basegfx::fround<tools::Long>(rTranslate.getX()), basegfx::fround<tools::Long>(rTranslate.getY()));
151
152
0
    const Size aDestSize(
153
0
        basegfx::fround<tools::Long>(rScale.getX() + rTranslate.getX()) - aDestPt.X(),
154
0
        basegfx::fround<tools::Long>(rScale.getY() + rTranslate.getY()) - aDestPt.Y());
155
156
0
    const Point aOrigin = GetMapMode().GetOrigin();
157
158
0
    if (!mpMetaFile && comphelper::LibreOfficeKit::isActive() && GetMapMode().GetMapUnit() != MapUnit::MapPixel)
159
0
    {
160
0
        aDestPt.Move(aOrigin.getX(), aOrigin.getY());
161
0
        EnableMapMode(false);
162
0
    }
163
164
0
    DrawBitmap(aDestPt, aDestSize, rBitmap);
165
0
    if (!mpMetaFile && comphelper::LibreOfficeKit::isActive() && GetMapMode().GetMapUnit() != MapUnit::MapPixel)
166
0
    {
167
0
        EnableMapMode();
168
0
        aDestPt.Move(-aOrigin.getX(), -aOrigin.getY());
169
0
    }
170
0
    return;
171
0
}
172
173
void OutputDevice::DrawMirroredBitmap(
174
        const basegfx::B2DVector& rScale, const basegfx::B2DVector& rTranslate,
175
        const Bitmap& rBitmap)
176
0
{
177
    // with no rotation or shear it can be mapped to DrawBitmap
178
    // do *not* execute the mirroring here, it's done in the fallback
179
    // #i124580# the correct DestSize needs to be calculated based on MaxXY values
180
0
    const Point aDestPt(basegfx::fround<tools::Long>(rTranslate.getX()), basegfx::fround<tools::Long>(rTranslate.getY()));
181
0
    const Size aDestSize(
182
0
        basegfx::fround<tools::Long>(rScale.getX() + rTranslate.getX()) - aDestPt.X(),
183
0
        basegfx::fround<tools::Long>(rScale.getY() + rTranslate.getY()) - aDestPt.Y());
184
185
0
    DrawBitmap(aDestPt, aDestSize, rBitmap);
186
0
}
187
188
void OutputDevice::DrawTransformedBitmapEx(
189
    const basegfx::B2DHomMatrix& rTransformation,
190
    const Bitmap& rBitmap,
191
    double fAlpha)
192
0
{
193
0
    assert(!is_double_buffered_window());
194
195
0
    if( ImplIsRecordLayout() )
196
0
        return;
197
198
0
    if(rBitmap.IsEmpty())
199
0
        return;
200
201
0
    if( fAlpha == 0.0 )
202
0
        return;
203
204
    // MM02 compared to other public methods of OutputDevice
205
    // this test was missing and led to zero-ptr-accesses
206
0
    if ( !mpGraphics && !AcquireGraphics() )
207
0
        return;
208
209
0
    if ( mbInitClipRegion )
210
0
        InitClipRegion();
211
212
    /*
213
       tdf#135325 typically in these OutputDevice methods, for the in
214
       record-to-metafile case the  MetaFile is already written to before the
215
       test against mbOutputClipped to determine that output to the current
216
       device would result in no visual output. In this case the metafile is
217
       written after the test, so we must continue past mbOutputClipped if
218
       recording to a metafile. It's typical to record with a device of nominal
219
       size and play back later against something of a totally different size.
220
     */
221
0
    if (mbOutputClipped && !mpMetaFile)
222
0
        return;
223
224
#ifdef DO_TIME_TEST
225
    // MM02 start time test when some data (not for trivial stuff). Will
226
    // trigger and show data when leaving this method by destructing helper
227
    static const char* pEnableBitmapDrawTimerTimer(getenv("SAL_ENABLE_TIMER_BITMAPDRAW"));
228
    static bool bUseTimer(nullptr != pEnableBitmapDrawTimerTimer);
229
    std::unique_ptr<LocalTimeTest> aTimeTest(
230
        bUseTimer && rBitmapEx.GetSizeBytes() > 10000
231
        ? new LocalTimeTest()
232
        : nullptr);
233
#endif
234
235
0
    Bitmap bitmap = rBitmap;
236
237
0
    const bool bInvert(RasterOp::Invert == meRasterOp);
238
0
    const bool bBitmapChangedColor(mnDrawMode & (DrawModeFlags::BlackBitmap | DrawModeFlags::WhiteBitmap | DrawModeFlags::GrayBitmap ));
239
0
    const bool bTryDirectPaint(!bInvert && !bBitmapChangedColor && !mpMetaFile);
240
    // tdf#130768 CAUTION(!) using GetViewTransformation() is *not* enough here, it may
241
    // be that mnOutOffX/mnOutOffY is used - see AOO bug 75163, mentioned at
242
    // ImplGetDeviceTransformation declaration
243
0
    basegfx::B2DHomMatrix aFullTransform(mpMapper->GetDeviceTransformation() * rTransformation);
244
245
    // First try to handle additional alpha blending, either directly, or modify the bitmap.
246
0
    if(!rtl::math::approxEqual( fAlpha, 1.0 ))
247
0
    {
248
0
        if(bTryDirectPaint)
249
0
        {
250
0
            if (DrawTransformedBitmap(aFullTransform, bitmap, fAlpha))
251
0
            {
252
                // we are done
253
0
                return;
254
0
            }
255
0
        }
256
        // Apply the alpha manually.
257
0
        sal_uInt8 nTransparency( static_cast<sal_uInt8>( ::basegfx::fround( 255.0*(1.0 - fAlpha) + .5) ) );
258
0
        AlphaMask aAlpha( bitmap.GetSizePixel(), &nTransparency );
259
0
        if( bitmap.HasAlpha())
260
0
            aAlpha.BlendWith( bitmap.CreateAlphaMask());
261
0
        bitmap = Bitmap(bitmap, aAlpha);
262
0
    }
263
264
    // If the backend's implementation is known to not need any optimizations here, pass to it directly.
265
    // With most backends it's more performant to try to simplify to DrawBitmap() first.
266
0
    if(bTryDirectPaint && mpGraphics->HasFastDrawTransformedBitmap() && DrawTransformedBitmap(aFullTransform, bitmap))
267
0
        return;
268
269
    // decompose matrix to check rotation and shear
270
0
    basegfx::B2DVector aScale, aTranslate;
271
0
    double fRotate, fShearX;
272
0
    rTransformation.decompose(aScale, aTranslate, fRotate, fShearX);
273
0
    const bool bRotated(!basegfx::fTools::equalZero(fRotate));
274
0
    const bool bSheared(!basegfx::fTools::equalZero(fShearX));
275
0
    const bool bMirroredX(aScale.getX() < 0.0);
276
0
    const bool bMirroredY(aScale.getY() < 0.0);
277
278
0
    if (!bRotated && !bSheared && !bMirroredX && !bMirroredY)
279
0
    {
280
0
        DrawScaledAndTranslatedBitmap(aScale, aTranslate, bitmap);
281
0
        return;
282
0
    }
283
284
    // Try the backend's implementation before resorting to the slower fallback here.
285
0
    if (bTryDirectPaint && DrawTransformedBitmap(aFullTransform, bitmap))
286
0
        return;
287
288
    // take the fallback when no rotate and shear, but mirror (else we would have done this above)
289
0
    if(!bRotated && !bSheared)
290
0
    {
291
0
        DrawMirroredBitmap(aScale, aTranslate, bitmap);
292
0
        return;
293
0
    }
294
295
    // at this point we are either sheared or rotated or both
296
0
    assert(bSheared || bRotated);
297
298
    // fallback; create transformed bitmap the hard way (back-transform
299
    // the pixels) and paint
300
0
    basegfx::B2DRange aVisibleRange(0.0, 0.0, 1.0, 1.0);
301
302
    // limit maximum area to something looking good for non-pixel-based targets (metafile, printer)
303
    // by using a fixed minimum (allow at least, but no need to utilize) for good smoothing and an area
304
    // dependent of original size for good quality when e.g. rotated/sheared. Still, limit to a maximum
305
    // to avoid crashes/resource problems (ca. 1500x3000 here)
306
0
    const Size aOriginalSizePixel(bitmap.GetSizePixel());
307
0
    const double fOrigArea(aOriginalSizePixel.Width() * aOriginalSizePixel.Height() * 0.5);
308
0
    const double fOrigAreaScaled(fOrigArea * 1.44);
309
0
    double fMaximumArea(std::clamp(fOrigAreaScaled, 1000000.0, 4500000.0));
310
311
0
    if(!mpMetaFile)
312
0
    {
313
0
        if ( !TransformAndReduceBitmapExToTargetRange( aFullTransform, aVisibleRange, fMaximumArea ) )
314
0
            return;
315
0
    }
316
317
0
    if(aVisibleRange.isEmpty())
318
0
        return;
319
320
0
    Bitmap aTransformed(bitmap);
321
322
    // #122923# when the result needs an alpha channel due to being rotated or sheared
323
    // and thus uncovering areas, add these channels so that the own transformer (used
324
    // in getTransformed) also creates a transformed alpha channel
325
0
    if(!aTransformed.HasAlpha() && (bSheared || bRotated))
326
0
    {
327
        // parts will be uncovered, extend aTransformed with a mask bitmap
328
0
        const Bitmap aContent(aTransformed.CreateColorBitmap());
329
330
0
        AlphaMask aMaskBmp(aContent.GetSizePixel());
331
0
        aMaskBmp.Erase(0);
332
333
0
        aTransformed = Bitmap(aContent, aMaskBmp);
334
0
    }
335
336
0
    basegfx::B2DVector aFullScale, aFullTranslate;
337
0
    double fFullRotate, fFullShearX;
338
0
    aFullTransform.decompose(aFullScale, aFullTranslate, fFullRotate, fFullShearX);
339
0
    if (aFullScale.getX() > 0 && aFullScale.getY() > 0
340
0
        && aOriginalSizePixel.getWidth() > aFullScale.getX()
341
0
        && aOriginalSizePixel.getHeight() > aFullScale.getY())
342
0
    {
343
        // aFullTransform would downscale the bitmap: avoid this, so the recorded metafile can be
344
        // better upscaled later.
345
0
        basegfx::B2DHomMatrix aTransform = basegfx::utils::createScaleB2DHomMatrix(
346
0
                aOriginalSizePixel.getWidth() / aFullScale.getX(),
347
0
                aOriginalSizePixel.getHeight() / aFullScale.getY());
348
0
        aFullTransform *= aTransform;
349
0
    }
350
351
0
    double fSourceRatio = 1.0;
352
0
    if (aOriginalSizePixel.getHeight() != 0)
353
0
    {
354
0
        fSourceRatio = aOriginalSizePixel.getWidth() / aOriginalSizePixel.getHeight();
355
0
    }
356
0
    double fTargetRatio = 1.0;
357
0
    if (aFullScale.getY() != 0)
358
0
    {
359
0
        fTargetRatio = aFullScale.getX() / aFullScale.getY();
360
0
    }
361
0
    bool bAspectRatioKept = rtl::math::approxEqual(fSourceRatio, fTargetRatio);
362
0
    if (bSheared || !bAspectRatioKept)
363
0
    {
364
        // Not only rotation, or scaling does not keep aspect ratio.
365
0
        aTransformed = aTransformed.getTransformed(
366
0
            aFullTransform,
367
0
            aVisibleRange,
368
0
            fMaximumArea);
369
0
    }
370
0
    else
371
0
    {
372
        // Just rotation, can do that directly.
373
0
        fFullRotate = fmod(fFullRotate * -1, 2 * M_PI);
374
0
        if (fFullRotate < 0)
375
0
        {
376
0
            fFullRotate += 2 * M_PI;
377
0
        }
378
0
        Degree10 nAngle10(basegfx::fround(basegfx::rad2deg<10>(fFullRotate)));
379
0
        aTransformed.Rotate(nAngle10, COL_TRANSPARENT);
380
0
    }
381
0
    basegfx::B2DRange aTargetRange(0.0, 0.0, 1.0, 1.0);
382
383
    // get logic object target range
384
0
    aTargetRange.transform(rTransformation);
385
386
    // get from unified/relative VisibleRange to logic one
387
0
    aVisibleRange.transform(
388
0
        basegfx::utils::createScaleTranslateB2DHomMatrix(
389
0
            aTargetRange.getRange(),
390
0
            aTargetRange.getMinimum()));
391
392
    // extract point and size; do not remove size, the bitmap may have been prepared reduced on purpose
393
    // #i124580# the correct DestSize needs to be calculated based on MaxXY values
394
0
    const Point aDestPt(basegfx::fround<tools::Long>(aVisibleRange.getMinX()), basegfx::fround<tools::Long>(aVisibleRange.getMinY()));
395
0
    const Size aDestSize(
396
0
        basegfx::fround<tools::Long>(aVisibleRange.getMaxX()) - aDestPt.X(),
397
0
        basegfx::fround<tools::Long>(aVisibleRange.getMaxY()) - aDestPt.Y());
398
399
0
    DrawBitmap(aDestPt, aDestSize, aTransformed);
400
0
}
401
402
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */