| /************************************************************** |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| *************************************************************/ |
| |
| // MARKER(update_precomp.py): autogen include statement, do not remove |
| #include "precompiled_svgio.hxx" |
| |
| #include <svgio/svgreader/svgtextpathnode.hxx> |
| #include <svgio/svgreader/svgstyleattributes.hxx> |
| #include <svgio/svgreader/svgpathnode.hxx> |
| #include <svgio/svgreader/svgdocument.hxx> |
| #include <svgio/svgreader/svgtrefnode.hxx> |
| #include <basegfx/polygon/b2dpolygon.hxx> |
| #include <basegfx/polygon/b2dpolygontools.hxx> |
| #include <drawinglayer/primitive2d/textbreakuphelper.hxx> |
| #include <drawinglayer/primitive2d/groupprimitive2d.hxx> |
| #include <basegfx/curve/b2dcubicbezier.hxx> |
| #include <basegfx/curve/b2dbeziertools.hxx> |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| |
| namespace svgio |
| { |
| namespace svgreader |
| { |
| class pathTextBreakupHelper : public drawinglayer::primitive2d::TextBreakupHelper |
| { |
| private: |
| const basegfx::B2DPolygon& mrPolygon; |
| const double mfBasegfxPathLength; |
| const double mfUserToBasegfx; |
| double mfPosition; |
| const basegfx::B2DPoint& mrTextStart; |
| |
| const sal_uInt32 mnMaxIndex; |
| sal_uInt32 mnIndex; |
| basegfx::B2DCubicBezier maCurrentSegment; |
| basegfx::B2DCubicBezierHelper* mpB2DCubicBezierHelper; |
| double mfCurrentSegmentLength; |
| double mfSegmentStartPosition; |
| |
| protected: |
| /// allow user callback to allow changes to the new TextTransformation. Default |
| /// does nothing. |
| virtual bool allowChange(sal_uInt32 nCount, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength); |
| |
| void freeB2DCubicBezierHelper(); |
| basegfx::B2DCubicBezierHelper* getB2DCubicBezierHelper(); |
| void advanceToPosition(double fNewPosition); |
| |
| public: |
| pathTextBreakupHelper( |
| const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource, |
| const basegfx::B2DPolygon& rPolygon, |
| const double fBasegfxPathLength, |
| const double fUserToBasegfx, |
| double fPosition, |
| const basegfx::B2DPoint& rTextStart); |
| virtual ~pathTextBreakupHelper(); |
| |
| // read access to evtl. advanced position |
| double getPosition() const { return mfPosition; } |
| |
| // get length of given text |
| double getLength(const rtl::OUString& rText) const; |
| }; |
| |
| double pathTextBreakupHelper::getLength(const rtl::OUString& rText) const |
| { |
| const sal_uInt32 nLength(rText.getLength()); |
| |
| if(nLength) |
| { |
| return getTextLayouter().getTextWidth(rText, 0, nLength); |
| } |
| |
| return 0.0; |
| } |
| |
| void pathTextBreakupHelper::freeB2DCubicBezierHelper() |
| { |
| if(mpB2DCubicBezierHelper) |
| { |
| delete mpB2DCubicBezierHelper; |
| mpB2DCubicBezierHelper = 0; |
| } |
| } |
| |
| basegfx::B2DCubicBezierHelper* pathTextBreakupHelper::getB2DCubicBezierHelper() |
| { |
| if(!mpB2DCubicBezierHelper && maCurrentSegment.isBezier()) |
| { |
| mpB2DCubicBezierHelper = new basegfx::B2DCubicBezierHelper(maCurrentSegment); |
| } |
| |
| return mpB2DCubicBezierHelper; |
| } |
| |
| void pathTextBreakupHelper::advanceToPosition(double fNewPosition) |
| { |
| while(mfSegmentStartPosition + mfCurrentSegmentLength < fNewPosition && mnIndex < mnMaxIndex) |
| { |
| mfSegmentStartPosition += mfCurrentSegmentLength; |
| mnIndex++; |
| |
| if(mnIndex < mnMaxIndex) |
| { |
| freeB2DCubicBezierHelper(); |
| mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment); |
| maCurrentSegment.testAndSolveTrivialBezier(); |
| mfCurrentSegmentLength = getB2DCubicBezierHelper() |
| ? getB2DCubicBezierHelper()->getLength() |
| : maCurrentSegment.getLength(); |
| } |
| } |
| |
| mfPosition = fNewPosition; |
| } |
| |
| pathTextBreakupHelper::pathTextBreakupHelper( |
| const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource, |
| const basegfx::B2DPolygon& rPolygon, |
| const double fBasegfxPathLength, |
| const double fUserToBasegfx, |
| double fPosition, |
| const basegfx::B2DPoint& rTextStart) |
| : drawinglayer::primitive2d::TextBreakupHelper(rSource), |
| mrPolygon(rPolygon), |
| mfBasegfxPathLength(fBasegfxPathLength), |
| mfUserToBasegfx(fUserToBasegfx), |
| mfPosition(0.0), |
| mrTextStart(rTextStart), |
| mnMaxIndex(rPolygon.isClosed() ? rPolygon.count() : rPolygon.count() - 1), |
| mnIndex(0), |
| maCurrentSegment(), |
| mpB2DCubicBezierHelper(0), |
| mfCurrentSegmentLength(0.0), |
| mfSegmentStartPosition(0.0) |
| { |
| mrPolygon.getBezierSegment(mnIndex % mrPolygon.count(), maCurrentSegment); |
| mfCurrentSegmentLength = maCurrentSegment.getLength(); |
| |
| advanceToPosition(fPosition); |
| } |
| |
| pathTextBreakupHelper::~pathTextBreakupHelper() |
| { |
| freeB2DCubicBezierHelper(); |
| } |
| |
| bool pathTextBreakupHelper::allowChange(sal_uInt32 /*nCount*/, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength) |
| { |
| bool bRetval(false); |
| |
| if(mfPosition < mfBasegfxPathLength && nLength && mnIndex < mnMaxIndex) |
| { |
| const double fSnippetWidth( |
| getTextLayouter().getTextWidth( |
| getSource().getText(), |
| nIndex, |
| nLength)); |
| |
| if(basegfx::fTools::more(fSnippetWidth, 0.0)) |
| { |
| const ::rtl::OUString aText(getSource().getText()); |
| const ::rtl::OUString aTrimmedChars(aText.copy(nIndex, nLength).trim()); |
| const double fEndPos(mfPosition + fSnippetWidth); |
| |
| if(aTrimmedChars.getLength() && (mfPosition < mfBasegfxPathLength || fEndPos > 0.0)) |
| { |
| const double fHalfSnippetWidth(fSnippetWidth * 0.5); |
| |
| advanceToPosition(mfPosition + fHalfSnippetWidth); |
| |
| // create representation for this snippet |
| bRetval = true; |
| |
| // get target position and tangent in that pint |
| basegfx::B2DPoint aPosition(0.0, 0.0); |
| basegfx::B2DVector aTangent(0.0, 1.0); |
| |
| if(mfPosition < 0.0) |
| { |
| // snippet center is left of first segment, but right edge is on it (SVG allows that) |
| aTangent = maCurrentSegment.getTangent(0.0); |
| aTangent.normalize(); |
| aPosition = maCurrentSegment.getStartPoint() + (aTangent * (mfPosition - mfSegmentStartPosition)); |
| } |
| else if(mfPosition > mfBasegfxPathLength) |
| { |
| // snippet center is right of last segment, but left edge is on it (SVG allows that) |
| aTangent = maCurrentSegment.getTangent(1.0); |
| aTangent.normalize(); |
| aPosition = maCurrentSegment.getEndPoint() + (aTangent * (mfPosition - mfSegmentStartPosition)); |
| } |
| else |
| { |
| // snippet center inside segment, interpolate |
| double fBezierDistance(mfPosition - mfSegmentStartPosition); |
| |
| if(getB2DCubicBezierHelper()) |
| { |
| // use B2DCubicBezierHelper to bridge the non-linear gap between |
| // length and bezier distances (if it's a bezier segment) |
| fBezierDistance = getB2DCubicBezierHelper()->distanceToRelative(fBezierDistance); |
| } |
| else |
| { |
| // linear relationship, make relative to segment length |
| fBezierDistance = fBezierDistance / mfCurrentSegmentLength; |
| } |
| |
| aPosition = maCurrentSegment.interpolatePoint(fBezierDistance); |
| aTangent = maCurrentSegment.getTangent(fBezierDistance); |
| aTangent.normalize(); |
| } |
| |
| // detect evtl. hor/ver translations (depends on text direction) |
| const basegfx::B2DPoint aBasePoint(rNewTransform * basegfx::B2DPoint(0.0, 0.0)); |
| const basegfx::B2DVector aOffset(aBasePoint - mrTextStart); |
| |
| if(!basegfx::fTools::equalZero(aOffset.getY())) |
| { |
| // ...and apply |
| aPosition.setY(aPosition.getY() + aOffset.getY()); |
| } |
| |
| // move target position from snippet center to left text start |
| aPosition -= fHalfSnippetWidth * aTangent; |
| |
| // remove current translation |
| rNewTransform.translate(-aBasePoint.getX(), -aBasePoint.getY()); |
| |
| // rotate due to tangent |
| rNewTransform.rotate(atan2(aTangent.getY(), aTangent.getX())); |
| |
| // add new translation |
| rNewTransform.translate(aPosition.getX(), aPosition.getY()); |
| } |
| |
| // advance to end |
| advanceToPosition(fEndPos); |
| } |
| } |
| |
| return bRetval; |
| } |
| |
| } // end of namespace svgreader |
| } // end of namespace svgio |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| |
| namespace svgio |
| { |
| namespace svgreader |
| { |
| SvgTextPathNode::SvgTextPathNode( |
| SvgDocument& rDocument, |
| SvgNode* pParent) |
| : SvgNode(SVGTokenTextPath, rDocument, pParent), |
| maSvgStyleAttributes(*this), |
| maXLink(), |
| maStartOffset(), |
| mbMethod(true), |
| mbSpacing(false) |
| { |
| } |
| |
| SvgTextPathNode::~SvgTextPathNode() |
| { |
| } |
| |
| const SvgStyleAttributes* SvgTextPathNode::getSvgStyleAttributes() const |
| { |
| return &maSvgStyleAttributes; |
| } |
| |
| void SvgTextPathNode::parseAttribute(const rtl::OUString& rTokenName, SVGToken aSVGToken, const rtl::OUString& aContent) |
| { |
| // call parent |
| SvgNode::parseAttribute(rTokenName, aSVGToken, aContent); |
| |
| // read style attributes |
| maSvgStyleAttributes.parseStyleAttribute(rTokenName, aSVGToken, aContent, false); |
| |
| // parse own |
| switch(aSVGToken) |
| { |
| case SVGTokenStyle: |
| { |
| readLocalCssStyle(aContent); |
| break; |
| } |
| case SVGTokenStartOffset: |
| { |
| SvgNumber aNum; |
| |
| if(readSingleNumber(aContent, aNum)) |
| { |
| if(aNum.isPositive()) |
| { |
| setStartOffset(aNum); |
| } |
| } |
| break; |
| } |
| case SVGTokenMethod: |
| { |
| if(aContent.getLength()) |
| { |
| static rtl::OUString aStrAlign(rtl::OUString::createFromAscii("align")); |
| static rtl::OUString aStrStretch(rtl::OUString::createFromAscii("stretch")); |
| |
| if(aContent.match(aStrAlign)) |
| { |
| setMethod(true); |
| } |
| else if(aContent.match(aStrStretch)) |
| { |
| setMethod(false); |
| } |
| } |
| break; |
| } |
| case SVGTokenSpacing: |
| { |
| if(aContent.getLength()) |
| { |
| static rtl::OUString aStrAuto(rtl::OUString::createFromAscii("auto")); |
| static rtl::OUString aStrExact(rtl::OUString::createFromAscii("exact")); |
| |
| if(aContent.match(aStrAuto)) |
| { |
| setSpacing(true); |
| } |
| else if(aContent.match(aStrExact)) |
| { |
| setSpacing(false); |
| } |
| } |
| break; |
| } |
| case SVGTokenXlinkHref: |
| { |
| const sal_Int32 nLen(aContent.getLength()); |
| |
| if(nLen && sal_Unicode('#') == aContent[0]) |
| { |
| maXLink = aContent.copy(1); |
| } |
| break; |
| } |
| default: |
| { |
| break; |
| } |
| } |
| } |
| |
| bool SvgTextPathNode::isValid() const |
| { |
| const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink)); |
| |
| if(!pSvgPathNode) |
| { |
| return false; |
| } |
| |
| const basegfx::B2DPolyPolygon* pPolyPolyPath = pSvgPathNode->getPath(); |
| |
| if(!pPolyPolyPath || !pPolyPolyPath->count()) |
| { |
| return false; |
| } |
| |
| const basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0)); |
| |
| if(!aPolygon.count()) |
| { |
| return false; |
| } |
| |
| const double fBasegfxPathLength(basegfx::tools::getLength(aPolygon)); |
| |
| if(basegfx::fTools::equalZero(fBasegfxPathLength)) |
| { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void SvgTextPathNode::decomposePathNode( |
| const drawinglayer::primitive2d::Primitive2DSequence& rPathContent, |
| drawinglayer::primitive2d::Primitive2DSequence& rTarget, |
| const basegfx::B2DPoint& rTextStart) const |
| { |
| if(rPathContent.hasElements()) |
| { |
| const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink)); |
| |
| if(pSvgPathNode) |
| { |
| const basegfx::B2DPolyPolygon* pPolyPolyPath = pSvgPathNode->getPath(); |
| |
| if(pPolyPolyPath && pPolyPolyPath->count()) |
| { |
| basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0)); |
| |
| if(pSvgPathNode->getTransform()) |
| { |
| aPolygon.transform(*pSvgPathNode->getTransform()); |
| } |
| |
| const double fBasegfxPathLength(basegfx::tools::getLength(aPolygon)); |
| |
| if(!basegfx::fTools::equalZero(fBasegfxPathLength)) |
| { |
| double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user |
| |
| if(pSvgPathNode->getPathLength().isSet()) |
| { |
| const double fUserLength(pSvgPathNode->getPathLength().solve(*this, length)); |
| |
| if(fUserLength > 0.0 && !basegfx::fTools::equal(fUserLength, fBasegfxPathLength)) |
| { |
| fUserToBasegfx = fUserLength / fBasegfxPathLength; |
| } |
| } |
| |
| double fPosition(0.0); |
| |
| if(getStartOffset().isSet()) |
| { |
| if(Unit_percent == getStartOffset().getUnit()) |
| { |
| // percent are relative to path length |
| fPosition = getStartOffset().getNumber() * 0.01 * fBasegfxPathLength; |
| } |
| else |
| { |
| fPosition = getStartOffset().solve(*this, length) * fUserToBasegfx; |
| } |
| } |
| |
| if(fPosition >= 0.0) |
| { |
| const sal_Int32 nLength(rPathContent.getLength()); |
| sal_Int32 nCurrent(0); |
| |
| while(fPosition < fBasegfxPathLength && nCurrent < nLength) |
| { |
| const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* pCandidate = 0; |
| const drawinglayer::primitive2d::Primitive2DReference xReference(rPathContent[nCurrent]); |
| |
| if(xReference.is()) |
| { |
| pCandidate = dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* >(xReference.get()); |
| } |
| |
| if(pCandidate) |
| { |
| const pathTextBreakupHelper aPathTextBreakupHelper( |
| *pCandidate, |
| aPolygon, |
| fBasegfxPathLength, |
| fUserToBasegfx, |
| fPosition, |
| rTextStart); |
| |
| const drawinglayer::primitive2d::Primitive2DSequence aResult( |
| aPathTextBreakupHelper.getResult(drawinglayer::primitive2d::BreakupUnit_character)); |
| |
| if(aResult.hasElements()) |
| { |
| drawinglayer::primitive2d::appendPrimitive2DSequenceToPrimitive2DSequence(rTarget, aResult); |
| } |
| |
| // advance position to consumed |
| fPosition = aPathTextBreakupHelper.getPosition(); |
| } |
| |
| nCurrent++; |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| } // end of namespace svgreader |
| } // end of namespace svgio |
| |
| ////////////////////////////////////////////////////////////////////////////// |
| // eof |