Coverage Report

Created: 2026-06-30 11:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/svgio/source/svgreader/svgcharacternode.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 <svgcharacternode.hxx>
21
#include <svgstyleattributes.hxx>
22
#include <drawinglayer/primitive2d/textprimitive2d.hxx>
23
#include <drawinglayer/primitive2d/textlayoutdevice.hxx>
24
#include <drawinglayer/primitive2d/textbreakuphelper.hxx>
25
#include <drawinglayer/primitive2d/textdecoratedprimitive2d.hxx>
26
#include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
27
#include <utility>
28
#include <o3tl/string_view.hxx>
29
#include <osl/diagnose.h>
30
31
using namespace drawinglayer::primitive2d;
32
33
namespace svgio::svgreader
34
{
35
        namespace {
36
37
        class localTextBreakupHelper : public TextBreakupHelper
38
        {
39
        private:
40
            SvgTextPosition&                    mrSvgTextPosition;
41
42
        protected:
43
            /// allow user callback to allow changes to the new TextTransformation. Default
44
            /// does nothing.
45
            virtual bool allowChange(sal_uInt32 nCount, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength) override;
46
47
        public:
48
            localTextBreakupHelper(
49
                const TextSimplePortionPrimitive2D& rSource,
50
                SvgTextPosition& rSvgTextPosition)
51
0
            :   TextBreakupHelper(rSource),
52
0
                mrSvgTextPosition(rSvgTextPosition)
53
0
            {
54
0
            }
55
        };
56
57
        }
58
59
        bool localTextBreakupHelper::allowChange(sal_uInt32 /*nCount*/, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 /*nIndex*/, sal_uInt32 /*nLength*/)
60
0
        {
61
0
            const double fRotation(mrSvgTextPosition.consumeRotation());
62
63
0
            if(0.0 != fRotation)
64
0
            {
65
0
                const basegfx::B2DPoint aBasePoint(rNewTransform * basegfx::B2DPoint(0.0, 0.0));
66
67
0
                rNewTransform.translate(-aBasePoint.getX(), -aBasePoint.getY());
68
0
                rNewTransform.rotate(fRotation);
69
0
                rNewTransform.translate(aBasePoint.getX(), aBasePoint.getY());
70
0
            }
71
72
0
            return true;
73
0
        }
74
75
        SvgCharacterNode::SvgCharacterNode(
76
            SvgDocument& rDocument,
77
            SvgNode* pParent,
78
            OUString aText)
79
0
        :   SvgNode(SVGToken::Character, rDocument, pParent),
80
0
            maText(std::move(aText)),
81
0
            mpParentLine(nullptr)
82
0
        {
83
0
        }
84
85
        SvgCharacterNode::~SvgCharacterNode()
86
0
        {
87
0
        }
88
89
        const SvgStyleAttributes* SvgCharacterNode::getSvgStyleAttributes() const
90
0
        {
91
            // no own style, use parent's
92
0
            if(getParent())
93
0
            {
94
0
                return getParent()->getSvgStyleAttributes();
95
0
            }
96
0
            else
97
0
            {
98
0
                return nullptr;
99
0
            }
100
0
        }
101
102
        drawinglayer::attribute::FontAttribute SvgCharacterNode::getFontAttribute(
103
            const SvgStyleAttributes& rSvgStyleAttributes)
104
0
        {
105
0
            const SvgStringVector& rFontFamilyVector = rSvgStyleAttributes.getFontFamily();
106
0
            OUString aFontFamily(u"Times New Roman"_ustr);
107
0
            if(!rFontFamilyVector.empty())
108
0
            {
109
0
                aFontFamily = rFontFamilyVector[0];
110
111
                // tdf#168372: remove surrounding apostrophes
112
0
                if (aFontFamily.getLength() > 1 && aFontFamily.startsWith("'") && aFontFamily.endsWith("'"))
113
0
                    aFontFamily = aFontFamily.copy(1, aFontFamily.getLength() - 2);
114
0
            }
115
116
            // #i122324# if the FontFamily name ends on ' embedded' it is probably a re-import
117
            // of a SVG export with font embedding. Remove this to make font matching work. This
118
            // is pretty safe since there should be no font family names ending on ' embedded'.
119
            // Remove again when FontEmbedding is implemented in SVG import
120
0
            if(aFontFamily.endsWith(" embedded"))
121
0
            {
122
0
                aFontFamily = aFontFamily.copy(0, aFontFamily.getLength() - 9);
123
0
            }
124
125
0
            const ::FontWeight nFontWeight(getVclFontWeight(rSvgStyleAttributes.getFontWeight()));
126
0
            bool bItalic(FontStyle::italic == rSvgStyleAttributes.getFontStyle() || FontStyle::oblique == rSvgStyleAttributes.getFontStyle());
127
0
            bool bRTL(FontDirection::RTL == rSvgStyleAttributes.getFontDirection());
128
0
            bool bUnicodeBidi(UnicodeBidi::bidi_override == rSvgStyleAttributes.getUnicodeBidi());
129
130
0
            return drawinglayer::attribute::FontAttribute(
131
0
                aFontFamily,
132
0
                OUString(),
133
0
                nFontWeight,
134
0
                false/*bSymbol*/,
135
0
                false/*bVertical*/,
136
0
                bItalic,
137
0
                false/*bMonospaced*/,
138
0
                false/*bOutline*/,
139
0
                bRTL,
140
0
                bUnicodeBidi);
141
0
        }
142
143
        rtl::Reference<BasePrimitive2D> SvgCharacterNode::createSimpleTextPrimitive(
144
            SvgTextPosition& rSvgTextPosition,
145
            const SvgStyleAttributes& rSvgStyleAttributes) const
146
0
        {
147
            // prepare retval, index and length
148
0
            const sal_uInt32 nLength(getText().getLength());
149
0
            if (!nLength)
150
0
                return nullptr;
151
152
0
            const sal_uInt32 nIndex(0);
153
154
            // prepare FontAttribute
155
0
            const drawinglayer::attribute::FontAttribute aFontAttribute(getFontAttribute(rSvgStyleAttributes));
156
157
            // prepare FontSizeNumber
158
0
            double fFontWidth(rSvgStyleAttributes.getFontSizeNumber().solve(*this));
159
160
0
            if (fFontWidth == 0)
161
0
                return nullptr;
162
163
0
            rtl::Reference<BasePrimitive2D> pRetval;
164
0
            double fFontHeight(fFontWidth);
165
166
            // prepare locale
167
0
            css::lang::Locale aLocale;
168
169
            // prepare TextLayouterDevice; use a larger font size for more linear size
170
            // calculations. Similar to nTextSizeFactor in sd/source/ui/view/sdview.cxx
171
            // (ViewRedirector::createRedirectedPrimitive2DSequence).
172
0
            const double sizeFactor = fFontHeight < 50000 ? 50000 / fFontHeight : 1.0;
173
0
            TextLayouterDevice aTextLayouterDevice;
174
0
            aTextLayouterDevice.setFontAttribute(aFontAttribute, fFontWidth * sizeFactor, fFontHeight * sizeFactor, aLocale);
175
176
            // prepare TextArray
177
0
            ::std::vector< double > aTextArray(rSvgTextPosition.getX());
178
0
            ::std::vector< double > aDxArray(rSvgTextPosition.getDx());
179
180
            // Do nothing when X and Dx arrays are empty
181
0
            if((!aTextArray.empty() || !aDxArray.empty()) && aTextArray.size() < nLength)
182
0
            {
183
0
                const sal_uInt32 nArray(aTextArray.size());
184
185
0
                double fStartX(0.0);
186
0
                if (!aTextArray.empty())
187
0
                {
188
0
                    if(rSvgTextPosition.getParent() && rSvgTextPosition.getParent()->getAbsoluteX())
189
0
                    {
190
0
                        fStartX = rSvgTextPosition.getParent()->getPosition().getX();
191
0
                    }
192
0
                    else
193
0
                    {
194
0
                        fStartX = aTextArray[nArray - 1];
195
0
                    }
196
0
                }
197
198
0
                ::std::vector< double > aExtendArray(aTextLayouterDevice.getTextArray(getText(), nArray, nLength - nArray));
199
0
                double fComulativeDx(0.0);
200
201
0
                aTextArray.reserve(nLength);
202
0
                for(size_t a = 0; a < aExtendArray.size(); ++a)
203
0
                {
204
0
                    if (a < aDxArray.size())
205
0
                    {
206
0
                        fComulativeDx += aDxArray[a];
207
0
                    }
208
0
                    aTextArray.push_back(aExtendArray[a] / sizeFactor + fStartX + fComulativeDx);
209
0
                }
210
0
            }
211
212
            // get current TextPosition and TextWidth in units
213
0
            basegfx::B2DPoint aPosition(rSvgTextPosition.getPosition());
214
0
            double fTextWidth(aTextLayouterDevice.getTextWidth(getText(), nIndex, nLength) / sizeFactor);
215
0
            double fSvgTextLength(rSvgTextPosition.getTextLength());
216
217
            // check for user-given TextLength
218
0
            if(0.0 != fSvgTextLength && 0.0 != fTextWidth && !basegfx::fTools::equal(fTextWidth, fSvgTextLength))
219
0
            {
220
0
                const double fFactor(fSvgTextLength / fTextWidth);
221
222
0
                if(rSvgTextPosition.getLengthAdjust())
223
0
                {
224
                    // spacing, need to create and expand TextArray
225
0
                    if(aTextArray.empty())
226
0
                    {
227
0
                        auto aExtendArray(aTextLayouterDevice.getTextArray(getText(), nIndex, nLength));
228
0
                        aTextArray.reserve(aExtendArray.size());
229
0
                        for (auto n : aExtendArray)
230
0
                            aTextArray.push_back(n / sizeFactor);
231
0
                    }
232
233
0
                    for(auto &a : aTextArray)
234
0
                    {
235
0
                        a *= fFactor;
236
0
                    }
237
0
                }
238
0
                else
239
0
                {
240
                    // spacing and glyphs, just apply to FontWidth
241
0
                    fFontWidth *= fFactor;
242
0
                }
243
244
0
                fTextWidth = fSvgTextLength;
245
0
            }
246
247
            // get TextAlign
248
0
            TextAlign aTextAlign(rSvgStyleAttributes.getTextAlign());
249
250
0
            bool bRTL(FontDirection::RTL == rSvgStyleAttributes.getFontDirection());
251
252
            // map TextAnchor to TextAlign, there seems not to be a difference
253
0
            if(TextAnchor::notset != rSvgStyleAttributes.getTextAnchor())
254
0
            {
255
0
                switch(rSvgStyleAttributes.getTextAnchor())
256
0
                {
257
0
                    case TextAnchor::start:
258
0
                    {
259
0
                        if (bRTL)
260
0
                            aTextAlign = TextAlign::right;
261
0
                        else
262
0
                            aTextAlign = TextAlign::left;
263
0
                        break;
264
0
                    }
265
0
                    case TextAnchor::middle:
266
0
                    {
267
0
                        aTextAlign = TextAlign::center;
268
0
                        break;
269
0
                    }
270
0
                    case TextAnchor::end:
271
0
                    {
272
0
                        if (bRTL)
273
0
                            aTextAlign = TextAlign::left;
274
0
                        else
275
0
                            aTextAlign = TextAlign::right;
276
0
                        break;
277
0
                    }
278
0
                    default:
279
0
                    {
280
0
                        break;
281
0
                    }
282
0
                }
283
0
            }
284
285
            // apply TextAlign
286
0
            switch(aTextAlign)
287
0
            {
288
0
                case TextAlign::right:
289
0
                {
290
0
                    aPosition.setX(aPosition.getX() - mpParentLine->getTextLineWidth());
291
0
                    break;
292
0
                }
293
0
                case TextAlign::center:
294
0
                {
295
0
                    aPosition.setX(aPosition.getX() - (mpParentLine->getTextLineWidth() * 0.5));
296
0
                    break;
297
0
                }
298
0
                case TextAlign::notset:
299
0
                case TextAlign::left:
300
0
                case TextAlign::justify:
301
0
                {
302
                    // TextAlign::notset, TextAlign::left: nothing to do
303
                    // TextAlign::justify is not clear currently; handle as TextAlign::left
304
0
                    break;
305
0
                }
306
0
            }
307
308
            // get DominantBaseline
309
0
            const DominantBaseline aDominantBaseline(rSvgStyleAttributes.getDominantBaseline());
310
311
0
            basegfx::B2DRange aRange(aTextLayouterDevice.getTextBoundRect(getText(), nIndex, nLength));
312
            // apply DominantBaseline
313
0
            switch(aDominantBaseline)
314
0
            {
315
0
                case DominantBaseline::Middle:
316
0
                case DominantBaseline::Central:
317
0
                {
318
0
                    aPosition.setY(aPosition.getY() - aRange.getCenterY() / sizeFactor);
319
0
                    break;
320
0
                }
321
0
                case DominantBaseline::Hanging:
322
0
                {
323
0
                    aPosition.setY(aPosition.getY() - aRange.getMinY() / sizeFactor);
324
0
                    break;
325
0
                }
326
0
                default: // DominantBaseline::Auto
327
0
                {
328
                    // nothing to do
329
0
                    break;
330
0
                }
331
0
            }
332
333
            // get BaselineShift
334
0
            const BaselineShift aBaselineShift(rSvgStyleAttributes.getBaselineShift());
335
336
            // apply BaselineShift
337
0
            switch(aBaselineShift)
338
0
            {
339
0
                case BaselineShift::Sub:
340
0
                {
341
0
                    aPosition.setY(aPosition.getY() + aTextLayouterDevice.getUnderlineOffset() / sizeFactor);
342
0
                    break;
343
0
                }
344
0
                case BaselineShift::Super:
345
0
                {
346
0
                    aPosition.setY(aPosition.getY() + aTextLayouterDevice.getOverlineOffset() / sizeFactor);
347
0
                    break;
348
0
                }
349
0
                case BaselineShift::Percentage:
350
0
                case BaselineShift::Length:
351
0
                {
352
0
                    const SvgNumber aNumber(rSvgStyleAttributes.getBaselineShiftNumber());
353
0
                    const double mfBaselineShift(aNumber.solve(*this));
354
355
0
                    aPosition.setY(aPosition.getY() - mfBaselineShift);
356
0
                    break;
357
0
                }
358
0
                default: // BaselineShift::Baseline
359
0
                {
360
                    // nothing to do
361
0
                    break;
362
0
                }
363
0
            }
364
365
            // get fill color
366
0
            basegfx::BColor aFill(0, 0, 0);
367
0
            if(rSvgStyleAttributes.getFill())
368
0
                aFill = *rSvgStyleAttributes.getFill();
369
370
            // get fill opacity
371
0
            double fFillOpacity = 1.0;
372
0
            if (rSvgStyleAttributes.getFillOpacity().isSet())
373
0
            {
374
0
                fFillOpacity = rSvgStyleAttributes.getFillOpacity().getNumber();
375
0
            }
376
377
            // prepare TextTransformation
378
0
            basegfx::B2DHomMatrix aTextTransform;
379
380
0
            aTextTransform.scale(fFontWidth, fFontHeight);
381
0
            aTextTransform.translate(aPosition.getX(), aPosition.getY());
382
383
            // check TextDecoration and if TextDecoratedPortionPrimitive2D is needed
384
0
            const TextDecoration aDeco(rSvgStyleAttributes.getTextDecoration());
385
386
0
            if(TextDecoration::underline == aDeco
387
0
                || TextDecoration::overline == aDeco
388
0
                || TextDecoration::line_through == aDeco)
389
0
            {
390
                // get the fill for decoration as described by SVG. We cannot
391
                // have different stroke colors/definitions for those, though
392
0
                const SvgStyleAttributes* pDecoDef = rSvgStyleAttributes.getTextDecorationDefiningSvgStyleAttributes();
393
394
0
                basegfx::BColor aDecoColor(aFill);
395
0
                if(pDecoDef && pDecoDef->getFill())
396
0
                    aDecoColor = *pDecoDef->getFill();
397
398
0
                TextLine eFontOverline = TEXT_LINE_NONE;
399
0
                if(TextDecoration::overline == aDeco)
400
0
                    eFontOverline = TEXT_LINE_SINGLE;
401
402
0
                TextLine eFontUnderline = TEXT_LINE_NONE;
403
0
                if(TextDecoration::underline == aDeco)
404
0
                    eFontUnderline = TEXT_LINE_SINGLE;
405
406
0
                TextStrikeout eTextStrikeout = TEXT_STRIKEOUT_NONE;
407
0
                if(TextDecoration::line_through == aDeco)
408
0
                    eTextStrikeout = TEXT_STRIKEOUT_SINGLE;
409
410
                // create decorated text primitive
411
0
                pRetval = new TextDecoratedPortionPrimitive2D(
412
0
                    aTextTransform,
413
0
                    getText(),
414
0
                    nIndex,
415
0
                    nLength,
416
0
                    std::move(aTextArray),
417
0
                    {},
418
0
                    aFontAttribute,
419
0
                    std::move(aLocale),
420
0
                    aFill,
421
0
                    COL_TRANSPARENT,
422
0
                    0,
423
0
                    false,
424
0
                    rSvgStyleAttributes.getFontVariations(),
425
0
                    100, 0,
426
427
                    // extra props for decorated
428
0
                    aDecoColor,
429
0
                    aDecoColor,
430
0
                    eFontOverline,
431
0
                    eFontUnderline,
432
0
                    false,
433
0
                    eTextStrikeout,
434
0
                    false,
435
0
                    TEXT_FONT_EMPHASIS_MARK_NONE,
436
0
                    true,
437
0
                    false,
438
0
                    TEXT_RELIEF_NONE,
439
0
                    false);
440
0
            }
441
0
            else
442
0
            {
443
                // create text primitive
444
0
                pRetval = new TextSimplePortionPrimitive2D(
445
0
                    aTextTransform,
446
0
                    getText(),
447
0
                    nIndex,
448
0
                    nLength,
449
0
                    std::move(aTextArray),
450
0
                    {},
451
0
                    aFontAttribute,
452
0
                    std::move(aLocale),
453
0
                    aFill,
454
0
                    COL_TRANSPARENT,
455
0
                    0,
456
0
                    false,
457
0
                    rSvgStyleAttributes.getFontVariations());
458
0
            }
459
460
0
            if (fFillOpacity != 1.0)
461
0
            {
462
0
                pRetval = new UnifiedTransparencePrimitive2D(
463
0
                    drawinglayer::primitive2d::Primitive2DContainer{ pRetval },
464
0
                    1.0 - fFillOpacity);
465
0
            }
466
467
            // advance current TextPosition
468
0
            rSvgTextPosition.setPosition(rSvgTextPosition.getPosition() + basegfx::B2DVector(fTextWidth, 0.0));
469
470
0
            return pRetval;
471
0
        }
472
473
        void SvgCharacterNode::decomposeTextWithStyle(
474
            Primitive2DContainer& rTarget,
475
            SvgTextPosition& rSvgTextPosition,
476
            const SvgStyleAttributes& rSvgStyleAttributes) const
477
0
        {
478
0
            const Primitive2DReference xRef(
479
0
                createSimpleTextPrimitive(
480
0
                    rSvgTextPosition,
481
0
                    rSvgStyleAttributes));
482
483
0
            if(!(xRef.is() && (Visibility::visible == rSvgStyleAttributes.getVisibility())))
484
0
                return;
485
486
0
            if(!rSvgTextPosition.isRotated())
487
0
            {
488
0
                rTarget.push_back(xRef);
489
0
            }
490
0
            else
491
0
            {
492
                // need to apply rotations to each character as given
493
0
                const TextSimplePortionPrimitive2D* pCandidate =
494
0
                    dynamic_cast< const TextSimplePortionPrimitive2D* >(xRef.get());
495
496
0
                if(pCandidate)
497
0
                {
498
0
                    localTextBreakupHelper alocalTextBreakupHelper(*pCandidate, rSvgTextPosition);
499
0
                    Primitive2DContainer aResult = alocalTextBreakupHelper.extractResult();
500
501
0
                    if(!aResult.empty())
502
0
                    {
503
0
                        rTarget.append(std::move(aResult));
504
0
                    }
505
506
                    // also consume for the implied single space
507
0
                    rSvgTextPosition.consumeRotation();
508
0
                }
509
0
                else
510
0
                {
511
0
                    OSL_ENSURE(false, "Used primitive is not a text primitive (!)");
512
0
                }
513
0
            }
514
0
        }
515
516
        SvgCharacterNode*
517
        SvgCharacterNode::whiteSpaceHandling(SvgCharacterNode* pPreviousCharacterNode)
518
0
        {
519
0
            bool bIsDefault(XmlSpace::Default == getXmlSpace());
520
            // if xml:space="default" then remove all newline characters, otherwise convert them to space
521
            // convert tab to space too
522
0
            maText = maText.replaceAll(u"\n", bIsDefault ? u"" : u" ").replaceAll(u"\t", u" ");
523
524
0
            if (!bIsDefault)
525
0
            {
526
0
                if (maText.isEmpty())
527
0
                {
528
                    // Ignore this empty node for the purpose of whitespace handling
529
0
                    return pPreviousCharacterNode;
530
0
                }
531
532
0
                if (pPreviousCharacterNode && pPreviousCharacterNode->mbHadTrailingSpace)
533
0
                {
534
                    // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default".
535
                    // Even if this xml:space="preserve" node is whitespace-only, the trailing space
536
                    // of the previous node is significant - restore it
537
0
                    pPreviousCharacterNode->maText += " ";
538
0
                }
539
540
0
                return this;
541
0
            }
542
543
0
            bool bHadLeadingSpace = maText.startsWith(" ");
544
0
            mbHadTrailingSpace = maText.endsWith(" "); // Only set for xml:space="default"
545
546
            // strip of all leading and trailing spaces
547
            // and consolidate contiguous space
548
0
            maText = consolidateContiguousSpace(maText.trim());
549
550
0
            if (pPreviousCharacterNode)
551
0
            {
552
0
                if (pPreviousCharacterNode->mbHadTrailingSpace)
553
0
                {
554
                    // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default".
555
                    // The previous node already has a pending trailing space.
556
0
                    if (maText.isEmpty())
557
0
                    {
558
                        // Leading spaces in this empty node are insignificant.
559
                        // Ignore this empty node for the purpose of whitespace handling
560
0
                        return pPreviousCharacterNode;
561
0
                    }
562
                    // The previous node's trailing space is significant - restore it. Note that
563
                    // it is incorrect to insert a space in this node instead: the spaces in
564
                    // different nodes may have different size
565
0
                    pPreviousCharacterNode->maText += " ";
566
0
                    return this;
567
0
                }
568
569
0
                if (bHadLeadingSpace)
570
0
                {
571
                    // This possibly whitespace-only xml:space="default" node goes after another
572
                    // node either having xml:space="default", but without a trailing space; or
573
                    // having xml:space="preserve" (in that case, it's irrelevant if that node had
574
                    // any trailing spaces).
575
0
                    if (!maText.isEmpty())
576
0
                    {
577
                        // The leading whitespace in this node is significant - restore it
578
0
                        maText = " " + maText;
579
0
                    }
580
                    // The trailing whitespace in this node may or may not be
581
                    // significant (it will be significant, if there will be more nodes). Keep it as
582
                    // it is (even empty), but return this, to participate in whitespace handling
583
0
                    return this;
584
0
                }
585
0
            }
586
587
            // No previous node, or no leading/trailing space on the previous node's boundary: if
588
            // this is whitespace-only, its whitespace is never significant
589
0
            return maText.isEmpty() ? pPreviousCharacterNode : this;
590
0
        }
591
592
        void SvgCharacterNode::concatenate(std::u16string_view rText)
593
0
        {
594
0
            maText += rText;
595
0
        }
596
597
        void SvgCharacterNode::decomposeText(Primitive2DContainer& rTarget, SvgTextPosition& rSvgTextPosition) const
598
0
        {
599
0
            if(!getText().isEmpty())
600
0
            {
601
0
                const SvgStyleAttributes* pSvgStyleAttributes = getSvgStyleAttributes();
602
603
0
                if(pSvgStyleAttributes)
604
0
                {
605
0
                    decomposeTextWithStyle(rTarget, rSvgTextPosition, *pSvgStyleAttributes);
606
0
                }
607
0
            }
608
0
        }
609
610
} // end of namespace svgio
611
612
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */