Bug 1080688 - Calculate SVG rect bounds using a simple rect transform rather than using a Moz2D Path. r=longsonr, a=lsblakk
authorJonathan Watt <jwatt@jwatt.org>
Sun, 26 Oct 2014 18:00:03 +0000
changeset 234066 0664db53c0289223cb29be28d8d664c0cc27682c
parent 234065 64f4fbfe44fe67840b7b32e1687b1a7f258a08ac
child 234067 f59f7d0605b854f4c6ece876960720b01ce00a08
push id1
push usersledru@mozilla.com
push dateThu, 04 Dec 2014 17:57:20 +0000
reviewerslongsonr, lsblakk
bugs1080688
milestone35.0a2
Bug 1080688 - Calculate SVG rect bounds using a simple rect transform rather than using a Moz2D Path. r=longsonr, a=lsblakk
content/svg/content/src/SVGRectElement.cpp
content/svg/content/src/SVGRectElement.h
content/svg/content/src/nsSVGPathGeometryElement.h
layout/style/nsStyleStruct.h
layout/svg/nsSVGPathGeometryFrame.cpp
--- a/content/svg/content/src/SVGRectElement.cpp
+++ b/content/svg/content/src/SVGRectElement.cpp
@@ -2,16 +2,18 @@
 /* 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 "mozilla/dom/SVGRectElement.h"
 #include "nsGkAtoms.h"
 #include "mozilla/dom/SVGRectElementBinding.h"
 #include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/gfx/Rect.h"
 #include "mozilla/gfx/PathHelpers.h"
 #include <algorithm>
 
 NS_IMPL_NS_NEW_NAMESPACED_SVG_ELEMENT(Rect)
 
 using namespace mozilla::gfx;
 
 namespace mozilla {
@@ -103,16 +105,46 @@ SVGRectElement::GetLengthInfo()
 {
   return LengthAttributesInfo(mLengthAttributes, sLengthInfo,
                               ArrayLength(sLengthInfo));
 }
 
 //----------------------------------------------------------------------
 // nsSVGPathGeometryElement methods
 
+bool
+SVGRectElement::GetGeometryBounds(Rect* aBounds, Float aStrokeWidth,
+                                  const Matrix& aTransform)
+{
+  Rect r;
+  Float rx, ry;
+  GetAnimatedLengthValues(&r.x, &r.y, &r.width, &r.height, &rx, &ry, nullptr);
+
+  if (r.IsEmpty()) {
+    // Rendering of the element disabled
+    r.SetEmpty(); // make sure width/height are actually zero
+    *aBounds = r;
+    return true;
+  }
+
+  rx = std::max(rx, 0.0f);
+  ry = std::max(ry, 0.0f);
+
+  if (rx != 0 || ry != 0) {
+    return false;
+  }
+
+  if (aStrokeWidth > 0.f) {
+    r.Inflate(aStrokeWidth / 2.f);
+  }
+
+  *aBounds = aTransform.TransformBounds(r);
+  return true;
+}
+
 TemporaryRef<Path>
 SVGRectElement::BuildPath(PathBuilder* aBuilder)
 {
   float x, y, width, height, rx, ry;
   GetAnimatedLengthValues(&x, &y, &width, &height, &rx, &ry, nullptr);
 
   if (width <= 0 || height <= 0) {
     return nullptr;
--- a/content/svg/content/src/SVGRectElement.h
+++ b/content/svg/content/src/SVGRectElement.h
@@ -25,16 +25,18 @@ protected:
   friend nsresult (::NS_NewSVGRectElement(nsIContent **aResult,
                                           already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo));
 
 public:
   // nsSVGSVGElement methods:
   virtual bool HasValidDimensions() const MOZ_OVERRIDE;
 
   // nsSVGPathGeometryElement methods:
+  virtual bool GetGeometryBounds(Rect* aBounds, Float aStrokeWidth,
+                                 const Matrix& aTransform) MOZ_OVERRIDE;
   virtual TemporaryRef<Path> BuildPath(PathBuilder* aBuilder = nullptr) MOZ_OVERRIDE;
 
   virtual nsresult Clone(mozilla::dom::NodeInfo *aNodeInfo, nsINode **aResult) const MOZ_OVERRIDE;
 
   // WebIDL
   already_AddRefed<SVGAnimatedLength> X();
   already_AddRefed<SVGAnimatedLength> Y();
   already_AddRefed<SVGAnimatedLength> Height();
--- a/content/svg/content/src/nsSVGPathGeometryElement.h
+++ b/content/svg/content/src/nsSVGPathGeometryElement.h
@@ -29,18 +29,20 @@ struct nsSVGMark {
 typedef mozilla::dom::SVGGraphicsElement nsSVGPathGeometryElementBase;
 
 class nsSVGPathGeometryElement : public nsSVGPathGeometryElementBase
 {
 protected:
   typedef mozilla::gfx::DrawTarget DrawTarget;
   typedef mozilla::gfx::FillRule FillRule;
   typedef mozilla::gfx::Float Float;
+  typedef mozilla::gfx::Matrix Matrix;
   typedef mozilla::gfx::Path Path;
   typedef mozilla::gfx::PathBuilder PathBuilder;
+  typedef mozilla::gfx::Rect Rect;
 
 public:
   explicit nsSVGPathGeometryElement(already_AddRefed<mozilla::dom::NodeInfo>& aNodeInfo);
 
   virtual nsresult AfterSetAttr(int32_t aNamespaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue, bool aNotify) MOZ_OVERRIDE;
 
   /**
@@ -62,16 +64,21 @@ public:
    * This could be moved up to a more general class so it can be used for non-leaf
    * elements, but that would require care and for now there's no need.
    */
   bool GeometryDependsOnCoordCtx();
 
   virtual bool IsMarkable();
   virtual void GetMarkPoints(nsTArray<nsSVGMark> *aMarks);
 
+  virtual bool GetGeometryBounds(Rect* aBounds, Float aStrokeWidth,
+                                 const Matrix& aTransform) {
+    return false;
+  }
+
   /**
    * Returns a Path that can be used to paint, hit-test or calculate bounds for
    * this element. May return nullptr if there is no [valid] path. The path
    * that is created may be cached and returned on subsequent calls.
    */
   virtual mozilla::TemporaryRef<Path> GetOrBuildPath(const DrawTarget& aDrawTarget,
                                                      FillRule fillRule);
 
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -3007,16 +3007,20 @@ struct nsStyleSVGReset {
     return NS_CombineHint(nsChangeHint_NeedReflow,
                           nsChangeHint_ClearAncestorIntrinsics);
   }
 
   bool HasFilters() const {
     return mFilters.Length() > 0;
   }
 
+  bool HasNonScalingStroke() const {
+    return mVectorEffect == NS_STYLE_VECTOR_EFFECT_NON_SCALING_STROKE;
+  }
+
   nsStyleClipPath mClipPath;          // [reset]
   nsTArray<nsStyleFilter> mFilters;   // [reset]
   nsCOMPtr<nsIURI> mMask;             // [reset]
   nscolor          mStopColor;        // [reset]
   nscolor          mFloodColor;       // [reset]
   nscolor          mLightingColor;    // [reset]
 
   float            mStopOpacity;      // [reset]
--- a/layout/svg/nsSVGPathGeometryFrame.cpp
+++ b/layout/svg/nsSVGPathGeometryFrame.cpp
@@ -457,123 +457,142 @@ nsSVGPathGeometryFrame::GetBBoxContribut
   if (aToBBoxUserspace.IsSingular()) {
     // XXX ReportToConsole
     return bbox;
   }
 
   nsSVGPathGeometryElement* element =
     static_cast<nsSVGPathGeometryElement*>(mContent);
 
-  RefPtr<DrawTarget> tmpDT;
-#ifdef XP_WIN
-  // Unfortunately D2D backed DrawTarget produces bounds with rounding errors
-  // when whole number results are expected, even in the case of trivial
-  // calculations. To avoid that and meet the expectations of web content we
-  // have to use a CAIRO DrawTarget. The most efficient way to do that is to
-  // wrap the cached cairo_surface_t from ScreenReferenceSurface():
-  nsRefPtr<gfxASurface> refSurf =
-    gfxPlatform::GetPlatform()->ScreenReferenceSurface();
-  tmpDT = gfxPlatform::GetPlatform()->
-    CreateDrawTargetForSurface(refSurf, IntSize(1, 1));
-#else
-  tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
-#endif
+  bool getFill = (aFlags & nsSVGUtils::eBBoxIncludeFillGeometry) ||
+                 ((aFlags & nsSVGUtils::eBBoxIncludeFill) &&
+                  StyleSVG()->mFill.mType != eStyleSVGPaintType_None);
+
+  bool getStroke = (aFlags & nsSVGUtils::eBBoxIncludeStrokeGeometry) ||
+                   ((aFlags & nsSVGUtils::eBBoxIncludeStroke) &&
+                    nsSVGUtils::HasStroke(this));
 
-  FillRule fillRule = nsSVGUtils::ToFillRule(StyleSVG()->mFillRule);
-  RefPtr<Path> pathInUserSpace = element->GetOrBuildPath(*tmpDT, fillRule);
-  if (!pathInUserSpace) {
-    return bbox;
-  }
-  RefPtr<Path> pathInBBoxSpace;
-  if (aToBBoxUserspace.IsIdentity()) {
-    pathInBBoxSpace = pathInUserSpace;
-  } else {
-    RefPtr<PathBuilder> builder =
-      pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule);
-    pathInBBoxSpace = builder->Finish();
-    if (!pathInBBoxSpace) {
-      return bbox;
+  bool gotSimpleBounds = false;
+  if (!StyleSVGReset()->HasNonScalingStroke()) {
+    Float strokeWidth = getStroke ? nsSVGUtils::GetStrokeWidth(this) : 0.f;
+    Rect simpleBounds;
+    gotSimpleBounds = element->GetGeometryBounds(&simpleBounds, strokeWidth,
+                                                 aToBBoxUserspace);
+    if (gotSimpleBounds) {
+      bbox = simpleBounds;
     }
   }
 
-  // Be careful when replacing the following logic to get the fill and stroke
-  // extents independently (instead of computing the stroke extents from the
-  // path extents). You may think that you can just use the stroke extents if
-  // there is both a fill and a stroke. In reality it's necessary to calculate
-  // both the fill and stroke extents, and take the union of the two. There are
-  // two reasons for this:
-  //
-  // # Due to stroke dashing, in certain cases the fill extents could actually
-  //   extend outside the stroke extents.
-  // # If the stroke is very thin, cairo won't paint any stroke, and so the
-  //   stroke bounds that it will return will be empty.
+  if (!gotSimpleBounds) {
+    // Get the bounds using a Moz2D Path object (more expensive):
+    RefPtr<DrawTarget> tmpDT;
+#ifdef XP_WIN
+    // Unfortunately D2D backed DrawTarget produces bounds with rounding errors
+    // when whole number results are expected, even in the case of trivial
+    // calculations. To avoid that and meet the expectations of web content we
+    // have to use a CAIRO DrawTarget. The most efficient way to do that is to
+    // wrap the cached cairo_surface_t from ScreenReferenceSurface():
+    nsRefPtr<gfxASurface> refSurf =
+      gfxPlatform::GetPlatform()->ScreenReferenceSurface();
+    tmpDT = gfxPlatform::GetPlatform()->
+      CreateDrawTargetForSurface(refSurf, IntSize(1, 1));
+#else
+    tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
+#endif
 
-  Rect pathBBoxExtents = pathInBBoxSpace->GetBounds();
-  if (!pathBBoxExtents.IsFinite()) {
-    // This can happen in the case that we only have a move-to command in the
-    // path commands, in which case we know nothing gets rendered.
-    return bbox;
-  }
+    FillRule fillRule = nsSVGUtils::ToFillRule(StyleSVG()->mFillRule);
+    RefPtr<Path> pathInUserSpace = element->GetOrBuildPath(*tmpDT, fillRule);
+    if (!pathInUserSpace) {
+      return bbox;
+    }
+    RefPtr<Path> pathInBBoxSpace;
+    if (aToBBoxUserspace.IsIdentity()) {
+      pathInBBoxSpace = pathInUserSpace;
+    } else {
+      RefPtr<PathBuilder> builder =
+        pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule);
+      pathInBBoxSpace = builder->Finish();
+      if (!pathInBBoxSpace) {
+        return bbox;
+      }
+    }
 
-  // Account for fill:
-  if ((aFlags & nsSVGUtils::eBBoxIncludeFillGeometry) ||
-      ((aFlags & nsSVGUtils::eBBoxIncludeFill) &&
-       StyleSVG()->mFill.mType != eStyleSVGPaintType_None)) {
-    bbox = pathBBoxExtents;
-  }
+    // Be careful when replacing the following logic to get the fill and stroke
+    // extents independently (instead of computing the stroke extents from the
+    // path extents). You may think that you can just use the stroke extents if
+    // there is both a fill and a stroke. In reality it's necessary to
+    // calculate both the fill and stroke extents, and take the union of the
+    // two. There are two reasons for this:
+    //
+    // # Due to stroke dashing, in certain cases the fill extents could
+    //   actually extend outside the stroke extents.
+    // # If the stroke is very thin, cairo won't paint any stroke, and so the
+    //   stroke bounds that it will return will be empty.
 
-  // Account for stroke:
-  if ((aFlags & nsSVGUtils::eBBoxIncludeStrokeGeometry) ||
-      ((aFlags & nsSVGUtils::eBBoxIncludeStroke) &&
-       nsSVGUtils::HasStroke(this))) {
+    Rect pathBBoxExtents = pathInBBoxSpace->GetBounds();
+    if (!pathBBoxExtents.IsFinite()) {
+      // This can happen in the case that we only have a move-to command in the
+      // path commands, in which case we know nothing gets rendered.
+      return bbox;
+    }
+
+    // Account for fill:
+    if (getFill) {
+      bbox = pathBBoxExtents;
+    }
+
+    // Account for stroke:
+    if (getStroke) {
 #if 0
-    // This disabled code is how we would calculate the stroke bounds using
-    // Moz2D Path::GetStrokedBounds(). Unfortunately at the time of writing it
-    // there are two problems that prevent us from using it.
-    //
-    // First, it seems that some of the Moz2D backends are really dumb. Not
-    // only do some GetStrokeOptions() implementations sometimes significantly
-    // overestimate the stroke bounds, but if an argument is passed for the
-    // aTransform parameter then they just return bounds-of-transformed-bounds.
-    // These two things combined can lead the bounds to be unacceptably
-    // oversized, leading to massive over-invalidation.
-    //
-    // Second, the way we account for non-scaling-stroke by transforming the
-    // path using the transform to the outer-<svg> element is not compatible
-    // with the way that nsSVGPathGeometryFrame::Reflow() inserts a scale into
-    // aToBBoxUserspace and then scales the bounds that we return.
-    SVGContentUtils::AutoStrokeOptions strokeOptions;
-    SVGContentUtils::GetStrokeOptions(&strokeOptions, element, StyleContext(),
-                                      nullptr, SVGContentUtils::eIgnoreStrokeDashing);
-    Rect strokeBBoxExtents;
-    gfxMatrix userToOuterSVG;
-    if (nsSVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) {
-      Matrix outerSVGToUser = ToMatrix(userToOuterSVG);
-      outerSVGToUser.Invert();
-      Matrix outerSVGToBBox = aToBBoxUserspace * outerSVGToUser;
-      RefPtr<PathBuilder> builder =
-        pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG));
-      RefPtr<Path> pathInOuterSVGSpace = builder->Finish();
-      strokeBBoxExtents =
-        pathInOuterSVGSpace->GetStrokedBounds(strokeOptions, outerSVGToBBox);
-    } else {
-      strokeBBoxExtents =
-        pathInUserSpace->GetStrokedBounds(strokeOptions, aToBBoxUserspace);
-    }
-    MOZ_ASSERT(strokeBBoxExtents.IsFinite(), "bbox is about to go bad");
-    bbox.UnionEdges(strokeBBoxExtents);
+      // This disabled code is how we would calculate the stroke bounds using
+      // Moz2D Path::GetStrokedBounds(). Unfortunately at the time of writing
+      // it there are two problems that prevent us from using it.
+      //
+      // First, it seems that some of the Moz2D backends are really dumb. Not
+      // only do some GetStrokeOptions() implementations sometimes
+      // significantly overestimate the stroke bounds, but if an argument is
+      // passed for the aTransform parameter then they just return bounds-of-
+      // transformed-bounds.  These two things combined can lead the bounds to
+      // be unacceptably oversized, leading to massive over-invalidation.
+      //
+      // Second, the way we account for non-scaling-stroke by transforming the
+      // path using the transform to the outer-<svg> element is not compatible
+      // with the way that nsSVGPathGeometryFrame::Reflow() inserts a scale
+      // into aToBBoxUserspace and then scales the bounds that we return.
+      SVGContentUtils::AutoStrokeOptions strokeOptions;
+      SVGContentUtils::GetStrokeOptions(&strokeOptions, element,
+                                        StyleContext(), nullptr,
+                                        SVGContentUtils::eIgnoreStrokeDashing);
+      Rect strokeBBoxExtents;
+      gfxMatrix userToOuterSVG;
+      if (nsSVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) {
+        Matrix outerSVGToUser = ToMatrix(userToOuterSVG);
+        outerSVGToUser.Invert();
+        Matrix outerSVGToBBox = aToBBoxUserspace * outerSVGToUser;
+        RefPtr<PathBuilder> builder =
+          pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG));
+        RefPtr<Path> pathInOuterSVGSpace = builder->Finish();
+        strokeBBoxExtents =
+          pathInOuterSVGSpace->GetStrokedBounds(strokeOptions, outerSVGToBBox);
+      } else {
+        strokeBBoxExtents =
+          pathInUserSpace->GetStrokedBounds(strokeOptions, aToBBoxUserspace);
+      }
+      MOZ_ASSERT(strokeBBoxExtents.IsFinite(), "bbox is about to go bad");
+      bbox.UnionEdges(strokeBBoxExtents);
 #else
     // For now we just use nsSVGUtils::PathExtentsToMaxStrokeExtents:
-    gfxRect strokeBBoxExtents =
-      nsSVGUtils::PathExtentsToMaxStrokeExtents(ThebesRect(pathBBoxExtents),
-                                                this,
-                                                ThebesMatrix(aToBBoxUserspace));
-    MOZ_ASSERT(ToRect(strokeBBoxExtents).IsFinite(), "bbox is about to go bad");
-    bbox.UnionEdges(strokeBBoxExtents);
+      gfxRect strokeBBoxExtents =
+        nsSVGUtils::PathExtentsToMaxStrokeExtents(ThebesRect(pathBBoxExtents),
+                                                  this,
+                                                  ThebesMatrix(aToBBoxUserspace));
+      MOZ_ASSERT(ToRect(strokeBBoxExtents).IsFinite(), "bbox is about to go bad");
+      bbox.UnionEdges(strokeBBoxExtents);
 #endif
+    }
   }
 
   // Account for markers:
   if ((aFlags & nsSVGUtils::eBBoxIncludeMarkers) != 0 &&
       static_cast<nsSVGPathGeometryElement*>(mContent)->IsMarkable()) {
 
     float strokeWidth = nsSVGUtils::GetStrokeWidth(this);
     MarkerProperties properties = GetMarkerProperties(this);