diff options
Diffstat (limited to '')
-rw-r--r-- | basegfx/source/polygon/b2dsvgpolypolygon.cxx | 931 |
1 files changed, 931 insertions, 0 deletions
diff --git a/basegfx/source/polygon/b2dsvgpolypolygon.cxx b/basegfx/source/polygon/b2dsvgpolypolygon.cxx new file mode 100644 index 000000000..aa0fedb2d --- /dev/null +++ b/basegfx/source/polygon/b2dsvgpolypolygon.cxx @@ -0,0 +1,931 @@ +/* -*- 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 <basegfx/polygon/b2dpolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygontools.hxx> +#include <basegfx/polygon/b2dpolypolygon.hxx> +#include <basegfx/matrix/b2dhommatrix.hxx> +#include <basegfx/matrix/b2dhommatrixtools.hxx> + +#include <rtl/ustring.hxx> +#include <sal/log.hxx> +#include <rtl/math.hxx> +#include <rtl/character.hxx> +#include <stringconversiontools.hxx> + +namespace +{ + +void putCommandChar(OUStringBuffer& rBuffer,sal_Unicode& rLastSVGCommand, sal_Unicode aChar, bool bToLower,bool bVerbose) +{ + const sal_Unicode aCommand = bToLower ? rtl::toAsciiLowerCase(aChar) : aChar; + + if (bVerbose && rBuffer.getLength()) + rBuffer.append(' '); + + if (bVerbose || rLastSVGCommand != aCommand) + { + rBuffer.append(aCommand); + rLastSVGCommand = aCommand; + } +} + +void putNumberChar(OUStringBuffer& rStr,double fValue, double fOldValue, bool bUseRelativeCoordinates,bool bVerbose) +{ + if (bUseRelativeCoordinates) + fValue -= fOldValue; + + const sal_Int32 aLen(rStr.getLength()); + if (bVerbose || (aLen && basegfx::internal::isOnNumberChar(rStr[aLen - 1], false) && fValue >= 0.0)) + rStr.append(' '); + + rStr.append(fValue); +} + +} + +namespace basegfx::utils +{ + bool PointIndex::operator<(const PointIndex& rComp) const + { + if(rComp.getPolygonIndex() == getPolygonIndex()) + { + return rComp.getPointIndex() < getPointIndex(); + } + + return rComp.getPolygonIndex() < getPolygonIndex(); + } + + bool importFromSvgD( + B2DPolyPolygon& o_rPolyPolygon, + std::u16string_view rSvgDStatement, + bool bHandleRelativeNextPointCompatible, + PointIndexSet* pHelpPointIndexSet) + { + o_rPolyPolygon.clear(); + const sal_Int32 nLen(rSvgDStatement.size()); + sal_Int32 nPos(0); + double nLastX( 0.0 ); + double nLastY( 0.0 ); + B2DPolygon aCurrPoly; + + // skip initial whitespace + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen) + { + bool bRelative(false); + const sal_Unicode aCurrChar(rSvgDStatement[nPos]); + + if(o_rPolyPolygon.count() && !aCurrPoly.count() && aCurrChar != 'm' && aCurrChar != 'M') + { + // we have a new sub-polygon starting, but without a 'moveto' command. + // this requires to add the current point as start point to the polygon + // (see SVG1.1 8.3.3 The "closepath" command) + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + switch(aCurrChar) + { + case 'z' : + case 'Z' : + { + // consume CurrChar and whitespace + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + // create closed polygon and reset import values + if(aCurrPoly.count()) + { + if(!bHandleRelativeNextPointCompatible) + { + // SVG defines that "the next subpath starts at the + // same initial point as the current subpath", so set the + // current point if we do not need to be compatible + nLastX = aCurrPoly.getB2DPoint(0).getX(); + nLastY = aCurrPoly.getB2DPoint(0).getY(); + } + + aCurrPoly.setClosed(true); + o_rPolyPolygon.append(aCurrPoly); + aCurrPoly.clear(); + } + + break; + } + + case 'm' : + case 'M' : + { + // create non-closed polygon and reset import values + if(aCurrPoly.count()) + { + o_rPolyPolygon.append(aCurrPoly); + aCurrPoly.clear(); + } + [[fallthrough]]; // to add coordinate data as 1st point of new polygon + } + case 'l' : + case 'L' : + { + if(aCurrChar == 'm' || aCurrChar == 'l') + { + bRelative = true; + } + + // consume CurrChar and whitespace + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + // set last position + nLastX = nX; + nLastY = nY; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 'h' : + { + bRelative = true; + [[fallthrough]]; + } + case 'H' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY(nLastY); + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + } + + // set last position + nLastX = nX; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 'v' : + { + bRelative = true; + [[fallthrough]]; + } + case 'V' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX(nLastX), nY; + + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nY += nLastY; + } + + // set last position + nLastY = nY; + + // add point + aCurrPoly.append(B2DPoint(nX, nY)); + } + break; + } + + case 's' : + { + bRelative = true; + [[fallthrough]]; + } + case 'S' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX2, nY2; + + if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX2 += nLastX; + nY2 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // get first control point. It's the reflection of the PrevControlPoint + // of the last point. If not existent, use current point (see SVG) + B2DPoint aPrevControl(B2DPoint(nLastX, nLastY)); + const sal_uInt32 nIndex(aCurrPoly.count() - 1); + + if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) + { + const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); + const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); + + // use mirrored previous control point + aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); + aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); + } + + // append curved edge + aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2, nY2), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + case 'c' : + { + bRelative = true; + [[fallthrough]]; + } + case 'C' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX1, nY1; + double nX2, nY2; + + if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX1 += nLastX; + nY1 += nLastY; + nX2 += nLastX; + nY2 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // append curved edge + aCurrPoly.appendBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX2, nY2), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + // #100617# quadratic beziers are imported as cubic ones + case 'q' : + { + bRelative = true; + [[fallthrough]]; + } + case 'Q' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double nX1, nY1; + + if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX1 += nLastX; + nY1 += nLastY; + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // append curved edge + aCurrPoly.appendQuadraticBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX, nY)); + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + // #100617# relative quadratic beziers are imported as cubic + case 't' : + { + bRelative = true; + [[fallthrough]]; + } + case 'T' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + // ensure existence of start point + if(!aCurrPoly.count()) + { + aCurrPoly.append(B2DPoint(nLastX, nLastY)); + } + + // get first control point. It's the reflection of the PrevControlPoint + // of the last point. If not existent, use current point (see SVG) + B2DPoint aPrevControl(B2DPoint(nLastX, nLastY)); + const sal_uInt32 nIndex(aCurrPoly.count() - 1); + const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); + + if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) + { + const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); + + // use mirrored previous control point + aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); + aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); + } + + if(!aPrevControl.equal(aPrevPoint)) + { + // there is a prev control point, and we have the already mirrored one + // in aPrevControl. We also need the quadratic control point for this + // new quadratic segment to calculate the 2nd cubic control point + const B2DPoint aQuadControlPoint( + ((3.0 * aPrevControl.getX()) - aPrevPoint.getX()) / 2.0, + ((3.0 * aPrevControl.getY()) - aPrevPoint.getY()) / 2.0); + + // calculate the cubic bezier coefficients from the quadratic ones. + const double nX2Prime((aQuadControlPoint.getX() * 2.0 + nX) / 3.0); + const double nY2Prime((aQuadControlPoint.getY() * 2.0 + nY) / 3.0); + + // append curved edge, use mirrored cubic control point directly + aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2Prime, nY2Prime), B2DPoint(nX, nY)); + } + else + { + // when no previous control, SVG says to use current point -> straight line. + // Just add end point + aCurrPoly.append(B2DPoint(nX, nY)); + } + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + case 'a' : + { + bRelative = true; + [[fallthrough]]; + } + case 'A' : + { + nPos++; + basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); + + while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) + { + double nX, nY; + double fRX, fRY, fPhi; + sal_Int32 bLargeArcFlag, bSweepFlag; + + if(!basegfx::internal::importDoubleAndSpaces(fRX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(fRY, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(fPhi, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importFlagAndSpaces(bLargeArcFlag, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importFlagAndSpaces(bSweepFlag, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; + + if(bRelative) + { + nX += nLastX; + nY += nLastY; + } + + if( rtl::math::approxEqual(nX, nLastX) && rtl::math::approxEqual(nY, nLastY) ) + continue; // start==end -> skip according to SVG spec + + if( fRX == 0.0 || fRY == 0.0 ) + { + // straight line segment according to SVG spec + aCurrPoly.append(B2DPoint(nX, nY)); + } + else + { + // normalize according to SVG spec + fRX=fabs(fRX); fRY=fabs(fRY); + + // from the SVG spec, appendix F.6.4 + + // |x1'| |cos phi sin phi| |(x1 - x2)/2| + // |y1'| = |-sin phi cos phi| |(y1 - y2)/2| + const B2DPoint p1(nLastX, nLastY); + const B2DPoint p2(nX, nY); + B2DHomMatrix aTransform(basegfx::utils::createRotateB2DHomMatrix( + -deg2rad(fPhi))); + + const B2DPoint p1_prime( aTransform * B2DPoint(((p1-p2)/2.0)) ); + + // ______________________________________ rx y1' + // |cx'| + / rx^2 ry^2 - rx^2 y1'^2 - ry^2 x1^2 ry + // |cy'| =-/ rx^2y1'^2 + ry^2 x1'^2 - ry x1' + // rx + // chose + if f_A != f_S + // chose - if f_A = f_S + B2DPoint aCenter_prime; + const double fRadicant( + (fRX*fRX*fRY*fRY - fRX*fRX*p1_prime.getY()*p1_prime.getY() - fRY*fRY*p1_prime.getX()*p1_prime.getX())/ + (fRX*fRX*p1_prime.getY()*p1_prime.getY() + fRY*fRY*p1_prime.getX()*p1_prime.getX())); + if( fRadicant < 0.0 ) + { + // no solution - according to SVG + // spec, scale up ellipse + // uniformly such that it passes + // through end points (denominator + // of radicant solved for fRY, + // with s=fRX/fRY) + const double fRatio(fRX/fRY); + const double fRadicant2( + p1_prime.getY()*p1_prime.getY() + + p1_prime.getX()*p1_prime.getX()/(fRatio*fRatio)); + if( fRadicant2 < 0.0 ) + { + // only trivial solution, one + // of the axes 0 -> straight + // line segment according to + // SVG spec + aCurrPoly.append(B2DPoint(nX, nY)); + continue; + } + + fRY=sqrt(fRadicant2); + fRX=fRatio*fRY; + + // keep center_prime forced to (0,0) + } + else + { + const double fFactor( + (bLargeArcFlag==bSweepFlag ? -1.0 : 1.0) * + sqrt(fRadicant)); + + // actually calculate center_prime + aCenter_prime = B2DPoint( + fFactor*fRX*p1_prime.getY()/fRY, + -fFactor*fRY*p1_prime.getX()/fRX); + } + + // + u - v + // angle(u,v) = arccos( ------------ ) (take the sign of (ux vy - uy vx)) + // - ||u|| ||v|| + + // 1 | (x1' - cx')/rx | + // theta1 = angle(( ), | | ) + // 0 | (y1' - cy')/ry | + const B2DPoint aRadii(fRX,fRY); + double fTheta1( + B2DVector(1.0,0.0).angle( + (p1_prime-aCenter_prime)/aRadii)); + + // |1| | (-x1' - cx')/rx | + // theta2 = angle( | | , | | ) + // |0| | (-y1' - cy')/ry | + double fTheta2( + B2DVector(1.0,0.0).angle( + (-p1_prime-aCenter_prime)/aRadii)); + + // map both angles to [0,2pi) + fTheta1 = fmod(2*M_PI+fTheta1,2*M_PI); + fTheta2 = fmod(2*M_PI+fTheta2,2*M_PI); + + // make sure the large arc is taken + // (since + // createPolygonFromEllipseSegment() + // normalizes to e.g. cw arc) + if( !bSweepFlag ) + std::swap(fTheta1,fTheta2); + + // finally, create bezier polygon from this + B2DPolygon aSegment( + utils::createPolygonFromUnitEllipseSegment( + fTheta1, fTheta2 )); + + // transform ellipse by rotation & move to final center + aTransform = basegfx::utils::createScaleB2DHomMatrix(fRX, fRY); + aTransform.translate(aCenter_prime.getX(), + aCenter_prime.getY()); + aTransform.rotate(deg2rad(fPhi)); + const B2DPoint aOffset((p1+p2)/2.0); + aTransform.translate(aOffset.getX(), + aOffset.getY()); + aSegment.transform(aTransform); + + // createPolygonFromEllipseSegment() + // always creates arcs that are + // positively oriented - flip polygon + // if we swapped angles above + if( !bSweepFlag ) + aSegment.flip(); + + // remember PointIndex of evtl. added pure helper points + sal_uInt32 nPointIndex(aCurrPoly.count() + 1); + aCurrPoly.append(aSegment); + + // if asked for, mark pure helper points by adding them to the index list of + // helper points + if(pHelpPointIndexSet && aCurrPoly.count() > 1) + { + const sal_uInt32 nPolyIndex(o_rPolyPolygon.count()); + + for(;nPointIndex + 1 < aCurrPoly.count(); nPointIndex++) + { + pHelpPointIndexSet->insert(PointIndex(nPolyIndex, nPointIndex)); + } + } + } + + // set last position + nLastX = nX; + nLastY = nY; + } + break; + } + + default: + { + SAL_WARN("basegfx", "importFromSvgD(): skipping tags in svg:d element (unknown: \"" + << OUString(aCurrChar) + << "\")!"); + ++nPos; + break; + } + } + } + + // if there is polygon data, create non-closed polygon + if(aCurrPoly.count()) + { + o_rPolyPolygon.append(aCurrPoly); + } + + return true; + } + + bool importFromSvgPoints( B2DPolygon& o_rPoly, + std::u16string_view rSvgPointsAttribute ) + { + o_rPoly.clear(); + const sal_Int32 nLen(rSvgPointsAttribute.size()); + sal_Int32 nPos(0); + double nX, nY; + + // skip initial whitespace + basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); + + while(nPos < nLen) + { + if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgPointsAttribute, nLen)) return false; + if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgPointsAttribute, nLen)) return false; + + // add point + o_rPoly.append(B2DPoint(nX, nY)); + + // skip to next number, or finish + basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); + } + + return true; + } + + OUString exportToSvgPoints( const B2DPolygon& rPoly ) + { + SAL_WARN_IF(rPoly.areControlPointsUsed(), "basegfx", "exportToSvgPoints: Only non-bezier polygons allowed (!)"); + const sal_uInt32 nPointCount(rPoly.count()); + OUStringBuffer aResult; + + for(sal_uInt32 a(0); a < nPointCount; a++) + { + const basegfx::B2DPoint aPoint(rPoly.getB2DPoint(a)); + + if(a) + { + aResult.append(' '); + } + + aResult.append(aPoint.getX()); + aResult.append(','); + aResult.append(aPoint.getY()); + } + + return aResult.makeStringAndClear(); + } + + OUString exportToSvgD( + const B2DPolyPolygon& rPolyPolygon, + bool bUseRelativeCoordinates, + bool bDetectQuadraticBeziers, + bool bHandleRelativeNextPointCompatible, + bool bOOXMLMotionPath) + { + const sal_uInt32 nCount(rPolyPolygon.count()); + sal_uInt32 nCombinedPointCount = 0; + for(sal_uInt32 i(0); i < nCount; i++) + { + const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); + nCombinedPointCount += aPolygon.count(); + } + + OUStringBuffer aResult(std::max<int>(nCombinedPointCount * 32,512)); + B2DPoint aCurrentSVGPosition(0.0, 0.0); // SVG assumes (0,0) as the initial current point + + for(sal_uInt32 i(0); i < nCount; i++) + { + const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); + const sal_uInt32 nPointCount(aPolygon.count()); + + if(nPointCount) + { + const bool bPolyUsesControlPoints(aPolygon.areControlPointsUsed()); + const sal_uInt32 nEdgeCount(aPolygon.isClosed() ? nPointCount : nPointCount - 1); + sal_Unicode aLastSVGCommand(' '); // last SVG command char + B2DPoint aLeft, aRight; // for quadratic bezier test + + // handle polygon start point + B2DPoint aEdgeStart(aPolygon.getB2DPoint(0)); + bool bUseRelativeCoordinatesForFirstPoint(bUseRelativeCoordinates); + + if(bHandleRelativeNextPointCompatible) + { + // To get around the error that the start point for the next polygon is the + // start point of the current one (and not the last as it was handled up to now) + // do force to write an absolute 'M' command as start for the next polygon + bUseRelativeCoordinatesForFirstPoint = false; + } + + // Write 'moveto' and the 1st coordinates, set aLastSVGCommand to 'lineto' + putCommandChar(aResult, aLastSVGCommand, 'M', bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); + aLastSVGCommand = bUseRelativeCoordinatesForFirstPoint ? 'l' : 'L'; + aCurrentSVGPosition = aEdgeStart; + + for(sal_uInt32 nIndex(0); nIndex < nEdgeCount; nIndex++) + { + // prepare access to next point + const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); + const B2DPoint aEdgeEnd(aPolygon.getB2DPoint(nNextIndex)); + + // handle edge from (aEdgeStart, aEdgeEnd) using indices (nIndex, nNextIndex) + const bool bEdgeIsBezier(bPolyUsesControlPoints + && (aPolygon.isNextControlPointUsed(nIndex) || aPolygon.isPrevControlPointUsed(nNextIndex))); + + if(bEdgeIsBezier) + { + // handle bezier edge + const B2DPoint aControlEdgeStart(aPolygon.getNextControlPoint(nIndex)); + const B2DPoint aControlEdgeEnd(aPolygon.getPrevControlPoint(nNextIndex)); + bool bIsQuadraticBezier(false); + + // check continuity at current edge's start point. For SVG, do NOT use an + // existing continuity since no 'S' or 's' statement should be written. At + // import, that 'previous' control vector is not available. SVG documentation + // says for interpretation: + + // "(If there is no previous command or if the previous command was + // not a C, c, S or s, assume the first control point is coincident + // with the current point.)" + + // That's what is done from our import, so avoid exporting it as first statement + // is necessary. + const bool bSymmetricAtEdgeStart( + !bOOXMLMotionPath && nIndex != 0 + && aPolygon.getContinuityInPoint(nIndex) == B2VectorContinuity::C2); + + if(bDetectQuadraticBeziers) + { + // check for quadratic beziers - that's + // the case if both control points are in + // the same place when they are prolonged + // to the common quadratic control point + + // Left: P = (3P1 - P0) / 2 + // Right: P = (3P2 - P3) / 2 + aLeft = B2DPoint((3.0 * aControlEdgeStart - aEdgeStart) / 2.0); + aRight= B2DPoint((3.0 * aControlEdgeEnd - aEdgeEnd) / 2.0); + bIsQuadraticBezier = aLeft.equal(aRight); + } + + if(bIsQuadraticBezier) + { + // approximately equal, export as quadratic bezier + if(bSymmetricAtEdgeStart) + { + putCommandChar(aResult, aLastSVGCommand, 'T', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + putCommandChar(aResult, aLastSVGCommand, 'Q', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aLeft.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aLeft.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + else + { + // export as cubic bezier + if(bSymmetricAtEdgeStart) + { + putCommandChar(aResult, aLastSVGCommand, 'S', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + putCommandChar(aResult, aLastSVGCommand, 'C', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aControlEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + } + else + { + // straight edge + if(nNextIndex == 0) + { + // it's a closed polygon's last edge and it's not a bezier edge, so there is + // no need to write it + } + else + { + const bool bXEqual(rtl::math::approxEqual(aEdgeStart.getX(), aEdgeEnd.getX())); + const bool bYEqual(rtl::math::approxEqual(aEdgeStart.getY(), aEdgeEnd.getY())); + + if(bXEqual && bYEqual) + { + // point is a double point; do not export at all + } + else if(bXEqual && !bOOXMLMotionPath) + { + // export as vertical line + putCommandChar(aResult, aLastSVGCommand, 'V', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else if(bYEqual && !bOOXMLMotionPath) + { + // export as horizontal line + putCommandChar(aResult, aLastSVGCommand, 'H', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + else + { + // export as line + putCommandChar(aResult, aLastSVGCommand, 'L', bUseRelativeCoordinates, bOOXMLMotionPath); + + putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); + putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); + aCurrentSVGPosition = aEdgeEnd; + } + } + } + + // prepare edge start for next loop step + aEdgeStart = aEdgeEnd; + } + + // close path if closed poly (Z and z are equivalent here, but looks nicer when case is matched) + if(aPolygon.isClosed()) + { + putCommandChar(aResult, aLastSVGCommand, 'Z', bUseRelativeCoordinates, bOOXMLMotionPath); + } + else if (bOOXMLMotionPath) + { + putCommandChar(aResult, aLastSVGCommand, 'E', bUseRelativeCoordinates, bOOXMLMotionPath); + } + + if(!bHandleRelativeNextPointCompatible) + { + // SVG defines that "the next subpath starts at the same initial point as the current subpath", + // so set aCurrentSVGPosition to the 1st point of the current, now ended and written path + aCurrentSVGPosition = aPolygon.getB2DPoint(0); + } + } + } + + return aResult.makeStringAndClear(); + } +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |