/* -*- 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 "SVGGeometryFrame.h" // Keep others in (case-insensitive) order: #include "gfx2DGlue.h" #include "gfxContext.h" #include "gfxPlatform.h" #include "gfxUtils.h" #include "mozilla/dom/SVGGeometryElement.h" #include "mozilla/dom/SVGGraphicsElement.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Helpers.h" #include "mozilla/ArrayUtils.h" #include "mozilla/PresShell.h" #include "mozilla/RefPtr.h" #include "mozilla/SVGContextPaint.h" #include "mozilla/SVGContentUtils.h" #include "mozilla/SVGObserverUtils.h" #include "mozilla/SVGUtils.h" #include "nsGkAtoms.h" #include "nsLayoutUtils.h" #include "SVGAnimatedTransformList.h" #include "SVGMarkerFrame.h" using namespace mozilla::dom; using namespace mozilla::gfx; using namespace mozilla::image; //---------------------------------------------------------------------- // Implementation nsIFrame* NS_NewSVGGeometryFrame(mozilla::PresShell* aPresShell, mozilla::ComputedStyle* aStyle) { return new (aPresShell) mozilla::SVGGeometryFrame(aStyle, aPresShell->GetPresContext()); } namespace mozilla { NS_IMPL_FRAMEARENA_HELPERS(SVGGeometryFrame) //---------------------------------------------------------------------- // nsQueryFrame methods NS_QUERYFRAME_HEAD(SVGGeometryFrame) NS_QUERYFRAME_ENTRY(ISVGDisplayableFrame) NS_QUERYFRAME_ENTRY(SVGGeometryFrame) NS_QUERYFRAME_TAIL_INHERITING(nsIFrame) //---------------------------------------------------------------------- // nsIFrame methods void SVGGeometryFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD); nsIFrame::Init(aContent, aParent, aPrevInFlow); } nsresult SVGGeometryFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, int32_t aModType) { // We don't invalidate for transform changes (the layers code does that). // Also note that SVGTransformableElement::GetAttributeChangeHint will // return nsChangeHint_UpdateOverflow for "transform" attribute changes // and cause DoApplyRenderingChangeToTree to make the SchedulePaint call. if (aNameSpaceID == kNameSpaceID_None && (static_cast(GetContent()) ->AttributeDefinesGeometry(aAttribute))) { nsLayoutUtils::PostRestyleEvent(mContent->AsElement(), RestyleHint{0}, nsChangeHint_InvalidateRenderingObservers); SVGUtils::ScheduleReflowSVG(this); } return NS_OK; } /* virtual */ void SVGGeometryFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { nsIFrame::DidSetComputedStyle(aOldComputedStyle); auto* element = static_cast(GetContent()); if (!aOldComputedStyle) { element->ClearAnyCachedPath(); return; } const auto* oldStyleSVG = aOldComputedStyle->StyleSVG(); if (!SVGContentUtils::ShapeTypeHasNoCorners(GetContent())) { if (StyleSVG()->mStrokeLinecap != oldStyleSVG->mStrokeLinecap && element->IsSVGElement(nsGkAtoms::path)) { // If the stroke-linecap changes to or from "butt" then our element // needs to update its cached Moz2D Path, since SVGPathData::BuildPath // decides whether or not to insert little lines into the path for zero // length subpaths base on that property. element->ClearAnyCachedPath(); } else if (HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD)) { if (StyleSVG()->mClipRule != oldStyleSVG->mClipRule) { // Moz2D Path objects are fill-rule specific. // For clipPath we use clip-rule as the path's fill-rule. element->ClearAnyCachedPath(); } } else { if (StyleSVG()->mFillRule != oldStyleSVG->mFillRule) { // Moz2D Path objects are fill-rule specific. element->ClearAnyCachedPath(); } } } if (element->IsGeometryChangedViaCSS(*Style(), *aOldComputedStyle)) { element->ClearAnyCachedPath(); } } bool SVGGeometryFrame::IsSVGTransformed( gfx::Matrix* aOwnTransform, gfx::Matrix* aFromParentTransform) const { return SVGUtils::IsSVGTransformed(this, aOwnTransform, aFromParentTransform); } void SVGGeometryFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { if (!static_cast(GetContent())->HasValidDimensions()) { return; } if (aBuilder->IsForPainting()) { if (!IsVisibleForPainting()) { return; } if (StyleEffects()->IsTransparent()) { return; } const auto* styleSVG = StyleSVG(); if (styleSVG->mFill.kind.IsNone() && styleSVG->mStroke.kind.IsNone() && !styleSVG->HasMarker()) { return; } aBuilder->BuildCompositorHitTestInfoIfNeeded(this, aLists.BorderBackground()); } DisplayOutline(aBuilder, aLists); aLists.Content()->AppendNewToTop(aBuilder, this); } //---------------------------------------------------------------------- // ISVGDisplayableFrame methods void SVGGeometryFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, imgDrawingParams& aImgParams) { if (!StyleVisibility()->IsVisible()) { return; } // Matrix to the geometry's user space: gfxMatrix newMatrix = aContext.CurrentMatrixDouble().PreMultiply(aTransform).NudgeToIntegers(); if (newMatrix.IsSingular()) { return; } uint32_t paintOrder = StyleSVG()->mPaintOrder; if (!paintOrder) { Render(&aContext, eRenderFill | eRenderStroke, newMatrix, aImgParams); PaintMarkers(aContext, aTransform, aImgParams); } else { while (paintOrder) { auto component = StylePaintOrder(paintOrder & kPaintOrderMask); switch (component) { case StylePaintOrder::Fill: Render(&aContext, eRenderFill, newMatrix, aImgParams); break; case StylePaintOrder::Stroke: Render(&aContext, eRenderStroke, newMatrix, aImgParams); break; case StylePaintOrder::Markers: PaintMarkers(aContext, aTransform, aImgParams); break; default: MOZ_FALLTHROUGH_ASSERT("Unknown paint-order variant, how?"); case StylePaintOrder::Normal: break; } paintOrder >>= kPaintOrderShift; } } } nsIFrame* SVGGeometryFrame::GetFrameForPoint(const gfxPoint& aPoint) { FillRule fillRule; uint16_t hitTestFlags; if (HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD)) { hitTestFlags = SVG_HIT_TEST_FILL; fillRule = SVGUtils::ToFillRule(StyleSVG()->mClipRule); } else { hitTestFlags = SVGUtils::GetGeometryHitTestFlags(this); if (!hitTestFlags) { return nullptr; } fillRule = SVGUtils::ToFillRule(StyleSVG()->mFillRule); } bool isHit = false; SVGGeometryElement* content = static_cast(GetContent()); // Using ScreenReferenceDrawTarget() opens us to Moz2D backend specific hit- // testing bugs. Maybe we should use a BackendType::CAIRO DT for hit-testing // so that we get more consistent/backwards compatible results? RefPtr drawTarget = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); RefPtr path = content->GetOrBuildPath(drawTarget, fillRule); if (!path) { return nullptr; // no path, so we don't paint anything that can be hit } if (hitTestFlags & SVG_HIT_TEST_FILL) { isHit = path->ContainsPoint(ToPoint(aPoint), {}); } if (!isHit && (hitTestFlags & SVG_HIT_TEST_STROKE)) { Point point = ToPoint(aPoint); SVGContentUtils::AutoStrokeOptions stroke; SVGContentUtils::GetStrokeOptions(&stroke, content, Style(), nullptr); gfxMatrix userToOuterSVG; if (SVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) { // We need to transform the path back into the appropriate ancestor // coordinate system in order for non-scaled stroke to be correct. // Naturally we also need to transform the point into the same // coordinate system in order to hit-test against the path. point = ToMatrix(userToOuterSVG).TransformPoint(point); RefPtr builder = path->TransformedCopyToBuilder(ToMatrix(userToOuterSVG), fillRule); path = builder->Finish(); } isHit = path->StrokeContainsPoint(stroke, point, {}); } if (isHit && SVGUtils::HitTestClip(this, aPoint)) { return this; } return nullptr; } void SVGGeometryFrame::ReflowSVG() { NS_ASSERTION(SVGUtils::OuterSVGIsCallingReflowSVG(this), "This call is probably a wasteful mistake"); MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), "ReflowSVG mechanism not designed for this"); if (!SVGUtils::NeedsReflowSVG(this)) { return; } uint32_t flags = SVGUtils::eBBoxIncludeFill | SVGUtils::eBBoxIncludeStroke | SVGUtils::eBBoxIncludeMarkers; // Our "visual" overflow rect needs to be valid for building display lists // for hit testing, which means that for certain values of 'pointer-events' // it needs to include the geometry of the fill or stroke even when the fill/ // stroke don't actually render (e.g. when stroke="none" or // stroke-opacity="0"). GetGeometryHitTestFlags() accounts for // 'pointer-events'. uint16_t hitTestFlags = SVGUtils::GetGeometryHitTestFlags(this); if (hitTestFlags & SVG_HIT_TEST_FILL) { flags |= SVGUtils::eBBoxIncludeFillGeometry; } if (hitTestFlags & SVG_HIT_TEST_STROKE) { flags |= SVGUtils::eBBoxIncludeStrokeGeometry; } gfxRect extent = GetBBoxContribution({}, flags).ToThebesRect(); mRect = nsLayoutUtils::RoundGfxRectToAppRect(extent, AppUnitsPerCSSPixel()); if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { // Make sure we have our filter property (if any) before calling // FinishAndStoreOverflow (subsequent filter changes are handled off // nsChangeHint_UpdateEffects): SVGObserverUtils::UpdateEffects(this); } nsRect overflow = nsRect(nsPoint(0, 0), mRect.Size()); OverflowAreas overflowAreas(overflow, overflow); FinishAndStoreOverflow(overflowAreas, mRect.Size()); RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN); // Invalidate, but only if this is not our first reflow (since if it is our // first reflow then we haven't had our first paint yet). if (!GetParent()->HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { InvalidateFrame(); } } void SVGGeometryFrame::NotifySVGChanged(uint32_t aFlags) { MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), "Invalidation logic may need adjusting"); // Changes to our ancestors may affect how we render when we are rendered as // part of our ancestor (specifically, if our coordinate context changes size // and we have percentage lengths defining our geometry, then we need to be // reflowed). However, ancestor changes cannot affect how we render when we // are rendered as part of any rendering observers that we may have. // Therefore no need to notify rendering observers here. // Don't try to be too smart trying to avoid the ScheduleReflowSVG calls // for the stroke properties examined below. Checking HasStroke() is not // enough, since what we care about is whether we include the stroke in our // overflow rects or not, and we sometimes deliberately include stroke // when it's not visible. See the complexities of GetBBoxContribution. if (aFlags & COORD_CONTEXT_CHANGED) { auto* geom = static_cast(GetContent()); // Stroke currently contributes to our mRect, which is why we have to take // account of stroke-width here. Note that we do not need to take account // of stroke-dashoffset since, although that can have a percentage value // that is resolved against our coordinate context, it does not affect our // mRect. const auto& strokeWidth = StyleSVG()->mStrokeWidth; if (geom->GeometryDependsOnCoordCtx() || (strokeWidth.IsLengthPercentage() && strokeWidth.AsLengthPercentage().HasPercent())) { geom->ClearAnyCachedPath(); SVGUtils::ScheduleReflowSVG(this); } } if ((aFlags & TRANSFORM_CHANGED) && StyleSVGReset()->HasNonScalingStroke()) { // Stroke currently contributes to our mRect, and our stroke depends on // the transform to our outer- if |vector-effect:non-scaling-stroke|. SVGUtils::ScheduleReflowSVG(this); } } SVGBBox SVGGeometryFrame::GetBBoxContribution(const Matrix& aToBBoxUserspace, uint32_t aFlags) { SVGBBox bbox; if (aToBBoxUserspace.IsSingular()) { // XXX ReportToConsole return bbox; } if ((aFlags & SVGUtils::eForGetClientRects) && aToBBoxUserspace.PreservesAxisAlignedRectangles()) { Rect rect = NSRectToRect(mRect, AppUnitsPerCSSPixel()); bbox = aToBBoxUserspace.TransformBounds(rect); return bbox; } SVGGeometryElement* element = static_cast(GetContent()); const bool getFill = (aFlags & SVGUtils::eBBoxIncludeFillGeometry) || ((aFlags & SVGUtils::eBBoxIncludeFill) && !StyleSVG()->mFill.kind.IsNone()); const bool getStroke = ((aFlags & SVGUtils::eBBoxIncludeStrokeGeometry) || ((aFlags & SVGUtils::eBBoxIncludeStroke) && SVGUtils::HasStroke(this))) && // If this frame has non-scaling-stroke and we would like to compute its // stroke, it may cause a potential cyclical dependency if the caller is // for transform. In this case, we have to fall back to fill-box, so make // |getStroke| be false. // https://github.com/w3c/csswg-drafts/issues/9640 // // Note: // 1. We don't care about the computation of the markers below in this // function because we know the callers don't set // SVGUtils::eBBoxIncludeMarkers. // See nsStyleTransformMatrix::GetSVGBox() and // MotionPathUtils::GetRayContainReferenceSize() for more details. // 2. We have to break the dependency here *again* because the geometry // frame may be in the subtree of a SVGContainerFrame, which may not // set non-scaling-stroke. !(StyleSVGReset()->HasNonScalingStroke() && (aFlags & SVGUtils::eAvoidCycleIfNonScalingStroke)); SVGContentUtils::AutoStrokeOptions strokeOptions; if (getStroke) { SVGContentUtils::GetStrokeOptions(&strokeOptions, element, Style(), nullptr, SVGContentUtils::eIgnoreStrokeDashing); } else { // Override the default line width of 1.f so that when we call // GetGeometryBounds below the result doesn't include stroke bounds. strokeOptions.mLineWidth = 0.f; } Rect simpleBounds; bool gotSimpleBounds = false; gfxMatrix userToOuterSVG; if (getStroke && SVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) { Matrix moz2dUserToOuterSVG = ToMatrix(userToOuterSVG); if (moz2dUserToOuterSVG.IsSingular()) { return bbox; } gotSimpleBounds = element->GetGeometryBounds( &simpleBounds, strokeOptions, aToBBoxUserspace, &moz2dUserToOuterSVG); } else { gotSimpleBounds = element->GetGeometryBounds(&simpleBounds, strokeOptions, aToBBoxUserspace); } if (gotSimpleBounds) { bbox = simpleBounds; } else { // Get the bounds using a Moz2D Path object (more expensive): RefPtr tmpDT; tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); FillRule fillRule = SVGUtils::ToFillRule( HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD) ? StyleSVG()->mClipRule : StyleSVG()->mFillRule); RefPtr pathInUserSpace = element->GetOrBuildPath(tmpDT, fillRule); if (!pathInUserSpace) { return bbox; } RefPtr pathInBBoxSpace; if (aToBBoxUserspace.IsIdentity()) { pathInBBoxSpace = pathInUserSpace; } else { RefPtr builder = pathInUserSpace->TransformedCopyToBuilder(aToBBoxUserspace, fillRule); pathInBBoxSpace = builder->Finish(); if (!pathInBBoxSpace) { return bbox; } } // Be careful when replacing the following logic to get the fill and stroke // extents independently (instead of computing the stroke extents from the // path extents). You may think that you can just use the stroke extents if // there is both a fill and a stroke. In reality it's necessary to // calculate both the fill and stroke extents, and take the union of the // two. There are two reasons for this: // // # Due to stroke dashing, in certain cases the fill extents could // actually extend outside the stroke extents. // # If the stroke is very thin, cairo won't paint any stroke, and so the // stroke bounds that it will return will be empty. Rect pathBBoxExtents = pathInBBoxSpace->GetBounds(); if (!pathBBoxExtents.IsFinite()) { // This can happen in the case that we only have a move-to command in the // path commands, in which case we know nothing gets rendered. return bbox; } // Account for fill: if (getFill) { bbox = pathBBoxExtents; } // Account for stroke: if (getStroke) { #if 0 // This disabled code is how we would calculate the stroke bounds using // Moz2D Path::GetStrokedBounds(). Unfortunately at the time of writing // it there are two problems that prevent us from using it. // // First, it seems that some of the Moz2D backends are really dumb. Not // only do some GetStrokeOptions() implementations sometimes // significantly overestimate the stroke bounds, but if an argument is // passed for the aTransform parameter then they just return bounds-of- // transformed-bounds. These two things combined can lead the bounds to // be unacceptably oversized, leading to massive over-invalidation. // // Second, the way we account for non-scaling-stroke by transforming the // path using the transform to the outer- element is not compatible // with the way that SVGGeometryFrame::Reflow() inserts a scale // into aToBBoxUserspace and then scales the bounds that we return. SVGContentUtils::AutoStrokeOptions strokeOptions; SVGContentUtils::GetStrokeOptions(&strokeOptions, element, Style(), nullptr, SVGContentUtils::eIgnoreStrokeDashing); Rect strokeBBoxExtents; gfxMatrix userToOuterSVG; if (SVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) { Matrix outerSVGToUser = ToMatrix(userToOuterSVG); outerSVGToUser.Invert(); Matrix outerSVGToBBox = aToBBoxUserspace * outerSVGToUser; RefPtr builder = pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG)); RefPtr pathInOuterSVGSpace = builder->Finish(); strokeBBoxExtents = pathInOuterSVGSpace->GetStrokedBounds(strokeOptions, outerSVGToBBox); } else { strokeBBoxExtents = pathInUserSpace->GetStrokedBounds(strokeOptions, aToBBoxUserspace); } MOZ_ASSERT(strokeBBoxExtents.IsFinite(), "bbox is about to go bad"); bbox.UnionEdges(strokeBBoxExtents); #else // For now we just use SVGUtils::PathExtentsToMaxStrokeExtents: gfxRect strokeBBoxExtents = SVGUtils::PathExtentsToMaxStrokeExtents( ThebesRect(pathBBoxExtents), this, ThebesMatrix(aToBBoxUserspace)); MOZ_ASSERT(ToRect(strokeBBoxExtents).IsFinite(), "bbox is about to go bad"); bbox.UnionEdges(strokeBBoxExtents); #endif } } // Account for markers: if ((aFlags & SVGUtils::eBBoxIncludeMarkers) && element->IsMarkable()) { SVGMarkerFrame* markerFrames[SVGMark::eTypeCount]; if (SVGObserverUtils::GetAndObserveMarkers(this, &markerFrames)) { nsTArray marks; element->GetMarkPoints(&marks); if (uint32_t num = marks.Length()) { float strokeWidth = SVGUtils::GetStrokeWidth(this); for (uint32_t i = 0; i < num; i++) { const SVGMark& mark = marks[i]; SVGMarkerFrame* frame = markerFrames[mark.type]; if (frame) { SVGBBox mbbox = frame->GetMarkBBoxContribution( aToBBoxUserspace, aFlags, this, mark, strokeWidth); MOZ_ASSERT(mbbox.IsFinite(), "bbox is about to go bad"); bbox.UnionEdges(mbbox); } } } } } return bbox; } //---------------------------------------------------------------------- // SVGGeometryFrame methods: gfxMatrix SVGGeometryFrame::GetCanvasTM() { NS_ASSERTION(GetParent(), "null parent"); auto* parent = static_cast(GetParent()); auto* content = static_cast(GetContent()); return content->PrependLocalTransformsTo(parent->GetCanvasTM()); } void SVGGeometryFrame::Render(gfxContext* aContext, uint32_t aRenderComponents, const gfxMatrix& aTransform, imgDrawingParams& aImgParams) { MOZ_ASSERT(!aTransform.IsSingular()); DrawTarget* drawTarget = aContext->GetDrawTarget(); MOZ_ASSERT(drawTarget); if (!drawTarget->IsValid()) { return; } FillRule fillRule = SVGUtils::ToFillRule( HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD) ? StyleSVG()->mClipRule : StyleSVG()->mFillRule); SVGGeometryElement* element = static_cast(GetContent()); AntialiasMode aaMode = (StyleSVG()->mShapeRendering == StyleShapeRendering::Optimizespeed || StyleSVG()->mShapeRendering == StyleShapeRendering::Crispedges) ? AntialiasMode::NONE : AntialiasMode::SUBPIXEL; // We wait as late as possible before setting the transform so that we don't // set it unnecessarily if we return early (it's an expensive operation for // some backends). gfxContextMatrixAutoSaveRestore autoRestoreTransform(aContext); aContext->SetMatrixDouble(aTransform); if (HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD)) { // We don't complicate this code with GetAsSimplePath since the cost of // masking will dwarf Path creation overhead anyway. RefPtr path = element->GetOrBuildPath(drawTarget, fillRule); if (path) { ColorPattern white(ToDeviceColor(sRGBColor(1.0f, 1.0f, 1.0f, 1.0f))); drawTarget->Fill(path, white, DrawOptions(1.0f, CompositionOp::OP_OVER, aaMode)); } return; } SVGGeometryElement::SimplePath simplePath; RefPtr path; element->GetAsSimplePath(&simplePath); if (!simplePath.IsPath()) { path = element->GetOrBuildPath(drawTarget, fillRule); if (!path) { return; } } SVGContextPaint* contextPaint = SVGContextPaint::GetContextPaint(GetContent()); if (aRenderComponents & eRenderFill) { GeneralPattern fillPattern; SVGUtils::MakeFillPatternFor(this, aContext, &fillPattern, aImgParams, contextPaint); if (fillPattern.GetPattern()) { DrawOptions drawOptions(1.0f, CompositionOp::OP_OVER, aaMode); if (simplePath.IsRect()) { drawTarget->FillRect(simplePath.AsRect(), fillPattern, drawOptions); } else if (path) { drawTarget->Fill(path, fillPattern, drawOptions); } } } if ((aRenderComponents & eRenderStroke) && SVGUtils::HasStroke(this, contextPaint)) { // Account for vector-effect:non-scaling-stroke: gfxMatrix userToOuterSVG; if (SVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) { // A simple Rect can't be transformed with rotate/skew, so let's switch // to using a real path: if (!path) { path = element->GetOrBuildPath(drawTarget, fillRule); if (!path) { return; } simplePath.Reset(); } // We need to transform the path back into the appropriate ancestor // coordinate system, and paint it it that coordinate system, in order // for non-scaled stroke to paint correctly. gfxMatrix outerSVGToUser = userToOuterSVG; outerSVGToUser.Invert(); aContext->Multiply(outerSVGToUser); RefPtr builder = path->TransformedCopyToBuilder(ToMatrix(userToOuterSVG), fillRule); path = builder->Finish(); } GeneralPattern strokePattern; SVGUtils::MakeStrokePatternFor(this, aContext, &strokePattern, aImgParams, contextPaint); if (strokePattern.GetPattern()) { SVGContentUtils::AutoStrokeOptions strokeOptions; SVGContentUtils::GetStrokeOptions(&strokeOptions, static_cast(GetContent()), Style(), contextPaint); // GetStrokeOptions may set the line width to zero as an optimization if (strokeOptions.mLineWidth <= 0) { return; } DrawOptions drawOptions(1.0f, CompositionOp::OP_OVER, aaMode); if (simplePath.IsRect()) { drawTarget->StrokeRect(simplePath.AsRect(), strokePattern, strokeOptions, drawOptions); } else if (simplePath.IsLine()) { drawTarget->StrokeLine(simplePath.Point1(), simplePath.Point2(), strokePattern, strokeOptions, drawOptions); } else { drawTarget->Stroke(path, strokePattern, strokeOptions, drawOptions); } } } } bool SVGGeometryFrame::IsInvisible() const { if (!StyleVisibility()->IsVisible()) { return true; } // Anything below will round to zero later down the pipeline. constexpr float opacity_threshold = 1.0 / 128.0; if (StyleEffects()->mOpacity <= opacity_threshold) { return true; } const nsStyleSVG* style = StyleSVG(); SVGContextPaint* contextPaint = SVGContextPaint::GetContextPaint(GetContent()); if (!style->mFill.kind.IsNone()) { float opacity = SVGUtils::GetOpacity(style->mFillOpacity, contextPaint); if (opacity > opacity_threshold) { return false; } } if (!style->mStroke.kind.IsNone()) { float opacity = SVGUtils::GetOpacity(style->mStrokeOpacity, contextPaint); if (opacity > opacity_threshold) { return false; } } if (style->HasMarker()) { return false; } return true; } bool SVGGeometryFrame::CreateWebRenderCommands( mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsDisplayListBuilder* aDisplayListBuilder, DisplaySVGGeometry* aItem, bool aDryRun) { if (!StyleVisibility()->IsVisible()) { return true; } SVGGeometryElement* element = static_cast(GetContent()); SVGGeometryElement::SimplePath simplePath; element->GetAsSimplePath(&simplePath); if (!simplePath.IsRect()) { return false; } const nsStyleSVG* style = StyleSVG(); MOZ_ASSERT(style); if (!style->mFill.kind.IsColor()) { return false; } switch (style->mFill.kind.tag) { case StyleSVGPaintKind::Tag::Color: break; default: return false; } if (!style->mStroke.kind.IsNone()) { return false; } if (StyleEffects()->HasMixBlendMode()) { // FIXME: not implemented return false; } if (style->HasMarker() && element->IsMarkable()) { // Markers aren't suppported yet. return false; } if (!aDryRun) { auto appUnitsPerDevPx = PresContext()->AppUnitsPerDevPixel(); float scale = (float)AppUnitsPerCSSPixel() / (float)appUnitsPerDevPx; auto rect = simplePath.AsRect(); rect.Scale(scale); auto offset = LayoutDevicePoint::FromAppUnits( aItem->ToReferenceFrame() - GetPosition(), appUnitsPerDevPx); rect.MoveBy(offset.x, offset.y); auto wrRect = wr::ToLayoutRect(rect); SVGContextPaint* contextPaint = SVGContextPaint::GetContextPaint(GetContent()); // At the moment this code path doesn't support strokes so it fine to // combine the rectangle's opacity (which has to be applied on the result) // of (filling + stroking) with the fill opacity. float elemOpacity = StyleEffects()->mOpacity; float fillOpacity = SVGUtils::GetOpacity(style->mFillOpacity, contextPaint); float opacity = elemOpacity * fillOpacity; auto c = nsLayoutUtils::GetColor(this, &nsStyleSVG::mFill); wr::ColorF color{ ((float)NS_GET_R(c)) / 255.0f, ((float)NS_GET_G(c)) / 255.0f, ((float)NS_GET_B(c)) / 255.0f, ((float)NS_GET_A(c)) / 255.0f * opacity}; aBuilder.PushRect(wrRect, wrRect, !aItem->BackfaceIsHidden(), true, false, color); } return true; } void SVGGeometryFrame::PaintMarkers(gfxContext& aContext, const gfxMatrix& aTransform, imgDrawingParams& aImgParams) { auto* element = static_cast(GetContent()); if (!element->IsMarkable()) { return; } SVGMarkerFrame* markerFrames[SVGMark::eTypeCount]; if (!SVGObserverUtils::GetAndObserveMarkers(this, &markerFrames)) { return; } nsTArray marks; element->GetMarkPoints(&marks); if (marks.IsEmpty()) { return; } float strokeWidth = GetStrokeWidthForMarkers(); for (const SVGMark& mark : marks) { if (auto* frame = markerFrames[mark.type]) { frame->PaintMark(aContext, aTransform, this, mark, strokeWidth, aImgParams); } } } float SVGGeometryFrame::GetStrokeWidthForMarkers() { float strokeWidth = SVGUtils::GetStrokeWidth( this, SVGContextPaint::GetContextPaint(GetContent())); gfxMatrix userToOuterSVG; if (SVGUtils::GetNonScalingStrokeTransform(this, &userToOuterSVG)) { // We're not interested in any translation here so we can treat this as // Singular Value Decomposition (SVD) of a 2 x 2 matrix. That would give us // sx and sy values as the X and Y scales. The value we want is the XY // scale i.e. the normalised hypotenuse, which is sqrt(sx^2 + sy^2) / // sqrt(2). If we use the formulae from // https://scicomp.stackexchange.com/a/14103, we discover that the // normalised hypotenuse is simply the square root of the sum of the squares // of all the 2D matrix elements divided by sqrt(2). // // Note that this may need adjusting to support 3D transforms properly. strokeWidth /= float(sqrt(userToOuterSVG._11 * userToOuterSVG._11 + userToOuterSVG._12 * userToOuterSVG._12 + userToOuterSVG._21 * userToOuterSVG._21 + userToOuterSVG._22 * userToOuterSVG._22) / M_SQRT2); } return strokeWidth; } } // namespace mozilla