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>
31 namespace svgio::svgreader
35 class pathTextBreakupHelper
: public drawinglayer::primitive2d::TextBreakupHelper
38 const basegfx::B2DPolygon
& mrPolygon
;
39 const double mfBasegfxPathLength
;
41 const basegfx::B2DPoint
& mrTextStart
;
43 const sal_uInt32 mnMaxIndex
;
45 basegfx::B2DCubicBezier maCurrentSegment
;
46 std::unique_ptr
<basegfx::B2DCubicBezierHelper
> mpB2DCubicBezierHelper
;
47 double mfCurrentSegmentLength
;
48 double mfSegmentStartPosition
;
51 /// allow user callback to allow changes to the new TextTransformation. Default
53 virtual bool allowChange(sal_uInt32 nCount
, basegfx::B2DHomMatrix
& rNewTransform
, sal_uInt32 nIndex
, sal_uInt32 nLength
) override
;
55 void freeB2DCubicBezierHelper();
56 basegfx::B2DCubicBezierHelper
* getB2DCubicBezierHelper();
57 void advanceToPosition(double fNewPosition
);
60 pathTextBreakupHelper(
61 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
& rSource
,
62 const basegfx::B2DPolygon
& rPolygon
,
63 const double fBasegfxPathLength
,
65 const basegfx::B2DPoint
& rTextStart
);
66 virtual ~pathTextBreakupHelper() override
;
68 // read access to evtl. advanced position
69 double getPosition() const { return mfPosition
; }
74 void pathTextBreakupHelper::freeB2DCubicBezierHelper()
76 mpB2DCubicBezierHelper
.reset();
79 basegfx::B2DCubicBezierHelper
* pathTextBreakupHelper::getB2DCubicBezierHelper()
81 if(!mpB2DCubicBezierHelper
&& maCurrentSegment
.isBezier())
83 mpB2DCubicBezierHelper
.reset(new basegfx::B2DCubicBezierHelper(maCurrentSegment
));
86 return mpB2DCubicBezierHelper
.get();
89 void pathTextBreakupHelper::advanceToPosition(double fNewPosition
)
91 while(mfSegmentStartPosition
+ mfCurrentSegmentLength
< fNewPosition
&& mnIndex
< mnMaxIndex
)
93 mfSegmentStartPosition
+= mfCurrentSegmentLength
;
96 if(mnIndex
< mnMaxIndex
)
98 freeB2DCubicBezierHelper();
99 mrPolygon
.getBezierSegment(mnIndex
% mrPolygon
.count(), maCurrentSegment
);
100 maCurrentSegment
.testAndSolveTrivialBezier();
101 mfCurrentSegmentLength
= getB2DCubicBezierHelper()
102 ? getB2DCubicBezierHelper()->getLength()
103 : maCurrentSegment
.getLength();
107 mfPosition
= fNewPosition
;
110 pathTextBreakupHelper::pathTextBreakupHelper(
111 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
& rSource
,
112 const basegfx::B2DPolygon
& rPolygon
,
113 const double fBasegfxPathLength
,
115 const basegfx::B2DPoint
& rTextStart
)
116 : drawinglayer::primitive2d::TextBreakupHelper(rSource
),
118 mfBasegfxPathLength(fBasegfxPathLength
),
120 mrTextStart(rTextStart
),
121 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 OUString
aTrimmedChars(aText
.copy(nIndex
, nLength
).trim());
154 const double fEndPos(mfPosition
+ fSnippetWidth
);
156 if(!aTrimmedChars
.isEmpty() && (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),
251 SvgTextPathNode::~SvgTextPathNode()
255 const SvgStyleAttributes
* SvgTextPathNode::getSvgStyleAttributes() const
257 return &maSvgStyleAttributes
;
260 void SvgTextPathNode::parseAttribute(const OUString
& rTokenName
, SVGToken aSVGToken
, const OUString
& aContent
)
263 SvgNode::parseAttribute(rTokenName
, aSVGToken
, aContent
);
265 // read style attributes
266 maSvgStyleAttributes
.parseStyleAttribute(aSVGToken
, aContent
, false);
271 case SVGToken::Style
:
273 readLocalCssStyle(aContent
);
276 case SVGToken::StartOffset
:
280 if(readSingleNumber(aContent
, aNum
))
282 if(aNum
.isPositive())
284 maStartOffset
= aNum
;
289 case SVGToken::Method
:
293 case SVGToken::Spacing
:
297 case SVGToken::XlinkHref
:
299 const sal_Int32
nLen(aContent
.getLength());
301 if(nLen
&& '#' == aContent
[0])
303 maXLink
= aContent
.copy(1);
314 bool SvgTextPathNode::isValid() const
316 const SvgPathNode
* pSvgPathNode
= dynamic_cast< const SvgPathNode
* >(getDocument().findSvgNodeById(maXLink
));
323 const basegfx::B2DPolyPolygon
* pPolyPolyPath
= pSvgPathNode
->getPath();
325 if(!pPolyPolyPath
|| !pPolyPolyPath
->count())
330 const basegfx::B2DPolygon
aPolygon(pPolyPolyPath
->getB2DPolygon(0));
332 if(!aPolygon
.count())
337 const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon
));
339 return !basegfx::fTools::equalZero(fBasegfxPathLength
);
342 void SvgTextPathNode::decomposePathNode(
343 const drawinglayer::primitive2d::Primitive2DContainer
& rPathContent
,
344 drawinglayer::primitive2d::Primitive2DContainer
& rTarget
,
345 const basegfx::B2DPoint
& rTextStart
) const
347 if(rPathContent
.empty())
350 const SvgPathNode
* pSvgPathNode
= dynamic_cast< const SvgPathNode
* >(getDocument().findSvgNodeById(maXLink
));
355 const basegfx::B2DPolyPolygon
* pPolyPolyPath
= pSvgPathNode
->getPath();
357 if(!(pPolyPolyPath
&& pPolyPolyPath
->count()))
360 basegfx::B2DPolygon
aPolygon(pPolyPolyPath
->getB2DPolygon(0));
362 if(pSvgPathNode
->getTransform())
364 aPolygon
.transform(*pSvgPathNode
->getTransform());
367 const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon
));
369 if(basegfx::fTools::equalZero(fBasegfxPathLength
))
372 double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user
374 if(pSvgPathNode
->getPathLength().isSet())
376 const double fUserLength(pSvgPathNode
->getPathLength().solve(*this));
378 if(fUserLength
> 0.0 && !basegfx::fTools::equal(fUserLength
, fBasegfxPathLength
))
380 fUserToBasegfx
= fUserLength
/ fBasegfxPathLength
;
384 double fPosition(0.0);
386 if(getStartOffset().isSet())
388 if (SvgUnit::percent
== getStartOffset().getUnit())
390 // percent are relative to path length
391 fPosition
= getStartOffset().getNumber() * 0.01 * fBasegfxPathLength
;
395 fPosition
= getStartOffset().solve(*this) * fUserToBasegfx
;
402 const sal_Int32
nLength(rPathContent
.size());
403 sal_Int32
nCurrent(0);
405 while(fPosition
< fBasegfxPathLength
&& nCurrent
< nLength
)
407 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
* pCandidate
= nullptr;
408 const drawinglayer::primitive2d::Primitive2DReference
xReference(rPathContent
[nCurrent
]);
412 pCandidate
= dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D
* >(xReference
.get());
417 const pathTextBreakupHelper
aPathTextBreakupHelper(
424 const drawinglayer::primitive2d::Primitive2DContainer
& aResult(
425 aPathTextBreakupHelper
.getResult());
429 rTarget
.append(aResult
);
432 // advance position to consumed
433 fPosition
= aPathTextBreakupHelper
.getPosition();
440 } // end of namespace svgio
442 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */