Bug 1754829 - Detect when an SVG path is a Rectangle. r=gfx-reviewers,mstange

This patch doesn't attempt to cache the result. I don't know if it will cause regressions in practice.

Differential Revision: https://phabricator.services.mozilla.com/D138465
This commit is contained in:
Nicolas Silva 2022-02-21 13:35:44 +00:00
Родитель bce38086e0
Коммит a7f5f3a538
5 изменённых файлов: 271 добавлений и 3 удалений

Просмотреть файл

@ -314,6 +314,23 @@ void SVGPathElement::GetMarkPoints(nsTArray<SVGMark>* aMarks) {
mD.GetAnimValue().GetMarkerPositioningData(aMarks);
}
void SVGPathElement::GetAsSimplePath(SimplePath* aSimplePath) {
aSimplePath->Reset();
auto callback = [&](const ComputedStyle* s) {
const nsStyleSVGReset* styleSVGReset = s->StyleSVGReset();
if (styleSVGReset->mD.IsPath()) {
auto pathData = styleSVGReset->mD.AsPath()._0.AsSpan();
auto maybeRect = SVGPathToAxisAlignedRect(pathData);
if (maybeRect.isSome()) {
Rect r = maybeRect.value();
aSimplePath->SetRect(r.x, r.y, r.width, r.height);
}
}
};
SVGGeometryProperty::DoForComputedStyle(this, callback);
}
already_AddRefed<Path> SVGPathElement::BuildPath(PathBuilder* aBuilder) {
// The Moz2D PathBuilder that our SVGPathData will be using only cares about
// the fill rule. However, in order to fulfill the requirements of the SVG

Просмотреть файл

@ -33,6 +33,8 @@ class SVGPathElement final : public SVGPathElementBase {
JS::Handle<JSObject*> aGivenProto) override;
explicit SVGPathElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo);
virtual void GetAsSimplePath(SimplePath* aSimplePath) override;
public:
NS_DECL_ADDSIZEOFEXCLUDINGTHIS

Просмотреть файл

@ -571,4 +571,241 @@ void SVGPathSegUtils::TraversePathSegment(const StylePathCommand& aCommand,
}
}
// 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<EdgeDir> GetDirection(Point v) {
if (!std::isfinite(v.x) || !std::isfinite(v.y)) {
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<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> 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},
};
auto ToGfxPoint = [](const StyleCoordPair& aPair) {
return Point(aPair._0, aPair._1);
};
for (const StylePathCommand& cmd : aPath) {
switch (cmd.tag) {
case StylePathCommand::Tag::MoveTo: {
Point to = ToGfxPoint(cmd.move_to.point);
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 = ToGfxPoint(cmd.line_to.point);
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

Просмотреть файл

@ -10,6 +10,7 @@
#include "mozilla/ArrayUtils.h"
#include "mozilla/dom/SVGPathSegBinding.h"
#include "mozilla/gfx/Point.h"
#include "mozilla/gfx/Rect.h"
#include "nsDebug.h"
namespace mozilla {
@ -266,6 +267,20 @@ class SVGPathSegUtils {
SVGPathTraversalState& aState);
};
/// Detect whether the path represents a rectangle (for both filling AND
/// stroking) and if so returns it.
///
/// This is typically useful for google slides which has many of these rectangle
/// shaped paths. It handles the same scenarios as skia's
/// SkPathPriv::IsRectContour which it is inspried from, including zero-length
/// edges and multiple points on edges of the rectangle, and doesn't attempt to
/// detect flat curves (that could easily be added but the expectation is that
/// since skia doesn't fast path it we're not likely to run into it in
/// practice).
///
/// We could implement something similar for polygons.
Maybe<gfx::Rect> SVGPathToAxisAlignedRect(Span<const StylePathCommand> aPath);
} // namespace mozilla
#endif // DOM_SVG_SVGPATHSEGUTILS_H_

Просмотреть файл

@ -1,3 +0,0 @@
[pathlength-001.svg]
expected:
FAIL