[SkSVGDevice] Initial clipping support

Implement SVG clips based on clip stack flattening -
which is now exposed in SkClipStack::asPath() and shared
with SkCanvas's simplify-clip code.

R=reed@google.com,mtklein@google.com

Review URL: https://codereview.chromium.org/876923003
diff --git a/experimental/svg/SkSVGDevice.cpp b/experimental/svg/SkSVGDevice.cpp
index bd503b7..9f4a54a8a 100644
--- a/experimental/svg/SkSVGDevice.cpp
+++ b/experimental/svg/SkSVGDevice.cpp
@@ -11,6 +11,7 @@
 #include "SkDraw.h"
 #include "SkPaint.h"
 #include "SkParsePath.h"
+#include "SkPathOps.h"
 #include "SkShader.h"
 #include "SkStream.h"
 #include "SkTypeface.h"
@@ -20,12 +21,10 @@
 namespace {
 
 static SkString svg_color(SkColor color) {
-    SkString colorStr;
-    colorStr.printf("rgb(%u,%u,%u)",
-                     SkColorGetR(color),
-                     SkColorGetG(color),
-                     SkColorGetB(color));
-    return colorStr;
+    return SkStringPrintf("rgb(%u,%u,%u)",
+                          SkColorGetR(color),
+                          SkColorGetG(color),
+                          SkColorGetB(color));
 }
 
 static SkScalar svg_opacity(SkColor color) {
@@ -102,6 +101,7 @@
         : fPaintServer(svg_color(paint.getColor())) {}
 
     SkString fPaintServer;
+    SkString fClip;
 };
 
 }
@@ -110,16 +110,19 @@
 // and deduplicate resources.
 class SkSVGDevice::ResourceBucket : ::SkNoncopyable {
 public:
-    ResourceBucket() : fGradientCount(0) {}
+    ResourceBucket() : fGradientCount(0), fClipCount(0) {}
 
     SkString addLinearGradient() {
-        SkString id;
-        id.printf("gradient_%d", fGradientCount++);
-        return id;
+        return SkStringPrintf("gradient_%d", fGradientCount++);
+    }
+
+    SkString addClip() {
+        return SkStringPrintf("clip_%d", fClipCount++);
     }
 
 private:
     uint32_t fGradientCount;
+    uint32_t fClipCount;
 };
 
 class SkSVGDevice::AutoElement : ::SkNoncopyable {
@@ -135,7 +138,7 @@
         : fWriter(writer)
         , fResourceBucket(bucket) {
 
-        Resources res = this->addResources(paint);
+        Resources res = this->addResources(draw, paint);
 
         fWriter->startElement(name);
 
@@ -167,11 +170,13 @@
         fWriter->addText(text.c_str());
     }
 
+    void addRectAttributes(const SkRect&);
     void addFontAttributes(const SkPaint&);
 
 private:
-    Resources addResources(const SkPaint& paint);
-    void addResourceDefs(const SkPaint& paint, Resources* resources);
+    Resources addResources(const SkDraw& draw, const SkPaint& paint);
+    void addClipResources(const SkDraw& draw, Resources* resources);
+    void addShaderResources(const SkPaint& paint, Resources* resources);
 
     void addPaint(const SkPaint& paint, const Resources& resources);
     void addTransform(const SkMatrix& transform, const char name[] = "transform");
@@ -200,6 +205,10 @@
     if (SK_AlphaOPAQUE != SkColorGetA(paint.getColor())) {
         this->addAttribute("opacity", svg_opacity(paint.getColor()));
     }
+
+    if (!resources.fClip.isEmpty()) {
+        this->addAttribute("clip-path", resources.fClip);
+    }
 }
 
 void SkSVGDevice::AutoElement::addTransform(const SkMatrix& t, const char name[]) {
@@ -233,18 +242,31 @@
     fWriter->addAttribute(name, tstr.c_str());
 }
 
-Resources SkSVGDevice::AutoElement::addResources(const SkPaint& paint) {
+Resources SkSVGDevice::AutoElement::addResources(const SkDraw& draw, const SkPaint& paint) {
     Resources resources(paint);
-    this->addResourceDefs(paint, &resources);
+
+    // FIXME: this is a weak heuristic and we end up with LOTS of redundant clips.
+    bool hasClip   = !draw.fClipStack->isWideOpen();
+    bool hasShader = SkToBool(paint.getShader());
+
+    if (hasClip || hasShader) {
+        AutoElement defs("defs", fWriter);
+
+        if (hasClip) {
+            this->addClipResources(draw, &resources);
+        }
+
+        if (hasShader) {
+            this->addShaderResources(paint, &resources);
+        }
+    }
+
     return resources;
 }
 
-void SkSVGDevice::AutoElement::addResourceDefs(const SkPaint& paint, Resources* resources) {
+void SkSVGDevice::AutoElement::addShaderResources(const SkPaint& paint, Resources* resources) {
     const SkShader* shader = paint.getShader();
-    if (!SkToBool(shader)) {
-        // TODO: clip support
-        return;
-    }
+    SkASSERT(SkToBool(shader));
 
     SkShader::GradientInfo grInfo;
     grInfo.fColorCount = 0;
@@ -254,21 +276,49 @@
         return;
     }
 
+    SkAutoSTArray<16, SkColor>  grColors(grInfo.fColorCount);
+    SkAutoSTArray<16, SkScalar> grOffsets(grInfo.fColorCount);
+    grInfo.fColors = grColors.get();
+    grInfo.fColorOffsets = grOffsets.get();
+
+    // One more call to get the actual colors/offsets.
+    shader->asAGradient(&grInfo);
+    SkASSERT(grInfo.fColorCount <= grColors.count());
+    SkASSERT(grInfo.fColorCount <= grOffsets.count());
+
+    resources->fPaintServer.printf("url(#%s)", addLinearGradientDef(grInfo, shader).c_str());
+}
+
+void SkSVGDevice::AutoElement::addClipResources(const SkDraw& draw, Resources* resources) {
+    SkASSERT(!draw.fClipStack->isWideOpen());
+
+    SkPath clipPath;
+    (void) draw.fClipStack->asPath(&clipPath);
+
+    SkString clipID = fResourceBucket->addClip();
+    const char* clipRule = clipPath.getFillType() == SkPath::kEvenOdd_FillType ?
+                           "evenodd" : "nonzero";
     {
-        AutoElement defs("defs", fWriter);
+        // clipPath is in device space, but since we're only pushing transform attributes
+        // to the leaf nodes, so are all our elements => SVG userSpaceOnUse == device space.
+        AutoElement clipPathElement("clipPath", fWriter);
+        clipPathElement.addAttribute("id", clipID);
 
-        SkAutoSTArray<16, SkColor>  grColors(grInfo.fColorCount);
-        SkAutoSTArray<16, SkScalar> grOffsets(grInfo.fColorCount);
-        grInfo.fColors = grColors.get();
-        grInfo.fColorOffsets = grOffsets.get();
-
-        // One more call to get the actual colors/offsets.
-        shader->asAGradient(&grInfo);
-        SkASSERT(grInfo.fColorCount <= grColors.count());
-        SkASSERT(grInfo.fColorCount <= grOffsets.count());
-
-        resources->fPaintServer.printf("url(#%s)", addLinearGradientDef(grInfo, shader).c_str());
+        SkRect clipRect = SkRect::MakeEmpty();
+        if (clipPath.isEmpty() || clipPath.isRect(&clipRect)) {
+            AutoElement rectElement("rect", fWriter);
+            rectElement.addRectAttributes(clipRect);
+            rectElement.addAttribute("clip-rule", clipRule);
+        } else {
+            AutoElement pathElement("path", fWriter);
+            SkString pathStr;
+            SkParsePath::ToSVGString(clipPath, &pathStr);
+            pathElement.addAttribute("d", pathStr.c_str());
+            pathElement.addAttribute("clip-rule", clipRule);
+        }
     }
+
+    resources->fClip.printf("url(#%s)", clipID.c_str());
 }
 
 SkString SkSVGDevice::AutoElement::addLinearGradientDef(const SkShader::GradientInfo& info,
@@ -307,6 +357,19 @@
     return id;
 }
 
+void SkSVGDevice::AutoElement::addRectAttributes(const SkRect& rect) {
+    // x, y default to 0
+    if (rect.x() != 0) {
+        this->addAttribute("x", rect.x());
+    }
+    if (rect.y() != 0) {
+        this->addAttribute("y", rect.y());
+    }
+
+    this->addAttribute("width", rect.width());
+    this->addAttribute("height", rect.height());
+}
+
 void SkSVGDevice::AutoElement::addFontAttributes(const SkPaint& paint) {
     this->addAttribute("font-size", paint.getTextSize());
 
@@ -365,10 +428,8 @@
 
 void SkSVGDevice::drawPaint(const SkDraw& draw, const SkPaint& paint) {
     AutoElement rect("rect", fWriter, fResourceBucket, draw, paint);
-    rect.addAttribute("x", 0);
-    rect.addAttribute("y", 0);
-    rect.addAttribute("width", this->width());
-    rect.addAttribute("height", this->height());
+    rect.addRectAttributes(SkRect::MakeWH(SkIntToScalar(this->width()),
+                                          SkIntToScalar(this->height())));
 }
 
 void SkSVGDevice::drawPoints(const SkDraw&, SkCanvas::PointMode mode, size_t count,
@@ -379,10 +440,7 @@
 
 void SkSVGDevice::drawRect(const SkDraw& draw, const SkRect& r, const SkPaint& paint) {
     AutoElement rect("rect", fWriter, fResourceBucket, draw, paint);
-    rect.addAttribute("x", r.fLeft);
-    rect.addAttribute("y", r.fTop);
-    rect.addAttribute("width", r.width());
-    rect.addAttribute("height", r.height());
+    rect.addRectAttributes(r);
 }
 
 void SkSVGDevice::drawOval(const SkDraw& draw, const SkRect& oval, const SkPaint& paint) {
diff --git a/include/core/SkClipStack.h b/include/core/SkClipStack.h
index 95e41e6..79789dc 100644
--- a/include/core/SkClipStack.h
+++ b/include/core/SkClipStack.h
@@ -323,6 +323,12 @@
      */
     bool quickContains(const SkRect& devRect) const;
 
+    /**
+     * Flattens the clip stack into a single SkPath. Returns true if any of
+     * the clip stack components requires anti-aliasing.
+     */
+    bool asPath(SkPath* path) const;
+
     void clipDevRect(const SkIRect& ir, SkRegion::Op op) {
         SkRect r;
         r.set(ir);
diff --git a/src/core/SkCanvas.cpp b/src/core/SkCanvas.cpp
index 5b37e08..56cfb8d 100644
--- a/src/core/SkCanvas.cpp
+++ b/src/core/SkCanvas.cpp
@@ -1446,30 +1446,11 @@
     fClipStack.clipDevPath(devPath, op, kSoft_ClipEdgeStyle == edgeStyle);
 
     if (fAllowSimplifyClip) {
-        devPath.reset();
-        devPath.setFillType(SkPath::kInverseEvenOdd_FillType);
-        const SkClipStack* clipStack = getClipStack();
-        SkClipStack::Iter iter(*clipStack, SkClipStack::Iter::kBottom_IterStart);
-        const SkClipStack::Element* element;
-        while ((element = iter.next())) {
-            SkClipStack::Element::Type type = element->getType();
-            SkPath operand;
-            if (type != SkClipStack::Element::kEmpty_Type) {
-                element->asPath(&operand);
-            }
-            SkRegion::Op elementOp = element->getOp();
-            if (elementOp == SkRegion::kReplace_Op) {
-                devPath = operand;
-            } else {
-                Op(devPath, operand, (SkPathOp) elementOp, &devPath);
-            }
-            // if the prev and curr clips disagree about aa -vs- not, favor the aa request.
-            // perhaps we need an API change to avoid this sort of mixed-signals about
-            // clipping.
-            if (element->isAA()) {
-                edgeStyle = kSoft_ClipEdgeStyle;
-            }
+        bool clipIsAA = getClipStack()->asPath(&devPath);
+        if (clipIsAA) {
+            edgeStyle = kSoft_ClipEdgeStyle;
         }
+
         op = SkRegion::kReplace_Op;
     }
 
diff --git a/src/core/SkClipStack.cpp b/src/core/SkClipStack.cpp
index 515596a..2d8c94f 100644
--- a/src/core/SkClipStack.cpp
+++ b/src/core/SkClipStack.cpp
@@ -8,6 +8,7 @@
 #include "SkCanvas.h"
 #include "SkClipStack.h"
 #include "SkPath.h"
+#include "SkPathOps.h"
 #include "SkThread.h"
 
 #include <new>
@@ -665,6 +666,35 @@
     return true;
 }
 
+bool SkClipStack::asPath(SkPath *path) const {
+    bool isAA = false;
+
+    path->reset();
+    path->setFillType(SkPath::kInverseEvenOdd_FillType);
+
+    SkClipStack::Iter iter(*this, SkClipStack::Iter::kBottom_IterStart);
+    while (const SkClipStack::Element* element = iter.next()) {
+        SkPath operand;
+        if (element->getType() != SkClipStack::Element::kEmpty_Type) {
+            element->asPath(&operand);
+        }
+
+        SkRegion::Op elementOp = element->getOp();
+        if (elementOp == SkRegion::kReplace_Op) {
+            *path = operand;
+        } else {
+            Op(*path, operand, (SkPathOp)elementOp, path);
+        }
+
+        // if the prev and curr clips disagree about aa -vs- not, favor the aa request.
+        // perhaps we need an API change to avoid this sort of mixed-signals about
+        // clipping.
+        isAA = (isAA || element->isAA());
+    }
+
+    return isAA;
+}
+
 void SkClipStack::pushElement(const Element& element) {
     // Use reverse iterator instead of back because Rect path may need previous
     SkDeque::Iter iter(fDeque, SkDeque::Iter::kBack_IterStart);