/* 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:
#include "SVGIntegrationUtils.h"

// Keep others in (case-insensitive) order:
#include "SVGPaintServerFrame.h"
#include "gfxContext.h"
#include "gfxDrawable.h"
#include "mozilla/CSSClipPathInstance.h"
#include "mozilla/FilterInstance.h"
#include "mozilla/SVGClipPathFrame.h"
#include "mozilla/SVGMaskFrame.h"
#include "mozilla/SVGObserverUtils.h"
#include "mozilla/SVGUtils.h"
#include "mozilla/StaticPrefs_layers.h"
#include "mozilla/dom/SVGElement.h"
#include "mozilla/gfx/Point.h"
#include "nsCSSRendering.h"
#include "nsDisplayList.h"
#include "nsLayoutUtils.h"

using namespace mozilla::dom;
using namespace mozilla::layers;
using namespace mozilla::gfx;
using namespace mozilla::image;

namespace mozilla {

/**
 * This class is used to get the pre-effects ink overflow rect of a frame,
 * or, in the case of a frame with continuations, to collect the union of the
 * pre-effects ink overflow rects of all the continuations. The result is
 * relative to the origin (top left corner of the border box) of the frame, or,
 * if the frame has continuations, the origin of the  _first_ continuation.
 */
class PreEffectsInkOverflowCollector : public nsLayoutUtils::BoxCallback {
 public:
  /**
   * If the pre-effects ink overflow rect of the frame being examined
   * happens to be known, it can be passed in as aCurrentFrame and its
   * pre-effects ink overflow rect can be passed in as
   * aCurrentFrameOverflowArea. This is just an optimization to save a
   * frame property lookup - these arguments are optional.
   */
  PreEffectsInkOverflowCollector(nsIFrame* aFirstContinuation,
                                 nsIFrame* aCurrentFrame,
                                 const nsRect& aCurrentFrameOverflowArea,
                                 bool aInReflow)
      : mFirstContinuation(aFirstContinuation),
        mCurrentFrame(aCurrentFrame),
        mCurrentFrameOverflowArea(aCurrentFrameOverflowArea),
        mInReflow(aInReflow) {
    NS_ASSERTION(!mFirstContinuation->GetPrevContinuation(),
                 "We want the first continuation here");
  }

  void AddBox(nsIFrame* aFrame) override {
    nsRect overflow = (aFrame == mCurrentFrame)
                          ? mCurrentFrameOverflowArea
                          : PreEffectsInkOverflowRect(aFrame, mInReflow);
    mResult.UnionRect(mResult,
                      overflow + aFrame->GetOffsetTo(mFirstContinuation));
  }

  nsRect GetResult() const { return mResult; }

 private:
  static nsRect PreEffectsInkOverflowRect(nsIFrame* aFrame, bool aInReflow) {
    nsRect* r = aFrame->GetProperty(nsIFrame::PreEffectsBBoxProperty());
    if (r) {
      return *r;
    }

#ifdef DEBUG
    // Having PreTransformOverflowAreasProperty cached means
    // InkOverflowRect() will return post-effect rect, which is not what
    // we want. This function intentional reports pre-effect rect. But it does
    // not matter if there is no SVG effect on this frame, since no effect
    // means post-effect rect matches pre-effect rect.
    //
    // This function may be called during reflow or painting. We should only
    // do this check in painting process since the PreEffectsBBoxProperty of
    // continuations are not set correctly while reflowing.
    if (SVGIntegrationUtils::UsingOverflowAffectingEffects(aFrame) &&
        !aInReflow) {
      OverflowAreas* preTransformOverflows =
          aFrame->GetProperty(nsIFrame::PreTransformOverflowAreasProperty());

      MOZ_ASSERT(!preTransformOverflows,
                 "InkOverflowRect() won't return the pre-effects rect!");
    }
#endif
    return aFrame->InkOverflowRectRelativeToSelf();
  }

  nsIFrame* mFirstContinuation;
  nsIFrame* mCurrentFrame;
  const nsRect& mCurrentFrameOverflowArea;
  nsRect mResult;
  bool mInReflow;
};

/**
 * Gets the union of the pre-effects ink overflow rects of all of a frame's
 * continuations, in "user space".
 */
static nsRect GetPreEffectsInkOverflowUnion(
    nsIFrame* aFirstContinuation, nsIFrame* aCurrentFrame,
    const nsRect& aCurrentFramePreEffectsOverflow,
    const nsPoint& aFirstContinuationToUserSpace, bool aInReflow) {
  NS_ASSERTION(!aFirstContinuation->GetPrevContinuation(),
               "Need first continuation here");
  PreEffectsInkOverflowCollector collector(aFirstContinuation, aCurrentFrame,
                                           aCurrentFramePreEffectsOverflow,
                                           aInReflow);
  // Compute union of all overflow areas relative to aFirstContinuation:
  nsLayoutUtils::GetAllInFlowBoxes(aFirstContinuation, &collector);
  // Return the result in user space:
  return collector.GetResult() + aFirstContinuationToUserSpace;
}

/**
 * Gets the pre-effects ink overflow rect of aCurrentFrame in "user space".
 */
static nsRect GetPreEffectsInkOverflow(
    nsIFrame* aFirstContinuation, nsIFrame* aCurrentFrame,
    const nsPoint& aFirstContinuationToUserSpace) {
  NS_ASSERTION(!aFirstContinuation->GetPrevContinuation(),
               "Need first continuation here");
  PreEffectsInkOverflowCollector collector(aFirstContinuation, nullptr,
                                           nsRect(), false);
  // Compute overflow areas of current frame relative to aFirstContinuation:
  nsLayoutUtils::AddBoxesForFrame(aCurrentFrame, &collector);
  // Return the result in user space:
  return collector.GetResult() + aFirstContinuationToUserSpace;
}

bool SVGIntegrationUtils::UsingOverflowAffectingEffects(
    const nsIFrame* aFrame) {
  // Currently overflow don't take account of SVG or other non-absolute
  // positioned clipping, or masking.
  return aFrame->StyleEffects()->HasFilters();
}

bool SVGIntegrationUtils::UsingEffectsForFrame(const nsIFrame* aFrame) {
  // Even when SVG display lists are disabled, returning true for SVG frames
  // does not adversely affect any of our callers. Therefore we don't bother
  // checking the SDL prefs here, since we don't know if we're being called for
  // painting or hit-testing anyway.
  const nsStyleSVGReset* style = aFrame->StyleSVGReset();
  const nsStyleEffects* effects = aFrame->StyleEffects();
  // TODO(cbrewster): remove backdrop-filter from this list once it is supported
  // in preserve-3d cases.
  return effects->HasFilters() || effects->HasBackdropFilters() ||
         style->HasClipPath() || style->HasMask();
}

nsPoint SVGIntegrationUtils::GetOffsetToBoundingBox(nsIFrame* aFrame) {
  if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
    // Do NOT call GetAllInFlowRectsUnion for SVG - it will get the
    // covered region relative to the SVGOuterSVGFrame, which is absolutely
    // not what we want. SVG frames are always in user space, so they have
    // no offset adjustment to make.
    return nsPoint();
  }

  // The GetAllInFlowRectsUnion() call gets the union of the frame border-box
  // rects over all continuations, relative to the origin (top-left of the
  // border box) of its second argument (here, aFrame, the first continuation).
  return -nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame).TopLeft();
}

struct EffectOffsets {
  // The offset between the reference frame and the bounding box of the
  // target frame in app unit.
  nsPoint offsetToBoundingBox;
  // The offset between the reference frame and the bounding box of the
  // target frame in app unit.
  nsPoint offsetToUserSpace;
  // The offset between the reference frame and the bounding box of the
  // target frame in device unit.
  gfxPoint offsetToUserSpaceInDevPx;
};

static EffectOffsets ComputeEffectOffset(
    nsIFrame* aFrame, const SVGIntegrationUtils::PaintFramesParams& aParams) {
  EffectOffsets result;

  result.offsetToBoundingBox =
      aParams.builder->ToReferenceFrame(aFrame) -
      SVGIntegrationUtils::GetOffsetToBoundingBox(aFrame);
  if (!aFrame->IsSVGFrame()) {
    /* Snap the offset if the reference frame is not a SVG frame,
     * since other frames will be snapped to pixel when rendering. */
    result.offsetToBoundingBox =
        nsPoint(aFrame->PresContext()->RoundAppUnitsToNearestDevPixels(
                    result.offsetToBoundingBox.x),
                aFrame->PresContext()->RoundAppUnitsToNearestDevPixels(
                    result.offsetToBoundingBox.y));
  }

  // After applying only "aOffsetToBoundingBox", aParams.ctx would have its
  // origin at the top left corner of frame's bounding box (over all
  // continuations).
  // However, SVG painting needs the origin to be located at the origin of the
  // SVG frame's "user space", i.e. the space in which, for example, the
  // frame's BBox lives.
  // SVG geometry frames and foreignObject frames apply their own offsets, so
  // their position is relative to their user space. So for these frame types,
  // if we want aParams.ctx to be in user space, we first need to subtract the
  // frame's position so that SVG painting can later add it again and the
  // frame is painted in the right place.
  gfxPoint toUserSpaceGfx =
      SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(aFrame);
  nsPoint toUserSpace =
      nsPoint(nsPresContext::CSSPixelsToAppUnits(float(toUserSpaceGfx.x)),
              nsPresContext::CSSPixelsToAppUnits(float(toUserSpaceGfx.y)));

  result.offsetToUserSpace = result.offsetToBoundingBox - toUserSpace;

#ifdef DEBUG
  bool hasSVGLayout = aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT);
  NS_ASSERTION(
      hasSVGLayout || result.offsetToBoundingBox == result.offsetToUserSpace,
      "For non-SVG frames there shouldn't be any additional offset");
#endif

  result.offsetToUserSpaceInDevPx = nsLayoutUtils::PointToGfxPoint(
      result.offsetToUserSpace, aFrame->PresContext()->AppUnitsPerDevPixel());

  return result;
}

/**
 * Setup transform matrix of a gfx context by a specific frame. Move the
 * origin of aParams.ctx to the user space of aFrame.
 */
static EffectOffsets MoveContextOriginToUserSpace(
    nsIFrame* aFrame, const SVGIntegrationUtils::PaintFramesParams& aParams) {
  EffectOffsets offset = ComputeEffectOffset(aFrame, aParams);

  aParams.ctx.SetMatrixDouble(aParams.ctx.CurrentMatrixDouble().PreTranslate(
      offset.offsetToUserSpaceInDevPx));

  return offset;
}

gfxPoint SVGIntegrationUtils::GetOffsetToUserSpaceInDevPx(
    nsIFrame* aFrame, const PaintFramesParams& aParams) {
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
  EffectOffsets offset = ComputeEffectOffset(firstFrame, aParams);
  return offset.offsetToUserSpaceInDevPx;
}

/* static */
nsSize SVGIntegrationUtils::GetContinuationUnionSize(nsIFrame* aNonSVGFrame) {
  NS_ASSERTION(!aNonSVGFrame->IsSVGFrame(), "SVG frames should not get here");
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aNonSVGFrame);
  return nsLayoutUtils::GetAllInFlowRectsUnion(firstFrame, firstFrame).Size();
}

/* static */ gfx::Size SVGIntegrationUtils::GetSVGCoordContextForNonSVGFrame(
    nsIFrame* aNonSVGFrame) {
  NS_ASSERTION(!aNonSVGFrame->IsSVGFrame(), "SVG frames should not get here");
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aNonSVGFrame);
  nsRect r = nsLayoutUtils::GetAllInFlowRectsUnion(firstFrame, firstFrame);
  return gfx::Size(nsPresContext::AppUnitsToFloatCSSPixels(r.width),
                   nsPresContext::AppUnitsToFloatCSSPixels(r.height));
}

gfxRect SVGIntegrationUtils::GetSVGBBoxForNonSVGFrame(
    nsIFrame* aNonSVGFrame, bool aUnionContinuations) {
  // Except for SVGOuterSVGFrame, we shouldn't be getting here with SVG
  // frames at all. This function is for elements that are laid out using the
  // CSS box model rules.
  NS_ASSERTION(!aNonSVGFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT),
               "Frames with SVG layout should not get here");

  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aNonSVGFrame);
  // 'r' is in "user space":
  nsRect r = (aUnionContinuations)
                 ? GetPreEffectsInkOverflowUnion(
                       firstFrame, nullptr, nsRect(),
                       GetOffsetToBoundingBox(firstFrame), false)
                 : GetPreEffectsInkOverflow(firstFrame, aNonSVGFrame,
                                            GetOffsetToBoundingBox(firstFrame));

  return nsLayoutUtils::RectToGfxRect(r, AppUnitsPerCSSPixel());
}

// XXX Since we're called during reflow, this method is broken for frames with
// continuations. When we're called for a frame with continuations, we're
// called for each continuation in turn as it's reflowed. However, it isn't
// until the last continuation is reflowed that this method's
// GetOffsetToBoundingBox() and GetPreEffectsInkOverflowUnion() calls will
// obtain valid border boxes for all the continuations. As a result, we'll
// end up returning bogus post-filter ink overflow rects for all the prior
// continuations. Unfortunately, by the time the last continuation is
// reflowed, it's too late to go back and set and propagate the overflow
// rects on the previous continuations.
//
// The reason that we need to pass an override bbox to
// GetPreEffectsInkOverflowUnion rather than just letting it call into our
// GetSVGBBoxForNonSVGFrame method is because we get called by
// ComputeEffectsRect when it has been called with
// aStoreRectProperties set to false. In this case the pre-effects visual
// overflow rect that it has been passed may be different to that stored on
// aFrame, resulting in a different bbox.
//
// XXXjwatt The pre-effects ink overflow rect passed to
// ComputeEffectsRect won't include continuation overflows, so
// for frames with continuation the following filter analysis will likely end
// up being carried out with a bbox created as if the frame didn't have
// continuations.
//
// XXXjwatt Using aPreEffectsOverflowRect to create the bbox isn't really right
// for SVG frames, since for SVG frames the SVG spec defines the bbox to be
// something quite different to the pre-effects ink overflow rect. However,
// we're essentially calculating an invalidation area here, and using the
// pre-effects overflow rect will actually overestimate that area which, while
// being a bit wasteful, isn't otherwise a problem.
//
nsRect SVGIntegrationUtils::ComputePostEffectsInkOverflowRect(
    nsIFrame* aFrame, const nsRect& aPreEffectsOverflowRect) {
  MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT),
             "Don't call this on SVG child frames");

  MOZ_ASSERT(aFrame->StyleEffects()->HasFilters(),
             "We should only be called if the frame is filtered, since filters "
             "are the only effect that affects overflow.");

  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
  // Note: we do not return here for HasNoRefs since we must still handle any
  // CSS filter functions.
  // TODO: we should really return an empty rect for HasRefsSomeInvalid since
  // in that case we disable painting of the element.
  nsTArray<SVGFilterFrame*> filterFrames;
  if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames) ==
      SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) {
    return aPreEffectsOverflowRect;
  }

  // Create an override bbox - see comment above:
  nsPoint firstFrameToBoundingBox = GetOffsetToBoundingBox(firstFrame);
  // overrideBBox is in "user space", in _CSS_ pixels:
  // XXX Why are we rounding out to pixel boundaries? We don't do that in
  // GetSVGBBoxForNonSVGFrame, and it doesn't appear to be necessary.
  gfxRect overrideBBox = nsLayoutUtils::RectToGfxRect(
      GetPreEffectsInkOverflowUnion(firstFrame, aFrame, aPreEffectsOverflowRect,
                                    firstFrameToBoundingBox, true),
      AppUnitsPerCSSPixel());
  overrideBBox.RoundOut();

  Maybe<nsRect> overflowRect = FilterInstance::GetPostFilterBounds(
      firstFrame, filterFrames, &overrideBBox);
  if (!overflowRect) {
    return aPreEffectsOverflowRect;
  }

  // Return overflowRect relative to aFrame, rather than "user space":
  return overflowRect.value() -
         (aFrame->GetOffsetTo(firstFrame) + firstFrameToBoundingBox);
}

nsRect SVGIntegrationUtils::GetRequiredSourceForInvalidArea(
    nsIFrame* aFrame, const nsRect& aDirtyRect) {
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);

  // If we have any filters to observe then we should have started doing that
  // during reflow/ComputeFrameEffectsRect, so we use GetFiltersIfObserving
  // here to avoid needless work (or masking bugs by setting up observers at
  // the wrong time).
  nsTArray<SVGFilterFrame*> filterFrames;
  if (!aFrame->StyleEffects()->HasFilters() ||
      SVGObserverUtils::GetFiltersIfObserving(firstFrame, &filterFrames) ==
          SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) {
    return aDirtyRect;
  }

  // Convert aDirtyRect into "user space" in app units:
  nsPoint toUserSpace =
      aFrame->GetOffsetTo(firstFrame) + GetOffsetToBoundingBox(firstFrame);
  nsRect postEffectsRect = aDirtyRect + toUserSpace;

  // Return ther result, relative to aFrame, not in user space:
  return FilterInstance::GetPreFilterNeededArea(firstFrame, filterFrames,
                                                postEffectsRect)
             .GetBounds() -
         toUserSpace;
}

bool SVGIntegrationUtils::HitTestFrameForEffects(nsIFrame* aFrame,
                                                 const nsPoint& aPt) {
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
  // Convert aPt to user space:
  nsPoint toUserSpace;
  if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
    // XXXmstange Isn't this wrong for svg:use and innerSVG frames?
    toUserSpace = aFrame->GetPosition();
  } else {
    toUserSpace =
        aFrame->GetOffsetTo(firstFrame) + GetOffsetToBoundingBox(firstFrame);
  }
  nsPoint pt = aPt + toUserSpace;
  gfxPoint userSpacePt = gfxPoint(pt.x, pt.y) / AppUnitsPerCSSPixel();
  return SVGUtils::HitTestClip(firstFrame, userSpacePt);
}

using PaintFramesParams = SVGIntegrationUtils::PaintFramesParams;

/**
 * Paint css-positioned-mask onto a given target(aMaskDT).
 * Return value indicates if mask is complete.
 */
static bool PaintMaskSurface(const PaintFramesParams& aParams,
                             DrawTarget* aMaskDT, float aOpacity,
                             const ComputedStyle* aSC,
                             const nsTArray<SVGMaskFrame*>& aMaskFrames,
                             const nsPoint& aOffsetToUserSpace) {
  MOZ_ASSERT(!aMaskFrames.IsEmpty());
  MOZ_ASSERT(aMaskDT->GetFormat() == SurfaceFormat::A8);
  MOZ_ASSERT(aOpacity == 1.0 || aMaskFrames.Length() == 1);

  const nsStyleSVGReset* svgReset = aSC->StyleSVGReset();
  gfxMatrix cssPxToDevPxMatrix = SVGUtils::GetCSSPxToDevPxMatrix(aParams.frame);

  nsPresContext* presContext = aParams.frame->PresContext();
  gfxPoint devPixelOffsetToUserSpace = nsLayoutUtils::PointToGfxPoint(
      aOffsetToUserSpace, presContext->AppUnitsPerDevPixel());

  gfxContext maskContext(aMaskDT, /* aPreserveTransform */ true);

  bool isMaskComplete = true;

  // Multiple SVG masks interleave with image mask. Paint each layer onto
  // aMaskDT one at a time.
  for (int i = aMaskFrames.Length() - 1; i >= 0; i--) {
    SVGMaskFrame* maskFrame = aMaskFrames[i];
    CompositionOp compositionOp =
        (i == int(aMaskFrames.Length() - 1))
            ? CompositionOp::OP_OVER
            : nsCSSRendering::GetGFXCompositeMode(
                  svgReset->mMask.mLayers[i].mComposite);

    // maskFrame != nullptr means we get a SVG mask.
    // maskFrame == nullptr means we get an image mask.
    if (maskFrame) {
      SVGMaskFrame::MaskParams params(
          maskContext.GetDrawTarget(), aParams.frame, cssPxToDevPxMatrix,
          aOpacity, svgReset->mMask.mLayers[i].mMaskMode, aParams.imgParams);
      RefPtr<SourceSurface> svgMask = maskFrame->GetMaskForMaskedFrame(params);
      if (svgMask) {
        Matrix tmp = aMaskDT->GetTransform();
        aMaskDT->SetTransform(Matrix());
        aMaskDT->MaskSurface(ColorPattern(DeviceColor(0.0, 0.0, 0.0, 1.0)),
                             svgMask, Point(0, 0),
                             DrawOptions(1.0, compositionOp));
        aMaskDT->SetTransform(tmp);
      }
    } else if (svgReset->mMask.mLayers[i].mImage.IsResolved()) {
      gfxContextMatrixAutoSaveRestore matRestore(&maskContext);

      maskContext.Multiply(gfxMatrix::Translation(-devPixelOffsetToUserSpace));
      nsCSSRendering::PaintBGParams params =
          nsCSSRendering::PaintBGParams::ForSingleLayer(
              *presContext, aParams.dirtyRect, aParams.borderArea,
              aParams.frame,
              aParams.builder->GetBackgroundPaintFlags() |
                  nsCSSRendering::PAINTBG_MASK_IMAGE,
              i, compositionOp, aOpacity);

      aParams.imgParams.result &= nsCSSRendering::PaintStyleImageLayerWithSC(
          params, maskContext, aSC, *aParams.frame->StyleBorder());
    } else {
      isMaskComplete = false;
    }
  }

  return isMaskComplete;
}

struct MaskPaintResult {
  RefPtr<SourceSurface> maskSurface;
  Matrix maskTransform;
  bool transparentBlackMask;
  bool opacityApplied;

  MaskPaintResult() : transparentBlackMask(false), opacityApplied(false) {}
};

static MaskPaintResult CreateAndPaintMaskSurface(
    const PaintFramesParams& aParams, float aOpacity, const ComputedStyle* aSC,
    const nsTArray<SVGMaskFrame*>& aMaskFrames,
    const nsPoint& aOffsetToUserSpace) {
  const nsStyleSVGReset* svgReset = aSC->StyleSVGReset();
  MOZ_ASSERT(!aMaskFrames.IsEmpty());
  MaskPaintResult paintResult;

  gfxContext& ctx = aParams.ctx;

  // Optimization for single SVG mask.
  if (aMaskFrames.Length() == 1 && aMaskFrames[0]) {
    gfxMatrix cssPxToDevPxMatrix =
        SVGUtils::GetCSSPxToDevPxMatrix(aParams.frame);
    paintResult.opacityApplied = true;
    SVGMaskFrame::MaskParams params(
        ctx.GetDrawTarget(), aParams.frame, cssPxToDevPxMatrix, aOpacity,
        svgReset->mMask.mLayers[0].mMaskMode, aParams.imgParams);
    paintResult.maskSurface = aMaskFrames[0]->GetMaskForMaskedFrame(params);
    paintResult.maskTransform = ctx.CurrentMatrix();
    paintResult.maskTransform.Invert();
    if (!paintResult.maskSurface) {
      paintResult.transparentBlackMask = true;
    }

    return paintResult;
  }

  const LayoutDeviceRect& maskSurfaceRect =
      aParams.maskRect.valueOr(LayoutDeviceRect());
  if (aParams.maskRect.isSome() && maskSurfaceRect.IsEmpty()) {
    // XXX: Is this ever true?
    paintResult.transparentBlackMask = true;
    return paintResult;
  }

  RefPtr<DrawTarget> maskDT = ctx.GetDrawTarget()->CreateClippedDrawTarget(
      maskSurfaceRect.ToUnknownRect(), SurfaceFormat::A8);
  if (!maskDT || !maskDT->IsValid()) {
    return paintResult;
  }

  // We can paint mask along with opacity only if
  // 1. There is only one mask, or
  // 2. No overlap among masks.
  // Collision detect in #2 is not that trivial, we only accept #1 here.
  paintResult.opacityApplied = (aMaskFrames.Length() == 1);

  // Set context's matrix on maskContext, offset by the maskSurfaceRect's
  // position. This makes sure that we combine the masks in device space.
  Matrix maskSurfaceMatrix = ctx.CurrentMatrix();

  bool isMaskComplete = PaintMaskSurface(
      aParams, maskDT, paintResult.opacityApplied ? aOpacity : 1.0, aSC,
      aMaskFrames, aOffsetToUserSpace);

  if (!isMaskComplete ||
      (aParams.imgParams.result != ImgDrawResult::SUCCESS &&
       aParams.imgParams.result != ImgDrawResult::SUCCESS_NOT_COMPLETE &&
       aParams.imgParams.result != ImgDrawResult::WRONG_SIZE)) {
    // Now we know the status of mask resource since we used it while painting.
    // According to the return value of PaintMaskSurface, we know whether mask
    // resource is resolvable or not.
    //
    // For a HTML doc:
    //   According to css-masking spec, always create a mask surface when
    //   we have any item in maskFrame even if all of those items are
    //   non-resolvable <mask-sources> or <images>.
    //   Set paintResult.transparentBlackMask as true,  the caller should stop
    //   painting masked content as if this mask is a transparent black one.
    // For a SVG doc:
    //   SVG 1.1 say that if we fail to resolve a mask, we should draw the
    //   object unmasked.
    //   Left paintResult.maskSurface empty, the caller should paint all
    //   masked content as if this mask is an opaque white one(no mask).
    paintResult.transparentBlackMask =
        !aParams.frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT);

    MOZ_ASSERT(!paintResult.maskSurface);
    return paintResult;
  }

  paintResult.maskTransform = maskSurfaceMatrix;
  if (!paintResult.maskTransform.Invert()) {
    return paintResult;
  }

  paintResult.maskSurface = maskDT->Snapshot();
  return paintResult;
}

static bool ValidateSVGFrame(nsIFrame* aFrame) {
  NS_ASSERTION(
      !aFrame->HasAllStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY),
      "Should not use SVGIntegrationUtils on this SVG frame");

  if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) {
#ifdef DEBUG
    ISVGDisplayableFrame* svgFrame = do_QueryFrame(aFrame);
    MOZ_ASSERT(svgFrame && aFrame->GetContent()->IsSVGElement(),
               "A non-SVG frame carries NS_FRAME_SVG_LAYOUT flag?");
#endif

    const nsIContent* content = aFrame->GetContent();
    if (!static_cast<const SVGElement*>(content)->HasValidDimensions()) {
      // The SVG spec says not to draw _anything_
      return false;
    }
  }

  return true;
}

bool SVGIntegrationUtils::PaintMask(const PaintFramesParams& aParams,
                                    bool& aOutIsMaskComplete) {
  aOutIsMaskComplete = true;

  SVGUtils::MaskUsage maskUsage =
      SVGUtils::DetermineMaskUsage(aParams.frame, aParams.handleOpacity);
  if (!maskUsage.ShouldDoSomething()) {
    return false;
  }

  nsIFrame* frame = aParams.frame;
  if (!ValidateSVGFrame(frame)) {
    return false;
  }

  gfxContext& ctx = aParams.ctx;
  RefPtr<DrawTarget> maskTarget = ctx.GetDrawTarget();

  if (maskUsage.ShouldGenerateMaskLayer() && maskUsage.HasSVGClip()) {
    // We will paint both mask of positioned mask and clip-path into
    // maskTarget.
    //
    // Create one extra draw target for drawing positioned mask, so that we do
    // not have to copy the content of maskTarget before painting
    // clip-path into it.
    maskTarget = maskTarget->CreateClippedDrawTarget(Rect(), SurfaceFormat::A8);
  }

  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame);
  nsTArray<SVGMaskFrame*> maskFrames;
  // XXX check return value?
  SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames);

  gfxGroupForBlendAutoSaveRestore autoPop(&ctx);
  bool shouldPushOpacity = !maskUsage.IsOpaque() && maskFrames.Length() != 1;
  if (shouldPushOpacity) {
    autoPop.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA,
                                  maskUsage.Opacity());
  }

  gfxContextMatrixAutoSaveRestore matSR;

  // Paint clip-path-basic-shape onto ctx
  gfxContextAutoSaveRestore basicShapeSR;
  if (maskUsage.ShouldApplyBasicShapeOrPath()) {
    matSR.SetContext(&ctx);

    MoveContextOriginToUserSpace(firstFrame, aParams);

    basicShapeSR.SetContext(&ctx);
    gfxMatrix mat = SVGUtils::GetCSSPxToDevPxMatrix(frame);
    if (!maskUsage.ShouldGenerateMaskLayer()) {
      // Only have basic-shape clip-path effect. Fill clipped region by
      // opaque white.
      ctx.SetDeviceColor(DeviceColor::MaskOpaqueWhite());
      RefPtr<Path> path = CSSClipPathInstance::CreateClipPathForFrame(
          ctx.GetDrawTarget(), frame, mat);
      if (path) {
        ctx.SetPath(path);
        ctx.Fill();
      }

      return true;
    }
    CSSClipPathInstance::ApplyBasicShapeOrPathClip(ctx, frame, mat);
  }

  // Paint mask into maskTarget.
  if (maskUsage.ShouldGenerateMaskLayer()) {
    matSR.Restore();
    matSR.SetContext(&ctx);

    EffectOffsets offsets = ComputeEffectOffset(frame, aParams);
    maskTarget->SetTransform(maskTarget->GetTransform().PreTranslate(
        ToPoint(offsets.offsetToUserSpaceInDevPx)));
    aOutIsMaskComplete = PaintMaskSurface(
        aParams, maskTarget, shouldPushOpacity ? 1.0f : maskUsage.Opacity(),
        firstFrame->Style(), maskFrames, offsets.offsetToUserSpace);
  }

  // Paint clip-path onto ctx.
  if (maskUsage.HasSVGClip()) {
    matSR.Restore();
    matSR.SetContext(&ctx);

    MoveContextOriginToUserSpace(firstFrame, aParams);
    Matrix clipMaskTransform;
    gfxMatrix cssPxToDevPxMatrix = SVGUtils::GetCSSPxToDevPxMatrix(frame);

    SVGClipPathFrame* clipPathFrame;
    // XXX check return value?
    SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame);
    RefPtr<SourceSurface> maskSurface =
        maskUsage.ShouldGenerateMaskLayer() ? maskTarget->Snapshot() : nullptr;
    clipPathFrame->PaintClipMask(ctx, frame, cssPxToDevPxMatrix, maskSurface);
  }

  return true;
}

template <class T>
void PaintMaskAndClipPathInternal(const PaintFramesParams& aParams,
                                  const T& aPaintChild) {
#ifdef DEBUG
  const nsStyleSVGReset* style = aParams.frame->StyleSVGReset();
  MOZ_ASSERT(style->HasClipPath() || style->HasMask(),
             "Should not use this method when no mask or clipPath effect"
             "on this frame");
#endif

  /* SVG defines the following rendering model:
   *
   *  1. Render geometry
   *  2. Apply filter
   *  3. Apply clipping, masking, group opacity
   *
   * We handle #3 here and perform a couple of optimizations:
   *
   * + Use cairo's clipPath when representable natively (single object
   *   clip region).
   *
   * + Merge opacity and masking if both used together.
   */
  nsIFrame* frame = aParams.frame;
  if (!ValidateSVGFrame(frame)) {
    return;
  }

  SVGUtils::MaskUsage maskUsage =
      SVGUtils::DetermineMaskUsage(aParams.frame, aParams.handleOpacity);

  if (maskUsage.IsTransparent()) {
    return;
  }

  gfxContext& context = aParams.ctx;
  gfxContextMatrixAutoSaveRestore matrixAutoSaveRestore(&context);

  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame);

  SVGClipPathFrame* clipPathFrame;
  // XXX check return value?
  SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame);

  nsTArray<SVGMaskFrame*> maskFrames;
  // XXX check return value?
  SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames);

  gfxMatrix cssPxToDevPxMatrix = SVGUtils::GetCSSPxToDevPxMatrix(frame);

  bool shouldPushMask = false;

  gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&context);

  /* Check if we need to do additional operations on this child's
   * rendering, which necessitates rendering into another surface. */
  if (maskUsage.ShouldGenerateMask()) {
    gfxContextMatrixAutoSaveRestore matSR;

    RefPtr<SourceSurface> maskSurface;
    bool opacityApplied = false;

    if (maskUsage.ShouldGenerateMaskLayer()) {
      matSR.SetContext(&context);

      // For css-mask, we want to generate a mask for each continuation frame,
      // so we setup context matrix by the position of the current frame,
      // instead of the first continuation frame.
      EffectOffsets offsets = MoveContextOriginToUserSpace(frame, aParams);
      MaskPaintResult paintResult = CreateAndPaintMaskSurface(
          aParams, maskUsage.Opacity(), firstFrame->Style(), maskFrames,
          offsets.offsetToUserSpace);

      if (paintResult.transparentBlackMask) {
        return;
      }

      maskSurface = paintResult.maskSurface;
      if (maskSurface) {
        shouldPushMask = true;

        opacityApplied = paintResult.opacityApplied;
      }
    }

    if (maskUsage.ShouldGenerateClipMaskLayer()) {
      matSR.Restore();
      matSR.SetContext(&context);

      MoveContextOriginToUserSpace(firstFrame, aParams);
      RefPtr<SourceSurface> clipMaskSurface = clipPathFrame->GetClipMask(
          context, frame, cssPxToDevPxMatrix, maskSurface);

      if (clipMaskSurface) {
        maskSurface = clipMaskSurface;
      } else {
        // Either entire surface is clipped out, or gfx buffer allocation
        // failure in SVGClipPathFrame::GetClipMask.
        return;
      }

      shouldPushMask = true;
    }

    // opacity != 1.0f.
    if (!maskUsage.ShouldGenerateLayer()) {
      MOZ_ASSERT(!maskUsage.IsOpaque());

      matSR.SetContext(&context);
      MoveContextOriginToUserSpace(firstFrame, aParams);
      shouldPushMask = true;
    }

    if (shouldPushMask) {
      // We want the mask to be untransformed so use the inverse of the
      // current transform as the maskTransform to compensate.
      Matrix maskTransform = context.CurrentMatrix();
      maskTransform.Invert();

      autoGroupForBlend.PushGroupForBlendBack(
          gfxContentType::COLOR_ALPHA,
          opacityApplied ? 1.0f : 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()) {
    gfxContextMatrixAutoSaveRestore matSR(&context);

    MoveContextOriginToUserSpace(firstFrame, aParams);

    MOZ_ASSERT(!maskUsage.ShouldApplyClipPath() ||
               !maskUsage.ShouldApplyBasicShapeOrPath());
    if (maskUsage.ShouldApplyClipPath()) {
      clipPathFrame->ApplyClipPath(context, frame, cssPxToDevPxMatrix);
    } else {
      CSSClipPathInstance::ApplyBasicShapeOrPathClip(context, frame,
                                                     cssPxToDevPxMatrix);
    }
  }

  /* Paint the child */
  context.SetMatrix(matrixAutoSaveRestore.Matrix());
  aPaintChild();

  if (StaticPrefs::layers_draw_mask_debug()) {
    gfxContextAutoSaveRestore saver(&context);

    context.NewPath();
    gfxRect drawingRect = nsLayoutUtils::RectToGfxRect(
        aParams.borderArea, frame->PresContext()->AppUnitsPerDevPixel());
    context.SnappedRectangle(drawingRect);
    sRGBColor overlayColor(0.0f, 0.0f, 0.0f, 0.8f);
    if (maskUsage.ShouldGenerateMaskLayer()) {
      overlayColor.r = 1.0f;  // red represents css positioned mask.
    }
    if (maskUsage.HasSVGClip()) {
      overlayColor.g = 1.0f;  // green represents clip-path:<clip-source>.
    }
    if (maskUsage.ShouldApplyBasicShapeOrPath()) {
      overlayColor.b = 1.0f;  // blue represents
                              // clip-path:<basic-shape>||<geometry-box>.
    }

    context.SetColor(overlayColor);
    context.Fill();
  }

  if (maskUsage.ShouldApplyClipPath() ||
      maskUsage.ShouldApplyBasicShapeOrPath()) {
    context.PopClip();
  }
}

void SVGIntegrationUtils::PaintMaskAndClipPath(
    const PaintFramesParams& aParams,
    const std::function<void()>& aPaintChild) {
  PaintMaskAndClipPathInternal(aParams, aPaintChild);
}

void SVGIntegrationUtils::PaintFilter(const PaintFramesParams& aParams,
                                      Span<const StyleFilter> aFilters,
                                      const SVGFilterPaintCallback& aCallback) {
  MOZ_ASSERT(!aParams.builder->IsForGenerateGlyphMask(),
             "Filter effect is discarded while generating glyph mask.");
  MOZ_ASSERT(!aFilters.IsEmpty(),
             "Should not use this method when no filter effect on this frame");

  nsIFrame* frame = aParams.frame;
  if (!ValidateSVGFrame(frame)) {
    return;
  }

  float opacity = SVGUtils::ComputeOpacity(frame, aParams.handleOpacity);
  if (opacity == 0.0f) {
    return;
  }

  // Properties are added lazily and may have been removed by a restyle, so make
  // sure all applicable ones are set again.
  nsIFrame* firstFrame =
      nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame);
  // Note: we do not return here for HasNoRefs since we must still handle any
  // CSS filter functions.
  // XXX: Do we need to check for HasRefsSomeInvalid here given that
  // nsDisplayFilter::BuildLayer returns nullptr for HasRefsSomeInvalid?
  // Or can we just assert !HasRefsSomeInvalid?
  nsTArray<SVGFilterFrame*> filterFrames;
  if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames) ==
      SVGObserverUtils::ReferenceState::HasRefsSomeInvalid) {
    aCallback(aParams.ctx, aParams.imgParams, nullptr, nullptr);
    return;
  }

  gfxContext& context = aParams.ctx;

  gfxContextAutoSaveRestore autoSR(&context);
  EffectOffsets offsets = MoveContextOriginToUserSpace(firstFrame, aParams);

  /* Paint the child and apply filters */
  nsRegion dirtyRegion = aParams.dirtyRect - offsets.offsetToBoundingBox;

  FilterInstance::PaintFilteredFrame(frame, aFilters, filterFrames, &context,
                                     aCallback, &dirtyRegion, aParams.imgParams,
                                     opacity);
}

WrFiltersStatus SVGIntegrationUtils::CreateWebRenderCSSFilters(
    Span<const StyleFilter> aFilters, nsIFrame* aFrame,
    WrFiltersHolder& aWrFilters) {
  // Check if prefs are set to convert the CSS filters to SVG filters and use
  // the new WebRender SVG filter rendering, rather than the existing CSS filter
  // support
  if (StaticPrefs::gfx_webrender_svg_filter_effects() &&
      StaticPrefs::
          gfx_webrender_svg_filter_effects_also_convert_css_filters()) {
    return WrFiltersStatus::BLOB_FALLBACK;
  }
  // All CSS filters are supported by WebRender. SVG filters are not fully
  // supported, those use NS_STYLE_FILTER_URL and are handled separately.

  // If there are too many filters to render, then just pretend that we
  // succeeded, and don't render any of them.
  if (aFilters.Length() >
      StaticPrefs::gfx_webrender_max_filter_ops_per_chain()) {
    return WrFiltersStatus::DISABLED_FOR_PERFORMANCE;
  }
  // Track status so we can do cleanup if unsupported filters are found.
  WrFiltersStatus status = WrFiltersStatus::CHAIN;
  aWrFilters.filters.SetCapacity(aFilters.Length());
  auto& wrFilters = aWrFilters.filters;
  for (const StyleFilter& filter : aFilters) {
    switch (filter.tag) {
      case StyleFilter::Tag::Brightness:
        wrFilters.AppendElement(
            wr::FilterOp::Brightness(filter.AsBrightness()));
        break;
      case StyleFilter::Tag::Contrast:
        wrFilters.AppendElement(wr::FilterOp::Contrast(filter.AsContrast()));
        break;
      case StyleFilter::Tag::Grayscale:
        wrFilters.AppendElement(wr::FilterOp::Grayscale(filter.AsGrayscale()));
        break;
      case StyleFilter::Tag::Invert:
        wrFilters.AppendElement(wr::FilterOp::Invert(filter.AsInvert()));
        break;
      case StyleFilter::Tag::Opacity: {
        float opacity = filter.AsOpacity();
        wrFilters.AppendElement(wr::FilterOp::Opacity(
            wr::PropertyBinding<float>::Value(opacity), opacity));
        break;
      }
      case StyleFilter::Tag::Saturate:
        wrFilters.AppendElement(wr::FilterOp::Saturate(filter.AsSaturate()));
        break;
      case StyleFilter::Tag::Sepia:
        wrFilters.AppendElement(wr::FilterOp::Sepia(filter.AsSepia()));
        break;
      case StyleFilter::Tag::HueRotate: {
        wrFilters.AppendElement(
            wr::FilterOp::HueRotate(filter.AsHueRotate().ToDegrees()));
        break;
      }
      case StyleFilter::Tag::Blur: {
        // TODO(emilio): we should go directly from css pixels -> device pixels.
        float appUnitsPerDevPixel =
            aFrame->PresContext()->AppUnitsPerDevPixel();
        float radius = NSAppUnitsToFloatPixels(filter.AsBlur().ToAppUnits(),
                                               appUnitsPerDevPixel);
        wrFilters.AppendElement(wr::FilterOp::Blur(radius, radius));
        break;
      }
      case StyleFilter::Tag::DropShadow: {
        float appUnitsPerDevPixel =
            aFrame->PresContext()->AppUnitsPerDevPixel();
        const StyleSimpleShadow& shadow = filter.AsDropShadow();
        nscolor color = shadow.color.CalcColor(aFrame);

        wr::Shadow wrShadow;
        wrShadow.offset = {
            NSAppUnitsToFloatPixels(shadow.horizontal.ToAppUnits(),
                                    appUnitsPerDevPixel),
            NSAppUnitsToFloatPixels(shadow.vertical.ToAppUnits(),
                                    appUnitsPerDevPixel)};
        wrShadow.blur_radius = NSAppUnitsToFloatPixels(shadow.blur.ToAppUnits(),
                                                       appUnitsPerDevPixel);
        wrShadow.color = {NS_GET_R(color) / 255.0f, NS_GET_G(color) / 255.0f,
                          NS_GET_B(color) / 255.0f, NS_GET_A(color) / 255.0f};
        wrFilters.AppendElement(wr::FilterOp::DropShadow(wrShadow));
        break;
      }
      default:
        status = WrFiltersStatus::BLOB_FALLBACK;
        break;
    }
    if (status != WrFiltersStatus::CHAIN) {
      break;
    }
  }
  if (status != WrFiltersStatus::CHAIN) {
    // Clean up the filters holder if we can't render filters this way.
    aWrFilters = {};
  }
  return status;
}

WrFiltersStatus SVGIntegrationUtils::BuildWebRenderFilters(
    nsIFrame* aFilteredFrame, Span<const StyleFilter> aFilters,
    StyleFilterType aStyleFilterType, WrFiltersHolder& aWrFilters,
    const nsPoint& aOffsetForSVGFilters) {
  return FilterInstance::BuildWebRenderFilters(aFilteredFrame, aFilters,
                                               aStyleFilterType, aWrFilters,
                                               aOffsetForSVGFilters);
}

bool SVGIntegrationUtils::CanCreateWebRenderFiltersForFrame(nsIFrame* aFrame) {
  WrFiltersHolder wrFilters;
  auto filterChain = aFrame->StyleEffects()->mFilters.AsSpan();
  WrFiltersStatus status =
      CreateWebRenderCSSFilters(filterChain, aFrame, wrFilters);
  if (status == WrFiltersStatus::BLOB_FALLBACK) {
    status = BuildWebRenderFilters(aFrame, filterChain, StyleFilterType::Filter,
                                   wrFilters, nsPoint());
  }
  return status == WrFiltersStatus::CHAIN || status == WrFiltersStatus::SVGFE;
}

bool SVGIntegrationUtils::UsesSVGEffectsNotSupportedInCompositor(
    nsIFrame* aFrame) {
  // WebRender supports masks / clip-paths and some filters in the compositor.
  if (aFrame->StyleEffects()->HasFilters()) {
    return !SVGIntegrationUtils::CanCreateWebRenderFiltersForFrame(aFrame);
  }
  return false;
}

class PaintFrameCallback : public gfxDrawingCallback {
 public:
  PaintFrameCallback(nsIFrame* aFrame, const nsSize aPaintServerSize,
                     const IntSize aRenderSize,
                     SVGIntegrationUtils::DecodeFlags aFlags)
      : mFrame(aFrame),
        mPaintServerSize(aPaintServerSize),
        mRenderSize(aRenderSize),
        mFlags(aFlags) {}
  virtual bool operator()(gfxContext* aContext, const gfxRect& aFillRect,
                          const SamplingFilter aSamplingFilter,
                          const gfxMatrix& aTransform) override;

 private:
  nsIFrame* mFrame;
  nsSize mPaintServerSize;
  IntSize mRenderSize;
  SVGIntegrationUtils::DecodeFlags mFlags;
};

bool PaintFrameCallback::operator()(gfxContext* aContext,
                                    const gfxRect& aFillRect,
                                    const SamplingFilter aSamplingFilter,
                                    const gfxMatrix& aTransform) {
  if (mFrame->HasAnyStateBits(NS_FRAME_DRAWING_AS_PAINTSERVER)) {
    return false;
  }

  AutoSetRestorePaintServerState paintServer(mFrame);

  aContext->Save();

  // Clip to aFillRect so that we don't paint outside.
  aContext->Clip(aFillRect);

  gfxMatrix invmatrix = aTransform;
  if (!invmatrix.Invert()) {
    return false;
  }
  aContext->Multiply(invmatrix);

  // nsLayoutUtils::PaintFrame will anchor its painting at mFrame. But we want
  // to have it anchored at the top left corner of the bounding box of all of
  // mFrame's continuations. So we add a translation transform.
  int32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel();
  nsPoint offset = SVGIntegrationUtils::GetOffsetToBoundingBox(mFrame);
  gfxPoint devPxOffset = gfxPoint(offset.x, offset.y) / appUnitsPerDevPixel;
  aContext->Multiply(gfxMatrix::Translation(devPxOffset));

  gfxSize paintServerSize =
      gfxSize(mPaintServerSize.width, mPaintServerSize.height) /
      mFrame->PresContext()->AppUnitsPerDevPixel();

  // nsLayoutUtils::PaintFrame wants to render with paintServerSize, but we
  // want it to render with mRenderSize, so we need to set up a scale transform.
  gfxFloat scaleX = mRenderSize.width / paintServerSize.width;
  gfxFloat scaleY = mRenderSize.height / paintServerSize.height;
  aContext->Multiply(gfxMatrix::Scaling(scaleX, scaleY));

  // Draw.
  nsRect dirty(-offset.x, -offset.y, mPaintServerSize.width,
               mPaintServerSize.height);

  using PaintFrameFlags = nsLayoutUtils::PaintFrameFlags;
  PaintFrameFlags flags = PaintFrameFlags::InTransform;
  if (mFlags.contains(SVGIntegrationUtils::DecodeFlag::SyncDecodeImages)) {
    flags |= PaintFrameFlags::SyncDecodeImages;
  }
  nsLayoutUtils::PaintFrame(aContext, mFrame, dirty, NS_RGBA(0, 0, 0, 0),
                            nsDisplayListBuilderMode::Painting, flags);

  nsIFrame* currentFrame = mFrame;
  while ((currentFrame = currentFrame->GetNextContinuation()) != nullptr) {
    offset = currentFrame->GetOffsetToCrossDoc(mFrame);
    devPxOffset = gfxPoint(offset.x, offset.y) / appUnitsPerDevPixel;

    aContext->Save();
    aContext->Multiply(gfxMatrix::Scaling(1 / scaleX, 1 / scaleY));
    aContext->Multiply(gfxMatrix::Translation(devPxOffset));
    aContext->Multiply(gfxMatrix::Scaling(scaleX, scaleY));

    nsLayoutUtils::PaintFrame(aContext, currentFrame, dirty - offset,
                              NS_RGBA(0, 0, 0, 0),
                              nsDisplayListBuilderMode::Painting, flags);

    aContext->Restore();
  }

  aContext->Restore();

  return true;
}

/* static */
already_AddRefed<gfxDrawable> SVGIntegrationUtils::DrawableFromPaintServer(
    nsIFrame* aFrame, nsIFrame* aTarget, const nsSize& aPaintServerSize,
    const IntSize& aRenderSize, const DrawTarget* aDrawTarget,
    const gfxMatrix& aContextMatrix, DecodeFlags aFlags) {
  // aPaintServerSize is the size that would be filled when using
  // background-repeat:no-repeat and background-size:auto. For normal background
  // images, this would be the intrinsic size of the image; for gradients and
  // patterns this would be the whole target frame fill area.
  // aRenderSize is what we will be actually filling after accounting for
  // background-size.
  if (SVGPaintServerFrame* server = do_QueryFrame(aFrame)) {
    // aFrame is either a pattern or a gradient. These fill the whole target
    // frame by default, so aPaintServerSize is the whole target background fill
    // area.
    gfxRect overrideBounds(0, 0, aPaintServerSize.width,
                           aPaintServerSize.height);
    overrideBounds.Scale(1.0 / aFrame->PresContext()->AppUnitsPerDevPixel());
    uint32_t imgFlags = imgIContainer::FLAG_ASYNC_NOTIFY;
    if (aFlags.contains(DecodeFlag::SyncDecodeImages) ||
        aFrame->UsedImageDecoding() == StyleImageDecoding::Sync) {
      imgFlags |= imgIContainer::FLAG_SYNC_DECODE;
    }
    imgDrawingParams imgParams(imgFlags);
    RefPtr<gfxPattern> pattern = server->GetPaintServerPattern(
        aTarget, aDrawTarget, aContextMatrix, &nsStyleSVG::mFill, 1.0,
        imgParams, &overrideBounds);

    if (!pattern) {
      return nullptr;
    }

    // pattern is now set up to fill aPaintServerSize. But we want it to
    // fill aRenderSize, so we need to add a scaling transform.
    // We couldn't just have set overrideBounds to aRenderSize - it would have
    // worked for gradients, but for patterns it would result in a different
    // pattern size.
    gfxFloat scaleX = overrideBounds.Width() / aRenderSize.width;
    gfxFloat scaleY = overrideBounds.Height() / aRenderSize.height;
    gfxMatrix scaleMatrix = gfxMatrix::Scaling(scaleX, scaleY);
    pattern->SetMatrix(scaleMatrix * pattern->GetMatrix());
    return MakeAndAddRef<gfxPatternDrawable>(pattern, aRenderSize);
  }

  if (aFrame->IsSVGFrame() &&
      !static_cast<ISVGDisplayableFrame*>(do_QueryFrame(aFrame))) {
    MOZ_ASSERT_UNREACHABLE(
        "We should prevent painting of unpaintable SVG "
        "before we get here");
    return nullptr;
  }

  // We don't want to paint into a surface as long as we don't need to, so we
  // set up a drawing callback.
  auto cb = MakeRefPtr<PaintFrameCallback>(aFrame, aPaintServerSize,
                                           aRenderSize, aFlags);
  return MakeAndAddRef<gfxCallbackDrawable>(cb, aRenderSize);
}

}  // namespace mozilla
