fune/layout/svg/SVGUtils.cpp
Emilio Cobos Álvarez 5730ee0ca5 Bug 1364813 - Remove IsFrameOfType, use non-virtual checks. r=jwatt
Extend the per-frame-class bit we have to devirtualize IsLeaf to also
devirtualize IsFrameOfType. That is, move this data to FrameClasses.py.

This was done by going through all the frame classes, trying to preserve
behavior.

The only quirky thing is that I had to add two more trivial frame
classes, `nsAudioFrame` for audio elements, and
`nsFloatingFirstLetterFrame`. That's because these frame classes were
returning different answers at runtime, but they do this only on
conditions that trigger frame reconstruction (floating, and being an
audio element, respectively).

Differential Revision: https://phabricator.services.mozilla.com/D194703
2023-11-26 22:17:28 +00:00

1552 lines
54 KiB
C++

/* -*- 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.
// in that case we disable painting of the element.
nsTArray<SVGFilterFrame*> filterFrames;
if (!aFrame->StyleEffects()->HasFilters() ||
SVGObserverUtils::GetAndObserveFilters(aFrame, &filterFrames) ==
SVGObserverUtils::eHasRefsSomeInvalid) {
return aPreFilterRect;
}
return FilterInstance::GetPostFilterBounds(aFrame, filterFrames, 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->IsSVGFrame(), "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->IsSVGFrame(), "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->IsSVGFrame(), "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(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()->IsSVGContainerFrame());
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->IsSVGFrame() || 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->IsSVGFrame()) {
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;
}
SVGUtils::MaskUsage SVGUtils::DetermineMaskUsage(const nsIFrame* aFrame,
bool aHandleOpacity) {
MaskUsage usage;
using ClipPathType = StyleClipPath::Tag;
usage.mOpacity = ComputeOpacity(aFrame, aHandleOpacity);
nsIFrame* firstFrame =
nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
const nsStyleSVGReset* svgReset = firstFrame->StyleSVGReset();
if (SVGObserverUtils::GetAndObserveMasks(firstFrame, nullptr) !=
SVGObserverUtils::eHasNoRefs) {
usage.mShouldGenerateMaskLayer = true;
}
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()) {
usage.mShouldApplyClipPath = true;
} else {
usage.mShouldGenerateClipMaskLayer = true;
}
}
break;
case ClipPathType::Shape: {
usage.mShouldApplyBasicShapeOrPath = true;
const auto& shape = svgReset->mClipPath.AsShape()._0;
usage.mIsSimpleClipShape =
!usage.mShouldGenerateMaskLayer &&
(shape->IsRect() || shape->IsCircle() || shape->IsEllipse());
break;
}
case ClipPathType::Box:
usage.mShouldApplyBasicShapeOrPath = true;
break;
case ClipPathType::None:
MOZ_ASSERT(!usage.mShouldGenerateClipMaskLayer &&
!usage.mShouldApplyClipPath &&
!usage.mShouldApplyBasicShapeOrPath);
break;
default:
MOZ_ASSERT_UNREACHABLE("Unsupported clip-path type.");
break;
}
return usage;
}
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);
if (maskUsage.IsTransparent()) {
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;
nsTArray<SVGFilterFrame*> filterFrames;
const bool hasInvalidFilter =
SVGObserverUtils::GetAndObserveFilters(aFrame, &filterFrames) ==
SVGObserverUtils::eHasRefsSomeInvalid;
SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame);
SVGObserverUtils::GetAndObserveMasks(aFrame, &maskFrames);
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 shouldPushMask = false;
if (maskUsage.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.ShouldGenerateLayer()) {
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.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()) {
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);
};
// If we're masking a userSpaceOnUse mask we may need to include the
// stroke too. Err on the side of caution and include it always.
gfxRect bbox = GetBBox(aFrame, SVGUtils::eUseFrameBoundsForOuterSVG |
SVGUtils::eBBoxIncludeFillGeometry |
SVGUtils::eBBoxIncludeStroke);
FilterInstance::PaintFilteredFrame(
aFrame, aFrame->StyleEffects()->mFilters.AsSpan(), filterFrames, target,
callback, nullptr, aImgParams, 1.0f, &bbox);
} 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.ptrOr(nullptr), 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