зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
bce38086e0
Коммит
a7f5f3a538
|
@ -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
|
Загрузка…
Ссылка в новой задаче