/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "SVGPathSegUtils.h" #include "mozilla/ArrayUtils.h" // MOZ_ARRAY_LENGTH #include "mozilla/ServoStyleConsts.h" // StylePathCommand #include "gfx2DGlue.h" #include "SVGPathDataParser.h" #include "nsMathUtils.h" #include "nsTextFormatter.h" using namespace mozilla::dom::SVGPathSeg_Binding; using namespace mozilla::gfx; namespace mozilla { static const float PATH_SEG_LENGTH_TOLERANCE = 0.0000001f; static const uint32_t MAX_RECURSION = 10; /* static */ void SVGPathSegUtils::GetValueAsString(const float* aSeg, nsAString& aValue) { // Adding new seg type? Is the formatting below acceptable for the new types? static_assert( NS_SVG_PATH_SEG_LAST_VALID_TYPE == PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL, "Update GetValueAsString for the new value."); static_assert(NS_SVG_PATH_SEG_MAX_ARGS == 7, "Add another case to the switch below."); uint32_t type = DecodeType(aSeg[0]); char16_t typeAsChar = GetPathSegTypeAsLetter(type); // Special case arcs: if (IsArcType(type)) { bool largeArcFlag = aSeg[4] != 0.0f; bool sweepFlag = aSeg[5] != 0.0f; nsTextFormatter::ssprintf(aValue, u"%c%g,%g %g %d,%d %g,%g", typeAsChar, aSeg[1], aSeg[2], aSeg[3], largeArcFlag, sweepFlag, aSeg[6], aSeg[7]); } else { switch (ArgCountForType(type)) { case 0: aValue = typeAsChar; break; case 1: nsTextFormatter::ssprintf(aValue, u"%c%g", typeAsChar, aSeg[1]); break; case 2: nsTextFormatter::ssprintf(aValue, u"%c%g,%g", typeAsChar, aSeg[1], aSeg[2]); break; case 4: nsTextFormatter::ssprintf(aValue, u"%c%g,%g %g,%g", typeAsChar, aSeg[1], aSeg[2], aSeg[3], aSeg[4]); break; case 6: nsTextFormatter::ssprintf(aValue, u"%c%g,%g %g,%g %g,%g", typeAsChar, aSeg[1], aSeg[2], aSeg[3], aSeg[4], aSeg[5], aSeg[6]); break; default: MOZ_ASSERT(false, "Unknown segment type"); aValue = u""; return; } } } static float CalcDistanceBetweenPoints(const Point& aP1, const Point& aP2) { return NS_hypot(aP2.x - aP1.x, aP2.y - aP1.y); } static void SplitQuadraticBezier(const Point* aCurve, Point* aLeft, Point* aRight) { aLeft[0].x = aCurve[0].x; aLeft[0].y = aCurve[0].y; aRight[2].x = aCurve[2].x; aRight[2].y = aCurve[2].y; aLeft[1].x = (aCurve[0].x + aCurve[1].x) / 2; aLeft[1].y = (aCurve[0].y + aCurve[1].y) / 2; aRight[1].x = (aCurve[1].x + aCurve[2].x) / 2; aRight[1].y = (aCurve[1].y + aCurve[2].y) / 2; aLeft[2].x = aRight[0].x = (aLeft[1].x + aRight[1].x) / 2; aLeft[2].y = aRight[0].y = (aLeft[1].y + aRight[1].y) / 2; } static void SplitCubicBezier(const Point* aCurve, Point* aLeft, Point* aRight) { Point tmp; tmp.x = (aCurve[1].x + aCurve[2].x) / 4; tmp.y = (aCurve[1].y + aCurve[2].y) / 4; aLeft[0].x = aCurve[0].x; aLeft[0].y = aCurve[0].y; aRight[3].x = aCurve[3].x; aRight[3].y = aCurve[3].y; aLeft[1].x = (aCurve[0].x + aCurve[1].x) / 2; aLeft[1].y = (aCurve[0].y + aCurve[1].y) / 2; aRight[2].x = (aCurve[2].x + aCurve[3].x) / 2; aRight[2].y = (aCurve[2].y + aCurve[3].y) / 2; aLeft[2].x = aLeft[1].x / 2 + tmp.x; aLeft[2].y = aLeft[1].y / 2 + tmp.y; aRight[1].x = aRight[2].x / 2 + tmp.x; aRight[1].y = aRight[2].y / 2 + tmp.y; aLeft[3].x = aRight[0].x = (aLeft[2].x + aRight[1].x) / 2; aLeft[3].y = aRight[0].y = (aLeft[2].y + aRight[1].y) / 2; } static float CalcBezLengthHelper(const Point* aCurve, uint32_t aNumPts, uint32_t aRecursionCount, void (*aSplit)(const Point*, Point*, Point*)) { Point left[4]; Point right[4]; float length = 0, dist; for (uint32_t i = 0; i < aNumPts - 1; i++) { length += CalcDistanceBetweenPoints(aCurve[i], aCurve[i + 1]); } dist = CalcDistanceBetweenPoints(aCurve[0], aCurve[aNumPts - 1]); if (length - dist > PATH_SEG_LENGTH_TOLERANCE && aRecursionCount < MAX_RECURSION) { aSplit(aCurve, left, right); ++aRecursionCount; return CalcBezLengthHelper(left, aNumPts, aRecursionCount, aSplit) + CalcBezLengthHelper(right, aNumPts, aRecursionCount, aSplit); } return length; } static inline float CalcLengthOfCubicBezier(const Point& aPos, const Point& aCP1, const Point& aCP2, const Point& aTo) { Point curve[4] = {aPos, aCP1, aCP2, aTo}; return CalcBezLengthHelper(curve, 4, 0, SplitCubicBezier); } static inline float CalcLengthOfQuadraticBezier(const Point& aPos, const Point& aCP, const Point& aTo) { Point curve[3] = {aPos, aCP, aTo}; return CalcBezLengthHelper(curve, 3, 0, SplitQuadraticBezier); } static void TraverseClosePath(const float* aArgs, SVGPathTraversalState& aState) { if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += CalcDistanceBetweenPoints(aState.pos, aState.start); aState.cp1 = aState.cp2 = aState.start; } aState.pos = aState.start; } static void TraverseMovetoAbs(const float* aArgs, SVGPathTraversalState& aState) { aState.start = aState.pos = Point(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { // aState.length is unchanged, since move commands don't affect path length. aState.cp1 = aState.cp2 = aState.start; } } static void TraverseMovetoRel(const float* aArgs, SVGPathTraversalState& aState) { aState.start = aState.pos += Point(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { // aState.length is unchanged, since move commands don't affect path length. aState.cp1 = aState.cp2 = aState.start; } } static void TraverseLinetoAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += CalcDistanceBetweenPoints(aState.pos, to); aState.cp1 = aState.cp2 = to; } aState.pos = to; } static void TraverseLinetoRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += CalcDistanceBetweenPoints(aState.pos, to); aState.cp1 = aState.cp2 = to; } aState.pos = to; } static void TraverseLinetoHorizontalAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[0], aState.pos.y); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.x - aState.pos.x); aState.cp1 = aState.cp2 = to; } aState.pos = to; } static void TraverseLinetoHorizontalRel(const float* aArgs, SVGPathTraversalState& aState) { aState.pos.x += aArgs[0]; if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(aArgs[0]); aState.cp1 = aState.cp2 = aState.pos; } } static void TraverseLinetoVerticalAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aState.pos.x, aArgs[0]); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.y - aState.pos.y); aState.cp1 = aState.cp2 = to; } aState.pos = to; } static void TraverseLinetoVerticalRel(const float* aArgs, SVGPathTraversalState& aState) { aState.pos.y += aArgs[0]; if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(aArgs[0]); aState.cp1 = aState.cp2 = aState.pos; } } static void TraverseCurvetoCubicAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[4], aArgs[5]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1(aArgs[0], aArgs[1]); Point cp2(aArgs[2], aArgs[3]); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; } static void TraverseCurvetoCubicSmoothAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[2], aArgs[3]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos - (aState.cp2 - aState.pos); Point cp2(aArgs[0], aArgs[1]); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; } static void TraverseCurvetoCubicRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[4], aArgs[5]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos + Point(aArgs[0], aArgs[1]); Point cp2 = aState.pos + Point(aArgs[2], aArgs[3]); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; } static void TraverseCurvetoCubicSmoothRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[2], aArgs[3]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos - (aState.cp2 - aState.pos); Point cp2 = aState.pos + Point(aArgs[0], aArgs[1]); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; } static void TraverseCurvetoQuadraticAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[2], aArgs[3]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp(aArgs[0], aArgs[1]); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; } static void TraverseCurvetoQuadraticSmoothAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = aState.pos - (aState.cp1 - aState.pos); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; } static void TraverseCurvetoQuadraticRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[2], aArgs[3]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = aState.pos + Point(aArgs[0], aArgs[1]); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; } static void TraverseCurvetoQuadraticSmoothRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[0], aArgs[1]); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = aState.pos - (aState.cp1 - aState.pos); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; } static void TraverseArcAbs(const float* aArgs, SVGPathTraversalState& aState) { Point to(aArgs[5], aArgs[6]); if (aState.ShouldUpdateLengthAndControlPoints()) { float dist = 0; Point radii(aArgs[0], aArgs[1]); if (radii.x == 0.0f || radii.y == 0.0f) { dist = CalcDistanceBetweenPoints(aState.pos, to); } else { Point bez[4] = {aState.pos, Point(0, 0), Point(0, 0), Point(0, 0)}; SVGArcConverter converter(aState.pos, to, radii, aArgs[2], aArgs[3] != 0, aArgs[4] != 0); while (converter.GetNextSegment(&bez[1], &bez[2], &bez[3])) { dist += CalcBezLengthHelper(bez, 4, 0, SplitCubicBezier); bez[0] = bez[3]; } } aState.length += dist; aState.cp1 = aState.cp2 = to; } aState.pos = to; } static void TraverseArcRel(const float* aArgs, SVGPathTraversalState& aState) { Point to = aState.pos + Point(aArgs[5], aArgs[6]); if (aState.ShouldUpdateLengthAndControlPoints()) { float dist = 0; Point radii(aArgs[0], aArgs[1]); if (radii.x == 0.0f || radii.y == 0.0f) { dist = CalcDistanceBetweenPoints(aState.pos, to); } else { Point bez[4] = {aState.pos, Point(0, 0), Point(0, 0), Point(0, 0)}; SVGArcConverter converter(aState.pos, to, radii, aArgs[2], aArgs[3] != 0, aArgs[4] != 0); while (converter.GetNextSegment(&bez[1], &bez[2], &bez[3])) { dist += CalcBezLengthHelper(bez, 4, 0, SplitCubicBezier); bez[0] = bez[3]; } } aState.length += dist; aState.cp1 = aState.cp2 = to; } aState.pos = to; } using TraverseFunc = void (*)(const float*, SVGPathTraversalState&); static TraverseFunc gTraverseFuncTable[NS_SVG_PATH_SEG_TYPE_COUNT] = { nullptr, // 0 == PATHSEG_UNKNOWN TraverseClosePath, TraverseMovetoAbs, TraverseMovetoRel, TraverseLinetoAbs, TraverseLinetoRel, TraverseCurvetoCubicAbs, TraverseCurvetoCubicRel, TraverseCurvetoQuadraticAbs, TraverseCurvetoQuadraticRel, TraverseArcAbs, TraverseArcRel, TraverseLinetoHorizontalAbs, TraverseLinetoHorizontalRel, TraverseLinetoVerticalAbs, TraverseLinetoVerticalRel, TraverseCurvetoCubicSmoothAbs, TraverseCurvetoCubicSmoothRel, TraverseCurvetoQuadraticSmoothAbs, TraverseCurvetoQuadraticSmoothRel}; /* static */ void SVGPathSegUtils::TraversePathSegment(const float* aData, SVGPathTraversalState& aState) { static_assert( MOZ_ARRAY_LENGTH(gTraverseFuncTable) == NS_SVG_PATH_SEG_TYPE_COUNT, "gTraverseFuncTable is out of date"); uint32_t type = DecodeType(aData[0]); gTraverseFuncTable[type](aData + 1, aState); } // Basically, this is just a variant version of the above TraverseXXX functions. // We just put those function inside this and use StylePathCommand instead. // This function and the above ones should be dropped by Bug 1388931. /* static */ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand, SVGPathTraversalState& aState) { switch (aCommand.tag) { case StylePathCommand::Tag::ClosePath: TraverseClosePath(nullptr, aState); break; case StylePathCommand::Tag::MoveTo: { const Point& p = aCommand.move_to.point.ConvertsToGfxPoint(); aState.start = aState.pos = aCommand.move_to.absolute == StyleIsAbsolute::Yes ? p : aState.pos + p; if (aState.ShouldUpdateLengthAndControlPoints()) { // aState.length is unchanged, since move commands don't affect path= // length. aState.cp1 = aState.cp2 = aState.start; } break; } case StylePathCommand::Tag::LineTo: { Point to = aCommand.line_to.absolute == StyleIsAbsolute::Yes ? aCommand.line_to.point.ConvertsToGfxPoint() : aState.pos + aCommand.line_to.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += CalcDistanceBetweenPoints(aState.pos, to); aState.cp1 = aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::CurveTo: { const bool isRelative = aCommand.curve_to.absolute == StyleIsAbsolute::No; Point to = isRelative ? aState.pos + aCommand.curve_to.point.ConvertsToGfxPoint() : aCommand.curve_to.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aCommand.curve_to.control1.ConvertsToGfxPoint(); Point cp2 = aCommand.curve_to.control2.ConvertsToGfxPoint(); if (isRelative) { cp1 += aState.pos; cp2 += aState.pos; } aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; break; } case StylePathCommand::Tag::QuadBezierCurveTo: { const bool isRelative = aCommand.curve_to.absolute == StyleIsAbsolute::No; Point to = isRelative ? aState.pos + aCommand.quad_bezier_curve_to.point.ConvertsToGfxPoint() : aCommand.quad_bezier_curve_to.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = isRelative ? aState.pos + aCommand.quad_bezier_curve_to.control1 .ConvertsToGfxPoint() : aCommand.quad_bezier_curve_to.control1.ConvertsToGfxPoint(); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::EllipticalArc: { Point to = aCommand.elliptical_arc.absolute == StyleIsAbsolute::Yes ? aCommand.elliptical_arc.point.ConvertsToGfxPoint() : aState.pos + aCommand.elliptical_arc.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { const auto& arc = aCommand.elliptical_arc; float dist = 0; Point radii(arc.rx, arc.ry); if (radii.x == 0.0f || radii.y == 0.0f) { dist = CalcDistanceBetweenPoints(aState.pos, to); } else { Point bez[4] = {aState.pos, Point(0, 0), Point(0, 0), Point(0, 0)}; SVGArcConverter converter(aState.pos, to, radii, arc.angle, arc.large_arc_flag._0, arc.sweep_flag._0); while (converter.GetNextSegment(&bez[1], &bez[2], &bez[3])) { dist += CalcBezLengthHelper(bez, 4, 0, SplitCubicBezier); bez[0] = bez[3]; } } aState.length += dist; aState.cp1 = aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::HorizontalLineTo: { Point to(aCommand.horizontal_line_to.absolute == StyleIsAbsolute::Yes ? aCommand.horizontal_line_to.x : aState.pos.x + aCommand.horizontal_line_to.x, aState.pos.y); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.x - aState.pos.x); aState.cp1 = aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::VerticalLineTo: { Point to(aState.pos.x, aCommand.vertical_line_to.absolute == StyleIsAbsolute::Yes ? aCommand.vertical_line_to.y : aState.pos.y + aCommand.vertical_line_to.y); if (aState.ShouldUpdateLengthAndControlPoints()) { aState.length += std::fabs(to.y - aState.pos.y); aState.cp1 = aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::SmoothCurveTo: { const bool isRelative = aCommand.smooth_curve_to.absolute == StyleIsAbsolute::No; Point to = isRelative ? aState.pos + aCommand.smooth_curve_to.point.ConvertsToGfxPoint() : aCommand.smooth_curve_to.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp1 = aState.pos - (aState.cp2 - aState.pos); Point cp2 = isRelative ? aState.pos + aCommand.smooth_curve_to.control2.ConvertsToGfxPoint() : aCommand.smooth_curve_to.control2.ConvertsToGfxPoint(); aState.length += (float)CalcLengthOfCubicBezier(aState.pos, cp1, cp2, to); aState.cp2 = cp2; aState.cp1 = to; } aState.pos = to; break; } case StylePathCommand::Tag::SmoothQuadBezierCurveTo: { Point to = aCommand.smooth_curve_to.absolute == StyleIsAbsolute::Yes ? aCommand.smooth_curve_to.point.ConvertsToGfxPoint() : aState.pos + aCommand.smooth_curve_to.point.ConvertsToGfxPoint(); if (aState.ShouldUpdateLengthAndControlPoints()) { Point cp = aState.pos - (aState.cp1 - aState.pos); aState.length += (float)CalcLengthOfQuadraticBezier(aState.pos, cp, to); aState.cp1 = cp; aState.cp2 = to; } aState.pos = to; break; } case StylePathCommand::Tag::Unknown: MOZ_ASSERT_UNREACHABLE("Unacceptable path segment type"); } } // Possible directions of an edge that doesn't immediately disqualify the path // as a rectangle. enum class EdgeDir { LEFT, RIGHT, UP, DOWN, // NONE represents (almost) zero-length edges, they should be ignored. NONE, }; Maybe GetDirection(Point v) { if (!std::isfinite(v.x.value) || !std::isfinite(v.y.value)) { return Nothing(); } bool x = fabs(v.x) > 0.001; bool y = fabs(v.y) > 0.001; if (x && y) { return Nothing(); } if (!x && !y) { return Some(EdgeDir::NONE); } if (x) { return Some(v.x > 0.0 ? EdgeDir::RIGHT : EdgeDir::LEFT); } return Some(v.y > 0.0 ? EdgeDir::DOWN : EdgeDir::UP); } EdgeDir OppositeDirection(EdgeDir dir) { switch (dir) { case EdgeDir::LEFT: return EdgeDir::RIGHT; case EdgeDir::RIGHT: return EdgeDir::LEFT; case EdgeDir::UP: return EdgeDir::DOWN; case EdgeDir::DOWN: return EdgeDir::UP; default: return EdgeDir::NONE; } } struct IsRectHelper { Point min; Point max; EdgeDir currentDir; // Index of the next corner. uint32_t idx; EdgeDir dirs[4]; bool Edge(Point from, Point to) { auto edge = to - from; auto maybeDir = GetDirection(edge); if (maybeDir.isNothing()) { return false; } EdgeDir dir = maybeDir.value(); if (dir == EdgeDir::NONE) { // zero-length edges aren't an issue. return true; } if (dir != currentDir) { // The edge forms a corner with the previous edge. if (idx >= 4) { // We are at the 5th corner, can't be a rectangle. return false; } if (dir == OppositeDirection(currentDir)) { // Can turn left or right but not a full 180 degrees. return false; } dirs[idx] = dir; idx += 1; currentDir = dir; } min.x = fmin(min.x, to.x); min.y = fmin(min.y, to.y); max.x = fmax(max.x, to.x); max.y = fmax(max.y, to.y); return true; } bool EndSubpath() { if (idx != 4) { return false; } if (dirs[0] != OppositeDirection(dirs[2]) || dirs[1] != OppositeDirection(dirs[3])) { return false; } return true; } }; bool ApproxEqual(gfx::Point a, gfx::Point b) { auto v = b - a; return fabs(v.x) < 0.001 && fabs(v.y) < 0.001; } Maybe SVGPathToAxisAlignedRect(Span aPath) { Point pathStart(0.0, 0.0); Point segStart(0.0, 0.0); IsRectHelper helper = { Point(0.0, 0.0), Point(0.0, 0.0), EdgeDir::NONE, 0, {EdgeDir::NONE, EdgeDir::NONE, EdgeDir::NONE, EdgeDir::NONE}, }; for (const StylePathCommand& cmd : aPath) { switch (cmd.tag) { case StylePathCommand::Tag::MoveTo: { Point to = cmd.move_to.point.ConvertsToGfxPoint(); if (helper.idx != 0) { // This is overly strict since empty moveto sequences such as "M 10 12 // M 3 2 M 0 0" render nothing, but I expect it won't make us miss a // lot of rect-shaped paths in practice and lets us avoidhandling // special caps for empty sub-paths like "M 0 0 L 0 0" and "M 1 2 Z". return Nothing(); } if (!ApproxEqual(pathStart, segStart)) { // If we were only interested in filling we could auto-close here // by calling helper.Edge like in the ClosePath case and detect some // unclosed paths as rectangles. // // For example: // - "M 1 0 L 0 0 L 0 1 L 1 1 L 1 0" are both rects for filling and // stroking. // - "M 1 0 L 0 0 L 0 1 L 1 1" fills a rect but the stroke is shaped // like a C. return Nothing(); } if (helper.idx != 0 && !helper.EndSubpath()) { return Nothing(); } if (cmd.move_to.absolute == StyleIsAbsolute::No) { to = segStart + to; } pathStart = to; segStart = to; if (helper.idx == 0) { helper.min = to; helper.max = to; } break; } case StylePathCommand::Tag::ClosePath: { if (!helper.Edge(segStart, pathStart)) { return Nothing(); } if (!helper.EndSubpath()) { return Nothing(); } pathStart = segStart; break; } case StylePathCommand::Tag::LineTo: { Point to = cmd.line_to.point.ConvertsToGfxPoint(); if (cmd.line_to.absolute == StyleIsAbsolute::No) { to = segStart + to; } if (!helper.Edge(segStart, to)) { return Nothing(); } segStart = to; break; } case StylePathCommand::Tag::HorizontalLineTo: { Point to = gfx::Point(cmd.horizontal_line_to.x, segStart.y); if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::No) { to.x += segStart.x; } if (!helper.Edge(segStart, to)) { return Nothing(); } segStart = to; break; } case StylePathCommand::Tag::VerticalLineTo: { Point to = gfx::Point(segStart.x, cmd.vertical_line_to.y); if (cmd.horizontal_line_to.absolute == StyleIsAbsolute::No) { to.y += segStart.y; } if (!helper.Edge(segStart, to)) { return Nothing(); } segStart = to; break; } default: return Nothing(); } } if (!ApproxEqual(pathStart, segStart)) { // Same situation as with moveto regarding stroking not fullly closed path // even though the fill is a rectangle. return Nothing(); } if (!helper.EndSubpath()) { return Nothing(); } auto size = (helper.max - helper.min); return Some(Rect(helper.min, Size(size.x, size.y))); } } // namespace mozilla