Coverage Report

Created: 2026-04-09 11:41

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/svgio/source/svgreader/svgtextpathnode.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 <svgtextpathnode.hxx>
21
#include <svgstyleattributes.hxx>
22
#include <svgpathnode.hxx>
23
#include <svgdocument.hxx>
24
#include <basegfx/polygon/b2dpolygon.hxx>
25
#include <basegfx/polygon/b2dpolygontools.hxx>
26
#include <drawinglayer/primitive2d/textbreakuphelper.hxx>
27
#include <drawinglayer/primitive2d/textprimitive2d.hxx>
28
#include <basegfx/curve/b2dcubicbezier.hxx>
29
#include <basegfx/curve/b2dbeziertools.hxx>
30
#include <o3tl/string_view.hxx>
31
32
namespace svgio::svgreader
33
{
34
        namespace {
35
36
        class pathTextBreakupHelper : public drawinglayer::primitive2d::TextBreakupHelper
37
        {
38
        private:
39
            const basegfx::B2DPolygon&      mrPolygon;
40
            const double                    mfBasegfxPathLength;
41
            double                          mfPosition;
42
            const basegfx::B2DPoint&        mrTextStart;
43
44
            const sal_uInt32                mnMaxIndex;
45
            sal_uInt32                      mnIndex;
46
            basegfx::B2DCubicBezier         maCurrentSegment;
47
            std::unique_ptr<basegfx::B2DCubicBezierHelper> mpB2DCubicBezierHelper;
48
            double                          mfCurrentSegmentLength;
49
            double                          mfSegmentStartPosition;
50
51
        protected:
52
            /// allow user callback to allow changes to the new TextTransformation. Default
53
            /// does nothing.
54
            virtual bool allowChange(sal_uInt32 nCount, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength) override;
55
56
            void freeB2DCubicBezierHelper();
57
            basegfx::B2DCubicBezierHelper* getB2DCubicBezierHelper();
58
            void advanceToPosition(double fNewPosition);
59
60
        public:
61
            pathTextBreakupHelper(
62
                const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource,
63
                const basegfx::B2DPolygon& rPolygon,
64
                const double fBasegfxPathLength,
65
                double fPosition,
66
                const basegfx::B2DPoint& rTextStart);
67
            virtual ~pathTextBreakupHelper() override;
68
69
            // read access to evtl. advanced position
70
0
            double getPosition() const { return mfPosition; }
71
        };
72
73
        }
74
75
        void pathTextBreakupHelper::freeB2DCubicBezierHelper()
76
0
        {
77
0
            mpB2DCubicBezierHelper.reset();
78
0
        }
79
80
        basegfx::B2DCubicBezierHelper* pathTextBreakupHelper::getB2DCubicBezierHelper()
81
0
        {
82
0
            if(!mpB2DCubicBezierHelper && maCurrentSegment.isBezier())
83
0
            {
84
0
                mpB2DCubicBezierHelper.reset(new basegfx::B2DCubicBezierHelper(maCurrentSegment));
85
0
            }
86
87
0
            return mpB2DCubicBezierHelper.get();
88
0
        }
89
90
        void pathTextBreakupHelper::advanceToPosition(double fNewPosition)
91
0
        {
92
0
            while(mfSegmentStartPosition + mfCurrentSegmentLength < fNewPosition && mnIndex < mnMaxIndex)
93
0
            {
94
0
                mfSegmentStartPosition += mfCurrentSegmentLength;
95
0
                mnIndex++;
96
97
0
                if(mnIndex < mnMaxIndex)
98
0
                {
99
0
                    freeB2DCubicBezierHelper();
100
0
                    mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment);
101
0
                    maCurrentSegment.testAndSolveTrivialBezier();
102
0
                    mfCurrentSegmentLength = getB2DCubicBezierHelper()
103
0
                        ? getB2DCubicBezierHelper()->getLength()
104
0
                        : maCurrentSegment.getLength();
105
0
                }
106
0
            }
107
108
0
            mfPosition = fNewPosition;
109
0
        }
110
111
        pathTextBreakupHelper::pathTextBreakupHelper(
112
            const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource,
113
            const basegfx::B2DPolygon& rPolygon,
114
            const double fBasegfxPathLength,
115
            double fPosition,
116
            const basegfx::B2DPoint& rTextStart)
117
0
        :   drawinglayer::primitive2d::TextBreakupHelper(rSource),
118
0
            mrPolygon(rPolygon),
119
0
            mfBasegfxPathLength(fBasegfxPathLength),
120
0
            mfPosition(0.0),
121
0
            mrTextStart(rTextStart),
122
0
            mnMaxIndex(rPolygon.isClosed() ? rPolygon.count() : rPolygon.count() - 1),
123
0
            mnIndex(0),
124
0
            mfCurrentSegmentLength(0.0),
125
0
            mfSegmentStartPosition(0.0)
126
0
        {
127
0
            mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment);
128
0
            mfCurrentSegmentLength = maCurrentSegment.getLength();
129
130
0
            advanceToPosition(fPosition);
131
0
        }
132
133
        pathTextBreakupHelper::~pathTextBreakupHelper()
134
0
        {
135
0
            freeB2DCubicBezierHelper();
136
0
        }
137
138
        bool pathTextBreakupHelper::allowChange(sal_uInt32 /*nCount*/, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength)
139
0
        {
140
0
            bool bRetval(false);
141
142
0
            if(mfPosition < mfBasegfxPathLength && nLength && mnIndex < mnMaxIndex)
143
0
            {
144
0
                const double fSnippetWidth(
145
0
                    getTextLayouter().getTextWidth(
146
0
                        getSource().getText(),
147
0
                        nIndex,
148
0
                        nLength));
149
150
0
                if (fSnippetWidth > 0.0 && !basegfx::fTools::equalZero(fSnippetWidth))
151
0
                {
152
0
                    const OUString aText(getSource().getText());
153
0
                    const std::u16string_view aTrimmedChars(o3tl::trim(aText.subView(nIndex, nLength)));
154
0
                    const double fEndPos(mfPosition + fSnippetWidth);
155
156
0
                    if(!aTrimmedChars.empty() && (mfPosition < mfBasegfxPathLength || fEndPos > 0.0))
157
0
                    {
158
0
                        const double fHalfSnippetWidth(fSnippetWidth * 0.5);
159
160
0
                        advanceToPosition(mfPosition + fHalfSnippetWidth);
161
162
                        // create representation for this snippet
163
0
                        bRetval = true;
164
165
                        // get target position and tangent in that point
166
0
                        basegfx::B2DPoint aPosition(0.0, 0.0);
167
0
                        basegfx::B2DVector aTangent(0.0, 1.0);
168
169
0
                        if(mfPosition < 0.0)
170
0
                        {
171
                            // snippet center is left of first segment, but right edge is on it (SVG allows that)
172
0
                            aTangent = maCurrentSegment.getTangent(0.0);
173
0
                            aTangent.normalize();
174
0
                            aPosition = maCurrentSegment.getStartPoint() + (aTangent * (mfPosition - mfSegmentStartPosition));
175
0
                        }
176
0
                        else if(mfPosition > mfBasegfxPathLength)
177
0
                        {
178
                            // snippet center is right of last segment, but left edge is on it (SVG allows that)
179
0
                            aTangent = maCurrentSegment.getTangent(1.0);
180
0
                            aTangent.normalize();
181
0
                            aPosition = maCurrentSegment.getEndPoint() + (aTangent * (mfPosition - mfSegmentStartPosition));
182
0
                        }
183
0
                        else
184
0
                        {
185
                            // snippet center inside segment, interpolate
186
0
                            double fBezierDistance(mfPosition - mfSegmentStartPosition);
187
188
0
                            if(getB2DCubicBezierHelper())
189
0
                            {
190
                                // use B2DCubicBezierHelper to bridge the non-linear gap between
191
                                // length and bezier distances (if it's a bezier segment)
192
0
                                fBezierDistance = getB2DCubicBezierHelper()->distanceToRelative(fBezierDistance);
193
0
                            }
194
0
                            else
195
0
                            {
196
                                // linear relationship, make relative to segment length
197
0
                                fBezierDistance = fBezierDistance / mfCurrentSegmentLength;
198
0
                            }
199
200
0
                            aPosition = maCurrentSegment.interpolatePoint(fBezierDistance);
201
0
                            aTangent = maCurrentSegment.getTangent(fBezierDistance);
202
0
                            aTangent.normalize();
203
0
                        }
204
205
                        // detect evtl. hor/ver translations (depends on text direction)
206
0
                        const basegfx::B2DPoint aBasePoint(rNewTransform * basegfx::B2DPoint(0.0, 0.0));
207
0
                        const basegfx::B2DVector aOffset(aBasePoint - mrTextStart);
208
209
0
                        if(!basegfx::fTools::equalZero(aOffset.getY()))
210
0
                        {
211
                            // ...and apply
212
0
                            aPosition.setY(aPosition.getY() + aOffset.getY());
213
0
                        }
214
215
                        // move target position from snippet center to left text start
216
0
                        aPosition -= fHalfSnippetWidth * aTangent;
217
218
                        // remove current translation
219
0
                        rNewTransform.translate(-aBasePoint.getX(), -aBasePoint.getY());
220
221
                        // rotate due to tangent
222
0
                        rNewTransform.rotate(atan2(aTangent.getY(), aTangent.getX()));
223
224
                        // add new translation
225
0
                        rNewTransform.translate(aPosition.getX(), aPosition.getY());
226
0
                    }
227
228
                    // advance to end
229
0
                    advanceToPosition(fEndPos);
230
0
                }
231
0
            }
232
233
0
            return bRetval;
234
0
        }
235
236
} // end of namespace svgio::svgreader
237
238
239
namespace svgio::svgreader
240
{
241
        SvgTextPathNode::SvgTextPathNode(
242
            SvgDocument& rDocument,
243
            SvgNode* pParent)
244
0
        :   SvgNode(SVGToken::TextPath, rDocument, pParent),
245
0
            maSvgStyleAttributes(*this)
246
0
        {
247
0
        }
248
249
        SvgTextPathNode::~SvgTextPathNode()
250
0
        {
251
0
        }
252
253
        const SvgStyleAttributes* SvgTextPathNode::getSvgStyleAttributes() const
254
0
        {
255
0
            return checkForCssStyle(maSvgStyleAttributes);
256
0
        }
257
258
        void SvgTextPathNode::parseAttribute(SVGToken aSVGToken, const OUString& aContent)
259
0
        {
260
            // call parent
261
0
            SvgNode::parseAttribute(aSVGToken, aContent);
262
263
            // read style attributes
264
0
            maSvgStyleAttributes.parseStyleAttribute(aSVGToken, aContent);
265
266
            // parse own
267
0
            switch(aSVGToken)
268
0
            {
269
0
                case SVGToken::Style:
270
0
                {
271
0
                    readLocalCssStyle(aContent);
272
0
                    break;
273
0
                }
274
0
                case SVGToken::StartOffset:
275
0
                {
276
0
                    SvgNumber aNum;
277
278
0
                    if(readSingleNumber(aContent, aNum))
279
0
                    {
280
0
                        if(aNum.isPositive())
281
0
                        {
282
0
                            maStartOffset = aNum;
283
0
                        }
284
0
                    }
285
0
                    break;
286
0
                }
287
0
                case SVGToken::Method:
288
0
                {
289
0
                    break;
290
0
                }
291
0
                case SVGToken::Spacing:
292
0
                {
293
0
                    break;
294
0
                }
295
0
                case SVGToken::Href:
296
0
                case SVGToken::XlinkHref:
297
0
                {
298
0
                    readLocalLink(aContent, maXLink);
299
0
                    break;
300
0
                }
301
0
                default:
302
0
                {
303
0
                    break;
304
0
                }
305
0
            }
306
0
        }
307
308
        bool SvgTextPathNode::isValid() const
309
0
        {
310
0
            const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink));
311
312
0
            if(!pSvgPathNode)
313
0
            {
314
0
                return false;
315
0
            }
316
317
0
            const std::optional<basegfx::B2DPolyPolygon>& pPolyPolyPath = pSvgPathNode->getPath();
318
319
0
            if(!pPolyPolyPath || !pPolyPolyPath->count())
320
0
            {
321
0
                return false;
322
0
            }
323
324
0
            const basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0));
325
326
0
            if(!aPolygon.count())
327
0
            {
328
0
                return false;
329
0
            }
330
331
0
            const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon));
332
333
0
            return !basegfx::fTools::equalZero(fBasegfxPathLength);
334
0
        }
335
336
        void SvgTextPathNode::decomposePathNode(
337
            const drawinglayer::primitive2d::Primitive2DContainer& rPathContent,
338
            drawinglayer::primitive2d::Primitive2DContainer& rTarget,
339
            const basegfx::B2DPoint& rTextStart) const
340
0
        {
341
0
            if(rPathContent.empty())
342
0
                return;
343
344
0
            const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink));
345
346
0
            if(!pSvgPathNode)
347
0
                return;
348
349
0
            const std::optional<basegfx::B2DPolyPolygon>& pPolyPolyPath = pSvgPathNode->getPath();
350
351
0
            if(!(pPolyPolyPath && pPolyPolyPath->count()))
352
0
                return;
353
354
0
            basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0));
355
356
0
            if(pSvgPathNode->getTransform())
357
0
            {
358
0
                aPolygon.transform(*pSvgPathNode->getTransform());
359
0
            }
360
361
0
            const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon));
362
363
0
            if(basegfx::fTools::equalZero(fBasegfxPathLength))
364
0
                return;
365
366
0
            assert(fBasegfxPathLength != 0 && "help coverity see it's not zero");
367
0
            double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user
368
369
0
            if(pSvgPathNode->getPathLength().isSet())
370
0
            {
371
0
                const double fUserLength(pSvgPathNode->getPathLength().solve(*this));
372
373
0
                if(fUserLength > 0.0 && !basegfx::fTools::equal(fUserLength, fBasegfxPathLength))
374
0
                {
375
0
                    fUserToBasegfx = fUserLength / fBasegfxPathLength;
376
0
                }
377
0
            }
378
379
0
            double fPosition(0.0);
380
381
0
            if(getStartOffset().isSet())
382
0
            {
383
0
                if (SvgUnit::percent == getStartOffset().getUnit())
384
0
                {
385
                    // percent are relative to path length
386
0
                    fPosition = getStartOffset().getNumber() * 0.01 * fBasegfxPathLength;
387
0
                }
388
0
                else
389
0
                {
390
0
                    fPosition = getStartOffset().solve(*this) * fUserToBasegfx;
391
0
                }
392
0
            }
393
394
0
            if(fPosition < 0.0)
395
0
                return;
396
397
0
            auto pathContentIt = rPathContent.begin();
398
399
0
            while(fPosition < fBasegfxPathLength && pathContentIt != rPathContent.end())
400
0
            {
401
0
                const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* pCandidate = nullptr;
402
0
                const drawinglayer::primitive2d::Primitive2DReference xReference(*pathContentIt);
403
404
0
                if(xReference.is())
405
0
                {
406
0
                    pCandidate = dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* >(xReference.get());
407
0
                }
408
409
0
                if(pCandidate)
410
0
                {
411
0
                    pathTextBreakupHelper aPathTextBreakupHelper(
412
0
                        *pCandidate,
413
0
                        aPolygon,
414
0
                        fBasegfxPathLength,
415
0
                        fPosition,
416
0
                        rTextStart);
417
418
0
                    drawinglayer::primitive2d::Primitive2DContainer aResult =
419
0
                        aPathTextBreakupHelper.extractResult();
420
421
0
                    if(!aResult.empty())
422
0
                    {
423
0
                        rTarget.append(std::move(aResult));
424
0
                    }
425
426
                    // advance position to consumed
427
0
                    fPosition = aPathTextBreakupHelper.getPosition();
428
0
                }
429
430
0
                ++pathContentIt;
431
0
            }
432
0
        }
433
434
} // end of namespace svgio
435
436
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */