Coverage Report

Created: 2026-04-09 11:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/drawinglayer/source/primitive2d/textprimitive2d.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/textprimitive2d.hxx>
21
#include <drawinglayer/primitive2d/textlayoutdevice.hxx>
22
#include <basegfx/polygon/b2dpolypolygon.hxx>
23
#include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
24
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
25
#include <drawinglayer/primitive2d/groupprimitive2d.hxx>
26
#include <primitive2d/texteffectprimitive2d.hxx>
27
#include <basegfx/matrix/b2dhommatrixtools.hxx>
28
#include <vcl/vcllayout.hxx>
29
#include <vcl/rendercontext/State.hxx>
30
#include <utility>
31
#include <osl/diagnose.h>
32
33
using namespace com::sun::star;
34
35
namespace drawinglayer::primitive2d
36
{
37
namespace
38
{
39
// adapts fontScale for usage with TextLayouter. Input is rScale which is the extracted
40
// scale from a text transformation. A copy is modified so that it contains only positive
41
// scalings and XY-equal scalings to allow to get a non-X-scaled Vcl-Font for TextLayouter.
42
// rScale is adapted accordingly to contain the corrected scale which would need to be
43
// applied to e.g. outlines received from TextLayouter under usage of fontScale. This
44
// includes Y-Scale, X-Scale-correction and mirrorings.
45
basegfx::B2DVector getCorrectedScaleAndFontScale(basegfx::B2DVector& rScale)
46
2.00k
{
47
    // copy input value
48
2.00k
    basegfx::B2DVector aFontScale(rScale);
49
50
    // correct FontHeight settings
51
2.00k
    if (basegfx::fTools::equalZero(aFontScale.getY()))
52
0
    {
53
        // no font height; choose one and adapt scale to get back to original scaling
54
0
        static const double fDefaultFontScale(100.0);
55
0
        rScale.setY(1.0 / fDefaultFontScale);
56
0
        aFontScale.setY(fDefaultFontScale);
57
0
    }
58
2.00k
    else if (aFontScale.getY() < 0.0)
59
0
    {
60
        // negative font height; invert and adapt scale to get back to original scaling
61
0
        aFontScale.setY(-aFontScale.getY());
62
0
        rScale.setY(-1.0);
63
0
    }
64
2.00k
    else
65
2.00k
    {
66
        // positive font height; adapt scale; scaling will be part of the polygons
67
2.00k
        rScale.setY(1.0);
68
2.00k
    }
69
70
    // correct FontWidth settings
71
2.00k
    if (basegfx::fTools::equal(aFontScale.getX(), aFontScale.getY()))
72
1.99k
    {
73
        // no FontScale, adapt scale
74
1.99k
        rScale.setX(1.0);
75
1.99k
    }
76
6
    else
77
6
    {
78
        // If FontScale is used, force to no FontScale to get a non-scaled VCL font.
79
        // Adapt scaling in X accordingly.
80
6
        rScale.setX(aFontScale.getX() / aFontScale.getY());
81
6
        aFontScale.setX(aFontScale.getY());
82
6
    }
83
84
2.00k
    return aFontScale;
85
2.00k
}
86
} // end of anonymous namespace
87
88
void TextSimplePortionPrimitive2D::getTextOutlinesAndTransformation(
89
    basegfx::B2DPolyPolygonVector& rTarget, basegfx::B2DHomMatrix& rTransformation) const
90
497
{
91
497
    if (!getTextLength())
92
0
        return;
93
94
    // decompose object transformation to single values
95
497
    basegfx::B2DVector aScale, aTranslate;
96
497
    double fRotate, fShearX;
97
98
    // if decomposition returns false, create no geometry since e.g. scaling may
99
    // be zero
100
497
    if (!(getTextTransform().decompose(aScale, aTranslate, fRotate, fShearX)
101
497
          && aScale.getX() != 0.0))
102
0
        return;
103
104
    // handle special case: If scale is negative in (x,y) (3rd quadrant), it can
105
    // be expressed as rotation by PI
106
497
    if (aScale.getX() < 0.0 && aScale.getY() < 0.0)
107
0
    {
108
0
        aScale = basegfx::absolute(aScale);
109
0
        fRotate += M_PI;
110
0
    }
111
112
    // for the TextLayouterDevice, it is necessary to have a scaling representing
113
    // the font size. Since we want to extract polygons here, it is okay to
114
    // work just with scaling and to ignore shear, rotation and translation,
115
    // all that can be applied to the polygons later
116
497
    const basegfx::B2DVector aFontScale(getCorrectedScaleAndFontScale(aScale));
117
118
    // prepare textlayoutdevice
119
497
    TextLayouterDevice aTextLayouter;
120
497
    aTextLayouter.setFontAttribute(getFontAttribute(), aFontScale.getX(), aFontScale.getY(),
121
497
                                   getLocale());
122
123
    // When getting outlines from stretched text (aScale.getX() != 1.0) it
124
    // is necessary to inverse-scale the DXArray (if used) to not get the
125
    // outlines already aligned to given, but wrong DXArray
126
497
    if (!getDXArray().empty() && !basegfx::fTools::equal(aScale.getX(), 1.0))
127
3
    {
128
3
        std::vector<double> aScaledDXArray = getDXArray();
129
3
        const double fDXArrayScale(1.0 / aScale.getX());
130
131
3
        for (double& a : aScaledDXArray)
132
3
        {
133
3
            a *= fDXArrayScale;
134
3
        }
135
136
        // get the text outlines
137
3
        aTextLayouter.getTextOutlines(rTarget, getText(), getTextPosition(), getTextLength(),
138
3
                                      aScaledDXArray, getKashidaArray());
139
3
    }
140
494
    else
141
494
    {
142
        // get the text outlines
143
494
        aTextLayouter.getTextOutlines(rTarget, getText(), getTextPosition(), getTextLength(),
144
494
                                      getDXArray(), getKashidaArray());
145
494
    }
146
147
    // create primitives for the outlines
148
497
    const sal_uInt32 nCount(rTarget.size());
149
150
497
    if (nCount)
151
382
    {
152
        // prepare object transformation for polygons
153
382
        rTransformation = basegfx::utils::createScaleShearXRotateTranslateB2DHomMatrix(
154
382
            aScale, fShearX, fRotate, aTranslate);
155
382
    }
156
497
}
157
158
Primitive2DReference TextSimplePortionPrimitive2D::create2DDecomposition(
159
    const geometry::ViewInformation2D& /*rViewInformation*/) const
160
497
{
161
497
    if (!getTextLength())
162
0
        return nullptr;
163
164
497
    basegfx::B2DPolyPolygonVector aB2DPolyPolyVector;
165
497
    basegfx::B2DHomMatrix aPolygonTransform;
166
167
    // get text outlines and their object transformation
168
497
    getTextOutlinesAndTransformation(aB2DPolyPolyVector, aPolygonTransform);
169
170
    // create primitives for the outlines
171
497
    const sal_uInt32 nCount(aB2DPolyPolyVector.size());
172
173
497
    if (!nCount)
174
115
        return nullptr;
175
176
    // alloc space for the primitives
177
382
    Primitive2DContainer aRetval;
178
382
    aRetval.resize(nCount);
179
180
    // color-filled polypolygons
181
787
    for (sal_uInt32 a(0); a < nCount; a++)
182
405
    {
183
        // prepare polypolygon
184
405
        basegfx::B2DPolyPolygon& rPolyPolygon = aB2DPolyPolyVector[a];
185
405
        rPolyPolygon.transform(aPolygonTransform);
186
405
        aRetval[a] = new PolyPolygonColorPrimitive2D(rPolyPolygon, getFontColor());
187
405
    }
188
189
382
    if (getFontAttribute().getOutline())
190
0
    {
191
        // decompose polygon transformation to single values
192
0
        basegfx::B2DVector aScale, aTranslate;
193
0
        double fRotate, fShearX;
194
0
        aPolygonTransform.decompose(aScale, aTranslate, fRotate, fShearX);
195
196
        // create outline text effect with current content and replace
197
0
        return new TextEffectPrimitive2D(std::move(aRetval), aTranslate, fRotate,
198
0
                                         TextEffectStyle2D::Outline);
199
0
    }
200
201
382
    return new GroupPrimitive2D(std::move(aRetval));
202
382
}
203
204
TextSimplePortionPrimitive2D::TextSimplePortionPrimitive2D(
205
    basegfx::B2DHomMatrix rNewTransform, OUString rText, sal_Int32 nTextPosition,
206
    sal_Int32 nTextLength, std::vector<double>&& rDXArray, std::vector<sal_Bool>&& rKashidaArray,
207
    attribute::FontAttribute aFontAttribute, css::lang::Locale aLocale,
208
    const basegfx::BColor& rFontColor, const Color& rTextFillColor, short nLetterSpacing,
209
    bool bOpticalSizing, const std::vector<vcl::font::Variation>& rFontVariations,
210
    sal_uInt8 nProportionalFontSize, short nEscapement)
211
1.50k
    : maTextTransform(std::move(rNewTransform))
212
1.50k
    , maText(std::move(rText))
213
1.50k
    , mnTextPosition(nTextPosition)
214
1.50k
    , mnTextLength(nTextLength)
215
1.50k
    , maDXArray(std::move(rDXArray))
216
1.50k
    , maKashidaArray(std::move(rKashidaArray))
217
1.50k
    , maFontAttribute(std::move(aFontAttribute))
218
1.50k
    , maLocale(std::move(aLocale))
219
1.50k
    , maFontColor(rFontColor)
220
1.50k
    , maTextFillColor(rTextFillColor)
221
1.50k
    , mnLetterSpacing(nLetterSpacing)
222
1.50k
    , mbOpticalSizing(bOpticalSizing)
223
1.50k
    , maFontVariations(rFontVariations)
224
1.50k
    , mnProportionalFontSize(nProportionalFontSize)
225
1.50k
    , mnEscapement(nEscapement)
226
1.50k
{
227
#if OSL_DEBUG_LEVEL > 0
228
    const sal_Int32 aStringLength(getText().getLength());
229
    OSL_ENSURE(aStringLength >= getTextPosition()
230
                   && aStringLength >= getTextPosition() + getTextLength(),
231
               "TextSimplePortionPrimitive2D with text out of range (!)");
232
#endif
233
1.50k
}
234
235
bool LocalesAreEqual(const css::lang::Locale& rA, const css::lang::Locale& rB)
236
0
{
237
0
    return (rA.Language == rB.Language && rA.Country == rB.Country && rA.Variant == rB.Variant);
238
0
}
239
240
bool TextSimplePortionPrimitive2D::hasTextRelief() const
241
0
{
242
    // not possible for TextSimplePortionPrimitive2D
243
0
    return false;
244
0
}
245
246
bool TextSimplePortionPrimitive2D::hasShadow() const
247
0
{
248
    // not possible for TextSimplePortionPrimitive2D
249
0
    return false;
250
0
}
251
252
bool TextSimplePortionPrimitive2D::hasTextDecoration() const
253
0
{
254
    // not possible for TextSimplePortionPrimitive2D
255
0
    return false;
256
0
}
257
258
bool TextSimplePortionPrimitive2D::hasOutline() const
259
0
{
260
    // not allowed with TextRelief, else defined in FontAttributes
261
0
    return !hasTextRelief() && getFontAttribute().getOutline();
262
0
}
263
264
bool TextSimplePortionPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
265
0
{
266
0
    if (BufferedDecompositionPrimitive2D::operator==(rPrimitive))
267
0
    {
268
0
        const TextSimplePortionPrimitive2D& rCompare
269
0
            = static_cast<const TextSimplePortionPrimitive2D&>(rPrimitive);
270
271
0
        return (getTextTransform() == rCompare.getTextTransform() && getText() == rCompare.getText()
272
0
                && getTextPosition() == rCompare.getTextPosition()
273
0
                && getTextLength() == rCompare.getTextLength()
274
0
                && getDXArray() == rCompare.getDXArray()
275
0
                && getKashidaArray() == rCompare.getKashidaArray()
276
0
                && getFontAttribute() == rCompare.getFontAttribute()
277
0
                && LocalesAreEqual(getLocale(), rCompare.getLocale())
278
0
                && getFontColor() == rCompare.getFontColor()
279
0
                && maTextFillColor == rCompare.maTextFillColor
280
0
                && getProportionalFontSize() == rCompare.getProportionalFontSize()
281
0
                && getEscapement() == rCompare.getEscapement());
282
0
    }
283
284
0
    return false;
285
0
}
286
287
basegfx::B2DRange TextSimplePortionPrimitive2D::getB2DRange(
288
    const geometry::ViewInformation2D& /*rViewInformation*/) const
289
3.30k
{
290
3.30k
    if (maB2DRange.isEmpty() && getTextLength())
291
1.50k
    {
292
        // get TextBoundRect as base size
293
        // decompose object transformation to single values
294
1.50k
        basegfx::B2DVector aScale, aTranslate;
295
1.50k
        double fRotate, fShearX;
296
297
1.50k
        if (getTextTransform().decompose(aScale, aTranslate, fRotate, fShearX))
298
1.50k
        {
299
            // for the TextLayouterDevice, it is necessary to have a scaling representing
300
            // the font size. Since we want to extract polygons here, it is okay to
301
            // work just with scaling and to ignore shear, rotation and translation,
302
            // all that can be applied to the polygons later
303
1.50k
            const basegfx::B2DVector aFontScale(getCorrectedScaleAndFontScale(aScale));
304
305
            // prepare textlayoutdevice
306
1.50k
            TextLayouterDevice aTextLayouter;
307
1.50k
            aTextLayouter.setFontAttribute(getFontAttribute(), aFontScale.getX(), aFontScale.getY(),
308
1.50k
                                           getLocale());
309
310
            // get basic text range
311
1.50k
            basegfx::B2DRange aNewRange(
312
1.50k
                aTextLayouter.getTextBoundRect(getText(), getTextPosition(), getTextLength()));
313
314
            // #i104432#, #i102556# take empty results into account
315
1.50k
            if (!aNewRange.isEmpty())
316
1.50k
            {
317
                // prepare object transformation for range
318
1.50k
                const basegfx::B2DHomMatrix aRangeTransformation(
319
1.50k
                    basegfx::utils::createScaleShearXRotateTranslateB2DHomMatrix(
320
1.50k
                        aScale, fShearX, fRotate, aTranslate));
321
322
                // apply range transformation to it
323
1.50k
                aNewRange.transform(aRangeTransformation);
324
325
                // assign to buffered value
326
1.50k
                const_cast<TextSimplePortionPrimitive2D*>(this)->maB2DRange = aNewRange;
327
1.50k
            }
328
1.50k
        }
329
1.50k
    }
330
331
3.30k
    return maB2DRange;
332
3.30k
}
333
334
void TextSimplePortionPrimitive2D::createTextLayouter(TextLayouterDevice& rTextLayouter) const
335
0
{
336
    // decompose primitive-local matrix to get local font scaling
337
0
    const basegfx::utils::B2DHomMatrixBufferedOnDemandDecompose aDecTrans(getTextTransform());
338
339
    // create a TextLayouter to access encapsulated VCL Text/Font related tooling
340
0
    rTextLayouter.setFontAttribute(getFontAttribute(), aDecTrans.getScale().getX(),
341
0
                                   aDecTrans.getScale().getY(), getLocale());
342
343
0
    if (getFontAttribute().getRTL())
344
0
    {
345
0
        vcl::text::ComplexTextLayoutFlags nRTLLayoutMode(rTextLayouter.getLayoutMode());
346
0
        nRTLLayoutMode |= vcl::text::ComplexTextLayoutFlags::BiDiRtl
347
0
                          | vcl::text::ComplexTextLayoutFlags::TextOriginLeft;
348
0
        if (getFontAttribute().getBiDiStrong())
349
0
            nRTLLayoutMode |= vcl::text::ComplexTextLayoutFlags::BiDiStrong;
350
0
        else
351
0
            nRTLLayoutMode = nRTLLayoutMode & ~vcl::text::ComplexTextLayoutFlags::BiDiStrong;
352
0
        rTextLayouter.setLayoutMode(nRTLLayoutMode);
353
0
    }
354
0
    else
355
0
    {
356
        // tdf#101686: This is LTR text, but the output device may have RTL state.
357
0
        vcl::text::ComplexTextLayoutFlags nLTRLayoutMode(rTextLayouter.getLayoutMode());
358
0
        nLTRLayoutMode = nLTRLayoutMode & ~vcl::text::ComplexTextLayoutFlags::BiDiRtl;
359
0
        nLTRLayoutMode = nLTRLayoutMode & ~vcl::text::ComplexTextLayoutFlags::BiDiStrong;
360
0
        rTextLayouter.setLayoutMode(nLTRLayoutMode);
361
0
    }
362
0
}
363
364
std::unique_ptr<SalLayout>
365
TextSimplePortionPrimitive2D::createSalLayout(const TextLayouterDevice& rTextLayouter) const
366
0
{
367
    // As mentioned above we can act in the
368
    // Text's local coordinate system without transformation at all
369
0
    const ::std::vector<double>& rDXArray(getDXArray());
370
371
    // create SalLayout. No need for a position, as mentioned text can work
372
    // without transformations, so start point is always 0,0
373
0
    return rTextLayouter.getSalLayout(getText(), getTextPosition(), getTextLength(),
374
0
                                      basegfx::B2DPoint(0.0, 0.0), rDXArray, getKashidaArray());
375
0
}
376
377
// provide unique ID
378
sal_uInt32 TextSimplePortionPrimitive2D::getPrimitive2DID() const
379
2.51k
{
380
2.51k
    return PRIMITIVE2D_ID_TEXTSIMPLEPORTIONPRIMITIVE2D;
381
2.51k
}
382
383
} // end of namespace
384
385
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */