summaryrefslogtreecommitdiffstats
path: root/svgio/source/svgreader/svgtextpathnode.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'svgio/source/svgreader/svgtextpathnode.cxx')
-rw-r--r--svgio/source/svgreader/svgtextpathnode.cxx441
1 files changed, 441 insertions, 0 deletions
diff --git a/svgio/source/svgreader/svgtextpathnode.cxx b/svgio/source/svgreader/svgtextpathnode.cxx
new file mode 100644
index 000000000..43fccfa43
--- /dev/null
+++ b/svgio/source/svgreader/svgtextpathnode.cxx
@@ -0,0 +1,441 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ * 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 .
+ */
+
+#include <svgtextpathnode.hxx>
+#include <svgstyleattributes.hxx>
+#include <svgpathnode.hxx>
+#include <svgdocument.hxx>
+#include <basegfx/polygon/b2dpolygon.hxx>
+#include <basegfx/polygon/b2dpolygontools.hxx>
+#include <drawinglayer/primitive2d/textbreakuphelper.hxx>
+#include <drawinglayer/primitive2d/textprimitive2d.hxx>
+#include <basegfx/curve/b2dcubicbezier.hxx>
+#include <basegfx/curve/b2dbeziertools.hxx>
+#include <o3tl/string_view.hxx>
+
+namespace svgio::svgreader
+{
+ namespace {
+
+ class pathTextBreakupHelper : public drawinglayer::primitive2d::TextBreakupHelper
+ {
+ private:
+ const basegfx::B2DPolygon& mrPolygon;
+ const double mfBasegfxPathLength;
+ double mfPosition;
+ const basegfx::B2DPoint& mrTextStart;
+
+ const sal_uInt32 mnMaxIndex;
+ sal_uInt32 mnIndex;
+ basegfx::B2DCubicBezier maCurrentSegment;
+ std::unique_ptr<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) override;
+
+ void freeB2DCubicBezierHelper();
+ basegfx::B2DCubicBezierHelper* getB2DCubicBezierHelper();
+ void advanceToPosition(double fNewPosition);
+
+ public:
+ pathTextBreakupHelper(
+ const drawinglayer::primitive2d::TextSimplePortionPrimitive2D& rSource,
+ const basegfx::B2DPolygon& rPolygon,
+ const double fBasegfxPathLength,
+ double fPosition,
+ const basegfx::B2DPoint& rTextStart);
+ virtual ~pathTextBreakupHelper() override;
+
+ // read access to evtl. advanced position
+ double getPosition() const { return mfPosition; }
+ };
+
+ }
+
+ void pathTextBreakupHelper::freeB2DCubicBezierHelper()
+ {
+ mpB2DCubicBezierHelper.reset();
+ }
+
+ basegfx::B2DCubicBezierHelper* pathTextBreakupHelper::getB2DCubicBezierHelper()
+ {
+ if(!mpB2DCubicBezierHelper && maCurrentSegment.isBezier())
+ {
+ mpB2DCubicBezierHelper.reset(new basegfx::B2DCubicBezierHelper(maCurrentSegment));
+ }
+
+ return mpB2DCubicBezierHelper.get();
+ }
+
+ 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,
+ double fPosition,
+ const basegfx::B2DPoint& rTextStart)
+ : drawinglayer::primitive2d::TextBreakupHelper(rSource),
+ mrPolygon(rPolygon),
+ mfBasegfxPathLength(fBasegfxPathLength),
+ mfPosition(0.0),
+ mrTextStart(rTextStart),
+ mnMaxIndex(rPolygon.isClosed() ? rPolygon.count() : rPolygon.count() - 1),
+ mnIndex(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 OUString aText(getSource().getText());
+ const std::u16string_view aTrimmedChars(o3tl::trim(aText.subView(nIndex, nLength)));
+ const double fEndPos(mfPosition + fSnippetWidth);
+
+ if(!aTrimmedChars.empty() && (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 point
+ 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 svgio::svgreader
+
+
+namespace svgio::svgreader
+{
+ SvgTextPathNode::SvgTextPathNode(
+ SvgDocument& rDocument,
+ SvgNode* pParent)
+ : SvgNode(SVGToken::TextPath, rDocument, pParent),
+ maSvgStyleAttributes(*this)
+ {
+ }
+
+ SvgTextPathNode::~SvgTextPathNode()
+ {
+ }
+
+ const SvgStyleAttributes* SvgTextPathNode::getSvgStyleAttributes() const
+ {
+ return &maSvgStyleAttributes;
+ }
+
+ void SvgTextPathNode::parseAttribute(const OUString& rTokenName, SVGToken aSVGToken, const OUString& aContent)
+ {
+ // call parent
+ SvgNode::parseAttribute(rTokenName, aSVGToken, aContent);
+
+ // read style attributes
+ maSvgStyleAttributes.parseStyleAttribute(aSVGToken, aContent, false);
+
+ // parse own
+ switch(aSVGToken)
+ {
+ case SVGToken::Style:
+ {
+ readLocalCssStyle(aContent);
+ break;
+ }
+ case SVGToken::StartOffset:
+ {
+ SvgNumber aNum;
+
+ if(readSingleNumber(aContent, aNum))
+ {
+ if(aNum.isPositive())
+ {
+ maStartOffset = aNum;
+ }
+ }
+ break;
+ }
+ case SVGToken::Method:
+ {
+ break;
+ }
+ case SVGToken::Spacing:
+ {
+ break;
+ }
+ case SVGToken::Href:
+ case SVGToken::XlinkHref:
+ {
+ const sal_Int32 nLen(aContent.getLength());
+
+ if(nLen && '#' == 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 std::optional<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::utils::getLength(aPolygon));
+
+ return !basegfx::fTools::equalZero(fBasegfxPathLength);
+ }
+
+ void SvgTextPathNode::decomposePathNode(
+ const drawinglayer::primitive2d::Primitive2DContainer& rPathContent,
+ drawinglayer::primitive2d::Primitive2DContainer& rTarget,
+ const basegfx::B2DPoint& rTextStart) const
+ {
+ if(rPathContent.empty())
+ return;
+
+ const SvgPathNode* pSvgPathNode = dynamic_cast< const SvgPathNode* >(getDocument().findSvgNodeById(maXLink));
+
+ if(!pSvgPathNode)
+ return;
+
+ const std::optional<basegfx::B2DPolyPolygon>& pPolyPolyPath = pSvgPathNode->getPath();
+
+ if(!(pPolyPolyPath && pPolyPolyPath->count()))
+ return;
+
+ basegfx::B2DPolygon aPolygon(pPolyPolyPath->getB2DPolygon(0));
+
+ if(pSvgPathNode->getTransform())
+ {
+ aPolygon.transform(*pSvgPathNode->getTransform());
+ }
+
+ const double fBasegfxPathLength(basegfx::utils::getLength(aPolygon));
+
+ if(basegfx::fTools::equalZero(fBasegfxPathLength))
+ return;
+
+ double fUserToBasegfx(1.0); // multiply: user->basegfx, divide: basegfx->user
+
+ if(pSvgPathNode->getPathLength().isSet())
+ {
+ const double fUserLength(pSvgPathNode->getPathLength().solve(*this));
+
+ if(fUserLength > 0.0 && !basegfx::fTools::equal(fUserLength, fBasegfxPathLength))
+ {
+ fUserToBasegfx = fUserLength / fBasegfxPathLength;
+ }
+ }
+
+ double fPosition(0.0);
+
+ if(getStartOffset().isSet())
+ {
+ if (SvgUnit::percent == getStartOffset().getUnit())
+ {
+ // percent are relative to path length
+ fPosition = getStartOffset().getNumber() * 0.01 * fBasegfxPathLength;
+ }
+ else
+ {
+ fPosition = getStartOffset().solve(*this) * fUserToBasegfx;
+ }
+ }
+
+ if(fPosition < 0.0)
+ return;
+
+ const sal_Int32 nLength(rPathContent.size());
+ sal_Int32 nCurrent(0);
+
+ while(fPosition < fBasegfxPathLength && nCurrent < nLength)
+ {
+ const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* pCandidate = nullptr;
+ const drawinglayer::primitive2d::Primitive2DReference xReference(rPathContent[nCurrent]);
+
+ if(xReference.is())
+ {
+ pCandidate = dynamic_cast< const drawinglayer::primitive2d::TextSimplePortionPrimitive2D* >(xReference.get());
+ }
+
+ if(pCandidate)
+ {
+ pathTextBreakupHelper aPathTextBreakupHelper(
+ *pCandidate,
+ aPolygon,
+ fBasegfxPathLength,
+ fPosition,
+ rTextStart);
+
+ drawinglayer::primitive2d::Primitive2DContainer aResult =
+ aPathTextBreakupHelper.extractResult();
+
+ if(!aResult.empty())
+ {
+ rTarget.append(std::move(aResult));
+ }
+
+ // advance position to consumed
+ fPosition = aPathTextBreakupHelper.getPosition();
+ }
+
+ nCurrent++;
+ }
+ }
+
+} // end of namespace svgio
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */