summaryrefslogtreecommitdiffstats
path: root/layout/svg/SVGUtils.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/svg/SVGUtils.cpp1558
1 files changed, 1558 insertions, 0 deletions
diff --git a/layout/svg/SVGUtils.cpp b/layout/svg/SVGUtils.cpp
new file mode 100644
index 0000000000..206f56cedd
--- /dev/null
+++ b/layout/svg/SVGUtils.cpp
@@ -0,0 +1,1558 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+// Main header first:
+// This is also necessary to ensure our definition of M_SQRT1_2 is picked up
+#include "SVGUtils.h"
+#include <algorithm>
+
+// Keep others in (case-insensitive) order:
+#include "gfx2DGlue.h"
+#include "gfxContext.h"
+#include "gfxMatrix.h"
+#include "gfxPlatform.h"
+#include "gfxRect.h"
+#include "gfxUtils.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsDisplayList.h"
+#include "nsFrameList.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsIFrame.h"
+#include "nsIFrameInlines.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsStyleStruct.h"
+#include "nsStyleTransformMatrix.h"
+#include "SVGAnimatedLength.h"
+#include "SVGPaintServerFrame.h"
+#include "nsTextFrame.h"
+#include "mozilla/CSSClipPathInstance.h"
+#include "mozilla/FilterInstance.h"
+#include "mozilla/ISVGDisplayableFrame.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_svg.h"
+#include "mozilla/SVGClipPathFrame.h"
+#include "mozilla/SVGContainerFrame.h"
+#include "mozilla/SVGContentUtils.h"
+#include "mozilla/SVGContextPaint.h"
+#include "mozilla/SVGForeignObjectFrame.h"
+#include "mozilla/SVGIntegrationUtils.h"
+#include "mozilla/SVGGeometryFrame.h"
+#include "mozilla/SVGMaskFrame.h"
+#include "mozilla/SVGObserverUtils.h"
+#include "mozilla/SVGOuterSVGFrame.h"
+#include "mozilla/SVGTextFrame.h"
+#include "mozilla/Unused.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/PatternHelpers.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/SVGClipPathElement.h"
+#include "mozilla/dom/SVGGeometryElement.h"
+#include "mozilla/dom/SVGPathElement.h"
+#include "mozilla/dom/SVGUnitTypesBinding.h"
+#include "mozilla/dom/SVGViewportElement.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::dom::SVGUnitTypes_Binding;
+using namespace mozilla::gfx;
+using namespace mozilla::image;
+
+bool NS_SVGNewGetBBoxEnabled() {
+ return mozilla::StaticPrefs::svg_new_getBBox_enabled();
+}
+
+namespace mozilla {
+
+// we only take the address of this:
+static gfx::UserDataKey sSVGAutoRenderStateKey;
+
+SVGAutoRenderState::SVGAutoRenderState(DrawTarget* aDrawTarget)
+ : mDrawTarget(aDrawTarget),
+ mOriginalRenderState(nullptr),
+ mPaintingToWindow(false) {
+ mOriginalRenderState = aDrawTarget->RemoveUserData(&sSVGAutoRenderStateKey);
+ // We always remove ourselves from aContext before it dies, so
+ // passing nullptr as the destroy function is okay.
+ aDrawTarget->AddUserData(&sSVGAutoRenderStateKey, this, nullptr);
+}
+
+SVGAutoRenderState::~SVGAutoRenderState() {
+ mDrawTarget->RemoveUserData(&sSVGAutoRenderStateKey);
+ if (mOriginalRenderState) {
+ mDrawTarget->AddUserData(&sSVGAutoRenderStateKey, mOriginalRenderState,
+ nullptr);
+ }
+}
+
+void SVGAutoRenderState::SetPaintingToWindow(bool aPaintingToWindow) {
+ mPaintingToWindow = aPaintingToWindow;
+}
+
+/* static */
+bool SVGAutoRenderState::IsPaintingToWindow(DrawTarget* aDrawTarget) {
+ void* state = aDrawTarget->GetUserData(&sSVGAutoRenderStateKey);
+ if (state) {
+ return static_cast<SVGAutoRenderState*>(state)->mPaintingToWindow;
+ }
+ return false;
+}
+
+// Unlike containers, leaf frames do not include GetPosition() in
+// GetCanvasTM().
+static bool FrameDoesNotIncludePositionInTM(const nsIFrame* aFrame) {
+ return aFrame->IsSVGGeometryFrame() || aFrame->IsSVGImageFrame() ||
+ aFrame->IsInSVGTextSubtree();
+}
+
+nsRect SVGUtils::GetPostFilterInkOverflowRect(nsIFrame* aFrame,
+ const nsRect& aPreFilterRect) {
+ MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT),
+ "Called on invalid frame type");
+
+ // Note: we do not return here for eHasNoRefs since we must still handle any
+ // CSS filter functions.
+ // TODO: We currently pass nullptr instead of an nsTArray* here, but we
+ // actually should get the filter frames and then pass them into
+ // GetPostFilterBounds below! See bug 1494263.
+ // TODO: we should really return an empty rect for eHasRefsSomeInvalid since
+ // in that case we disable painting of the element.
+ if (!aFrame->StyleEffects()->HasFilters() ||
+ SVGObserverUtils::GetAndObserveFilters(aFrame, nullptr) ==
+ SVGObserverUtils::eHasRefsSomeInvalid) {
+ return aPreFilterRect;
+ }
+
+ return FilterInstance::GetPostFilterBounds(aFrame, nullptr, &aPreFilterRect)
+ .valueOr(aPreFilterRect);
+}
+
+bool SVGUtils::OuterSVGIsCallingReflowSVG(nsIFrame* aFrame) {
+ return GetOuterSVGFrame(aFrame)->IsCallingReflowSVG();
+}
+
+bool SVGUtils::AnyOuterSVGIsCallingReflowSVG(nsIFrame* aFrame) {
+ SVGOuterSVGFrame* outer = GetOuterSVGFrame(aFrame);
+ do {
+ if (outer->IsCallingReflowSVG()) {
+ return true;
+ }
+ outer = GetOuterSVGFrame(outer->GetParent());
+ } while (outer);
+ return false;
+}
+
+void SVGUtils::ScheduleReflowSVG(nsIFrame* aFrame) {
+ MOZ_ASSERT(aFrame->IsFrameOfType(nsIFrame::eSVG), "Passed bad frame!");
+
+ // If this is triggered, the callers should be fixed to call us before
+ // ReflowSVG is called. If we try to mark dirty bits on frames while we're
+ // in the process of removing them, things will get messed up.
+ MOZ_ASSERT(!OuterSVGIsCallingReflowSVG(aFrame),
+ "Do not call under ISVGDisplayableFrame::ReflowSVG!");
+
+ // We don't call SVGObserverUtils::InvalidateRenderingObservers here because
+ // we should only be called under InvalidateAndScheduleReflowSVG (which
+ // calls InvalidateBounds) or SVGDisplayContainerFrame::InsertFrames
+ // (at which point the frame has no observers).
+
+ if (aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
+ return;
+ }
+
+ if (aFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_FIRST_REFLOW)) {
+ // Nothing to do if we're already dirty, or if the outer-<svg>
+ // hasn't yet had its initial reflow.
+ return;
+ }
+
+ SVGOuterSVGFrame* outerSVGFrame = nullptr;
+
+ // We must not add dirty bits to the SVGOuterSVGFrame or else
+ // PresShell::FrameNeedsReflow won't work when we pass it in below.
+ if (aFrame->IsSVGOuterSVGFrame()) {
+ outerSVGFrame = static_cast<SVGOuterSVGFrame*>(aFrame);
+ } else {
+ aFrame->MarkSubtreeDirty();
+
+ nsIFrame* f = aFrame->GetParent();
+ while (f && !f->IsSVGOuterSVGFrame()) {
+ if (f->HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN)) {
+ return;
+ }
+ f->AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN);
+ f = f->GetParent();
+ MOZ_ASSERT(f->IsFrameOfType(nsIFrame::eSVG),
+ "IsSVGOuterSVGFrame check above not valid!");
+ }
+
+ outerSVGFrame = static_cast<SVGOuterSVGFrame*>(f);
+
+ MOZ_ASSERT(outerSVGFrame && outerSVGFrame->IsSVGOuterSVGFrame(),
+ "Did not find SVGOuterSVGFrame!");
+ }
+
+ if (outerSVGFrame->HasAnyStateBits(NS_FRAME_IN_REFLOW)) {
+ // We're currently under an SVGOuterSVGFrame::Reflow call so there is no
+ // need to call PresShell::FrameNeedsReflow, since we have an
+ // SVGOuterSVGFrame::DidReflow call pending.
+ return;
+ }
+
+ nsFrameState dirtyBit =
+ (outerSVGFrame == aFrame ? NS_FRAME_IS_DIRTY
+ : NS_FRAME_HAS_DIRTY_CHILDREN);
+
+ aFrame->PresShell()->FrameNeedsReflow(outerSVGFrame, IntrinsicDirty::None,
+ dirtyBit);
+}
+
+bool SVGUtils::NeedsReflowSVG(const nsIFrame* aFrame) {
+ MOZ_ASSERT(aFrame->IsFrameOfType(nsIFrame::eSVG),
+ "SVG uses bits differently!");
+
+ // The flags we test here may change, hence why we have this separate
+ // function.
+ return aFrame->IsSubtreeDirty();
+}
+
+Size SVGUtils::GetContextSize(const nsIFrame* aFrame) {
+ Size size;
+
+ MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast");
+ const SVGElement* element = static_cast<SVGElement*>(aFrame->GetContent());
+
+ SVGViewportElement* ctx = element->GetCtx();
+ if (ctx) {
+ size.width = ctx->GetLength(SVGContentUtils::X);
+ size.height = ctx->GetLength(SVGContentUtils::Y);
+ }
+ return size;
+}
+
+float SVGUtils::ObjectSpace(const gfxRect& aRect,
+ const SVGAnimatedLength* aLength) {
+ float axis;
+
+ switch (aLength->GetCtxType()) {
+ case SVGContentUtils::X:
+ axis = aRect.Width();
+ break;
+ case SVGContentUtils::Y:
+ axis = aRect.Height();
+ break;
+ case SVGContentUtils::XY:
+ axis = float(SVGContentUtils::ComputeNormalizedHypotenuse(
+ aRect.Width(), aRect.Height()));
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("unexpected ctx type");
+ axis = 0.0f;
+ break;
+ }
+ if (aLength->IsPercentage()) {
+ // Multiply first to avoid precision errors:
+ return axis * aLength->GetAnimValInSpecifiedUnits() / 100;
+ }
+ return aLength->GetAnimValue(static_cast<SVGViewportElement*>(nullptr)) *
+ axis;
+}
+
+float SVGUtils::UserSpace(SVGElement* aSVGElement,
+ const SVGAnimatedLength* aLength) {
+ return aLength->GetAnimValue(aSVGElement);
+}
+
+float SVGUtils::UserSpace(nsIFrame* aNonSVGContext,
+ const SVGAnimatedLength* aLength) {
+ MOZ_ASSERT(!aNonSVGContext->IsTextFrame(), "Not expecting text content");
+ return aLength->GetAnimValue(aNonSVGContext);
+}
+
+float SVGUtils::UserSpace(const UserSpaceMetrics& aMetrics,
+ const SVGAnimatedLength* aLength) {
+ return aLength->GetAnimValue(aMetrics);
+}
+
+SVGOuterSVGFrame* SVGUtils::GetOuterSVGFrame(nsIFrame* aFrame) {
+ return static_cast<SVGOuterSVGFrame*>(nsLayoutUtils::GetClosestFrameOfType(
+ aFrame, LayoutFrameType::SVGOuterSVG));
+}
+
+nsIFrame* SVGUtils::GetOuterSVGFrameAndCoveredRegion(nsIFrame* aFrame,
+ nsRect* aRect) {
+ ISVGDisplayableFrame* svg = do_QueryFrame(aFrame);
+ if (!svg) {
+ return nullptr;
+ }
+ SVGOuterSVGFrame* outer = GetOuterSVGFrame(aFrame);
+ if (outer == svg) {
+ return nullptr;
+ }
+
+ if (aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
+ *aRect = nsRect(0, 0, 0, 0);
+ } else {
+ uint32_t flags = SVGUtils::eForGetClientRects | SVGUtils::eBBoxIncludeFill |
+ SVGUtils::eBBoxIncludeStroke |
+ SVGUtils::eBBoxIncludeMarkers |
+ SVGUtils::eUseUserSpaceOfUseElement;
+
+ auto ctm = nsLayoutUtils::GetTransformToAncestor(RelativeTo{aFrame},
+ RelativeTo{outer});
+
+ float initPositionX = NSAppUnitsToFloatPixels(aFrame->GetPosition().x,
+ AppUnitsPerCSSPixel()),
+ initPositionY = NSAppUnitsToFloatPixels(aFrame->GetPosition().y,
+ AppUnitsPerCSSPixel());
+
+ Matrix mm;
+ ctm.ProjectTo2D();
+ ctm.CanDraw2D(&mm);
+ gfxMatrix m = ThebesMatrix(mm);
+
+ float appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel();
+ float devPixelPerCSSPixel =
+ float(AppUnitsPerCSSPixel()) / appUnitsPerDevPixel;
+
+ // The matrix that GetBBox accepts should operate on "user space",
+ // i.e. with CSS pixel unit.
+ m = m.PreScale(devPixelPerCSSPixel, devPixelPerCSSPixel);
+
+ // Both SVGUtils::GetBBox and nsLayoutUtils::GetTransformToAncestor
+ // will count this displacement, we should remove it here to avoid
+ // double-counting.
+ m = m.PreTranslate(-initPositionX, -initPositionY);
+
+ gfxRect bbox = SVGUtils::GetBBox(aFrame, flags, &m);
+ *aRect = nsLayoutUtils::RoundGfxRectToAppRect(bbox, appUnitsPerDevPixel);
+ }
+
+ return outer;
+}
+
+gfxMatrix SVGUtils::GetCanvasTM(nsIFrame* aFrame) {
+ // XXX yuck, we really need a common interface for GetCanvasTM
+
+ if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
+ return GetCSSPxToDevPxMatrix(aFrame);
+ }
+
+ if (aFrame->IsSVGForeignObjectFrame()) {
+ return static_cast<SVGForeignObjectFrame*>(aFrame)->GetCanvasTM();
+ }
+
+ if (SVGContainerFrame* containerFrame = do_QueryFrame(aFrame)) {
+ return containerFrame->GetCanvasTM();
+ }
+
+ MOZ_ASSERT(aFrame->GetParent()->IsFrameOfType(nsIFrame::eSVGContainer));
+
+ auto* parent = static_cast<SVGContainerFrame*>(aFrame->GetParent());
+ auto* content = static_cast<SVGElement*>(aFrame->GetContent());
+
+ return content->PrependLocalTransformsTo(parent->GetCanvasTM());
+}
+
+bool SVGUtils::IsSVGTransformed(const nsIFrame* aFrame,
+ gfx::Matrix* aOwnTransform,
+ gfx::Matrix* aFromParentTransform) {
+ MOZ_ASSERT(aFrame->HasAllStateBits(NS_FRAME_SVG_LAYOUT |
+ NS_FRAME_MAY_BE_TRANSFORMED),
+ "Expecting an SVG frame that can be transformed");
+ bool foundTransform = false;
+
+ // Check if our parent has children-only transforms:
+ if (SVGContainerFrame* parent = do_QueryFrame(aFrame->GetParent())) {
+ foundTransform = parent->HasChildrenOnlyTransform(aFromParentTransform);
+ }
+
+ if (auto* content = SVGElement::FromNode(aFrame->GetContent())) {
+ auto* transformList = content->GetAnimatedTransformList();
+ if ((transformList && transformList->HasTransform()) ||
+ content->GetAnimateMotionTransform()) {
+ if (aOwnTransform) {
+ *aOwnTransform = gfx::ToMatrix(
+ content->PrependLocalTransformsTo(gfxMatrix(), eUserSpaceToParent));
+ }
+ foundTransform = true;
+ }
+ }
+ return foundTransform;
+}
+
+void SVGUtils::NotifyChildrenOfSVGChange(nsIFrame* aFrame, uint32_t aFlags) {
+ for (nsIFrame* kid : aFrame->PrincipalChildList()) {
+ ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid);
+ if (SVGFrame) {
+ SVGFrame->NotifySVGChanged(aFlags);
+ } else {
+ NS_ASSERTION(
+ kid->IsFrameOfType(nsIFrame::eSVG) || kid->IsInSVGTextSubtree(),
+ "SVG frame expected");
+ // recurse into the children of container frames e.g. <clipPath>, <mask>
+ // in case they have child frames with transformation matrices
+ if (kid->IsFrameOfType(nsIFrame::eSVG)) {
+ NotifyChildrenOfSVGChange(kid, aFlags);
+ }
+ }
+ }
+}
+
+// ************************************************************
+
+float SVGUtils::ComputeOpacity(const nsIFrame* aFrame, bool aHandleOpacity) {
+ const auto* styleEffects = aFrame->StyleEffects();
+
+ if (!styleEffects->IsOpaque() &&
+ (SVGUtils::CanOptimizeOpacity(aFrame) || !aHandleOpacity)) {
+ return 1.0f;
+ }
+
+ return styleEffects->mOpacity;
+}
+
+void SVGUtils::DetermineMaskUsage(const nsIFrame* aFrame, bool aHandleOpacity,
+ MaskUsage& aUsage) {
+ using ClipPathType = StyleClipPath::Tag;
+
+ aUsage.opacity = ComputeOpacity(aFrame, aHandleOpacity);
+
+ nsIFrame* firstFrame =
+ nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
+
+ const nsStyleSVGReset* svgReset = firstFrame->StyleSVGReset();
+
+ nsTArray<SVGMaskFrame*> maskFrames;
+ // XXX check return value?
+ SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames);
+ aUsage.shouldGenerateMaskLayer = (maskFrames.Length() > 0);
+
+ SVGClipPathFrame* clipPathFrame;
+ // XXX check return value?
+ SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame);
+ MOZ_ASSERT(!clipPathFrame || svgReset->mClipPath.IsUrl());
+
+ switch (svgReset->mClipPath.tag) {
+ case ClipPathType::Url:
+ if (clipPathFrame) {
+ if (clipPathFrame->IsTrivial()) {
+ aUsage.shouldApplyClipPath = true;
+ } else {
+ aUsage.shouldGenerateClipMaskLayer = true;
+ }
+ }
+ break;
+ case ClipPathType::Shape:
+ case ClipPathType::Box:
+ case ClipPathType::Path:
+ aUsage.shouldApplyBasicShapeOrPath = true;
+ break;
+ case ClipPathType::None:
+ MOZ_ASSERT(!aUsage.shouldGenerateClipMaskLayer &&
+ !aUsage.shouldApplyClipPath &&
+ !aUsage.shouldApplyBasicShapeOrPath);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported clip-path type.");
+ break;
+ }
+}
+
+class MixModeBlender {
+ public:
+ using Factory = gfx::Factory;
+
+ MixModeBlender(nsIFrame* aFrame, gfxContext* aContext)
+ : mFrame(aFrame), mSourceCtx(aContext) {
+ MOZ_ASSERT(mFrame && mSourceCtx);
+ }
+
+ bool ShouldCreateDrawTargetForBlend() const {
+ return mFrame->StyleEffects()->HasMixBlendMode();
+ }
+
+ gfxContext* CreateBlendTarget(const gfxMatrix& aTransform) {
+ MOZ_ASSERT(ShouldCreateDrawTargetForBlend());
+
+ // Create a temporary context to draw to so we can blend it back with
+ // another operator.
+ IntRect drawRect = ComputeClipExtsInDeviceSpace(aTransform);
+ if (drawRect.IsEmpty()) {
+ return nullptr;
+ }
+
+ RefPtr<DrawTarget> targetDT =
+ mSourceCtx->GetDrawTarget()->CreateSimilarDrawTarget(
+ drawRect.Size(), SurfaceFormat::B8G8R8A8);
+ if (!targetDT || !targetDT->IsValid()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!mTargetCtx,
+ "CreateBlendTarget is designed to be used once only.");
+
+ mTargetCtx = gfxContext::CreateOrNull(targetDT);
+ MOZ_ASSERT(mTargetCtx); // already checked the draw target above
+ mTargetCtx->SetMatrix(mSourceCtx->CurrentMatrix() *
+ Matrix::Translation(-drawRect.TopLeft()));
+
+ mTargetOffset = drawRect.TopLeft();
+
+ return mTargetCtx.get();
+ }
+
+ void BlendToTarget() {
+ MOZ_ASSERT(ShouldCreateDrawTargetForBlend());
+ MOZ_ASSERT(mTargetCtx,
+ "BlendToTarget should be used after CreateBlendTarget.");
+
+ RefPtr<SourceSurface> targetSurf = mTargetCtx->GetDrawTarget()->Snapshot();
+
+ gfxContextAutoSaveRestore save(mSourceCtx);
+ mSourceCtx->SetMatrix(Matrix()); // This will be restored right after.
+ RefPtr<gfxPattern> pattern = new gfxPattern(
+ targetSurf, Matrix::Translation(mTargetOffset.x, mTargetOffset.y));
+ mSourceCtx->SetPattern(pattern);
+ mSourceCtx->Paint();
+ }
+
+ private:
+ MixModeBlender() = delete;
+
+ IntRect ComputeClipExtsInDeviceSpace(const gfxMatrix& aTransform) {
+ // These are used if we require a temporary surface for a custom blend
+ // mode. Clip the source context first, so that we can generate a smaller
+ // temporary surface. (Since we will clip this context in
+ // SetupContextMatrix, a pair of save/restore is needed.)
+ gfxContextAutoSaveRestore saver;
+
+ if (!mFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) {
+ saver.SetContext(mSourceCtx);
+ // aFrame has a valid ink overflow rect, so clip to it before calling
+ // PushGroup() to minimize the size of the surfaces we'll composite:
+ gfxContextMatrixAutoSaveRestore matrixAutoSaveRestore(mSourceCtx);
+ mSourceCtx->Multiply(aTransform);
+ nsRect overflowRect = mFrame->InkOverflowRectRelativeToSelf();
+ if (FrameDoesNotIncludePositionInTM(mFrame)) {
+ overflowRect = overflowRect + mFrame->GetPosition();
+ }
+ mSourceCtx->Clip(NSRectToSnappedRect(
+ overflowRect, mFrame->PresContext()->AppUnitsPerDevPixel(),
+ *mSourceCtx->GetDrawTarget()));
+ }
+
+ // Get the clip extents in device space.
+ gfxRect clippedFrameSurfaceRect =
+ mSourceCtx->GetClipExtents(gfxContext::eDeviceSpace);
+ clippedFrameSurfaceRect.RoundOut();
+
+ IntRect result;
+ ToRect(clippedFrameSurfaceRect).ToIntRect(&result);
+
+ return Factory::CheckSurfaceSize(result.Size()) ? result : IntRect();
+ }
+
+ nsIFrame* mFrame;
+ gfxContext* mSourceCtx;
+ UniquePtr<gfxContext> mTargetCtx;
+ IntPoint mTargetOffset;
+};
+
+void SVGUtils::PaintFrameWithEffects(nsIFrame* aFrame, gfxContext& aContext,
+ const gfxMatrix& aTransform,
+ imgDrawingParams& aImgParams) {
+ NS_ASSERTION(aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) ||
+ aFrame->PresContext()->Document()->IsSVGGlyphsDocument(),
+ "Only painting of non-display SVG should take this code path");
+
+ ISVGDisplayableFrame* svgFrame = do_QueryFrame(aFrame);
+ if (!svgFrame) {
+ return;
+ }
+
+ MaskUsage maskUsage;
+ DetermineMaskUsage(aFrame, true, maskUsage);
+ if (maskUsage.opacity == 0.0f) {
+ return;
+ }
+
+ if (auto* svg = SVGElement::FromNode(aFrame->GetContent())) {
+ if (!svg->HasValidDimensions()) {
+ return;
+ }
+ }
+
+ /* SVG defines the following rendering model:
+ *
+ * 1. Render fill
+ * 2. Render stroke
+ * 3. Render markers
+ * 4. Apply filter
+ * 5. Apply clipping, masking, group opacity
+ *
+ * We follow this, but perform a couple of optimizations:
+ *
+ * + Use cairo's clipPath when representable natively (single object
+ * clip region).
+ *
+ * + Merge opacity and masking if both used together.
+ */
+
+ /* Properties are added lazily and may have been removed by a restyle,
+ so make sure all applicable ones are set again. */
+ SVGClipPathFrame* clipPathFrame;
+ nsTArray<SVGMaskFrame*> maskFrames;
+ // TODO: We currently pass nullptr instead of an nsTArray* here, but we
+ // actually should get the filter frames and then pass them into
+ // PaintFilteredFrame below! See bug 1494263.
+ const bool hasInvalidFilter =
+ SVGObserverUtils::GetAndObserveFilters(aFrame, nullptr) ==
+ SVGObserverUtils::eHasRefsSomeInvalid;
+ if (SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame) ==
+ SVGObserverUtils::eHasRefsSomeInvalid ||
+ SVGObserverUtils::GetAndObserveMasks(aFrame, &maskFrames) ==
+ SVGObserverUtils::eHasRefsSomeInvalid) {
+ // Some resource is invalid. We shouldn't paint anything.
+ return;
+ }
+
+ SVGMaskFrame* maskFrame = maskFrames.IsEmpty() ? nullptr : maskFrames[0];
+
+ MixModeBlender blender(aFrame, &aContext);
+ gfxContext* target = blender.ShouldCreateDrawTargetForBlend()
+ ? blender.CreateBlendTarget(aTransform)
+ : &aContext;
+
+ if (!target) {
+ return;
+ }
+
+ /* Check if we need to do additional operations on this child's
+ * rendering, which necessitates rendering into another surface. */
+ bool shouldGenerateMask =
+ (maskUsage.opacity != 1.0f || maskUsage.shouldGenerateClipMaskLayer ||
+ maskUsage.shouldGenerateMaskLayer);
+ bool shouldPushMask = false;
+
+ if (shouldGenerateMask) {
+ RefPtr<SourceSurface> maskSurface;
+
+ // maskFrame can be nullptr even if maskUsage.shouldGenerateMaskLayer is
+ // true. That happens when a user gives an unresolvable mask-id, such as
+ // mask:url()
+ // mask:url(#id-which-does-not-exist)
+ // Since we only uses SVGUtils with SVG elements, not like mask on an
+ // HTML element, we should treat an unresolvable mask as no-mask here.
+ if (maskUsage.shouldGenerateMaskLayer && maskFrame) {
+ StyleMaskMode maskMode =
+ aFrame->StyleSVGReset()->mMask.mLayers[0].mMaskMode;
+ SVGMaskFrame::MaskParams params(aContext.GetDrawTarget(), aFrame,
+ aTransform, maskUsage.opacity, maskMode,
+ aImgParams);
+
+ maskSurface = maskFrame->GetMaskForMaskedFrame(params);
+
+ if (!maskSurface) {
+ // Either entire surface is clipped out, or gfx buffer allocation
+ // failure in SVGMaskFrame::GetMaskForMaskedFrame.
+ return;
+ }
+ shouldPushMask = true;
+ }
+
+ if (maskUsage.shouldGenerateClipMaskLayer) {
+ RefPtr<SourceSurface> clipMaskSurface =
+ clipPathFrame->GetClipMask(aContext, aFrame, aTransform, maskSurface);
+ if (clipMaskSurface) {
+ maskSurface = clipMaskSurface;
+ } else {
+ // Either entire surface is clipped out, or gfx buffer allocation
+ // failure in SVGClipPathFrame::GetClipMask.
+ return;
+ }
+ shouldPushMask = true;
+ }
+
+ if (!maskUsage.shouldGenerateClipMaskLayer &&
+ !maskUsage.shouldGenerateMaskLayer) {
+ shouldPushMask = true;
+ }
+
+ // SVG mask multiply opacity into maskSurface already, so we do not bother
+ // to apply opacity again.
+ if (shouldPushMask) {
+ // We want the mask to be untransformed so use the inverse of the
+ // current transform as the maskTransform to compensate.
+ Matrix maskTransform = aContext.CurrentMatrix();
+ maskTransform.Invert();
+ target->PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
+ maskFrame ? 1.0 : maskUsage.opacity,
+ maskSurface, maskTransform);
+ }
+ }
+
+ /* If this frame has only a trivial clipPath, set up cairo's clipping now so
+ * we can just do normal painting and get it clipped appropriately.
+ */
+ if (maskUsage.shouldApplyClipPath || maskUsage.shouldApplyBasicShapeOrPath) {
+ if (maskUsage.shouldApplyClipPath) {
+ clipPathFrame->ApplyClipPath(aContext, aFrame, aTransform);
+ } else {
+ CSSClipPathInstance::ApplyBasicShapeOrPathClip(aContext, aFrame,
+ aTransform);
+ }
+ }
+
+ /* Paint the child */
+
+ // Invalid filters should render the unfiltered contents per spec.
+ if (aFrame->StyleEffects()->HasFilters() && !hasInvalidFilter) {
+ gfxContextMatrixAutoSaveRestore autoSR(target);
+
+ // 'target' is currently scaled such that its user space units are CSS
+ // pixels (SVG user space units). But PaintFilteredFrame expects it to be
+ // scaled in such a way that its user space units are device pixels. So we
+ // have to adjust the scale.
+ gfxMatrix reverseScaleMatrix = SVGUtils::GetCSSPxToDevPxMatrix(aFrame);
+ DebugOnly<bool> invertible = reverseScaleMatrix.Invert();
+ target->SetMatrixDouble(reverseScaleMatrix * aTransform *
+ target->CurrentMatrixDouble());
+
+ auto callback = [&](gfxContext& aContext, imgDrawingParams& aImgParams,
+ const gfxMatrix* aFilterTransform,
+ const nsIntRect* aDirtyRect) {
+ svgFrame->PaintSVG(aContext,
+ aFilterTransform
+ ? SVGUtils::GetCSSPxToDevPxMatrix(aFrame)
+ : aTransform,
+ aImgParams);
+ };
+ FilterInstance::PaintFilteredFrame(
+ aFrame, aFrame->StyleEffects()->mFilters.AsSpan(), target, callback,
+ nullptr, aImgParams);
+ } else {
+ svgFrame->PaintSVG(*target, aTransform, aImgParams);
+ }
+
+ if (maskUsage.shouldApplyClipPath || maskUsage.shouldApplyBasicShapeOrPath) {
+ aContext.PopClip();
+ }
+
+ if (shouldPushMask) {
+ target->PopGroupAndBlend();
+ }
+
+ if (blender.ShouldCreateDrawTargetForBlend()) {
+ MOZ_ASSERT(target != &aContext);
+ blender.BlendToTarget();
+ }
+}
+
+bool SVGUtils::HitTestClip(nsIFrame* aFrame, const gfxPoint& aPoint) {
+ const nsStyleSVGReset* svgReset = aFrame->StyleSVGReset();
+ if (!svgReset->HasClipPath()) {
+ return true;
+ }
+ if (svgReset->mClipPath.IsUrl()) {
+ // If the clip-path property references non-existent or invalid clipPath
+ // element(s) we ignore it.
+ SVGClipPathFrame* clipPathFrame;
+ SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame);
+ return !clipPathFrame ||
+ clipPathFrame->PointIsInsideClipPath(aFrame, aPoint);
+ }
+ return CSSClipPathInstance::HitTestBasicShapeOrPathClip(aFrame, aPoint);
+}
+
+IntSize SVGUtils::ConvertToSurfaceSize(const gfxSize& aSize,
+ bool* aResultOverflows) {
+ IntSize surfaceSize(ClampToInt(ceil(aSize.width)),
+ ClampToInt(ceil(aSize.height)));
+
+ *aResultOverflows = surfaceSize.width != ceil(aSize.width) ||
+ surfaceSize.height != ceil(aSize.height);
+
+ if (!Factory::AllowedSurfaceSize(surfaceSize)) {
+ surfaceSize.width =
+ std::min(NS_SVG_OFFSCREEN_MAX_DIMENSION, surfaceSize.width);
+ surfaceSize.height =
+ std::min(NS_SVG_OFFSCREEN_MAX_DIMENSION, surfaceSize.height);
+ *aResultOverflows = true;
+ }
+
+ return surfaceSize;
+}
+
+bool SVGUtils::HitTestRect(const gfx::Matrix& aMatrix, float aRX, float aRY,
+ float aRWidth, float aRHeight, float aX, float aY) {
+ gfx::Rect rect(aRX, aRY, aRWidth, aRHeight);
+ if (rect.IsEmpty() || aMatrix.IsSingular()) {
+ return false;
+ }
+ gfx::Matrix toRectSpace = aMatrix;
+ toRectSpace.Invert();
+ gfx::Point p = toRectSpace.TransformPoint(gfx::Point(aX, aY));
+ return rect.x <= p.x && p.x <= rect.XMost() && rect.y <= p.y &&
+ p.y <= rect.YMost();
+}
+
+gfxRect SVGUtils::GetClipRectForFrame(const nsIFrame* aFrame, float aX,
+ float aY, float aWidth, float aHeight) {
+ const nsStyleDisplay* disp = aFrame->StyleDisplay();
+ const nsStyleEffects* effects = aFrame->StyleEffects();
+
+ bool clipApplies = disp->mOverflowX == StyleOverflow::Hidden ||
+ disp->mOverflowY == StyleOverflow::Hidden;
+
+ if (!clipApplies || effects->mClip.IsAuto()) {
+ return gfxRect(aX, aY, aWidth, aHeight);
+ }
+
+ const auto& rect = effects->mClip.AsRect();
+ nsRect coordClipRect = rect.ToLayoutRect();
+ nsIntRect clipPxRect = coordClipRect.ToOutsidePixels(
+ aFrame->PresContext()->AppUnitsPerDevPixel());
+ gfxRect clipRect =
+ gfxRect(clipPxRect.x, clipPxRect.y, clipPxRect.width, clipPxRect.height);
+ if (rect.right.IsAuto()) {
+ clipRect.width = aWidth - clipRect.X();
+ }
+ if (rect.bottom.IsAuto()) {
+ clipRect.height = aHeight - clipRect.Y();
+ }
+ if (disp->mOverflowX != StyleOverflow::Hidden) {
+ clipRect.x = aX;
+ clipRect.width = aWidth;
+ }
+ if (disp->mOverflowY != StyleOverflow::Hidden) {
+ clipRect.y = aY;
+ clipRect.height = aHeight;
+ }
+ return clipRect;
+}
+
+gfxRect SVGUtils::GetBBox(nsIFrame* aFrame, uint32_t aFlags,
+ const gfxMatrix* aToBoundsSpace) {
+ if (aFrame->IsTextFrame()) {
+ aFrame = aFrame->GetParent();
+ }
+
+ if (aFrame->IsInSVGTextSubtree()) {
+ // It is possible to apply a gradient, pattern, clipping path, mask or
+ // filter to text. When one of these facilities is applied to text
+ // the bounding box is the entire text element in all cases.
+ aFrame =
+ nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::SVGText);
+ }
+
+ ISVGDisplayableFrame* svg = do_QueryFrame(aFrame);
+ const bool hasSVGLayout = aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT);
+ if (hasSVGLayout && !svg) {
+ // An SVG frame, but not one that can be displayed directly (for
+ // example, nsGradientFrame). These can't contribute to the bbox.
+ return gfxRect();
+ }
+
+ const bool isOuterSVG = svg && !hasSVGLayout;
+ MOZ_ASSERT(!isOuterSVG || aFrame->IsSVGOuterSVGFrame());
+ if (!svg || (isOuterSVG && (aFlags & eUseFrameBoundsForOuterSVG))) {
+ // An HTML element or an SVG outer frame.
+ MOZ_ASSERT(!hasSVGLayout);
+ bool onlyCurrentFrame = aFlags & eIncludeOnlyCurrentFrameForNonSVGElement;
+ return SVGIntegrationUtils::GetSVGBBoxForNonSVGFrame(
+ aFrame,
+ /* aUnionContinuations = */ !onlyCurrentFrame);
+ }
+
+ MOZ_ASSERT(svg);
+
+ if (auto* element = SVGElement::FromNodeOrNull(aFrame->GetContent())) {
+ if (!element->HasValidDimensions()) {
+ return gfxRect();
+ }
+ }
+
+ // Clean out flags which have no effects on returning bbox from now, so that
+ // we can cache and reuse ObjectBoundingBoxProperty() in the code below.
+ aFlags &=
+ ~(eIncludeOnlyCurrentFrameForNonSVGElement | eUseFrameBoundsForOuterSVG);
+ if (!aFrame->IsSVGUseFrame()) {
+ aFlags &= ~eUseUserSpaceOfUseElement;
+ }
+
+ if (aFlags == eBBoxIncludeFillGeometry &&
+ // We only cache bbox in element's own user space
+ !aToBoundsSpace) {
+ gfxRect* prop = aFrame->GetProperty(ObjectBoundingBoxProperty());
+ if (prop) {
+ return *prop;
+ }
+ }
+
+ gfxMatrix matrix;
+ if (aToBoundsSpace) {
+ matrix = *aToBoundsSpace;
+ }
+
+ if (aFrame->IsSVGForeignObjectFrame() ||
+ aFlags & SVGUtils::eUseUserSpaceOfUseElement) {
+ // The spec says getBBox "Returns the tight bounding box in *current user
+ // space*". So we should really be doing this for all elements, but that
+ // needs investigation to check that we won't break too much content.
+ // NOTE: When changing this to apply to other frame types, make sure to
+ // also update SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset.
+ MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast");
+ SVGElement* element = static_cast<SVGElement*>(aFrame->GetContent());
+ matrix = element->PrependLocalTransformsTo(matrix, eChildToUserSpace);
+ }
+ gfxRect bbox =
+ svg->GetBBoxContribution(ToMatrix(matrix), aFlags).ToThebesRect();
+ // Account for 'clipped'.
+ if (aFlags & SVGUtils::eBBoxIncludeClipped) {
+ gfxRect clipRect;
+ float x, y, width, height;
+ gfxRect fillBBox =
+ svg->GetBBoxContribution({}, SVGUtils::eBBoxIncludeFill).ToThebesRect();
+ x = fillBBox.x;
+ y = fillBBox.y;
+ width = fillBBox.width;
+ height = fillBBox.height;
+ bool hasClip = aFrame->StyleDisplay()->IsScrollableOverflow();
+ if (hasClip) {
+ clipRect = SVGUtils::GetClipRectForFrame(aFrame, x, y, width, height);
+ if (aFrame->IsSVGForeignObjectFrame() || aFrame->IsSVGUseFrame()) {
+ clipRect = matrix.TransformBounds(clipRect);
+ }
+ }
+ SVGClipPathFrame* clipPathFrame;
+ if (SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame) ==
+ SVGObserverUtils::eHasRefsSomeInvalid) {
+ bbox = gfxRect(0, 0, 0, 0);
+ } else {
+ if (clipPathFrame) {
+ SVGClipPathElement* clipContent =
+ static_cast<SVGClipPathElement*>(clipPathFrame->GetContent());
+ if (clipContent->IsUnitsObjectBoundingBox()) {
+ matrix.PreTranslate(gfxPoint(x, y));
+ matrix.PreScale(width, height);
+ } else if (aFrame->IsSVGForeignObjectFrame()) {
+ matrix = gfxMatrix();
+ }
+ matrix *= SVGUtils::GetTransformMatrixInUserSpace(clipPathFrame);
+
+ bbox = clipPathFrame->GetBBoxForClipPathFrame(bbox, matrix, aFlags)
+ .ToThebesRect();
+ }
+
+ if (hasClip) {
+ bbox = bbox.Intersect(clipRect);
+ }
+
+ if (bbox.IsEmpty()) {
+ bbox = gfxRect(0, 0, 0, 0);
+ }
+ }
+ }
+
+ if (aFlags == eBBoxIncludeFillGeometry &&
+ // We only cache bbox in element's own user space
+ !aToBoundsSpace) {
+ // Obtaining the bbox for objectBoundingBox calculations is common so we
+ // cache the result for future calls, since calculation can be expensive:
+ aFrame->SetProperty(ObjectBoundingBoxProperty(), new gfxRect(bbox));
+ }
+
+ return bbox;
+}
+
+gfxPoint SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(const nsIFrame* aFrame) {
+ if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
+ // The user space for non-SVG frames is defined as the bounding box of the
+ // frame's border-box rects over all continuations.
+ return gfxPoint();
+ }
+
+ // Leaf frames apply their own offset inside their user space.
+ if (FrameDoesNotIncludePositionInTM(aFrame)) {
+ return nsLayoutUtils::RectToGfxRect(aFrame->GetRect(),
+ AppUnitsPerCSSPixel())
+ .TopLeft();
+ }
+
+ // For foreignObject frames, SVGUtils::GetBBox applies their local
+ // transform, so we need to do the same here.
+ if (aFrame->IsSVGForeignObjectFrame()) {
+ gfxMatrix transform =
+ static_cast<SVGElement*>(aFrame->GetContent())
+ ->PrependLocalTransformsTo(gfxMatrix(), eChildToUserSpace);
+ NS_ASSERTION(!transform.HasNonTranslation(),
+ "we're relying on this being an offset-only transform");
+ return transform.GetTranslation();
+ }
+
+ return gfxPoint();
+}
+
+static gfxRect GetBoundingBoxRelativeRect(const SVGAnimatedLength* aXYWH,
+ const gfxRect& aBBox) {
+ return gfxRect(aBBox.x + SVGUtils::ObjectSpace(aBBox, &aXYWH[0]),
+ aBBox.y + SVGUtils::ObjectSpace(aBBox, &aXYWH[1]),
+ SVGUtils::ObjectSpace(aBBox, &aXYWH[2]),
+ SVGUtils::ObjectSpace(aBBox, &aXYWH[3]));
+}
+
+gfxRect SVGUtils::GetRelativeRect(uint16_t aUnits,
+ const SVGAnimatedLength* aXYWH,
+ const gfxRect& aBBox,
+ const UserSpaceMetrics& aMetrics) {
+ if (aUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
+ return GetBoundingBoxRelativeRect(aXYWH, aBBox);
+ }
+ return gfxRect(UserSpace(aMetrics, &aXYWH[0]), UserSpace(aMetrics, &aXYWH[1]),
+ UserSpace(aMetrics, &aXYWH[2]),
+ UserSpace(aMetrics, &aXYWH[3]));
+}
+
+gfxRect SVGUtils::GetRelativeRect(uint16_t aUnits,
+ const SVGAnimatedLength* aXYWH,
+ const gfxRect& aBBox, nsIFrame* aFrame) {
+ if (aUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
+ return GetBoundingBoxRelativeRect(aXYWH, aBBox);
+ }
+ if (SVGElement* svgElement = SVGElement::FromNode(aFrame->GetContent())) {
+ return GetRelativeRect(aUnits, aXYWH, aBBox, SVGElementMetrics(svgElement));
+ }
+ return GetRelativeRect(aUnits, aXYWH, aBBox,
+ NonSVGFrameUserSpaceMetrics(aFrame));
+}
+
+bool SVGUtils::CanOptimizeOpacity(const nsIFrame* aFrame) {
+ if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
+ return false;
+ }
+ auto* content = aFrame->GetContent();
+ if (!content->IsSVGGeometryElement() &&
+ !content->IsSVGElement(nsGkAtoms::image)) {
+ return false;
+ }
+ if (aFrame->StyleEffects()->HasFilters()) {
+ return false;
+ }
+ // XXX The SVG WG is intending to allow fill, stroke and markers on <image>
+ if (content->IsSVGElement(nsGkAtoms::image)) {
+ return true;
+ }
+ const nsStyleSVG* style = aFrame->StyleSVG();
+ if (style->HasMarker() &&
+ static_cast<SVGGeometryElement*>(content)->IsMarkable()) {
+ return false;
+ }
+
+ if (nsLayoutUtils::HasAnimationOfPropertySet(
+ aFrame, nsCSSPropertyIDSet::OpacityProperties())) {
+ return false;
+ }
+
+ return !style->HasFill() || !HasStroke(aFrame);
+}
+
+gfxMatrix SVGUtils::AdjustMatrixForUnits(const gfxMatrix& aMatrix,
+ const SVGAnimatedEnumeration* aUnits,
+ nsIFrame* aFrame, uint32_t aFlags) {
+ if (aFrame && aUnits->GetAnimValue() == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) {
+ gfxRect bbox = GetBBox(aFrame, aFlags);
+ gfxMatrix tm = aMatrix;
+ tm.PreTranslate(gfxPoint(bbox.X(), bbox.Y()));
+ tm.PreScale(bbox.Width(), bbox.Height());
+ return tm;
+ }
+ return aMatrix;
+}
+
+bool SVGUtils::GetNonScalingStrokeTransform(const nsIFrame* aFrame,
+ gfxMatrix* aUserToOuterSVG) {
+ if (aFrame->GetContent()->IsText()) {
+ aFrame = aFrame->GetParent();
+ }
+
+ if (!aFrame->StyleSVGReset()->HasNonScalingStroke()) {
+ return false;
+ }
+
+ MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "should be an SVG element");
+
+ *aUserToOuterSVG = ThebesMatrix(SVGContentUtils::GetCTM(
+ static_cast<SVGElement*>(aFrame->GetContent()), true));
+
+ return aUserToOuterSVG->HasNonTranslation();
+}
+
+// The logic here comes from _cairo_stroke_style_max_distance_from_path
+static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+ const nsIFrame* aFrame,
+ double aStyleExpansionFactor,
+ const gfxMatrix& aMatrix) {
+ double style_expansion =
+ aStyleExpansionFactor * SVGUtils::GetStrokeWidth(aFrame);
+
+ gfxMatrix matrix = aMatrix;
+
+ gfxMatrix outerSVGToUser;
+ if (SVGUtils::GetNonScalingStrokeTransform(aFrame, &outerSVGToUser)) {
+ outerSVGToUser.Invert();
+ matrix.PreMultiply(outerSVGToUser);
+ }
+
+ double dx = style_expansion * (fabs(matrix._11) + fabs(matrix._21));
+ double dy = style_expansion * (fabs(matrix._22) + fabs(matrix._12));
+
+ gfxRect strokeExtents = aPathExtents;
+ strokeExtents.Inflate(dx, dy);
+ return strokeExtents;
+}
+
+/*static*/
+gfxRect SVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+ const nsTextFrame* aFrame,
+ const gfxMatrix& aMatrix) {
+ NS_ASSERTION(aFrame->IsInSVGTextSubtree(),
+ "expected an nsTextFrame for SVG text");
+ return mozilla::PathExtentsToMaxStrokeExtents(aPathExtents, aFrame, 0.5,
+ aMatrix);
+}
+
+/*static*/
+gfxRect SVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents,
+ const SVGGeometryFrame* aFrame,
+ const gfxMatrix& aMatrix) {
+ bool strokeMayHaveCorners =
+ !SVGContentUtils::ShapeTypeHasNoCorners(aFrame->GetContent());
+
+ // For a shape without corners the stroke can only extend half the stroke
+ // width from the path in the x/y-axis directions. For shapes with corners
+ // the stroke can extend by sqrt(1/2) (think 45 degree rotated rect, or line
+ // with stroke-linecaps="square").
+ double styleExpansionFactor = strokeMayHaveCorners ? M_SQRT1_2 : 0.5;
+
+ // The stroke can extend even further for paths that can be affected by
+ // stroke-miterlimit.
+ // We only need to do this if the limit is greater than 1, but it's probably
+ // not worth optimizing for that.
+ bool affectedByMiterlimit = aFrame->GetContent()->IsAnyOfSVGElements(
+ nsGkAtoms::path, nsGkAtoms::polyline, nsGkAtoms::polygon);
+
+ if (affectedByMiterlimit) {
+ const nsStyleSVG* style = aFrame->StyleSVG();
+ if (style->mStrokeLinejoin == StyleStrokeLinejoin::Miter &&
+ styleExpansionFactor < style->mStrokeMiterlimit / 2.0) {
+ styleExpansionFactor = style->mStrokeMiterlimit / 2.0;
+ }
+ }
+
+ return mozilla::PathExtentsToMaxStrokeExtents(aPathExtents, aFrame,
+ styleExpansionFactor, aMatrix);
+}
+
+// ----------------------------------------------------------------------
+
+/* static */
+nscolor SVGUtils::GetFallbackOrPaintColor(
+ const ComputedStyle& aStyle, StyleSVGPaint nsStyleSVG::*aFillOrStroke,
+ nscolor aDefaultContextFallbackColor) {
+ const auto& paint = aStyle.StyleSVG()->*aFillOrStroke;
+ nscolor color;
+ switch (paint.kind.tag) {
+ case StyleSVGPaintKind::Tag::PaintServer:
+ color = paint.fallback.IsColor()
+ ? paint.fallback.AsColor().CalcColor(aStyle)
+ : NS_RGBA(0, 0, 0, 0);
+ break;
+ case StyleSVGPaintKind::Tag::ContextStroke:
+ case StyleSVGPaintKind::Tag::ContextFill:
+ color = paint.fallback.IsColor()
+ ? paint.fallback.AsColor().CalcColor(aStyle)
+ : aDefaultContextFallbackColor;
+ break;
+ default:
+ color = paint.kind.AsColor().CalcColor(aStyle);
+ break;
+ }
+ if (const auto* styleIfVisited = aStyle.GetStyleIfVisited()) {
+ const auto& paintIfVisited = styleIfVisited->StyleSVG()->*aFillOrStroke;
+ // To prevent Web content from detecting if a user has visited a URL
+ // (via URL loading triggered by paint servers or performance
+ // differences between paint servers or between a paint server and a
+ // color), we do not allow whether links are visited to change which
+ // paint server is used or switch between paint servers and simple
+ // colors. A :visited style may only override a simple color with
+ // another simple color.
+ if (paintIfVisited.kind.IsColor() && paint.kind.IsColor()) {
+ nscolor colors[2] = {
+ color, paintIfVisited.kind.AsColor().CalcColor(*styleIfVisited)};
+ return ComputedStyle::CombineVisitedColors(colors,
+ aStyle.RelevantLinkVisited());
+ }
+ }
+ return color;
+}
+
+/* static */
+void SVGUtils::MakeFillPatternFor(nsIFrame* aFrame, gfxContext* aContext,
+ GeneralPattern* aOutPattern,
+ imgDrawingParams& aImgParams,
+ SVGContextPaint* aContextPaint) {
+ const nsStyleSVG* style = aFrame->StyleSVG();
+ if (style->mFill.kind.IsNone()) {
+ return;
+ }
+
+ const auto* styleEffects = aFrame->StyleEffects();
+
+ float fillOpacity = GetOpacity(style->mFillOpacity, aContextPaint);
+ if (!styleEffects->IsOpaque() && SVGUtils::CanOptimizeOpacity(aFrame)) {
+ // Combine the group opacity into the fill opacity (we will have skipped
+ // creating an offscreen surface to apply the group opacity).
+ fillOpacity *= styleEffects->mOpacity;
+ }
+
+ const DrawTarget* dt = aContext->GetDrawTarget();
+
+ SVGPaintServerFrame* ps =
+ SVGObserverUtils::GetAndObservePaintServer(aFrame, &nsStyleSVG::mFill);
+
+ if (ps) {
+ RefPtr<gfxPattern> pattern =
+ ps->GetPaintServerPattern(aFrame, dt, aContext->CurrentMatrixDouble(),
+ &nsStyleSVG::mFill, fillOpacity, aImgParams);
+ if (pattern) {
+ pattern->CacheColorStops(dt);
+ aOutPattern->Init(*pattern->GetPattern(dt));
+ return;
+ }
+ }
+
+ if (aContextPaint) {
+ RefPtr<gfxPattern> pattern;
+ switch (style->mFill.kind.tag) {
+ case StyleSVGPaintKind::Tag::ContextFill:
+ pattern = aContextPaint->GetFillPattern(
+ dt, fillOpacity, aContext->CurrentMatrixDouble(), aImgParams);
+ break;
+ case StyleSVGPaintKind::Tag::ContextStroke:
+ pattern = aContextPaint->GetStrokePattern(
+ dt, fillOpacity, aContext->CurrentMatrixDouble(), aImgParams);
+ break;
+ default:;
+ }
+ if (pattern) {
+ aOutPattern->Init(*pattern->GetPattern(dt));
+ return;
+ }
+ }
+
+ if (style->mFill.fallback.IsNone()) {
+ return;
+ }
+
+ // On failure, use the fallback colour in case we have an
+ // objectBoundingBox where the width or height of the object is zero.
+ // See http://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox
+ sRGBColor color(sRGBColor::FromABGR(GetFallbackOrPaintColor(
+ *aFrame->Style(), &nsStyleSVG::mFill, NS_RGB(0, 0, 0))));
+ color.a *= fillOpacity;
+ aOutPattern->InitColorPattern(ToDeviceColor(color));
+}
+
+/* static */
+void SVGUtils::MakeStrokePatternFor(nsIFrame* aFrame, gfxContext* aContext,
+ GeneralPattern* aOutPattern,
+ imgDrawingParams& aImgParams,
+ SVGContextPaint* aContextPaint) {
+ const nsStyleSVG* style = aFrame->StyleSVG();
+ if (style->mStroke.kind.IsNone()) {
+ return;
+ }
+
+ const auto* styleEffects = aFrame->StyleEffects();
+
+ float strokeOpacity = GetOpacity(style->mStrokeOpacity, aContextPaint);
+ if (!styleEffects->IsOpaque() && SVGUtils::CanOptimizeOpacity(aFrame)) {
+ // Combine the group opacity into the stroke opacity (we will have skipped
+ // creating an offscreen surface to apply the group opacity).
+ strokeOpacity *= styleEffects->mOpacity;
+ }
+
+ const DrawTarget* dt = aContext->GetDrawTarget();
+
+ SVGPaintServerFrame* ps =
+ SVGObserverUtils::GetAndObservePaintServer(aFrame, &nsStyleSVG::mStroke);
+
+ if (ps) {
+ RefPtr<gfxPattern> pattern = ps->GetPaintServerPattern(
+ aFrame, dt, aContext->CurrentMatrixDouble(), &nsStyleSVG::mStroke,
+ strokeOpacity, aImgParams);
+ if (pattern) {
+ pattern->CacheColorStops(dt);
+ aOutPattern->Init(*pattern->GetPattern(dt));
+ return;
+ }
+ }
+
+ if (aContextPaint) {
+ RefPtr<gfxPattern> pattern;
+ switch (style->mStroke.kind.tag) {
+ case StyleSVGPaintKind::Tag::ContextFill:
+ pattern = aContextPaint->GetFillPattern(
+ dt, strokeOpacity, aContext->CurrentMatrixDouble(), aImgParams);
+ break;
+ case StyleSVGPaintKind::Tag::ContextStroke:
+ pattern = aContextPaint->GetStrokePattern(
+ dt, strokeOpacity, aContext->CurrentMatrixDouble(), aImgParams);
+ break;
+ default:;
+ }
+ if (pattern) {
+ aOutPattern->Init(*pattern->GetPattern(dt));
+ return;
+ }
+ }
+
+ if (style->mStroke.fallback.IsNone()) {
+ return;
+ }
+
+ // On failure, use the fallback colour in case we have an
+ // objectBoundingBox where the width or height of the object is zero.
+ // See http://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox
+ sRGBColor color(sRGBColor::FromABGR(GetFallbackOrPaintColor(
+ *aFrame->Style(), &nsStyleSVG::mStroke, NS_RGBA(0, 0, 0, 0))));
+ color.a *= strokeOpacity;
+ aOutPattern->InitColorPattern(ToDeviceColor(color));
+}
+
+/* static */
+float SVGUtils::GetOpacity(const StyleSVGOpacity& aOpacity,
+ const SVGContextPaint* aContextPaint) {
+ float opacity = 1.0f;
+ switch (aOpacity.tag) {
+ case StyleSVGOpacity::Tag::Opacity:
+ return aOpacity.AsOpacity();
+ case StyleSVGOpacity::Tag::ContextFillOpacity:
+ if (aContextPaint) {
+ opacity = aContextPaint->GetFillOpacity();
+ }
+ break;
+ case StyleSVGOpacity::Tag::ContextStrokeOpacity:
+ if (aContextPaint) {
+ opacity = aContextPaint->GetStrokeOpacity();
+ }
+ break;
+ }
+ return opacity;
+}
+
+bool SVGUtils::HasStroke(const nsIFrame* aFrame,
+ const SVGContextPaint* aContextPaint) {
+ const nsStyleSVG* style = aFrame->StyleSVG();
+ return style->HasStroke() && GetStrokeWidth(aFrame, aContextPaint) > 0;
+}
+
+float SVGUtils::GetStrokeWidth(const nsIFrame* aFrame,
+ const SVGContextPaint* aContextPaint) {
+ nsIContent* content = aFrame->GetContent();
+ if (content->IsText()) {
+ content = content->GetParent();
+ }
+
+ auto* ctx = SVGElement::FromNode(content);
+ return SVGContentUtils::GetStrokeWidth(ctx, aFrame->Style(), aContextPaint);
+}
+
+void SVGUtils::SetupStrokeGeometry(nsIFrame* aFrame, gfxContext* aContext,
+ SVGContextPaint* aContextPaint) {
+ MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast");
+ SVGContentUtils::AutoStrokeOptions strokeOptions;
+ SVGContentUtils::GetStrokeOptions(&strokeOptions,
+ SVGElement::FromNode(aFrame->GetContent()),
+ aFrame->Style(), aContextPaint);
+
+ if (strokeOptions.mLineWidth <= 0) {
+ return;
+ }
+
+ // SVGContentUtils::GetStrokeOptions gets the stroke options in CSS px;
+ // convert to device pixels for gfxContext.
+ float devPxPerCSSPx = aFrame->PresContext()->CSSToDevPixelScale().scale;
+
+ aContext->SetLineWidth(strokeOptions.mLineWidth * devPxPerCSSPx);
+ aContext->SetLineCap(strokeOptions.mLineCap);
+ aContext->SetMiterLimit(strokeOptions.mMiterLimit);
+ aContext->SetLineJoin(strokeOptions.mLineJoin);
+ aContext->SetDash(strokeOptions.mDashPattern, strokeOptions.mDashLength,
+ strokeOptions.mDashOffset, devPxPerCSSPx);
+}
+
+uint16_t SVGUtils::GetGeometryHitTestFlags(const nsIFrame* aFrame) {
+ uint16_t flags = 0;
+
+ switch (aFrame->Style()->PointerEvents()) {
+ case StylePointerEvents::None:
+ break;
+ case StylePointerEvents::Auto:
+ case StylePointerEvents::Visiblepainted:
+ if (aFrame->StyleVisibility()->IsVisible()) {
+ if (!aFrame->StyleSVG()->mFill.kind.IsNone()) {
+ flags = SVG_HIT_TEST_FILL;
+ }
+ if (!aFrame->StyleSVG()->mStroke.kind.IsNone()) {
+ flags |= SVG_HIT_TEST_STROKE;
+ }
+ }
+ break;
+ case StylePointerEvents::Visiblefill:
+ if (aFrame->StyleVisibility()->IsVisible()) {
+ flags = SVG_HIT_TEST_FILL;
+ }
+ break;
+ case StylePointerEvents::Visiblestroke:
+ if (aFrame->StyleVisibility()->IsVisible()) {
+ flags = SVG_HIT_TEST_STROKE;
+ }
+ break;
+ case StylePointerEvents::Visible:
+ if (aFrame->StyleVisibility()->IsVisible()) {
+ flags = SVG_HIT_TEST_FILL | SVG_HIT_TEST_STROKE;
+ }
+ break;
+ case StylePointerEvents::Painted:
+ if (!aFrame->StyleSVG()->mFill.kind.IsNone()) {
+ flags = SVG_HIT_TEST_FILL;
+ }
+ if (!aFrame->StyleSVG()->mStroke.kind.IsNone()) {
+ flags |= SVG_HIT_TEST_STROKE;
+ }
+ break;
+ case StylePointerEvents::Fill:
+ flags = SVG_HIT_TEST_FILL;
+ break;
+ case StylePointerEvents::Stroke:
+ flags = SVG_HIT_TEST_STROKE;
+ break;
+ case StylePointerEvents::All:
+ flags = SVG_HIT_TEST_FILL | SVG_HIT_TEST_STROKE;
+ break;
+ default:
+ NS_ERROR("not reached");
+ break;
+ }
+
+ return flags;
+}
+
+void SVGUtils::PaintSVGGlyph(Element* aElement, gfxContext* aContext) {
+ nsIFrame* frame = aElement->GetPrimaryFrame();
+ ISVGDisplayableFrame* svgFrame = do_QueryFrame(frame);
+ if (!svgFrame) {
+ return;
+ }
+ gfxMatrix m;
+ if (frame->GetContent()->IsSVGElement()) {
+ // PaintSVG() expects the passed transform to be the transform to its own
+ // SVG user space, so we need to account for any 'transform' attribute:
+ m = SVGUtils::GetTransformMatrixInUserSpace(frame);
+ }
+
+ // SVG-in-OpenType is not allowed to paint external resources, so we can
+ // just pass a dummy params into PatintSVG.
+ imgDrawingParams dummy;
+ svgFrame->PaintSVG(*aContext, m, dummy);
+}
+
+bool SVGUtils::GetSVGGlyphExtents(const Element* aElement,
+ const gfxMatrix& aSVGToAppSpace,
+ gfxRect* aResult) {
+ nsIFrame* frame = aElement->GetPrimaryFrame();
+ ISVGDisplayableFrame* svgFrame = do_QueryFrame(frame);
+ if (!svgFrame) {
+ return false;
+ }
+
+ gfxMatrix transform(aSVGToAppSpace);
+ if (auto* svg = SVGElement::FromNode(frame->GetContent())) {
+ transform = svg->PrependLocalTransformsTo(aSVGToAppSpace);
+ }
+
+ *aResult =
+ svgFrame
+ ->GetBBoxContribution(gfx::ToMatrix(transform),
+ SVGUtils::eBBoxIncludeFill |
+ SVGUtils::eBBoxIncludeFillGeometry |
+ SVGUtils::eBBoxIncludeStroke |
+ SVGUtils::eBBoxIncludeStrokeGeometry |
+ SVGUtils::eBBoxIncludeMarkers)
+ .ToThebesRect();
+ return true;
+}
+
+nsRect SVGUtils::ToCanvasBounds(const gfxRect& aUserspaceRect,
+ const gfxMatrix& aToCanvas,
+ const nsPresContext* presContext) {
+ return nsLayoutUtils::RoundGfxRectToAppRect(
+ aToCanvas.TransformBounds(aUserspaceRect),
+ presContext->AppUnitsPerDevPixel());
+}
+
+gfxMatrix SVGUtils::GetCSSPxToDevPxMatrix(const nsIFrame* aNonSVGFrame) {
+ float devPxPerCSSPx = aNonSVGFrame->PresContext()->CSSToDevPixelScale().scale;
+
+ return gfxMatrix(devPxPerCSSPx, 0.0, 0.0, devPxPerCSSPx, 0.0, 0.0);
+}
+
+gfxMatrix SVGUtils::GetTransformMatrixInUserSpace(const nsIFrame* aFrame) {
+ // We check element instead of aFrame directly because SVG element
+ // may have non-SVG frame, <tspan> for example.
+ MOZ_ASSERT(aFrame->GetContent() && aFrame->GetContent()->IsSVGElement(),
+ "Only use this wrapper for SVG elements");
+
+ if (!aFrame->IsTransformed()) {
+ return {};
+ }
+
+ nsStyleTransformMatrix::TransformReferenceBox refBox(aFrame);
+ nsDisplayTransform::FrameTransformProperties properties{
+ aFrame, refBox, AppUnitsPerCSSPixel()};
+
+ // SVG elements can have x/y offset, their default transform origin
+ // is the origin of user space, not the top left point of the frame.
+ Point3D svgTransformOrigin{
+ properties.mToTransformOrigin.x - CSSPixel::FromAppUnits(refBox.X()),
+ properties.mToTransformOrigin.y - CSSPixel::FromAppUnits(refBox.Y()),
+ properties.mToTransformOrigin.z};
+
+ Matrix svgTransform;
+ Matrix4x4 trans;
+ (void)aFrame->IsSVGTransformed(&svgTransform);
+
+ if (properties.HasTransform()) {
+ trans = nsStyleTransformMatrix::ReadTransforms(
+ properties.mTranslate, properties.mRotate, properties.mScale,
+ properties.mMotion, properties.mTransform, refBox,
+ AppUnitsPerCSSPixel());
+ } else {
+ trans = Matrix4x4::From2D(svgTransform);
+ }
+
+ trans.ChangeBasis(svgTransformOrigin);
+
+ Matrix mm;
+ trans.ProjectTo2D();
+ (void)trans.CanDraw2D(&mm);
+
+ return ThebesMatrix(mm);
+}
+
+} // namespace mozilla