From c2bb3314cbac75f2fe262d7ac3dec4996fa0599d Mon Sep 17 00:00:00 2001 From: Jonathan Watt Date: Sun, 26 Oct 2014 18:00:03 +0000 Subject: [PATCH] Bug 1080688 - Calculate SVG rect bounds using a simple rect transform rather than using a Moz2D Path. r=longsonr --- dom/svg/SVGRectElement.cpp | 32 ++++ dom/svg/SVGRectElement.h | 2 + dom/svg/nsSVGPathGeometryElement.h | 6 + layout/style/nsStyleStruct.h | 4 + layout/svg/nsSVGPathGeometryFrame.cpp | 211 ++++++++++++++------------ 5 files changed, 159 insertions(+), 96 deletions(-) diff --git a/dom/svg/SVGRectElement.cpp b/dom/svg/SVGRectElement.cpp index f3b9c8833fd9..8f32cf14b10f 100644 --- a/dom/svg/SVGRectElement.cpp +++ b/dom/svg/SVGRectElement.cpp @@ -7,6 +7,8 @@ #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 @@ -108,6 +110,36 @@ SVGRectElement::GetLengthInfo() //---------------------------------------------------------------------- // 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; +} + void SVGRectElement::GetAsSimplePath(SimplePath* aSimplePath) { diff --git a/dom/svg/SVGRectElement.h b/dom/svg/SVGRectElement.h index 58ed60470aae..3340ecf45ce1 100644 --- a/dom/svg/SVGRectElement.h +++ b/dom/svg/SVGRectElement.h @@ -30,6 +30,8 @@ public: virtual bool HasValidDimensions() const MOZ_OVERRIDE; // nsSVGPathGeometryElement methods: + virtual bool GetGeometryBounds(Rect* aBounds, Float aStrokeWidth, + const Matrix& aTransform) MOZ_OVERRIDE; virtual void GetAsSimplePath(SimplePath* aSimplePath) MOZ_OVERRIDE; virtual TemporaryRef BuildPath(PathBuilder* aBuilder = nullptr) MOZ_OVERRIDE; diff --git a/dom/svg/nsSVGPathGeometryElement.h b/dom/svg/nsSVGPathGeometryElement.h index 44d81a3627da..c55021b48f3a 100644 --- a/dom/svg/nsSVGPathGeometryElement.h +++ b/dom/svg/nsSVGPathGeometryElement.h @@ -34,6 +34,7 @@ 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::Point Point; typedef mozilla::gfx::PathBuilder PathBuilder; @@ -69,6 +70,11 @@ public: virtual bool IsMarkable(); virtual void GetMarkPoints(nsTArray *aMarks); + virtual bool GetGeometryBounds(Rect* aBounds, Float aStrokeWidth, + const Matrix& aTransform) { + return false; + } + /** * For use with GetAsSimplePath. */ diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h index 291645414fe2..ce642c5aea61 100644 --- a/layout/style/nsStyleStruct.h +++ b/layout/style/nsStyleStruct.h @@ -3040,6 +3040,10 @@ struct nsStyleSVGReset { return mFilters.Length() > 0; } + bool HasNonScalingStroke() const { + return mVectorEffect == NS_STYLE_VECTOR_EFFECT_NON_SCALING_STROKE; + } + nsStyleClipPath mClipPath; // [reset] nsTArray mFilters; // [reset] nsCOMPtr mMask; // [reset] diff --git a/layout/svg/nsSVGPathGeometryFrame.cpp b/layout/svg/nsSVGPathGeometryFrame.cpp index fca4d96002e0..b7e8fd503fcd 100644 --- a/layout/svg/nsSVGPathGeometryFrame.cpp +++ b/layout/svg/nsSVGPathGeometryFrame.cpp @@ -463,113 +463,132 @@ nsSVGPathGeometryFrame::GetBBoxContribution(const Matrix &aToBBoxUserspace, nsSVGPathGeometryElement* element = static_cast(mContent); - RefPtr tmpDT; + bool getFill = (aFlags & nsSVGUtils::eBBoxIncludeFillGeometry) || + ((aFlags & nsSVGUtils::eBBoxIncludeFill) && + StyleSVG()->mFill.mType != eStyleSVGPaintType_None); + + bool getStroke = (aFlags & nsSVGUtils::eBBoxIncludeStrokeGeometry) || + ((aFlags & nsSVGUtils::eBBoxIncludeStroke) && + nsSVGUtils::HasStroke(this)); + + 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; + } + } + + if (!gotSimpleBounds) { + // Get the bounds using a Moz2D Path object (more expensive): + RefPtr 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 refSurf = - gfxPlatform::GetPlatform()->ScreenReferenceSurface(); - tmpDT = gfxPlatform::GetPlatform()-> - CreateDrawTargetForSurface(refSurf, IntSize(1, 1)); + // 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 refSurf = + gfxPlatform::GetPlatform()->ScreenReferenceSurface(); + tmpDT = gfxPlatform::GetPlatform()-> + CreateDrawTargetForSurface(refSurf, IntSize(1, 1)); #else - tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); #endif - FillRule fillRule = nsSVGUtils::ToFillRule(StyleSVG()->mFillRule); - RefPtr pathInUserSpace = element->GetOrBuildPath(*tmpDT, fillRule); - if (!pathInUserSpace) { - return bbox; - } - RefPtr pathInBBoxSpace; - if (aToBBoxUserspace.IsIdentity()) { - pathInBBoxSpace = pathInUserSpace; - } else { - RefPtr builder = - pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule); - pathInBBoxSpace = builder->Finish(); - if (!pathInBBoxSpace) { + FillRule fillRule = nsSVGUtils::ToFillRule(StyleSVG()->mFillRule); + RefPtr pathInUserSpace = element->GetOrBuildPath(*tmpDT, fillRule); + if (!pathInUserSpace) { return bbox; } - } - - // 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. - - 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 ((aFlags & nsSVGUtils::eBBoxIncludeFillGeometry) || - ((aFlags & nsSVGUtils::eBBoxIncludeFill) && - StyleSVG()->mFill.mType != eStyleSVGPaintType_None)) { - bbox = pathBBoxExtents; - } - - // Account for stroke: - if ((aFlags & nsSVGUtils::eBBoxIncludeStrokeGeometry) || - ((aFlags & nsSVGUtils::eBBoxIncludeStroke) && - nsSVGUtils::HasStroke(this))) { -#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- 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 builder = - pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG)); - RefPtr pathInOuterSVGSpace = builder->Finish(); - strokeBBoxExtents = - pathInOuterSVGSpace->GetStrokedBounds(strokeOptions, outerSVGToBBox); + RefPtr pathInBBoxSpace; + if (aToBBoxUserspace.IsIdentity()) { + pathInBBoxSpace = pathInUserSpace; } else { - strokeBBoxExtents = - pathInUserSpace->GetStrokedBounds(strokeOptions, aToBBoxUserspace); + RefPtr builder = + pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule); + pathInBBoxSpace = builder->Finish(); + if (!pathInBBoxSpace) { + return bbox; + } } - MOZ_ASSERT(strokeBBoxExtents.IsFinite(), "bbox is about to go bad"); - bbox.UnionEdges(strokeBBoxExtents); + + // 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. + + 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- 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 builder = + pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG)); + RefPtr 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: