1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 // MARKER(update_precomp.py): autogen include statement, do not remove
23 #include "precompiled_svgio.hxx"
24 
25 #include <svgio/svgreader/svgtextpathnode.hxx>
26 #include <svgio/svgreader/svgstyleattributes.hxx>
27 #include <svgio/svgreader/svgpathnode.hxx>
28 #include <svgio/svgreader/svgdocument.hxx>
29 #include <svgio/svgreader/svgtrefnode.hxx>
30 #include <basegfx/polygon/b2dpolygon.hxx>
31 #include <basegfx/polygon/b2dpolygontools.hxx>
32 #include <drawinglayer/primitive2d/textbreakuphelper.hxx>
33 #include <drawinglayer/primitive2d/groupprimitive2d.hxx>
34 #include <basegfx/curve/b2dcubicbezier.hxx>
35 #include <basegfx/curve/b2dbeziertools.hxx>
36 
37 //////////////////////////////////////////////////////////////////////////////
38 
39 namespace svgio
40 {
41     namespace svgreader
42     {
43         class pathTextBreakupHelper : public drawinglayer::primitive2d::TextBreakupHelper
44         {
45         private:
46             const basegfx::B2DPolygon&      mrPolygon;
47             const double                    mfBasegfxPathLength;
48             const double                    mfUserToBasegfx;
49             double                          mfPosition;
50             const basegfx::B2DPoint&        mrTextStart;
51 
52             const sal_uInt32                mnMaxIndex;
53             sal_uInt32                      mnIndex;
54             basegfx::B2DCubicBezier         maCurrentSegment;
55             basegfx::B2DCubicBezierHelper*  mpB2DCubicBezierHelper;
56             double                          mfCurrentSegmentLength;
57             double                          mfSegmentStartPosition;
58 
59         protected:
60             /// allow user callback to allow changes to the new TextTransformation. Default
61             /// does nothing.
62             virtual bool allowChange(sal_uInt32 nCount, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength);
63 
64             void freeB2DCubicBezierHelper();
65             basegfx::B2DCubicBezierHelper* getB2DCubicBezierHelper();
66             void advanceToPosition(double fNewPosition);
67 
68         public:
69             pathTextBreakupHelper(
70                 const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource,
71                 const basegfx::B2DPolygon& rPolygon,
72                 const double fBasegfxPathLength,
73                 const double fUserToBasegfx,
74                 double fPosition,
75                 const basegfx::B2DPoint& rTextStart);
76             virtual ~pathTextBreakupHelper();
77 
78             // read access to evtl. advanced position
getPosition() const79             double getPosition() const { return mfPosition; }
80 
81             // get length of given text
82             double getLength(const rtl::OUString& rText) const;
83         };
84 
getLength(const rtl::OUString & rText) const85         double pathTextBreakupHelper::getLength(const rtl::OUString& rText) const
86         {
87             const sal_uInt32 nLength(rText.getLength());
88 
89             if(nLength)
90             {
91                 return getTextLayouter().getTextWidth(rText, 0, nLength);
92             }
93 
94             return 0.0;
95         }
96 
freeB2DCubicBezierHelper()97         void pathTextBreakupHelper::freeB2DCubicBezierHelper()
98         {
99             if(mpB2DCubicBezierHelper)
100             {
101                 delete mpB2DCubicBezierHelper;
102                 mpB2DCubicBezierHelper = 0;
103             }
104         }
105 
getB2DCubicBezierHelper()106         basegfx::B2DCubicBezierHelper* pathTextBreakupHelper::getB2DCubicBezierHelper()
107         {
108             if(!mpB2DCubicBezierHelper && maCurrentSegment.isBezier())
109             {
110                 mpB2DCubicBezierHelper = new basegfx::B2DCubicBezierHelper(maCurrentSegment);
111             }
112 
113             return mpB2DCubicBezierHelper;
114         }
115 
advanceToPosition(double fNewPosition)116         void pathTextBreakupHelper::advanceToPosition(double fNewPosition)
117         {
118             while(mfSegmentStartPosition + mfCurrentSegmentLength < fNewPosition && mnIndex < mnMaxIndex)
119             {
120                 mfSegmentStartPosition += mfCurrentSegmentLength;
121                 mnIndex++;
122 
123                 if(mnIndex < mnMaxIndex)
124                 {
125                     freeB2DCubicBezierHelper();
126                     mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment);
127                     maCurrentSegment.testAndSolveTrivialBezier();
128                     mfCurrentSegmentLength = getB2DCubicBezierHelper()
129                         ? getB2DCubicBezierHelper()->getLength()
130                         : maCurrentSegment.getLength();
131                 }
132             }
133 
134             mfPosition = fNewPosition;
135         }
136 
pathTextBreakupHelper(const drawinglayer::primitive2d::TextSimplePortionPrimitive2D & rSource,const basegfx::B2DPolygon & rPolygon,const double fBasegfxPathLength,const double fUserToBasegfx,double fPosition,const basegfx::B2DPoint & rTextStart)137         pathTextBreakupHelper::pathTextBreakupHelper(
138             const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource,
139             const basegfx::B2DPolygon& rPolygon,
140             const double fBasegfxPathLength,
141             const double fUserToBasegfx,
142             double fPosition,
143             const basegfx::B2DPoint& rTextStart)
144         :   drawinglayer::primitive2d::TextBreakupHelper(rSource),
145             mrPolygon(rPolygon),
146             mfBasegfxPathLength(fBasegfxPathLength),
147             mfUserToBasegfx(fUserToBasegfx),
148             mfPosition(0.0),
149             mrTextStart(rTextStart),
150             mnMaxIndex(rPolygon.isClosed() ? rPolygon.count() : rPolygon.count() - 1),
151             mnIndex(0),
152             maCurrentSegment(),
153             mpB2DCubicBezierHelper(0),
154             mfCurrentSegmentLength(0.0),
155             mfSegmentStartPosition(0.0)
156         {
157             mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment);
158             mfCurrentSegmentLength = maCurrentSegment.getLength();
159 
160             advanceToPosition(fPosition);
161         }
162 
~pathTextBreakupHelper()163         pathTextBreakupHelper::~pathTextBreakupHelper()
164         {
165             freeB2DCubicBezierHelper();
166         }
167 
allowChange(sal_uInt32,basegfx::B2DHomMatrix & rNewTransform,sal_uInt32 nIndex,sal_uInt32 nLength)168         bool pathTextBreakupHelper::allowChange(sal_uInt32 /*nCount*/, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength)
169         {
170             bool bRetval(false);
171 
172             if(mfPosition < mfBasegfxPathLength && nLength && mnIndex < mnMaxIndex)
173             {
174                 const double fSnippetWidth(
175                     getTextLayouter().getTextWidth(
176                         getSource().getText(),
177                         nIndex,
178                         nLength));
179 
180                 if(basegfx::fTools::more(fSnippetWidth, 0.0))
181                 {
182                     const ::rtl::OUString aText(getSource().getText());
183                     const ::rtl::OUString aTrimmedChars(aText.copy(nIndex, nLength).trim());
184                     const double fEndPos(mfPosition + fSnippetWidth);
185 
186                     if(aTrimmedChars.getLength() && (mfPosition < mfBasegfxPathLength || fEndPos > 0.0))
187                     {
188                         const double fHalfSnippetWidth(fSnippetWidth * 0.5);
189 
190                         advanceToPosition(mfPosition + fHalfSnippetWidth);
191 
192                         // create representation for this snippet
193                         bRetval = true;
194 
195                         // get target position and tangent in that pint
196                         basegfx::B2DPoint aPosition(0.0, 0.0);
197                         basegfx::B2DVector aTangent(0.0, 1.0);
198 
199                         if(mfPosition < 0.0)
200                         {
201                             // snippet center is left of first segment, but right edge is on it (SVG allows that)
202                             aTangent = maCurrentSegment.getTangent(0.0);
203                             aTangent.normalize();
204                             aPosition = maCurrentSegment.getStartPoint() + (aTangent * (mfPosition - mfSegmentStartPosition));
205                         }
206                         else if(mfPosition > mfBasegfxPathLength)
207                         {
208                             // snippet center is right of last segment, but left edge is on it (SVG allows that)
209                             aTangent = maCurrentSegment.getTangent(1.0);
210                             aTangent.normalize();
211                             aPosition = maCurrentSegment.getEndPoint() + (aTangent * (mfPosition - mfSegmentStartPosition));
212                         }
213                         else
214                         {
215                             // snippet center inside segment, interpolate
216                             double fBezierDistance(mfPosition - mfSegmentStartPosition);
217 
218                             if(getB2DCubicBezierHelper())
219                             {
220                                 // use B2DCubicBezierHelper to bridge the non-linear gap between
221                                 // length and bezier distances (if it's a bezier segment)
222                                 fBezierDistance = getB2DCubicBezierHelper()->distanceToRelative(fBezierDistance);
223                             }
224                             else
225                             {
226                                 // linear relationship, make relative to segment length
227                                 fBezierDistance = fBezierDistance / mfCurrentSegmentLength;
228                             }
229 
230                             aPosition = maCurrentSegment.interpolatePoint(fBezierDistance);
231                             aTangent = maCurrentSegment.getTangent(fBezierDistance);
232                             aTangent.normalize();
233                         }
234 
235                         // detect evtl. hor/ver translations (depends on text direction)
236                         const basegfx::B2DPoint aBasePoint(rNewTransform * basegfx::B2DPoint(0.0, 0.0));
237                         const basegfx::B2DVector aOffset(aBasePoint - mrTextStart);
238 
239                         if(!basegfx::fTools::equalZero(aOffset.getY()))
240                         {
241                             // ...and apply
242                             aPosition.setY(aPosition.getY() + aOffset.getY());
243                         }
244 
245                         // move target position from snippet center to left text start
246                         aPosition -= fHalfSnippetWidth * aTangent;
247 
248                         // remove current translation
249                         rNewTransform.translate(-aBasePoint.getX(), -aBasePoint.getY());
250 
251                         // rotate due to tangent
252                         rNewTransform.rotate(atan2(aTangent.getY(), aTangent.getX()));
253 
254                         // add new translation
255                         rNewTransform.translate(aPosition.getX(), aPosition.getY());
256                     }
257 
258                     // advance to end
259                     advanceToPosition(fEndPos);
260                 }
261             }
262 
263             return bRetval;
264         }
265 
266     } // end of namespace svgreader
267 } // end of namespace svgio
268 
269 //////////////////////////////////////////////////////////////////////////////
270 
271 namespace svgio
272 {
273     namespace svgreader
274     {
SvgTextPathNode(SvgDocument & rDocument,SvgNode * pParent)275         SvgTextPathNode::SvgTextPathNode(
276             SvgDocument& rDocument,
277             SvgNode* pParent)
278         :   SvgNode(SVGTokenTextPath, rDocument, pParent),
279             maSvgStyleAttributes(*this),
280             maXLink(),
281             maStartOffset(),
282             mbMethod(true),
283             mbSpacing(false)
284         {
285         }
286 
~SvgTextPathNode()287         SvgTextPathNode::~SvgTextPathNode()
288         {
289         }
290 
getSvgStyleAttributes() const291         const SvgStyleAttributes* SvgTextPathNode::getSvgStyleAttributes() const
292         {
293             return &maSvgStyleAttributes;
294         }
295 
parseAttribute(const rtl::OUString & rTokenName,SVGToken aSVGToken,const rtl::OUString & aContent)296         void SvgTextPathNode::parseAttribute(const rtl::OUString& rTokenName, SVGToken aSVGToken, const rtl::OUString& aContent)
297         {
298             // call parent
299             SvgNode::parseAttribute(rTokenName, aSVGToken, aContent);
300 
301             // read style attributes
302             maSvgStyleAttributes.parseStyleAttribute(rTokenName, aSVGToken, aContent, false);
303 
304             // parse own
305             switch(aSVGToken)
306             {
307                 case SVGTokenStyle:
308                 {
309                     readLocalCssStyle(aContent);
310                     break;
311                 }
312                 case SVGTokenStartOffset:
313                 {
314                     SvgNumber aNum;
315 
316                     if(readSingleNumber(aContent, aNum))
317                     {
318                         if(aNum.isPositive())
319                         {
320                             setStartOffset(aNum);
321                         }
322                     }
323                     break;
324                 }
325                 case SVGTokenMethod:
326                 {
327                     if(aContent.getLength())
328                     {
329                         static rtl::OUString aStrAlign(rtl::OUString::createFromAscii("align"));
330                         static rtl::OUString aStrStretch(rtl::OUString::createFromAscii("stretch"));
331 
332                         if(aContent.match(aStrAlign))
333                         {
334                             setMethod(true);
335                         }
336                         else if(aContent.match(aStrStretch))
337                         {
338                             setMethod(false);
339                         }
340                     }
341                     break;
342                 }
343                 case SVGTokenSpacing:
344                 {
345                     if(aContent.getLength())
346                     {
347                         static rtl::OUString aStrAuto(rtl::OUString::createFromAscii("auto"));
348                         static rtl::OUString aStrExact(rtl::OUString::createFromAscii("exact"));
349 
350                         if(aContent.match(aStrAuto))
351                         {
352                             setSpacing(true);
353                         }
354                         else if(aContent.match(aStrExact))
355                         {
356                             setSpacing(false);
357                         }
358                     }
359                     break;
360                 }
361                 case SVGTokenXlinkHref:
362                 {
363                     const sal_Int32 nLen(aContent.getLength());
364 
365                     if(nLen && sal_Unicode('#') == aContent[0])
366                     {
367                         maXLink = aContent.copy(1);
368                     }
369                     break;
370                 }
371                 default:
372                 {
373                     break;
374                 }
375             }
376         }
377 
isValid() const378         bool SvgTextPathNode::isValid() const
379         {
380             const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink));
381 
382             if(!pSvgPathNode)
383             {
384                 return false;
385             }
386 
387             const basegfx::B2DPolyPolygon* pPolyPolyPath = pSvgPathNode->getPath();
388 
389             if(!pPolyPolyPath || !pPolyPolyPath->count())
390             {
391                 return false;
392             }
393 
394             const basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0));
395 
396             if(!aPolygon.count())
397             {
398                 return false;
399             }
400 
401             const double fBasegfxPathLength(basegfx::tools::getLength(aPolygon));
402 
403             if(basegfx::fTools::equalZero(fBasegfxPathLength))
404             {
405                 return false;
406             }
407 
408             return true;
409         }
410 
decomposePathNode(const drawinglayer::primitive2d::Primitive2DSequence & rPathContent,drawinglayer::primitive2d::Primitive2DSequence & rTarget,const basegfx::B2DPoint & rTextStart) const411         void SvgTextPathNode::decomposePathNode(
412             const drawinglayer::primitive2d::Primitive2DSequence& rPathContent,
413             drawinglayer::primitive2d::Primitive2DSequence& rTarget,
414             const basegfx::B2DPoint& rTextStart) const
415         {
416             if(rPathContent.hasElements())
417             {
418                 const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink));
419 
420                 if(pSvgPathNode)
421                 {
422                     const basegfx::B2DPolyPolygon* pPolyPolyPath = pSvgPathNode->getPath();
423 
424                     if(pPolyPolyPath && pPolyPolyPath->count())
425                     {
426                         basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0));
427 
428                         if(pSvgPathNode->getTransform())
429                         {
430                             aPolygon.transform(*pSvgPathNode->getTransform());
431                         }
432 
433                         const double fBasegfxPathLength(basegfx::tools::getLength(aPolygon));
434 
435                         if(!basegfx::fTools::equalZero(fBasegfxPathLength))
436                         {
437                             double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user
438 
439                             if(pSvgPathNode->getPathLength().isSet())
440                             {
441                                 const double fUserLength(pSvgPathNode->getPathLength().solve(*this, length));
442 
443                                 if(fUserLength > 0.0 && !basegfx::fTools::equal(fUserLength, fBasegfxPathLength))
444                                 {
445                                     fUserToBasegfx = fUserLength / fBasegfxPathLength;
446                                 }
447                             }
448 
449                             double fPosition(0.0);
450 
451                             if(getStartOffset().isSet())
452                             {
453                                 if(Unit_percent == getStartOffset().getUnit())
454                                 {
455                                     // percent are relative to path length
456                                     fPosition = getStartOffset().getNumber() * 0.01 * fBasegfxPathLength;
457                                 }
458                                 else
459                                 {
460                                     fPosition = getStartOffset().solve(*this, length) * fUserToBasegfx;
461                                 }
462                             }
463 
464                             if(fPosition >= 0.0)
465                             {
466                                 const sal_Int32 nLength(rPathContent.getLength());
467                                 sal_Int32 nCurrent(0);
468 
469                                 while(fPosition < fBasegfxPathLength && nCurrent < nLength)
470                                 {
471                                     const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* pCandidate = 0;
472                                     const drawinglayer::primitive2d::Primitive2DReference xReference(rPathContent[nCurrent]);
473 
474                                     if(xReference.is())
475                                     {
476                                         pCandidate = dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* >(xReference.get());
477                                     }
478 
479                                     if(pCandidate)
480                                     {
481                                         const pathTextBreakupHelper aPathTextBreakupHelper(
482                                             *pCandidate,
483                                             aPolygon,
484                                             fBasegfxPathLength,
485                                             fUserToBasegfx,
486                                             fPosition,
487                                             rTextStart);
488 
489                                         const drawinglayer::primitive2d::Primitive2DSequence aResult(
490                                             aPathTextBreakupHelper.getResult(drawinglayer::primitive2d::BreakupUnit_character));
491 
492                                         if(aResult.hasElements())
493                                         {
494                                             drawinglayer::primitive2d::appendPrimitive2DSequenceToPrimitive2DSequence(rTarget, aResult);
495                                         }
496 
497                                         // advance position to consumed
498                                         fPosition = aPathTextBreakupHelper.getPosition();
499                                     }
500 
501                                     nCurrent++;
502                                 }
503                             }
504                         }
505                     }
506                 }
507             }
508         }
509 
510     } // end of namespace svgreader
511 } // end of namespace svgio
512 
513 //////////////////////////////////////////////////////////////////////////////
514 // eof
515