/* -*- 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: #include "SVGIntegrationUtils.h" // Keep others in (case-insensitive) order: #include "gfxDrawable.h" #include "nsCSSAnonBoxes.h" #include "nsCSSRendering.h" #include "nsDisplayList.h" #include "nsLayoutUtils.h" #include "gfxContext.h" #include "SVGPaintServerFrame.h" #include "mozilla/gfx/Point.h" #include "mozilla/CSSClipPathInstance.h" #include "mozilla/FilterInstance.h" #include "mozilla/StaticPrefs_layers.h" #include "mozilla/SVGClipPathFrame.h" #include "mozilla/SVGObserverUtils.h" #include "mozilla/SVGMaskFrame.h" #include "mozilla/SVGUtils.h" #include "mozilla/Unused.h" #include "mozilla/dom/SVGElement.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 eHasNoRefs since we must still handle any // CSS filter functions. // TODO: we should really return an empty rect for eHasRefsSomeInvalid since // in that case we disable painting of the element. nsTArray filterFrames; if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames) == SVGObserverUtils::eHasRefsSomeInvalid) { 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 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 filterFrames; if (!aFrame->StyleEffects()->HasFilters() || SVGObserverUtils::GetFiltersIfObserving(firstFrame, &filterFrames) == SVGObserverUtils::eHasRefsSomeInvalid) { 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& aMaskFrames, const nsPoint& aOffsetToUserSpace) { MOZ_ASSERT(aMaskFrames.Length() > 0); 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 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 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& aMaskFrames, const nsPoint& aOffsetToUserSpace) { const nsStyleSVGReset* svgReset = aSC->StyleSVGReset(); MOZ_ASSERT(aMaskFrames.Length() > 0); 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 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 or . // 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(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 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 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 = 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 maskSurface = maskUsage.ShouldGenerateMaskLayer() ? maskTarget->Snapshot() : nullptr; clipPathFrame->PaintClipMask(ctx, frame, cssPxToDevPxMatrix, maskSurface); } return true; } template 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 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 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 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:. } if (maskUsage.ShouldApplyBasicShapeOrPath()) { overlayColor.b = 1.0f; // blue represents // clip-path:||. } context.SetColor(overlayColor); context.Fill(); } if (maskUsage.ShouldApplyClipPath() || maskUsage.ShouldApplyBasicShapeOrPath()) { context.PopClip(); } } void SVGIntegrationUtils::PaintMaskAndClipPath( const PaintFramesParams& aParams, const std::function& aPaintChild) { PaintMaskAndClipPathInternal(aParams, aPaintChild); } void SVGIntegrationUtils::PaintFilter(const PaintFramesParams& aParams, Span 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 eHasNoRefs since we must still handle any // CSS filter functions. // XXX: Do we need to check for eHasRefsSomeInvalid here given that // nsDisplayFilter::BuildLayer returns nullptr for eHasRefsSomeInvalid? // Or can we just assert !eHasRefsSomeInvalid? nsTArray filterFrames; if (SVGObserverUtils::GetAndObserveFilters(firstFrame, &filterFrames) == SVGObserverUtils::eHasRefsSomeInvalid) { 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); } bool SVGIntegrationUtils::CreateWebRenderCSSFilters( Span aFilters, nsIFrame* aFrame, WrFiltersHolder& aWrFilters) { // 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 true; } 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::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: return false; } } return true; } bool SVGIntegrationUtils::BuildWebRenderFilters( nsIFrame* aFilteredFrame, Span aFilters, StyleFilterType aStyleFilterType, WrFiltersHolder& aWrFilters, bool& aInitialized) { return FilterInstance::BuildWebRenderFilters( aFilteredFrame, aFilters, aStyleFilterType, aWrFilters, aInitialized); } bool SVGIntegrationUtils::CanCreateWebRenderFiltersForFrame(nsIFrame* aFrame) { WrFiltersHolder wrFilters; auto filterChain = aFrame->StyleEffects()->mFilters.AsSpan(); bool initialized = true; return CreateWebRenderCSSFilters(filterChain, aFrame, wrFilters) || BuildWebRenderFilters(aFrame, filterChain, StyleFilterType::Filter, wrFilters, initialized); } 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, uint32_t 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; uint32_t 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 & SVGIntegrationUtils::FLAG_SYNC_DECODE_IMAGES) { 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 SVGIntegrationUtils::DrawableFromPaintServer( nsIFrame* aFrame, nsIFrame* aTarget, const nsSize& aPaintServerSize, const IntSize& aRenderSize, const DrawTarget* aDrawTarget, const gfxMatrix& aContextMatrix, uint32_t 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 & SVGIntegrationUtils::FLAG_SYNC_DECODE_IMAGES) { imgFlags |= imgIContainer::FLAG_SYNC_DECODE; } imgDrawingParams imgParams(imgFlags); RefPtr 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 do_AddRef(new gfxPatternDrawable(pattern, aRenderSize)); } if (aFrame->IsSVGFrame() && !static_cast(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. RefPtr cb = new PaintFrameCallback(aFrame, aPaintServerSize, aRenderSize, aFlags); return do_AddRef(new gfxCallbackDrawable(cb, aRenderSize)); } } // namespace mozilla