1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
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>
32 namespace svgio::svgreader
36 class pathTextBreakupHelper
: public drawinglayer::primitive2d::TextBreakupHelper
39 const basegfx::B2DPolygon
& mrPolygon
;
40 const double mfBasegfxPathLength
;
42 const basegfx::B2DPoint
& mrTextStart
;
44 const sal_uInt32 mnMaxIndex
;
46 basegfx::B2DCubicBezier maCurrentSegment
;
47 std::unique_ptr
<basegfx::B2DCubicBezierHelper
> mpB2DCubicBezierHelper
;
48 double mfCurrentSegmentLength
;
49 double mfSegmentStartPosition
;
52 /// allow user callback to allow changes to the new TextTransformation. Default
54 virtual bool allowChange(sal_uInt32 nCount
, basegfx::B2DHomMatrix
& rNewTransform
, sal_uInt32 nIndex
, sal_uInt32 nLength
) override
;
56 void freeB2DCubicBezierHelper();
57 basegfx::B2DCubicBezierHelper
* getB2DCubicBezierHelper();
58 void advanceToPosition(double fNewPosition
);
61 pathTextBreakupHelper(
62 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
& rSource
,
63 const basegfx::B2DPolygon
& rPolygon
,
64 const double fBasegfxPathLength
,
66 const basegfx::B2DPoint
& rTextStart
);
67 virtual ~pathTextBreakupHelper() override
;
69 // read access to evtl. advanced position
70 double getPosition() const { return mfPosition
; }
75 void pathTextBreakupHelper::freeB2DCubicBezierHelper()
77 mpB2DCubicBezierHelper
.reset();
80 basegfx::B2DCubicBezierHelper
* pathTextBreakupHelper::getB2DCubicBezierHelper()
82 if(!mpB2DCubicBezierHelper
&& maCurrentSegment
.isBezier())
84 mpB2DCubicBezierHelper
.reset(new basegfx::B2DCubicBezierHelper(maCurrentSegment
));
87 return mpB2DCubicBezierHelper
.get();
90 void pathTextBreakupHelper::advanceToPosition(double fNewPosition
)
92 while(mfSegmentStartPosition
+ mfCurrentSegmentLength
< fNewPosition
&& mnIndex
< mnMaxIndex
)
94 mfSegmentStartPosition
+= mfCurrentSegmentLength
;
97 if(mnIndex
< mnMaxIndex
)
99 freeB2DCubicBezierHelper();
100 mrPolygon
.getBezierSegment(mnIndex
% mrPolygon
.count(), maCurrentSegment
);
101 maCurrentSegment
.testAndSolveTrivialBezier();
102 mfCurrentSegmentLength
= getB2DCubicBezierHelper()
103 ? getB2DCubicBezierHelper()->getLength()
104 : maCurrentSegment
.getLength();
108 mfPosition
= fNewPosition
;
111 pathTextBreakupHelper::pathTextBreakupHelper(
112 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
& rSource
,
113 const basegfx::B2DPolygon
& rPolygon
,
114 const double fBasegfxPathLength
,
116 const basegfx::B2DPoint
& rTextStart
)
117 : drawinglayer::primitive2d::TextBreakupHelper(rSource
),
119 mfBasegfxPathLength(fBasegfxPathLength
),
121 mrTextStart(rTextStart
),
122 mnMaxIndex(rPolygon
.isClosed() ? rPolygon
.count() : rPolygon
.count() - 1),
124 mfCurrentSegmentLength(0.0),
125 mfSegmentStartPosition(0.0)
127 mrPolygon
.getBezierSegment(mnIndex
% mrPolygon
.count(), maCurrentSegment
);
128 mfCurrentSegmentLength
= maCurrentSegment
.getLength();
130 advanceToPosition(fPosition
);
133 pathTextBreakupHelper::~pathTextBreakupHelper()
135 freeB2DCubicBezierHelper();
138 bool pathTextBreakupHelper::allowChange(sal_uInt32
/*nCount*/, basegfx::B2DHomMatrix
& rNewTransform
, sal_uInt32 nIndex
, sal_uInt32 nLength
)
142 if(mfPosition
< mfBasegfxPathLength
&& nLength
&& mnIndex
< mnMaxIndex
)
144 const double fSnippetWidth(
145 getTextLayouter().getTextWidth(
146 getSource().getText(),
150 if(basegfx::fTools::more(fSnippetWidth
, 0.0))
152 const OUString
aText(getSource().getText());
153 const std::u16string_view
aTrimmedChars(o3tl::trim(aText
.subView(nIndex
, nLength
)));
154 const double fEndPos(mfPosition
+ fSnippetWidth
);
156 if(!aTrimmedChars
.empty() && (mfPosition
< mfBasegfxPathLength
|| fEndPos
> 0.0))
158 const double fHalfSnippetWidth(fSnippetWidth
* 0.5);
160 advanceToPosition(mfPosition
+ fHalfSnippetWidth
);
162 // create representation for this snippet
165 // get target position and tangent in that point
166 basegfx::B2DPoint
aPosition(0.0, 0.0);
167 basegfx::B2DVector
aTangent(0.0, 1.0);
171 // snippet center is left of first segment, but right edge is on it (SVG allows that)
172 aTangent
= maCurrentSegment
.getTangent(0.0);
173 aTangent
.normalize();
174 aPosition
= maCurrentSegment
.getStartPoint() + (aTangent
* (mfPosition
- mfSegmentStartPosition
));
176 else if(mfPosition
> mfBasegfxPathLength
)
178 // snippet center is right of last segment, but left edge is on it (SVG allows that)
179 aTangent
= maCurrentSegment
.getTangent(1.0);
180 aTangent
.normalize();
181 aPosition
= maCurrentSegment
.getEndPoint() + (aTangent
* (mfPosition
- mfSegmentStartPosition
));
185 // snippet center inside segment, interpolate
186 double fBezierDistance(mfPosition
- mfSegmentStartPosition
);
188 if(getB2DCubicBezierHelper())
190 // use B2DCubicBezierHelper to bridge the non-linear gap between
191 // length and bezier distances (if it's a bezier segment)
192 fBezierDistance
= getB2DCubicBezierHelper()->distanceToRelative(fBezierDistance
);
196 // linear relationship, make relative to segment length
197 fBezierDistance
= fBezierDistance
/ mfCurrentSegmentLength
;
200 aPosition
= maCurrentSegment
.interpolatePoint(fBezierDistance
);
201 aTangent
= maCurrentSegment
.getTangent(fBezierDistance
);
202 aTangent
.normalize();
205 // detect evtl. hor/ver translations (depends on text direction)
206 const basegfx::B2DPoint
aBasePoint(rNewTransform
* basegfx::B2DPoint(0.0, 0.0));
207 const basegfx::B2DVector
aOffset(aBasePoint
- mrTextStart
);
209 if(!basegfx::fTools::equalZero(aOffset
.getY()))
212 aPosition
.setY(aPosition
.getY() + aOffset
.getY());
215 // move target position from snippet center to left text start
216 aPosition
-= fHalfSnippetWidth
* aTangent
;
218 // remove current translation
219 rNewTransform
.translate(-aBasePoint
.getX(), -aBasePoint
.getY());
221 // rotate due to tangent
222 rNewTransform
.rotate(atan2(aTangent
.getY(), aTangent
.getX()));
224 // add new translation
225 rNewTransform
.translate(aPosition
.getX(), aPosition
.getY());
229 advanceToPosition(fEndPos
);
236 } // end of namespace svgio::svgreader
239 namespace svgio::svgreader
241 SvgTextPathNode::SvgTextPathNode(
242 SvgDocument
& rDocument
,
244 : SvgNode(SVGToken::TextPath
, rDocument
, pParent
),
245 maSvgStyleAttributes(*this)
249 SvgTextPathNode::~SvgTextPathNode()
253 const SvgStyleAttributes
* SvgTextPathNode::getSvgStyleAttributes() const
255 return &maSvgStyleAttributes
;
258 void SvgTextPathNode::parseAttribute(const OUString
& rTokenName
, SVGToken aSVGToken
, const OUString
& aContent
)
261 SvgNode::parseAttribute(rTokenName
, aSVGToken
, aContent
);
263 // read style attributes
264 maSvgStyleAttributes
.parseStyleAttribute(aSVGToken
, aContent
);
269 case SVGToken::Style
:
271 readLocalCssStyle(aContent
);
274 case SVGToken::StartOffset
:
278 if(readSingleNumber(aContent
, aNum
))
280 if(aNum
.isPositive())
282 maStartOffset
= aNum
;
287 case SVGToken::Method
:
291 case SVGToken::Spacing
:
296 case SVGToken::XlinkHref
:
298 readLocalLink(aContent
, maXLink
);
308 bool SvgTextPathNode::isValid() const
310 const SvgPathNode
* pSvgPathNode
= dynamic_cast< const SvgPathNode
* >(getDocument().findSvgNodeById(maXLink
));
317 const std::optional
<basegfx::B2DPolyPolygon
>& pPolyPolyPath
= pSvgPathNode
->getPath();
319 if(!pPolyPolyPath
|| !pPolyPolyPath
->count())
324 const basegfx::B2DPolygon
aPolygon(pPolyPolyPath
->getB2DPolygon(0));
326 if(!aPolygon
.count())
331 const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon
));
333 return !basegfx::fTools::equalZero(fBasegfxPathLength
);
336 void SvgTextPathNode::decomposePathNode(
337 const drawinglayer::primitive2d::Primitive2DContainer
& rPathContent
,
338 drawinglayer::primitive2d::Primitive2DContainer
& rTarget
,
339 const basegfx::B2DPoint
& rTextStart
) const
341 if(rPathContent
.empty())
344 const SvgPathNode
* pSvgPathNode
= dynamic_cast< const SvgPathNode
* >(getDocument().findSvgNodeById(maXLink
));
349 const std::optional
<basegfx::B2DPolyPolygon
>& pPolyPolyPath
= pSvgPathNode
->getPath();
351 if(!(pPolyPolyPath
&& pPolyPolyPath
->count()))
354 basegfx::B2DPolygon
aPolygon(pPolyPolyPath
->getB2DPolygon(0));
356 if(pSvgPathNode
->getTransform())
358 aPolygon
.transform(*pSvgPathNode
->getTransform());
361 const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon
));
363 if(basegfx::fTools::equalZero(fBasegfxPathLength
))
366 double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user
368 if(pSvgPathNode
->getPathLength().isSet())
370 const double fUserLength(pSvgPathNode
->getPathLength().solve(*this));
372 if(fUserLength
> 0.0 && !basegfx::fTools::equal(fUserLength
, fBasegfxPathLength
))
374 fUserToBasegfx
= fUserLength
/ fBasegfxPathLength
;
378 double fPosition(0.0);
380 if(getStartOffset().isSet())
382 if (SvgUnit::percent
== getStartOffset().getUnit())
384 // percent are relative to path length
385 fPosition
= getStartOffset().getNumber() * 0.01 * fBasegfxPathLength
;
389 fPosition
= getStartOffset().solve(*this) * fUserToBasegfx
;
396 const sal_Int32
nLength(rPathContent
.size());
397 sal_Int32
nCurrent(0);
399 while(fPosition
< fBasegfxPathLength
&& nCurrent
< nLength
)
401 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
* pCandidate
= nullptr;
402 const drawinglayer::primitive2d::Primitive2DReference
xReference(rPathContent
[nCurrent
]);
406 pCandidate
= dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
* >(xReference
.get());
411 pathTextBreakupHelper
aPathTextBreakupHelper(
418 drawinglayer::primitive2d::Primitive2DContainer aResult
=
419 aPathTextBreakupHelper
.extractResult();
423 rTarget
.append(std::move(aResult
));
426 // advance position to consumed
427 fPosition
= aPathTextBreakupHelper
.getPosition();
434 } // end of namespace svgio
436 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */