diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /layout/svg | |
parent | Initial commit. (diff) | |
download | thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
357 files changed, 30499 insertions, 0 deletions
diff --git a/layout/svg/AutoReferenceChainGuard.h b/layout/svg/AutoReferenceChainGuard.h new file mode 100644 index 0000000000..c25ba0ea70 --- /dev/null +++ b/layout/svg/AutoReferenceChainGuard.h @@ -0,0 +1,169 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_AUTOREFERENCECHAINGUARD_H_ +#define LAYOUT_SVG_AUTOREFERENCECHAINGUARD_H_ + +#include "Element.h" +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/ReentrancyGuard.h" +#include "mozilla/Likely.h" +#include "nsDebug.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" + +namespace mozilla { + +/** + * This helper class helps us to protect against two related issues that can + * occur in SVG content: reference loops, and reference chains that we deem to + * be too long. + * + * Some SVG effects can reference another effect of the same type to produce + * a chain of effects to be applied (e.g. clipPath), while in other cases it is + * possible that while processing an effect of a certain type another effect + * of the same type may be encountered indirectly (e.g. pattern). In order to + * avoid stack overflow crashes and performance issues we need to impose an + * arbitrary limit on the length of the reference chains that SVG content may + * try to create. (Some SVG authoring tools have been known to create absurdly + * long reference chains. For example, bug 1253590 details a case where Adobe + * Illustrator was used to created an SVG with a chain of 5000 clip paths which + * could cause us to run out of stack space and crash.) + * + * This class is intended to be used with the nsIFrame's of SVG effects that + * may involve reference chains. To use it add a boolean member, something + * like this: + * + * // Flag used to indicate whether a methods that may reenter due to + * // following a reference to another instance is currently executing. + * bool mIsBeingProcessed; + * + * Make sure to initialize the member to false in the class' constructons. + * + * Then add the following to the top of any methods that may be reentered due + * to following a reference: + * + * static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + * + * AutoReferenceChainGuard refChainGuard(this, &mIsBeingProcessed, + * &sRefChainLengthCounter); + * if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + * return; // Break reference chain + * } + * + * Note that mIsBeingProcessed and sRefChainLengthCounter should never be used + * by the frame except when it initialize them as indicated above. + */ +class MOZ_RAII AutoReferenceChainGuard { + static const int16_t sDefaultMaxChainLength = 10; // arbitrary length + + public: + static const int16_t noChain = -2; + + /** + * @param aFrame The frame for an effect that may involve a reference chain. + * @param aFrameInUse The member variable on aFrame that is used to indicate + * whether one of aFrame's methods that may involve following a reference + * to another effect of the same type is currently being executed. + * @param aChainCounter A static variable in the method in which this class + * is instantiated that is used to keep track of how many times the method + * is reentered (and thus how long the a reference chain is). + * @param aMaxChainLength The maximum number of links that are allowed in + * a reference chain. + */ + AutoReferenceChainGuard(nsIFrame* aFrame, bool* aFrameInUse, + int16_t* aChainCounter, + int16_t aMaxChainLength = sDefaultMaxChainLength) + : mFrame(aFrame), + mFrameInUse(aFrameInUse), + mChainCounter(aChainCounter), + mMaxChainLength(aMaxChainLength), + mBrokeReference(false) { + MOZ_ASSERT(aFrame && aFrameInUse && aChainCounter); + MOZ_ASSERT(aMaxChainLength > 0); + MOZ_ASSERT(*aChainCounter == noChain || + (*aChainCounter >= 0 && *aChainCounter < aMaxChainLength)); + } + + ~AutoReferenceChainGuard() { + if (mBrokeReference) { + // We didn't change mFrameInUse or mChainCounter + return; + } + + *mFrameInUse = false; + + // If we fail this assert then there were more destructor calls than + // Reference() calls (a consumer forgot to to call Reference()), or else + // someone messed with the variable pointed to by mChainCounter. + MOZ_ASSERT(*mChainCounter < mMaxChainLength); + + (*mChainCounter)++; + + if (*mChainCounter == mMaxChainLength) { + *mChainCounter = noChain; // reset ready for use next time + } + } + + /** + * Returns true on success (no reference loop/reference chain length is + * within the specified limits), else returns false on failure (there is a + * reference loop/the reference chain has exceeded the specified limits). + * If it returns false then an error message will be reported to the DevTools + * console (only once). + */ + [[nodiscard]] bool Reference() { + if (MOZ_UNLIKELY(*mFrameInUse)) { + mBrokeReference = true; + ReportErrorToConsole(); + return false; + } + + if (*mChainCounter == noChain) { + // Initialize - we start at aMaxChainLength and decrement towards zero. + *mChainCounter = mMaxChainLength; + } else { + // If we fail this assertion then either a consumer failed to break a + // reference loop/chain, or else they called Reference() more than once + MOZ_ASSERT(*mChainCounter >= 0); + + if (MOZ_UNLIKELY(*mChainCounter < 1)) { + mBrokeReference = true; + ReportErrorToConsole(); + return false; + } + } + + // We only set these once we know we're returning true. + *mFrameInUse = true; + (*mChainCounter)--; + + return true; + } + + private: + void ReportErrorToConsole() { + AutoTArray<nsString, 2> params; + dom::Element* element = mFrame->GetContent()->AsElement(); + element->GetTagName(*params.AppendElement()); + element->GetId(*params.AppendElement()); + auto doc = mFrame->GetContent()->OwnerDoc(); + auto warning = *mFrameInUse ? dom::Document::eSVGRefLoop + : dom::Document::eSVGRefChainLengthExceeded; + doc->WarnOnceAbout(warning, /* asError */ true, params); + } + + nsIFrame* mFrame; + bool* mFrameInUse; + int16_t* mChainCounter; + const int16_t mMaxChainLength; + bool mBrokeReference; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_AUTOREFERENCECHAINGUARD_H_ diff --git a/layout/svg/CSSClipPathInstance.cpp b/layout/svg/CSSClipPathInstance.cpp new file mode 100644 index 0000000000..8fd51b2799 --- /dev/null +++ b/layout/svg/CSSClipPathInstance.cpp @@ -0,0 +1,243 @@ +/* -*- 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 "CSSClipPathInstance.h" + +#include "mozilla/dom/SVGElement.h" +#include "mozilla/dom/SVGPathData.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/ShapeUtils.h" +#include "mozilla/SVGUtils.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "nsCSSRendering.h" +#include "nsIFrame.h" +#include "nsLayoutUtils.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; + +namespace mozilla { + +/* static*/ +void CSSClipPathInstance::ApplyBasicShapeOrPathClip( + gfxContext& aContext, nsIFrame* aFrame, const gfxMatrix& aTransform) { + RefPtr<Path> path = + CreateClipPathForFrame(aContext.GetDrawTarget(), aFrame, aTransform); + if (!path) { + // This behavior matches |SVGClipPathFrame::ApplyClipPath()|. + // https://www.w3.org/TR/css-masking-1/#ClipPathElement: + // "An empty clipping path will completely clip away the element that had + // the clip-path property applied." + aContext.Clip(Rect()); + return; + } + aContext.Clip(path); +} + +/* static*/ +RefPtr<Path> CSSClipPathInstance::CreateClipPathForFrame( + gfx::DrawTarget* aDt, nsIFrame* aFrame, const gfxMatrix& aTransform) { + const auto& clipPathStyle = aFrame->StyleSVGReset()->mClipPath; + MOZ_ASSERT(clipPathStyle.IsShape() || clipPathStyle.IsBox() || + clipPathStyle.IsPath(), + "This is used with basic-shape, geometry-box, and path() only"); + + CSSClipPathInstance instance(aFrame, clipPathStyle); + + return instance.CreateClipPath(aDt, aTransform); +} + +/* static*/ +bool CSSClipPathInstance::HitTestBasicShapeOrPathClip(nsIFrame* aFrame, + const gfxPoint& aPoint) { + const auto& clipPathStyle = aFrame->StyleSVGReset()->mClipPath; + MOZ_ASSERT(!clipPathStyle.IsNone(), "unexpected none value"); + MOZ_ASSERT(!clipPathStyle.IsUrl(), "unexpected url value"); + + CSSClipPathInstance instance(aFrame, clipPathStyle); + + RefPtr<DrawTarget> drawTarget = + gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + RefPtr<Path> path = instance.CreateClipPath( + drawTarget, SVGUtils::GetCSSPxToDevPxMatrix(aFrame)); + float pixelRatio = float(AppUnitsPerCSSPixel()) / + aFrame->PresContext()->AppUnitsPerDevPixel(); + return path && path->ContainsPoint(ToPoint(aPoint) * pixelRatio, Matrix()); +} + +/* static */ +Maybe<Rect> CSSClipPathInstance::GetBoundingRectForBasicShapeOrPathClip( + nsIFrame* aFrame, const StyleClipPath& aClipPathStyle) { + MOZ_ASSERT(aClipPathStyle.IsShape() || aClipPathStyle.IsBox() || + aClipPathStyle.IsPath()); + + CSSClipPathInstance instance(aFrame, aClipPathStyle); + + RefPtr<DrawTarget> drawTarget = + gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + RefPtr<Path> path = instance.CreateClipPath( + drawTarget, SVGUtils::GetCSSPxToDevPxMatrix(aFrame)); + return path ? Some(path->GetBounds()) : Nothing(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPath( + DrawTarget* aDrawTarget, const gfxMatrix& aTransform) { + if (mClipPathStyle.IsPath()) { + return CreateClipPathPath(aDrawTarget); + } + + nscoord appUnitsPerDevPixel = + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + + nsRect r; + if (mClipPathStyle.IsBox()) { + r = nsLayoutUtils::ComputeGeometryBox(mTargetFrame, mClipPathStyle.AsBox()); + } else { + r = nsLayoutUtils::ComputeGeometryBox(mTargetFrame, + mClipPathStyle.AsShape()._1); + } + + gfxRect rr(r.x, r.y, r.width, r.height); + rr.Scale(1.0 / AppUnitsPerCSSPixel()); + rr = aTransform.TransformRect(rr); + rr.Scale(appUnitsPerDevPixel); + rr.Round(); + + r = nsRect(int(rr.x), int(rr.y), int(rr.width), int(rr.height)); + + if (mClipPathStyle.IsBox()) { + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + AppendRectToPath(builder, NSRectToRect(r, appUnitsPerDevPixel), true); + return builder->Finish(); + } + + MOZ_ASSERT(mClipPathStyle.IsShape()); + + r = ToAppUnits(r.ToNearestPixels(appUnitsPerDevPixel), appUnitsPerDevPixel); + + const auto& basicShape = *mClipPathStyle.AsShape()._0; + switch (basicShape.tag) { + case StyleBasicShape::Tag::Circle: + return CreateClipPathCircle(aDrawTarget, r); + case StyleBasicShape::Tag::Ellipse: + return CreateClipPathEllipse(aDrawTarget, r); + case StyleBasicShape::Tag::Polygon: + return CreateClipPathPolygon(aDrawTarget, r); + case StyleBasicShape::Tag::Inset: + return CreateClipPathInset(aDrawTarget, r); + break; + default: + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unexpected shape type"); + } + // Return an empty Path: + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + return builder->Finish(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPathCircle( + DrawTarget* aDrawTarget, const nsRect& aRefBox) { + const auto& basicShape = *mClipPathStyle.AsShape()._0; + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + + nsPoint center = + ShapeUtils::ComputeCircleOrEllipseCenter(basicShape, aRefBox); + nscoord r = ShapeUtils::ComputeCircleRadius(basicShape, center, aRefBox); + nscoord appUnitsPerDevPixel = + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + builder->Arc(Point(center.x, center.y) / appUnitsPerDevPixel, + r / appUnitsPerDevPixel, 0, Float(2 * M_PI)); + builder->Close(); + return builder->Finish(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPathEllipse( + DrawTarget* aDrawTarget, const nsRect& aRefBox) { + const auto& basicShape = *mClipPathStyle.AsShape()._0; + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + + nsPoint center = + ShapeUtils::ComputeCircleOrEllipseCenter(basicShape, aRefBox); + nsSize radii = ShapeUtils::ComputeEllipseRadii(basicShape, center, aRefBox); + nscoord appUnitsPerDevPixel = + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + EllipseToBezier(builder.get(), + Point(center.x, center.y) / appUnitsPerDevPixel, + Size(radii.width, radii.height) / appUnitsPerDevPixel); + builder->Close(); + return builder->Finish(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPathPolygon( + DrawTarget* aDrawTarget, const nsRect& aRefBox) { + const auto& basicShape = *mClipPathStyle.AsShape()._0; + auto fillRule = basicShape.AsPolygon().fill == StyleFillRule::Nonzero + ? FillRule::FILL_WINDING + : FillRule::FILL_EVEN_ODD; + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(fillRule); + + nsTArray<nsPoint> vertices = + ShapeUtils::ComputePolygonVertices(basicShape, aRefBox); + if (vertices.IsEmpty()) { + MOZ_ASSERT_UNREACHABLE( + "ComputePolygonVertices() should've given us some vertices!"); + } else { + nscoord appUnitsPerDevPixel = + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + builder->MoveTo(NSPointToPoint(vertices[0], appUnitsPerDevPixel)); + for (size_t i = 1; i < vertices.Length(); ++i) { + builder->LineTo(NSPointToPoint(vertices[i], appUnitsPerDevPixel)); + } + } + builder->Close(); + return builder->Finish(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPathInset( + DrawTarget* aDrawTarget, const nsRect& aRefBox) { + const auto& basicShape = *mClipPathStyle.AsShape()._0; + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder(); + + nscoord appUnitsPerDevPixel = + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + + const nsRect insetRect = ShapeUtils::ComputeInsetRect(basicShape, aRefBox); + const Rect insetRectPixels = NSRectToRect(insetRect, appUnitsPerDevPixel); + nscoord appUnitsRadii[8]; + + if (ShapeUtils::ComputeInsetRadii(basicShape, aRefBox, insetRect, + appUnitsRadii)) { + RectCornerRadii corners; + nsCSSRendering::ComputePixelRadii(appUnitsRadii, appUnitsPerDevPixel, + &corners); + + AppendRoundedRectToPath(builder, insetRectPixels, corners, true); + } else { + AppendRectToPath(builder, insetRectPixels, true); + } + return builder->Finish(); +} + +already_AddRefed<Path> CSSClipPathInstance::CreateClipPathPath( + DrawTarget* aDrawTarget) { + const auto& path = mClipPathStyle.AsPath(); + + RefPtr<PathBuilder> builder = aDrawTarget->CreatePathBuilder( + path.fill == StyleFillRule::Nonzero ? FillRule::FILL_WINDING + : FillRule::FILL_EVEN_ODD); + float scale = float(AppUnitsPerCSSPixel()) / + mTargetFrame->PresContext()->AppUnitsPerDevPixel(); + return SVGPathData::BuildPath(path.path._0.AsSpan(), builder, + StyleStrokeLinecap::Butt, 0.0, scale); +} + +} // namespace mozilla diff --git a/layout/svg/CSSClipPathInstance.h b/layout/svg/CSSClipPathInstance.h new file mode 100644 index 0000000000..90ef52a6fb --- /dev/null +++ b/layout/svg/CSSClipPathInstance.h @@ -0,0 +1,69 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_CSSCLIPPATHINSTANCE_H_ +#define LAYOUT_SVG_CSSCLIPPATHINSTANCE_H_ + +#include "gfxMatrix.h" +#include "gfxPoint.h" +#include "mozilla/gfx/2D.h" +#include "nsRect.h" +#include "nsStyleStruct.h" + +class nsIFrame; +class gfxContext; + +namespace mozilla { + +class MOZ_STACK_CLASS CSSClipPathInstance { + using DrawTarget = gfx::DrawTarget; + using Path = gfx::Path; + using Rect = gfx::Rect; + + public: + static void ApplyBasicShapeOrPathClip(gfxContext& aContext, nsIFrame* aFrame, + const gfxMatrix& aTransform); + static RefPtr<Path> CreateClipPathForFrame(gfx::DrawTarget* aDt, + nsIFrame* aFrame, + const gfxMatrix& aTransform); + // aPoint is in CSS pixels. + static bool HitTestBasicShapeOrPathClip(nsIFrame* aFrame, + const gfxPoint& aPoint); + + static Maybe<Rect> GetBoundingRectForBasicShapeOrPathClip( + nsIFrame* aFrame, const StyleClipPath&); + + private: + explicit CSSClipPathInstance(nsIFrame* aFrame, const StyleClipPath& aClipPath) + : mTargetFrame(aFrame), mClipPathStyle(aClipPath) {} + + already_AddRefed<Path> CreateClipPath(DrawTarget* aDrawTarget, + const gfxMatrix& aTransform); + + already_AddRefed<Path> CreateClipPathCircle(DrawTarget* aDrawTarget, + const nsRect& aRefBox); + + already_AddRefed<Path> CreateClipPathEllipse(DrawTarget* aDrawTarget, + const nsRect& aRefBox); + + already_AddRefed<Path> CreateClipPathPolygon(DrawTarget* aDrawTarget, + const nsRect& aRefBox); + + already_AddRefed<Path> CreateClipPathInset(DrawTarget* aDrawTarget, + const nsRect& aRefBox); + + already_AddRefed<Path> CreateClipPathPath(DrawTarget* aDrawTarget); + + /** + * The frame for the element that is currently being clipped. + */ + nsIFrame* mTargetFrame; + const StyleClipPath& mClipPathStyle; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_CSSCLIPPATHINSTANCE_H_ diff --git a/layout/svg/CSSFilterInstance.cpp b/layout/svg/CSSFilterInstance.cpp new file mode 100644 index 0000000000..06bb92f334 --- /dev/null +++ b/layout/svg/CSSFilterInstance.cpp @@ -0,0 +1,356 @@ +/* -*- 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 "CSSFilterInstance.h" + +// Keep others in (case-insensitive) order: +#include "FilterDescription.h" +#include "gfx2DGlue.h" +#include "gfxUtils.h" +#include "nsIFrame.h" +#include "nsStyleStruct.h" +#include "nsTArray.h" + +using namespace mozilla::gfx; + +namespace mozilla { + +static float ClampFactor(float aFactor) { + if (aFactor > 1) { + return 1; + } + if (aFactor < 0) { + MOZ_ASSERT_UNREACHABLE("A negative value should not have been parsed."); + return 0; + } + + return aFactor; +} + +CSSFilterInstance::CSSFilterInstance( + const StyleFilter& aFilter, nscolor aShadowFallbackColor, + const nsIntRect& aTargetBoundsInFilterSpace, + const gfxMatrix& aFrameSpaceInCSSPxToFilterSpaceTransform) + : mFilter(aFilter), + mShadowFallbackColor(aShadowFallbackColor), + mTargetBoundsInFilterSpace(aTargetBoundsInFilterSpace), + mFrameSpaceInCSSPxToFilterSpaceTransform( + aFrameSpaceInCSSPxToFilterSpaceTransform) {} + +nsresult CSSFilterInstance::BuildPrimitives( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + bool aInputIsTainted) { + FilterPrimitiveDescription descr = + CreatePrimitiveDescription(aPrimitiveDescrs, aInputIsTainted); + nsresult result; + switch (mFilter.tag) { + case StyleFilter::Tag::Blur: + result = SetAttributesForBlur(descr); + break; + case StyleFilter::Tag::Brightness: + result = SetAttributesForBrightness(descr); + break; + case StyleFilter::Tag::Contrast: + result = SetAttributesForContrast(descr); + break; + case StyleFilter::Tag::DropShadow: + result = SetAttributesForDropShadow(descr); + break; + case StyleFilter::Tag::Grayscale: + result = SetAttributesForGrayscale(descr); + break; + case StyleFilter::Tag::HueRotate: + result = SetAttributesForHueRotate(descr); + break; + case StyleFilter::Tag::Invert: + result = SetAttributesForInvert(descr); + break; + case StyleFilter::Tag::Opacity: + result = SetAttributesForOpacity(descr); + break; + case StyleFilter::Tag::Saturate: + result = SetAttributesForSaturate(descr); + break; + case StyleFilter::Tag::Sepia: + result = SetAttributesForSepia(descr); + break; + default: + MOZ_ASSERT_UNREACHABLE("not a valid CSS filter type"); + return NS_ERROR_FAILURE; + } + + if (NS_FAILED(result)) { + return result; + } + + // Compute the primitive's bounds now that we've determined its attributes. + // Some attributes like blur radius can influence the bounds. + SetBounds(descr, aPrimitiveDescrs); + + // Add this primitive to the filter chain. + aPrimitiveDescrs.AppendElement(std::move(descr)); + return NS_OK; +} + +FilterPrimitiveDescription CSSFilterInstance::CreatePrimitiveDescription( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + bool aInputIsTainted) { + FilterPrimitiveDescription descr; + int32_t inputIndex = GetLastResultIndex(aPrimitiveDescrs); + descr.SetInputPrimitive(0, inputIndex); + descr.SetIsTainted(inputIndex < 0 ? aInputIsTainted + : aPrimitiveDescrs[inputIndex].IsTainted()); + descr.SetInputColorSpace(0, ColorSpace::SRGB); + descr.SetOutputColorSpace(ColorSpace::SRGB); + return descr; +} + +nsresult CSSFilterInstance::SetAttributesForBlur( + FilterPrimitiveDescription& aDescr) { + const Length& radiusInFrameSpace = mFilter.AsBlur(); + Size radiusInFilterSpace = + BlurRadiusToFilterSpace(radiusInFrameSpace.ToAppUnits()); + GaussianBlurAttributes atts; + atts.mStdDeviation = radiusInFilterSpace; + aDescr.Attributes() = AsVariant(atts); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForBrightness( + FilterPrimitiveDescription& aDescr) { + float value = mFilter.AsBrightness(); + float intercept = 0.0f; + ComponentTransferAttributes atts; + + // Set transfer functions for RGB. + atts.mTypes[kChannelROrRGB] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_LINEAR; + atts.mTypes[kChannelG] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + atts.mTypes[kChannelB] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + float slopeIntercept[2]; + slopeIntercept[kComponentTransferSlopeIndex] = value; + slopeIntercept[kComponentTransferInterceptIndex] = intercept; + atts.mValues[kChannelROrRGB].AppendElements(slopeIntercept, 2); + + atts.mTypes[kChannelA] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY; + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForContrast( + FilterPrimitiveDescription& aDescr) { + float value = mFilter.AsContrast(); + float intercept = -(0.5 * value) + 0.5; + ComponentTransferAttributes atts; + + // Set transfer functions for RGB. + atts.mTypes[kChannelROrRGB] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_LINEAR; + atts.mTypes[kChannelG] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + atts.mTypes[kChannelB] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + float slopeIntercept[2]; + slopeIntercept[kComponentTransferSlopeIndex] = value; + slopeIntercept[kComponentTransferInterceptIndex] = intercept; + atts.mValues[kChannelROrRGB].AppendElements(slopeIntercept, 2); + + atts.mTypes[kChannelA] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY; + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForDropShadow( + FilterPrimitiveDescription& aDescr) { + const auto& shadow = mFilter.AsDropShadow(); + + DropShadowAttributes atts; + + // Set drop shadow blur radius. + Size radiusInFilterSpace = BlurRadiusToFilterSpace(shadow.blur.ToAppUnits()); + atts.mStdDeviation = radiusInFilterSpace; + + // Set offset. + IntPoint offsetInFilterSpace = OffsetToFilterSpace( + shadow.horizontal.ToAppUnits(), shadow.vertical.ToAppUnits()); + atts.mOffset = offsetInFilterSpace; + + // Set color. If unspecified, use the CSS color property. + nscolor shadowColor = shadow.color.CalcColor(mShadowFallbackColor); + atts.mColor = ToAttributeColor(shadowColor); + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForGrayscale( + FilterPrimitiveDescription& aDescr) { + ColorMatrixAttributes atts; + // Set color matrix type. + atts.mType = (uint32_t)SVG_FECOLORMATRIX_TYPE_SATURATE; + + // Set color matrix values. + float value = 1 - ClampFactor(mFilter.AsGrayscale()); + atts.mValues.AppendElements(&value, 1); + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForHueRotate( + FilterPrimitiveDescription& aDescr) { + ColorMatrixAttributes atts; + // Set color matrix type. + atts.mType = (uint32_t)SVG_FECOLORMATRIX_TYPE_HUE_ROTATE; + + // Set color matrix values. + float value = mFilter.AsHueRotate().ToDegrees(); + atts.mValues.AppendElements(&value, 1); + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForInvert( + FilterPrimitiveDescription& aDescr) { + ComponentTransferAttributes atts; + float value = ClampFactor(mFilter.AsInvert()); + + // Set transfer functions for RGB. + float invertTableValues[2]; + invertTableValues[0] = value; + invertTableValues[1] = 1 - value; + + // Set transfer functions for RGB. + atts.mTypes[kChannelROrRGB] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_TABLE; + atts.mTypes[kChannelG] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + atts.mTypes[kChannelB] = (uint8_t)SVG_FECOMPONENTTRANSFER_SAME_AS_R; + atts.mValues[kChannelROrRGB].AppendElements(invertTableValues, 2); + + atts.mTypes[kChannelA] = (uint8_t)SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY; + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForOpacity( + FilterPrimitiveDescription& aDescr) { + OpacityAttributes atts; + float value = ClampFactor(mFilter.AsOpacity()); + + atts.mOpacity = value; + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForSaturate( + FilterPrimitiveDescription& aDescr) { + ColorMatrixAttributes atts; + // Set color matrix type. + atts.mType = (uint32_t)SVG_FECOLORMATRIX_TYPE_SATURATE; + + // Set color matrix values. + float value = mFilter.AsSaturate(); + atts.mValues.AppendElements(&value, 1); + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +nsresult CSSFilterInstance::SetAttributesForSepia( + FilterPrimitiveDescription& aDescr) { + ColorMatrixAttributes atts; + // Set color matrix type. + atts.mType = (uint32_t)SVG_FECOLORMATRIX_TYPE_SEPIA; + + // Set color matrix values. + float value = ClampFactor(mFilter.AsSepia()); + atts.mValues.AppendElements(&value, 1); + + aDescr.Attributes() = AsVariant(std::move(atts)); + return NS_OK; +} + +Size CSSFilterInstance::BlurRadiusToFilterSpace(nscoord aRadiusInFrameSpace) { + float radiusInFrameSpaceInCSSPx = + nsPresContext::AppUnitsToFloatCSSPixels(aRadiusInFrameSpace); + + // Convert the radius to filter space. + Size radiusInFilterSpace(radiusInFrameSpaceInCSSPx, + radiusInFrameSpaceInCSSPx); + // Narrow the scale factors. They will only be used with types containing + // floating point types. + auto frameSpaceInCSSPxToFilterSpaceScale = + mFrameSpaceInCSSPxToFilterSpaceTransform.ScaleFactors() + .ConvertTo<float>(); + radiusInFilterSpace = + radiusInFilterSpace * frameSpaceInCSSPxToFilterSpaceScale; + + // Check the radius limits. + if (radiusInFilterSpace.width < 0 || radiusInFilterSpace.height < 0) { + MOZ_ASSERT_UNREACHABLE( + "we shouldn't have parsed a negative radius in the " + "style"); + return Size(); + } + + Float maxStdDeviation = (Float)kMaxStdDeviation; + radiusInFilterSpace.width = + std::min(radiusInFilterSpace.width, maxStdDeviation); + radiusInFilterSpace.height = + std::min(radiusInFilterSpace.height, maxStdDeviation); + + return radiusInFilterSpace; +} + +IntPoint CSSFilterInstance::OffsetToFilterSpace(nscoord aXOffsetInFrameSpace, + nscoord aYOffsetInFrameSpace) { + gfxPoint offsetInFilterSpace( + nsPresContext::AppUnitsToFloatCSSPixels(aXOffsetInFrameSpace), + nsPresContext::AppUnitsToFloatCSSPixels(aYOffsetInFrameSpace)); + + // Convert the radius to filter space. + auto frameSpaceInCSSPxToFilterSpaceScale = + mFrameSpaceInCSSPxToFilterSpaceTransform.ScaleFactors(); + offsetInFilterSpace.x *= frameSpaceInCSSPxToFilterSpaceScale.xScale; + offsetInFilterSpace.y *= frameSpaceInCSSPxToFilterSpaceScale.yScale; + + return IntPoint(int32_t(offsetInFilterSpace.x), + int32_t(offsetInFilterSpace.y)); +} + +sRGBColor CSSFilterInstance::ToAttributeColor(nscolor aColor) { + return sRGBColor(NS_GET_R(aColor) / 255.0, NS_GET_G(aColor) / 255.0, + NS_GET_B(aColor) / 255.0, NS_GET_A(aColor) / 255.0); +} + +int32_t CSSFilterInstance::GetLastResultIndex( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs) { + uint32_t numPrimitiveDescrs = aPrimitiveDescrs.Length(); + return !numPrimitiveDescrs + ? FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic + : numPrimitiveDescrs - 1; +} + +void CSSFilterInstance::SetBounds( + FilterPrimitiveDescription& aDescr, + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs) { + int32_t inputIndex = GetLastResultIndex(aPrimitiveDescrs); + nsIntRect inputBounds = + (inputIndex < 0) ? mTargetBoundsInFilterSpace + : aPrimitiveDescrs[inputIndex].PrimitiveSubregion(); + + AutoTArray<nsIntRegion, 8> inputExtents; + inputExtents.AppendElement(inputBounds); + + nsIntRegion outputExtents = + FilterSupport::PostFilterExtentsForPrimitive(aDescr, inputExtents); + IntRect outputBounds = outputExtents.GetBounds(); + + aDescr.SetPrimitiveSubregion(outputBounds); + aDescr.SetFilterSpaceBounds(outputBounds); +} + +} // namespace mozilla diff --git a/layout/svg/CSSFilterInstance.h b/layout/svg/CSSFilterInstance.h new file mode 100644 index 0000000000..cdf21ee2bf --- /dev/null +++ b/layout/svg/CSSFilterInstance.h @@ -0,0 +1,146 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_CSSFILTERINSTANCE_H_ +#define LAYOUT_SVG_CSSFILTERINSTANCE_H_ + +#include "FilterSupport.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "mozilla/gfx/Point.h" +#include "mozilla/gfx/Types.h" +#include "nsColor.h" +#include "mozilla/ServoStyleConsts.h" + +namespace mozilla { + +/** + * This class helps FilterInstance build its filter graph. It turns a CSS + * filter function (e.g. blur(3px)) from the style system into a + * FilterPrimitiveDescription connected to the filter graph. + */ +class CSSFilterInstance { + using sRGBColor = gfx::sRGBColor; + using FilterPrimitiveDescription = gfx::FilterPrimitiveDescription; + using IntPoint = gfx::IntPoint; + using Size = gfx::Size; + + public: + /** + * @param aFilter The CSS filter from the style system. This class stores + * aFilter by reference, so callers should avoid modifying or deleting + * aFilter during the lifetime of CSSFilterInstance. + * @param aShadowFallbackColor The color that should be used for + * drop-shadow() filters that don't specify a shadow color. + * @param aTargetBoundsInFilterSpace The pre-filter ink overflow rect of + * the frame being filtered, in filter space. + * @param aFrameSpaceInCSSPxToFilterSpaceTransform The transformation from + * the filtered element's frame space in CSS pixels to filter space. + */ + CSSFilterInstance(const StyleFilter& aFilter, nscolor aShadowFallbackColor, + const nsIntRect& aTargetBoundsInFilterSpace, + const gfxMatrix& aFrameSpaceInCSSPxToFilterSpaceTransform); + + /** + * Creates at least one new FilterPrimitiveDescription based on the filter + * from the style system. Appends the new FilterPrimitiveDescription(s) to the + * aPrimitiveDescrs list. + * aInputIsTainted describes whether the input to this filter is tainted, i.e. + * whether it contains security-sensitive content. This is needed to propagate + * taintedness to the FilterPrimitive that take tainted inputs. Something + * being tainted means that it contains security sensitive content. The input + * to this filter is the previous filter's output, i.e. the last element in + * aPrimitiveDescrs, or the SourceGraphic input if this is the first filter in + * the filter chain. + */ + nsresult BuildPrimitives( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + bool aInputIsTainted); + + private: + /** + * Returns a new FilterPrimitiveDescription with its basic properties set up. + * See the comment above BuildPrimitives for the meaning of aInputIsTainted. + */ + FilterPrimitiveDescription CreatePrimitiveDescription( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + bool aInputIsTainted); + + /** + * Sets aDescr's attributes using the style info in mFilter. + */ + nsresult SetAttributesForBlur(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForBrightness(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForContrast(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForDropShadow(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForGrayscale(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForHueRotate(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForInvert(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForOpacity(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForSaturate(FilterPrimitiveDescription& aDescr); + nsresult SetAttributesForSepia(FilterPrimitiveDescription& aDescr); + + /** + * Returns the index of the last result in the aPrimitiveDescrs, which we'll + * use as the input to this CSS filter. + */ + int32_t GetLastResultIndex( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs); + + /** + * Sets aDescr's filter region and primitive subregion to appropriate values + * based on this CSS filter's input and its attributes. For example, a CSS + * blur filter will have bounds equal to its input bounds, inflated by the + * blur extents. + */ + void SetBounds(FilterPrimitiveDescription& aDescr, + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs); + + /** + * Converts an nscolor to a Color, suitable for use as a + * FilterPrimitiveDescription attribute. + */ + sRGBColor ToAttributeColor(nscolor aColor); + + /** + * Converts a blur radius in frame space to filter space. + */ + Size BlurRadiusToFilterSpace(nscoord aRadiusInFrameSpace); + + /** + * Converts a point defined by a pair of nscoord x, y coordinates from frame + * space to filter space. + */ + IntPoint OffsetToFilterSpace(nscoord aXOffsetInFrameSpace, + nscoord aYOffsetInFrameSpace); + + /** + * The CSS filter originally from the style system. + */ + const StyleFilter& mFilter; + + /** + * The color that should be used for drop-shadow() filters that don't + * specify a shadow color. + */ + nscolor mShadowFallbackColor; + + /** + * The pre-filter overflow rect of the frame being filtered, in filter space. + * Used for input bounds if this CSS filter is the first in the filter chain. + */ + nsIntRect mTargetBoundsInFilterSpace; + + /** + * The transformation from the filtered element's frame space in CSS pixels to + * filter space. Used to transform style values to filter space. + */ + gfxMatrix mFrameSpaceInCSSPxToFilterSpaceTransform; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_CSSFILTERINSTANCE_H_ diff --git a/layout/svg/DisplaySVGItem.cpp b/layout/svg/DisplaySVGItem.cpp new file mode 100644 index 0000000000..dfc77c6747 --- /dev/null +++ b/layout/svg/DisplaySVGItem.cpp @@ -0,0 +1,58 @@ +/* -*- 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 "DisplaySVGItem.h" + +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/SVGUtils.h" +#include "nsLayoutUtils.h" +#include "nsPoint.h" + +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +void DisplaySVGItem::HitTest(nsDisplayListBuilder* aBuilder, + const nsRect& aRect, HitTestState* aState, + nsTArray<nsIFrame*>* aOutFrames) { + ISVGDisplayableFrame* svgFrame = do_QueryFrame(mFrame); + MOZ_ASSERT(svgFrame, "Unexpected frame type"); + + nsPoint pointRelativeToReferenceFrame = aRect.Center(); + // ToReferenceFrame() includes mFrame->GetPosition(), our user + // space position. + nsPoint userSpacePtInAppUnits = pointRelativeToReferenceFrame - + (ToReferenceFrame() - mFrame->GetPosition()); + gfxPoint userSpacePt = + gfxPoint(userSpacePtInAppUnits.x, userSpacePtInAppUnits.y) / + AppUnitsPerCSSPixel(); + if (auto* target = svgFrame->GetFrameForPoint(userSpacePt)) { + aOutFrames->AppendElement(target); + } +} + +void DisplaySVGItem::Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) { + ISVGDisplayableFrame* svgFrame = do_QueryFrame(mFrame); + MOZ_ASSERT(svgFrame, "Unexpected frame type"); + int32_t appUnitsPerDevPixel = mFrame->PresContext()->AppUnitsPerDevPixel(); + + // ToReferenceFrame() includes our mRect offset, but painting takes + // account of that too. To avoid double counting, we subtract that + // here. + nsPoint offset = ToReferenceFrame() - mFrame->GetPosition(); + + gfxPoint devPixelOffset = + nsLayoutUtils::PointToGfxPoint(offset, appUnitsPerDevPixel); + + gfxMatrix tm = SVGUtils::GetCSSPxToDevPxMatrix(mFrame) * + gfxMatrix::Translation(devPixelOffset); + imgDrawingParams imgParams(aBuilder->GetImageDecodeFlags()); + svgFrame->PaintSVG(*aCtx, tm, imgParams); +} + +} // namespace mozilla diff --git a/layout/svg/DisplaySVGItem.h b/layout/svg/DisplaySVGItem.h new file mode 100644 index 0000000000..1ff2074695 --- /dev/null +++ b/layout/svg/DisplaySVGItem.h @@ -0,0 +1,42 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_DISPLAYSVGITEM_H_ +#define LAYOUT_SVG_DISPLAYSVGITEM_H_ + +#include "nsDisplayList.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// Display list item: + +class DisplaySVGItem : public nsPaintedDisplayItem { + public: + DisplaySVGItem(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) + : nsPaintedDisplayItem(aBuilder, aFrame) { + MOZ_ASSERT(aFrame, "Must have a frame!"); + } + + /** + * Hit testing for display lists. + * @param aRect the point or rect being tested, relative to aFrame. + * If the width and height are both 1 app unit, it indicates we're + * hit testing a point, not a rect. + * @param aOutFrames each item appends the frame(s) in this display item that + * the rect is considered over (if any) to aOutFrames. + */ + void HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect, + HitTestState* aState, nsTArray<nsIFrame*>* aOutFrames) override; + /** + * Paint the frame to some rendering context. + */ + void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_DISPLAYSVGITEM_H_ diff --git a/layout/svg/FilterInstance.cpp b/layout/svg/FilterInstance.cpp new file mode 100644 index 0000000000..0cf8eb67f4 --- /dev/null +++ b/layout/svg/FilterInstance.cpp @@ -0,0 +1,901 @@ +/* -*- 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 "FilterInstance.h" + +// MFBT headers next: +#include "mozilla/UniquePtr.h" + +// Keep others in (case-insensitive) order: +#include "FilterSupport.h" +#include "ImgDrawResult.h" +#include "SVGContentUtils.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxPlatform.h" + +#include "gfxUtils.h" +#include "mozilla/Unused.h" +#include "mozilla/gfx/Filters.h" +#include "mozilla/gfx/Helpers.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/gfx/PatternHelpers.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/SVGFilterInstance.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/Document.h" +#include "nsLayoutUtils.h" +#include "CSSFilterInstance.h" +#include "SVGIntegrationUtils.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +FilterDescription FilterInstance::GetFilterDescription( + nsIContent* aFilteredElement, Span<const StyleFilter> aFilterChain, + bool aFilterInputIsTainted, const UserSpaceMetrics& aMetrics, + const gfxRect& aBBox, + nsTArray<RefPtr<SourceSurface>>& aOutAdditionalImages) { + gfxMatrix identity; + FilterInstance instance(nullptr, aFilteredElement, aMetrics, aFilterChain, + aFilterInputIsTainted, nullptr, identity, nullptr, + nullptr, nullptr, &aBBox); + if (!instance.IsInitialized()) { + return FilterDescription(); + } + return instance.ExtractDescriptionAndAdditionalImages(aOutAdditionalImages); +} + +static UniquePtr<UserSpaceMetrics> UserSpaceMetricsForFrame(nsIFrame* aFrame) { + if (auto* element = SVGElement::FromNodeOrNull(aFrame->GetContent())) { + return MakeUnique<SVGElementMetrics>(element); + } + return MakeUnique<NonSVGFrameUserSpaceMetrics>(aFrame); +} + +void FilterInstance::PaintFilteredFrame( + nsIFrame* aFilteredFrame, Span<const StyleFilter> aFilterChain, + gfxContext* aCtx, const SVGFilterPaintCallback& aPaintCallback, + const nsRegion* aDirtyArea, imgDrawingParams& aImgParams, float aOpacity) { + UniquePtr<UserSpaceMetrics> metrics = + UserSpaceMetricsForFrame(aFilteredFrame); + + gfxContextMatrixAutoSaveRestore autoSR(aCtx); + auto scaleFactors = aCtx->CurrentMatrixDouble().ScaleFactors(); + if (scaleFactors.xScale == 0 || scaleFactors.yScale == 0) { + return; + } + + gfxMatrix scaleMatrix(scaleFactors.xScale, 0.0f, 0.0f, scaleFactors.yScale, + 0.0f, 0.0f); + + gfxMatrix reverseScaleMatrix = scaleMatrix; + DebugOnly<bool> invertible = reverseScaleMatrix.Invert(); + MOZ_ASSERT(invertible); + + gfxMatrix scaleMatrixInDevUnits = + scaleMatrix * SVGUtils::GetCSSPxToDevPxMatrix(aFilteredFrame); + + // Hardcode InputIsTainted to true because we don't want JS to be able to + // read the rendered contents of aFilteredFrame. + FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), + *metrics, aFilterChain, /* InputIsTainted */ true, + aPaintCallback, scaleMatrixInDevUnits, aDirtyArea, + nullptr, nullptr, nullptr); + if (instance.IsInitialized()) { + // Pull scale vector out of aCtx's transform, put all scale factors, which + // includes css and css-to-dev-px scale, into scaleMatrixInDevUnits. + aCtx->SetMatrixDouble(reverseScaleMatrix * aCtx->CurrentMatrixDouble()); + + instance.Render(aCtx, aImgParams, aOpacity); + } else { + // Render the unfiltered contents. + aPaintCallback(*aCtx, aImgParams, nullptr, nullptr); + } +} + +static mozilla::wr::ComponentTransferFuncType FuncTypeToWr(uint8_t aFuncType) { + MOZ_ASSERT(aFuncType != SVG_FECOMPONENTTRANSFER_SAME_AS_R); + switch (aFuncType) { + case SVG_FECOMPONENTTRANSFER_TYPE_TABLE: + return mozilla::wr::ComponentTransferFuncType::Table; + case SVG_FECOMPONENTTRANSFER_TYPE_DISCRETE: + return mozilla::wr::ComponentTransferFuncType::Discrete; + case SVG_FECOMPONENTTRANSFER_TYPE_LINEAR: + return mozilla::wr::ComponentTransferFuncType::Linear; + case SVG_FECOMPONENTTRANSFER_TYPE_GAMMA: + return mozilla::wr::ComponentTransferFuncType::Gamma; + case SVG_FECOMPONENTTRANSFER_TYPE_IDENTITY: + default: + return mozilla::wr::ComponentTransferFuncType::Identity; + } + MOZ_ASSERT_UNREACHABLE("all func types not handled?"); + return mozilla::wr::ComponentTransferFuncType::Identity; +} + +bool FilterInstance::BuildWebRenderFilters(nsIFrame* aFilteredFrame, + Span<const StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, + bool& aInitialized) { + bool status = BuildWebRenderFiltersImpl(aFilteredFrame, aFilters, aWrFilters, + aInitialized); + if (!status) { + aFilteredFrame->PresContext()->Document()->SetUseCounter( + eUseCounter_custom_WrFilterFallback); + } + + return status; +} + +bool FilterInstance::BuildWebRenderFiltersImpl(nsIFrame* aFilteredFrame, + Span<const StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, + bool& aInitialized) { + aWrFilters.filters.Clear(); + aWrFilters.filter_datas.Clear(); + aWrFilters.values.Clear(); + + UniquePtr<UserSpaceMetrics> metrics = + UserSpaceMetricsForFrame(aFilteredFrame); + + // TODO: simply using an identity matrix here, was pulling the scale from a + // gfx context for the non-wr path. + gfxMatrix scaleMatrix; + gfxMatrix scaleMatrixInDevUnits = + scaleMatrix * SVGUtils::GetCSSPxToDevPxMatrix(aFilteredFrame); + + // Hardcode inputIsTainted to true because we don't want JS to be able to + // read the rendered contents of aFilteredFrame. + bool inputIsTainted = true; + FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), + *metrics, aFilters, inputIsTainted, nullptr, + scaleMatrixInDevUnits, nullptr, nullptr, nullptr, + nullptr); + + if (!instance.IsInitialized()) { + aInitialized = false; + return true; + } + + // If there are too many filters to render, then just pretend that we + // succeeded, and don't render any of them. + if (instance.mFilterDescription.mPrimitives.Length() > + StaticPrefs::gfx_webrender_max_filter_ops_per_chain()) { + return true; + } + + Maybe<IntRect> finalClip; + bool srgb = true; + // We currently apply the clip on the stacking context after applying filters, + // but primitive subregions imply clipping after each filter and not just the + // end of the chain. For some types of filter it doesn't matter, but for those + // which sample outside of the location of the destination pixel like blurs, + // only clipping after could produce incorrect results, so we bail out in this + // case. + // We can lift this restriction once we have added support for primitive + // subregions to WebRender's filters. + for (uint32_t i = 0; i < instance.mFilterDescription.mPrimitives.Length(); + i++) { + const auto& primitive = instance.mFilterDescription.mPrimitives[i]; + + // WebRender only supports filters with one input. + if (primitive.NumberOfInputs() != 1) { + return false; + } + // The first primitive must have the source graphic as the input, all + // other primitives must have the prior primitive as the input, otherwise + // it's not supported by WebRender. + if (i == 0) { + if (primitive.InputPrimitiveIndex(0) != + FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic) { + return false; + } + } else if (primitive.InputPrimitiveIndex(0) != int32_t(i - 1)) { + return false; + } + + bool previousSrgb = srgb; + bool primNeedsSrgb = primitive.InputColorSpace(0) == gfx::ColorSpace::SRGB; + if (srgb && !primNeedsSrgb) { + aWrFilters.filters.AppendElement(wr::FilterOp::SrgbToLinear()); + } else if (!srgb && primNeedsSrgb) { + aWrFilters.filters.AppendElement(wr::FilterOp::LinearToSrgb()); + } + srgb = primitive.OutputColorSpace() == gfx::ColorSpace::SRGB; + + const PrimitiveAttributes& attr = primitive.Attributes(); + + bool filterIsNoop = false; + + if (attr.is<OpacityAttributes>()) { + float opacity = attr.as<OpacityAttributes>().mOpacity; + aWrFilters.filters.AppendElement(wr::FilterOp::Opacity( + wr::PropertyBinding<float>::Value(opacity), opacity)); + } else if (attr.is<ColorMatrixAttributes>()) { + const ColorMatrixAttributes& attributes = + attr.as<ColorMatrixAttributes>(); + + float transposed[20]; + if (gfx::ComputeColorMatrix(attributes, transposed)) { + float matrix[20] = { + transposed[0], transposed[5], transposed[10], transposed[15], + transposed[1], transposed[6], transposed[11], transposed[16], + transposed[2], transposed[7], transposed[12], transposed[17], + transposed[3], transposed[8], transposed[13], transposed[18], + transposed[4], transposed[9], transposed[14], transposed[19]}; + + aWrFilters.filters.AppendElement(wr::FilterOp::ColorMatrix(matrix)); + } else { + filterIsNoop = true; + } + } else if (attr.is<GaussianBlurAttributes>()) { + if (finalClip) { + // There's a clip that needs to apply before the blur filter, but + // WebRender only lets us apply the clip at the end of the filter + // chain. Clipping after a blur is not equivalent to clipping before + // a blur, so bail out. + return false; + } + + const GaussianBlurAttributes& blur = attr.as<GaussianBlurAttributes>(); + + const Size& stdDev = blur.mStdDeviation; + if (stdDev.width != 0.0 || stdDev.height != 0.0) { + aWrFilters.filters.AppendElement( + wr::FilterOp::Blur(stdDev.width, stdDev.height)); + } else { + filterIsNoop = true; + } + } else if (attr.is<DropShadowAttributes>()) { + if (finalClip) { + // We have to bail out for the same reason we would with a blur filter. + return false; + } + + const DropShadowAttributes& shadow = attr.as<DropShadowAttributes>(); + + const Size& stdDev = shadow.mStdDeviation; + if (stdDev.width != stdDev.height) { + return false; + } + + sRGBColor color = shadow.mColor; + if (!primNeedsSrgb) { + color = sRGBColor(gsRGBToLinearRGBMap[uint8_t(color.r * 255)], + gsRGBToLinearRGBMap[uint8_t(color.g * 255)], + gsRGBToLinearRGBMap[uint8_t(color.b * 255)], color.a); + } + wr::Shadow wrShadow; + wrShadow.offset = {shadow.mOffset.x, shadow.mOffset.y}; + wrShadow.color = wr::ToColorF(ToDeviceColor(color)); + wrShadow.blur_radius = stdDev.width; + wr::FilterOp filterOp = wr::FilterOp::DropShadow(wrShadow); + + aWrFilters.filters.AppendElement(filterOp); + } else if (attr.is<ComponentTransferAttributes>()) { + const ComponentTransferAttributes& attributes = + attr.as<ComponentTransferAttributes>(); + + size_t numValues = + attributes.mValues[0].Length() + attributes.mValues[1].Length() + + attributes.mValues[2].Length() + attributes.mValues[3].Length(); + if (numValues > 1024) { + // Depending on how the wr shaders are implemented we may need to + // limit the total number of values. + return false; + } + + wr::FilterOp filterOp = {wr::FilterOp::Tag::ComponentTransfer}; + wr::WrFilterData filterData; + aWrFilters.values.AppendElement(nsTArray<float>()); + nsTArray<float>* values = + &aWrFilters.values[aWrFilters.values.Length() - 1]; + values->SetCapacity(numValues); + + filterData.funcR_type = FuncTypeToWr(attributes.mTypes[0]); + size_t R_startindex = values->Length(); + values->AppendElements(attributes.mValues[0]); + filterData.R_values_count = attributes.mValues[0].Length(); + + size_t indexToUse = + attributes.mTypes[1] == SVG_FECOMPONENTTRANSFER_SAME_AS_R ? 0 : 1; + filterData.funcG_type = FuncTypeToWr(attributes.mTypes[indexToUse]); + size_t G_startindex = values->Length(); + values->AppendElements(attributes.mValues[indexToUse]); + filterData.G_values_count = attributes.mValues[indexToUse].Length(); + + indexToUse = + attributes.mTypes[2] == SVG_FECOMPONENTTRANSFER_SAME_AS_R ? 0 : 2; + filterData.funcB_type = FuncTypeToWr(attributes.mTypes[indexToUse]); + size_t B_startindex = values->Length(); + values->AppendElements(attributes.mValues[indexToUse]); + filterData.B_values_count = attributes.mValues[indexToUse].Length(); + + filterData.funcA_type = FuncTypeToWr(attributes.mTypes[3]); + size_t A_startindex = values->Length(); + values->AppendElements(attributes.mValues[3]); + filterData.A_values_count = attributes.mValues[3].Length(); + + filterData.R_values = + filterData.R_values_count > 0 ? &((*values)[R_startindex]) : nullptr; + filterData.G_values = + filterData.G_values_count > 0 ? &((*values)[G_startindex]) : nullptr; + filterData.B_values = + filterData.B_values_count > 0 ? &((*values)[B_startindex]) : nullptr; + filterData.A_values = + filterData.A_values_count > 0 ? &((*values)[A_startindex]) : nullptr; + + aWrFilters.filters.AppendElement(filterOp); + aWrFilters.filter_datas.AppendElement(filterData); + } else { + return false; + } + + if (filterIsNoop && aWrFilters.filters.Length() > 0 && + (aWrFilters.filters.LastElement().tag == + wr::FilterOp::Tag::SrgbToLinear || + aWrFilters.filters.LastElement().tag == + wr::FilterOp::Tag::LinearToSrgb)) { + // We pushed a color space conversion filter in prevision of applying + // another filter which turned out to be a no-op, so the conversion is + // unnecessary. Remove it from the filter list. + // This is both an optimization and a way to pass the wptest + // css/filter-effects/filter-scale-001.html for which the needless + // sRGB->linear->no-op->sRGB roundtrip introduces a slight error and we + // cannot add fuzziness to the test. + Unused << aWrFilters.filters.PopLastElement(); + srgb = previousSrgb; + } + + if (!filterIsNoop) { + if (finalClip.isNothing()) { + finalClip = Some(primitive.PrimitiveSubregion()); + } else { + finalClip = + Some(primitive.PrimitiveSubregion().Intersect(finalClip.value())); + } + } + } + + if (!srgb) { + aWrFilters.filters.AppendElement(wr::FilterOp::LinearToSrgb()); + } + + if (finalClip) { + aWrFilters.post_filters_clip = + Some(instance.FilterSpaceToFrameSpace(finalClip.value())); + } + return true; +} + +nsRegion FilterInstance::GetPreFilterNeededArea( + nsIFrame* aFilteredFrame, const nsRegion& aPostFilterDirtyRegion) { + gfxMatrix tm = SVGUtils::GetCanvasTM(aFilteredFrame); + auto filterChain = aFilteredFrame->StyleEffects()->mFilters.AsSpan(); + UniquePtr<UserSpaceMetrics> metrics = + UserSpaceMetricsForFrame(aFilteredFrame); + // Hardcode InputIsTainted to true because we don't want JS to be able to + // read the rendered contents of aFilteredFrame. + FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), + *metrics, filterChain, /* InputIsTainted */ true, + nullptr, tm, &aPostFilterDirtyRegion); + if (!instance.IsInitialized()) { + return nsRect(); + } + + // Now we can ask the instance to compute the area of the source + // that's needed. + return instance.ComputeSourceNeededRect(); +} + +Maybe<nsRect> FilterInstance::GetPostFilterBounds( + nsIFrame* aFilteredFrame, const gfxRect* aOverrideBBox, + const nsRect* aPreFilterBounds) { + MOZ_ASSERT(!aFilteredFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT) || + !aFilteredFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Non-display SVG do not maintain ink overflow rects"); + + nsRegion preFilterRegion; + nsRegion* preFilterRegionPtr = nullptr; + if (aPreFilterBounds) { + preFilterRegion = *aPreFilterBounds; + preFilterRegionPtr = &preFilterRegion; + } + + gfxMatrix tm = SVGUtils::GetCanvasTM(aFilteredFrame); + auto filterChain = aFilteredFrame->StyleEffects()->mFilters.AsSpan(); + UniquePtr<UserSpaceMetrics> metrics = + UserSpaceMetricsForFrame(aFilteredFrame); + // Hardcode InputIsTainted to true because we don't want JS to be able to + // read the rendered contents of aFilteredFrame. + FilterInstance instance(aFilteredFrame, aFilteredFrame->GetContent(), + *metrics, filterChain, /* InputIsTainted */ true, + nullptr, tm, nullptr, preFilterRegionPtr, + aPreFilterBounds, aOverrideBBox); + if (!instance.IsInitialized()) { + return Nothing(); + } + + return Some(instance.ComputePostFilterExtents()); +} + +FilterInstance::FilterInstance( + nsIFrame* aTargetFrame, nsIContent* aTargetContent, + const UserSpaceMetrics& aMetrics, Span<const StyleFilter> aFilterChain, + bool aFilterInputIsTainted, const SVGFilterPaintCallback& aPaintCallback, + const gfxMatrix& aPaintTransform, const nsRegion* aPostFilterDirtyRegion, + const nsRegion* aPreFilterDirtyRegion, + const nsRect* aPreFilterInkOverflowRectOverride, + const gfxRect* aOverrideBBox) + : mTargetFrame(aTargetFrame), + mTargetContent(aTargetContent), + mMetrics(aMetrics), + mPaintCallback(aPaintCallback), + mPaintTransform(aPaintTransform), + mInitialized(false) { + if (aOverrideBBox) { + mTargetBBox = *aOverrideBBox; + } else { + MOZ_ASSERT(mTargetFrame, + "Need to supply a frame when there's no aOverrideBBox"); + mTargetBBox = + SVGUtils::GetBBox(mTargetFrame, SVGUtils::eUseFrameBoundsForOuterSVG | + SVGUtils::eBBoxIncludeFillGeometry); + } + + // Compute user space to filter space transforms. + if (!ComputeUserSpaceToFilterSpaceScale()) { + return; + } + + if (!ComputeTargetBBoxInFilterSpace()) { + return; + } + + // Get various transforms: + gfxMatrix filterToUserSpace(mFilterSpaceToUserSpaceScale.xScale, 0.0f, 0.0f, + mFilterSpaceToUserSpaceScale.yScale, 0.0f, 0.0f); + + mFilterSpaceToFrameSpaceInCSSPxTransform = + filterToUserSpace * GetUserSpaceToFrameSpaceInCSSPxTransform(); + // mFilterSpaceToFrameSpaceInCSSPxTransform is always invertible + mFrameSpaceInCSSPxToFilterSpaceTransform = + mFilterSpaceToFrameSpaceInCSSPxTransform; + mFrameSpaceInCSSPxToFilterSpaceTransform.Invert(); + + nsIntRect targetBounds; + if (aPreFilterInkOverflowRectOverride) { + targetBounds = FrameSpaceToFilterSpace(aPreFilterInkOverflowRectOverride); + } else if (mTargetFrame) { + nsRect preFilterVOR = mTargetFrame->PreEffectsInkOverflowRect(); + targetBounds = FrameSpaceToFilterSpace(&preFilterVOR); + } + mTargetBounds.UnionRect(mTargetBBoxInFilterSpace, targetBounds); + + // Build the filter graph. + if (NS_FAILED( + BuildPrimitives(aFilterChain, aTargetFrame, aFilterInputIsTainted))) { + return; + } + + // Convert the passed in rects from frame space to filter space: + mPostFilterDirtyRegion = FrameSpaceToFilterSpace(aPostFilterDirtyRegion); + mPreFilterDirtyRegion = FrameSpaceToFilterSpace(aPreFilterDirtyRegion); + + mInitialized = true; +} + +bool FilterInstance::ComputeTargetBBoxInFilterSpace() { + gfxRect targetBBoxInFilterSpace = UserSpaceToFilterSpace(mTargetBBox); + targetBBoxInFilterSpace.RoundOut(); + + return gfxUtils::GfxRectToIntRect(targetBBoxInFilterSpace, + &mTargetBBoxInFilterSpace); +} + +bool FilterInstance::ComputeUserSpaceToFilterSpaceScale() { + if (mTargetFrame) { + mUserSpaceToFilterSpaceScale = mPaintTransform.ScaleFactors(); + if (mUserSpaceToFilterSpaceScale.xScale <= 0.0f || + mUserSpaceToFilterSpaceScale.yScale <= 0.0f) { + // Nothing should be rendered. + return false; + } + } else { + mUserSpaceToFilterSpaceScale = MatrixScalesDouble(); + } + + mFilterSpaceToUserSpaceScale = + MatrixScalesDouble(1.0f / mUserSpaceToFilterSpaceScale.xScale, + 1.0f / mUserSpaceToFilterSpaceScale.yScale); + + return true; +} + +gfxRect FilterInstance::UserSpaceToFilterSpace( + const gfxRect& aUserSpaceRect) const { + gfxRect filterSpaceRect = aUserSpaceRect; + filterSpaceRect.Scale(mUserSpaceToFilterSpaceScale); + return filterSpaceRect; +} + +gfxRect FilterInstance::FilterSpaceToUserSpace( + const gfxRect& aFilterSpaceRect) const { + gfxRect userSpaceRect = aFilterSpaceRect; + userSpaceRect.Scale(mFilterSpaceToUserSpaceScale); + return userSpaceRect; +} + +nsresult FilterInstance::BuildPrimitives(Span<const StyleFilter> aFilterChain, + nsIFrame* aTargetFrame, + bool aFilterInputIsTainted) { + AutoTArray<FilterPrimitiveDescription, 8> primitiveDescriptions; + + for (uint32_t i = 0; i < aFilterChain.Length(); i++) { + bool inputIsTainted = primitiveDescriptions.IsEmpty() + ? aFilterInputIsTainted + : primitiveDescriptions.LastElement().IsTainted(); + nsresult rv = BuildPrimitivesForFilter( + aFilterChain[i], aTargetFrame, inputIsTainted, primitiveDescriptions); + if (NS_FAILED(rv)) { + return rv; + } + } + + mFilterDescription = FilterDescription(std::move(primitiveDescriptions)); + + return NS_OK; +} + +nsresult FilterInstance::BuildPrimitivesForFilter( + const StyleFilter& aFilter, nsIFrame* aTargetFrame, bool aInputIsTainted, + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescriptions) { + NS_ASSERTION(mUserSpaceToFilterSpaceScale.xScale > 0.0f && + mFilterSpaceToUserSpaceScale.yScale > 0.0f, + "scale factors between spaces should be positive values"); + + if (aFilter.IsUrl()) { + // Build primitives for an SVG filter. + SVGFilterInstance svgFilterInstance(aFilter, aTargetFrame, mTargetContent, + mMetrics, mTargetBBox, + mUserSpaceToFilterSpaceScale); + if (!svgFilterInstance.IsInitialized()) { + return NS_ERROR_FAILURE; + } + + return svgFilterInstance.BuildPrimitives(aPrimitiveDescriptions, + mInputImages, aInputIsTainted); + } + + // Build primitives for a CSS filter. + + // If we don't have a frame, use opaque black for shadows with unspecified + // shadow colors. + nscolor shadowFallbackColor = + mTargetFrame ? mTargetFrame->StyleText()->mColor.ToColor() + : NS_RGB(0, 0, 0); + + CSSFilterInstance cssFilterInstance(aFilter, shadowFallbackColor, + mTargetBounds, + mFrameSpaceInCSSPxToFilterSpaceTransform); + return cssFilterInstance.BuildPrimitives(aPrimitiveDescriptions, + aInputIsTainted); +} + +static void UpdateNeededBounds(const nsIntRegion& aRegion, nsIntRect& aBounds) { + aBounds = aRegion.GetBounds(); + + bool overflow; + IntSize surfaceSize = + SVGUtils::ConvertToSurfaceSize(SizeDouble(aBounds.Size()), &overflow); + if (overflow) { + aBounds.SizeTo(surfaceSize); + } +} + +void FilterInstance::ComputeNeededBoxes() { + if (mFilterDescription.mPrimitives.IsEmpty()) { + return; + } + + nsIntRegion sourceGraphicNeededRegion; + nsIntRegion fillPaintNeededRegion; + nsIntRegion strokePaintNeededRegion; + + FilterSupport::ComputeSourceNeededRegions( + mFilterDescription, mPostFilterDirtyRegion, sourceGraphicNeededRegion, + fillPaintNeededRegion, strokePaintNeededRegion); + + sourceGraphicNeededRegion.And(sourceGraphicNeededRegion, mTargetBounds); + + UpdateNeededBounds(sourceGraphicNeededRegion, mSourceGraphic.mNeededBounds); + UpdateNeededBounds(fillPaintNeededRegion, mFillPaint.mNeededBounds); + UpdateNeededBounds(strokePaintNeededRegion, mStrokePaint.mNeededBounds); +} + +void FilterInstance::BuildSourcePaint(SourceInfo* aSource, + imgDrawingParams& aImgParams) { + MOZ_ASSERT(mTargetFrame); + nsIntRect neededRect = aSource->mNeededBounds; + if (neededRect.IsEmpty()) { + return; + } + + RefPtr<DrawTarget> offscreenDT = + gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + neededRect.Size(), SurfaceFormat::B8G8R8A8); + if (!offscreenDT || !offscreenDT->IsValid()) { + return; + } + + gfxContext ctx(offscreenDT); + gfxContextAutoSaveRestore saver(&ctx); + + ctx.SetMatrixDouble(mPaintTransform * + gfxMatrix::Translation(-neededRect.TopLeft())); + GeneralPattern pattern; + if (aSource == &mFillPaint) { + SVGUtils::MakeFillPatternFor(mTargetFrame, &ctx, &pattern, aImgParams); + } else if (aSource == &mStrokePaint) { + SVGUtils::MakeStrokePatternFor(mTargetFrame, &ctx, &pattern, aImgParams); + } + + if (pattern.GetPattern()) { + offscreenDT->FillRect( + ToRect(FilterSpaceToUserSpace(ThebesRect(neededRect))), pattern); + } + + aSource->mSourceSurface = offscreenDT->Snapshot(); + aSource->mSurfaceRect = neededRect; +} + +void FilterInstance::BuildSourcePaints(imgDrawingParams& aImgParams) { + if (!mFillPaint.mNeededBounds.IsEmpty()) { + BuildSourcePaint(&mFillPaint, aImgParams); + } + + if (!mStrokePaint.mNeededBounds.IsEmpty()) { + BuildSourcePaint(&mStrokePaint, aImgParams); + } +} + +void FilterInstance::BuildSourceImage(DrawTarget* aDest, + imgDrawingParams& aImgParams, + FilterNode* aFilter, FilterNode* aSource, + const Rect& aSourceRect) { + MOZ_ASSERT(mTargetFrame); + + nsIntRect neededRect = mSourceGraphic.mNeededBounds; + if (neededRect.IsEmpty()) { + return; + } + + RefPtr<DrawTarget> offscreenDT; + SurfaceFormat format = SurfaceFormat::B8G8R8A8; + if (aDest->CanCreateSimilarDrawTarget(neededRect.Size(), format)) { + offscreenDT = aDest->CreateSimilarDrawTargetForFilter( + neededRect.Size(), format, aFilter, aSource, aSourceRect, Point(0, 0)); + } + if (!offscreenDT || !offscreenDT->IsValid()) { + return; + } + + gfxRect r = FilterSpaceToUserSpace(ThebesRect(neededRect)); + r.RoundOut(); + nsIntRect dirty; + if (!gfxUtils::GfxRectToIntRect(r, &dirty)) { + return; + } + + // SVG graphics paint to device space, so we need to set an initial device + // space to filter space transform on the gfxContext that SourceGraphic + // and SourceAlpha will paint to. + // + // (In theory it would be better to minimize error by having filtered SVG + // graphics temporarily paint to user space when painting the sources and + // only set a user space to filter space transform on the gfxContext + // (since that would eliminate the transform multiplications from user + // space to device space and back again). However, that would make the + // code more complex while being hard to get right without introducing + // subtle bugs, and in practice it probably makes no real difference.) + gfxContext ctx(offscreenDT); + gfxMatrix devPxToCssPxTM = SVGUtils::GetCSSPxToDevPxMatrix(mTargetFrame); + DebugOnly<bool> invertible = devPxToCssPxTM.Invert(); + MOZ_ASSERT(invertible); + ctx.SetMatrixDouble(devPxToCssPxTM * mPaintTransform * + gfxMatrix::Translation(-neededRect.TopLeft())); + + auto imageFlags = aImgParams.imageFlags; + if (mTargetFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // We're coming from a mask or pattern instance. Patterns + // are painted into a separate surface and it seems we can't + // handle the differently sized surface that might be returned + // with FLAG_HIGH_QUALITY_SCALING + imageFlags &= ~imgIContainer::FLAG_HIGH_QUALITY_SCALING; + } + imgDrawingParams imgParams(imageFlags); + mPaintCallback(ctx, imgParams, &mPaintTransform, &dirty); + aImgParams.result = imgParams.result; + + mSourceGraphic.mSourceSurface = offscreenDT->Snapshot(); + mSourceGraphic.mSurfaceRect = neededRect; +} + +void FilterInstance::Render(gfxContext* aCtx, imgDrawingParams& aImgParams, + float aOpacity) { + MOZ_ASSERT(mTargetFrame, "Need a frame for rendering"); + + if (mFilterDescription.mPrimitives.IsEmpty()) { + // An filter without any primitive. Treat it as success and paint nothing. + return; + } + + nsIntRect filterRect = + mPostFilterDirtyRegion.GetBounds().Intersect(OutputFilterSpaceBounds()); + if (filterRect.IsEmpty() || mPaintTransform.IsSingular()) { + return; + } + + gfxContextMatrixAutoSaveRestore autoSR(aCtx); + aCtx->SetMatrix( + aCtx->CurrentMatrix().PreTranslate(filterRect.x, filterRect.y)); + + ComputeNeededBoxes(); + + Rect renderRect = IntRectToRect(filterRect); + RefPtr<DrawTarget> dt = aCtx->GetDrawTarget(); + + MOZ_ASSERT(dt); + if (!dt->IsValid()) { + return; + } + + BuildSourcePaints(aImgParams); + RefPtr<FilterNode> sourceGraphic, fillPaint, strokePaint; + if (mFillPaint.mSourceSurface) { + fillPaint = FilterWrappers::ForSurface(dt, mFillPaint.mSourceSurface, + mFillPaint.mSurfaceRect.TopLeft()); + } + if (mStrokePaint.mSourceSurface) { + strokePaint = FilterWrappers::ForSurface( + dt, mStrokePaint.mSourceSurface, mStrokePaint.mSurfaceRect.TopLeft()); + } + + // We make the sourceGraphic filter but don't set its inputs until after so + // that we can make the sourceGraphic size depend on the filter chain + sourceGraphic = dt->CreateFilter(FilterType::TRANSFORM); + if (sourceGraphic) { + // Make sure we set the translation before calling BuildSourceImage + // so that CreateSimilarDrawTargetForFilter works properly + IntPoint offset = mSourceGraphic.mNeededBounds.TopLeft(); + sourceGraphic->SetAttribute(ATT_TRANSFORM_MATRIX, + Matrix::Translation(offset.x, offset.y)); + } + + RefPtr<FilterNode> resultFilter = FilterNodeGraphFromDescription( + dt, mFilterDescription, renderRect, sourceGraphic, + mSourceGraphic.mSurfaceRect, fillPaint, strokePaint, mInputImages); + + if (!resultFilter) { + gfxWarning() << "Filter is NULL."; + return; + } + + BuildSourceImage(dt, aImgParams, resultFilter, sourceGraphic, renderRect); + if (sourceGraphic) { + if (mSourceGraphic.mSourceSurface) { + sourceGraphic->SetInput(IN_TRANSFORM_IN, mSourceGraphic.mSourceSurface); + } else { + RefPtr<FilterNode> clear = FilterWrappers::Clear(aCtx->GetDrawTarget()); + sourceGraphic->SetInput(IN_TRANSFORM_IN, clear); + } + } + + dt->DrawFilter(resultFilter, renderRect, Point(0, 0), DrawOptions(aOpacity)); +} + +nsRegion FilterInstance::ComputePostFilterDirtyRegion() { + if (mPreFilterDirtyRegion.IsEmpty() || + mFilterDescription.mPrimitives.IsEmpty()) { + return nsRegion(); + } + + nsIntRegion resultChangeRegion = FilterSupport::ComputeResultChangeRegion( + mFilterDescription, mPreFilterDirtyRegion, nsIntRegion(), nsIntRegion()); + return FilterSpaceToFrameSpace(resultChangeRegion); +} + +nsRect FilterInstance::ComputePostFilterExtents() { + if (mFilterDescription.mPrimitives.IsEmpty()) { + return nsRect(); + } + + nsIntRegion postFilterExtents = FilterSupport::ComputePostFilterExtents( + mFilterDescription, mTargetBounds); + return FilterSpaceToFrameSpace(postFilterExtents.GetBounds()); +} + +nsRect FilterInstance::ComputeSourceNeededRect() { + ComputeNeededBoxes(); + return FilterSpaceToFrameSpace(mSourceGraphic.mNeededBounds); +} + +nsIntRect FilterInstance::OutputFilterSpaceBounds() const { + uint32_t numPrimitives = mFilterDescription.mPrimitives.Length(); + if (numPrimitives <= 0) { + return nsIntRect(); + } + + return mFilterDescription.mPrimitives[numPrimitives - 1].PrimitiveSubregion(); +} + +nsIntRect FilterInstance::FrameSpaceToFilterSpace(const nsRect* aRect) const { + nsIntRect rect = OutputFilterSpaceBounds(); + if (aRect) { + if (aRect->IsEmpty()) { + return nsIntRect(); + } + gfxRect rectInCSSPx = + nsLayoutUtils::RectToGfxRect(*aRect, AppUnitsPerCSSPixel()); + gfxRect rectInFilterSpace = + mFrameSpaceInCSSPxToFilterSpaceTransform.TransformBounds(rectInCSSPx); + rectInFilterSpace.RoundOut(); + nsIntRect intRect; + if (gfxUtils::GfxRectToIntRect(rectInFilterSpace, &intRect)) { + rect = intRect; + } + } + return rect; +} + +nsRect FilterInstance::FilterSpaceToFrameSpace(const nsIntRect& aRect) const { + if (aRect.IsEmpty()) { + return nsRect(); + } + gfxRect r(aRect.x, aRect.y, aRect.width, aRect.height); + r = mFilterSpaceToFrameSpaceInCSSPxTransform.TransformBounds(r); + // nsLayoutUtils::RoundGfxRectToAppRect rounds out. + return nsLayoutUtils::RoundGfxRectToAppRect(r, AppUnitsPerCSSPixel()); +} + +nsIntRegion FilterInstance::FrameSpaceToFilterSpace( + const nsRegion* aRegion) const { + if (!aRegion) { + return OutputFilterSpaceBounds(); + } + nsIntRegion result; + for (auto iter = aRegion->RectIter(); !iter.Done(); iter.Next()) { + // FrameSpaceToFilterSpace rounds out, so this works. + nsRect rect = iter.Get(); + result.Or(result, FrameSpaceToFilterSpace(&rect)); + } + return result; +} + +nsRegion FilterInstance::FilterSpaceToFrameSpace( + const nsIntRegion& aRegion) const { + nsRegion result; + for (auto iter = aRegion.RectIter(); !iter.Done(); iter.Next()) { + // FilterSpaceToFrameSpace rounds out, so this works. + result.Or(result, FilterSpaceToFrameSpace(iter.Get())); + } + return result; +} + +gfxMatrix FilterInstance::GetUserSpaceToFrameSpaceInCSSPxTransform() const { + if (!mTargetFrame) { + return gfxMatrix(); + } + return gfxMatrix::Translation( + -SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(mTargetFrame)); +} + +} // namespace mozilla diff --git a/layout/svg/FilterInstance.h b/layout/svg/FilterInstance.h new file mode 100644 index 0000000000..de59f2aaf7 --- /dev/null +++ b/layout/svg/FilterInstance.h @@ -0,0 +1,405 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_FILTERINSTANCE_H_ +#define LAYOUT_SVG_FILTERINSTANCE_H_ + +#include "gfxMatrix.h" +#include "gfxPoint.h" +#include "gfxRect.h" +#include "nsCOMPtr.h" +#include "FilterDescription.h" +#include "nsHashKeys.h" +#include "nsPoint.h" +#include "nsRect.h" +#include "nsSize.h" +#include "nsTArray.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/webrender/WebRenderTypes.h" +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/SVGIntegrationUtils.h" + +class gfxContext; +class nsIContent; +class nsIFrame; +struct WrFiltersHolder; + +namespace mozilla { + +namespace dom { +class UserSpaceMetrics; +} // namespace dom + +namespace image { +struct imgDrawingParams; +} + +/** + * This class performs all filter processing. + * + * We build a graph of the filter image data flow, essentially + * converting the filter graph to SSA. This lets us easily propagate + * analysis data (such as bounding-boxes) over the filter primitive graph. + * + * Definition of "filter space": filter space is a coordinate system that is + * aligned with the user space of the filtered element, with its origin located + * at the top left of the filter region, and with one unit equal in size to one + * pixel of the offscreen surface into which the filter output would/will be + * painted. + * + * The definition of "filter region" can be found here: + * http://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion + */ +class FilterInstance { + using IntRect = gfx::IntRect; + using SourceSurface = gfx::SourceSurface; + using DrawTarget = gfx::DrawTarget; + using FilterPrimitiveDescription = gfx::FilterPrimitiveDescription; + using FilterDescription = gfx::FilterDescription; + using UserSpaceMetrics = dom::UserSpaceMetrics; + using imgDrawingParams = image::imgDrawingParams; + using SVGFilterPaintCallback = SVGIntegrationUtils::SVGFilterPaintCallback; + + public: + /** + * Create a FilterDescription for the supplied filter. All coordinates in + * the description are in filter space. + * @param aFilterInputIsTainted Describes whether the SourceImage / + * SourceAlpha input is tainted. This affects whether feDisplacementMap + * will respect the filter input as its map input, and it affects the + * IsTainted() state on the filter primitives in the FilterDescription. + * "Tainted" is a term from the filters spec and means security-sensitive + * content, i.e. pixels that JS should not be able to read in any way. + * @param aOutAdditionalImages Will contain additional images needed to + * render the filter (from feImage primitives). + * @return A FilterDescription describing the filter. + */ + static FilterDescription GetFilterDescription( + nsIContent* aFilteredElement, Span<const StyleFilter> aFilterChain, + bool aFilterInputIsTainted, const UserSpaceMetrics& aMetrics, + const gfxRect& aBBox, + nsTArray<RefPtr<SourceSurface>>& aOutAdditionalImages); + + /** + * Paint the given filtered frame. + * @param aDirtyArea The area than needs to be painted, in aFilteredFrame's + * frame space (i.e. relative to its origin, the top-left corner of its + * border box). + */ + static void PaintFilteredFrame(nsIFrame* aFilteredFrame, + Span<const StyleFilter> aFilterChain, + gfxContext* aCtx, + const SVGFilterPaintCallback& aPaintCallback, + const nsRegion* aDirtyArea, + imgDrawingParams& aImgParams, + float aOpacity = 1.0f); + + /** + * Returns the post-filter area that could be dirtied when the given + * pre-filter area of aFilteredFrame changes. + * @param aPreFilterDirtyRegion The pre-filter area of aFilteredFrame that + * has changed, relative to aFilteredFrame, in app units. + */ + static nsRegion GetPostFilterDirtyArea(nsIFrame* aFilteredFrame, + const nsRegion& aPreFilterDirtyRegion); + + /** + * Returns the pre-filter area that is needed from aFilteredFrame when the + * given post-filter area needs to be repainted. + * @param aPostFilterDirtyRegion The post-filter area that is dirty, relative + * to aFilteredFrame, in app units. + */ + static nsRegion GetPreFilterNeededArea( + nsIFrame* aFilteredFrame, const nsRegion& aPostFilterDirtyRegion); + + /** + * Returns the post-filter ink overflow rect (paint bounds) of + * aFilteredFrame. + * @param aOverrideBBox A user space rect, in user units, that should be used + * as aFilteredFrame's bbox ('bbox' is a specific SVG term), if non-null. + * @param aPreFilterBounds The pre-filter ink overflow rect of + * aFilteredFrame, if non-null. + */ + static Maybe<nsRect> GetPostFilterBounds( + nsIFrame* aFilteredFrame, const gfxRect* aOverrideBBox = nullptr, + const nsRect* aPreFilterBounds = nullptr); + + /** + * Try to build WebRender filters for a frame if the filters applied to it are + * supported. aInitialized is set to true if the filter has been initialized + * and false otherwise (e.g. a bad url). If aInitialized is false the filter + * the filter contents should not be drawn. + */ + static bool BuildWebRenderFilters( + nsIFrame* aFilteredFrame, + mozilla::Span<const mozilla::StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, bool& aInitialized); + + private: + /** + * @param aTargetFrame The frame of the filtered element under consideration, + * may be null. + * @param aTargetContent The filtered element itself. + * @param aMetrics The metrics to resolve SVG lengths against. + * @param aFilterChain The list of filters to apply. + * @param aFilterInputIsTainted Describes whether the SourceImage / + * SourceAlpha input is tainted. This affects whether feDisplacementMap + * will respect the filter input as its map input. + * @param aPaintCallback [optional] The callback that Render() should use to + * paint. Only required if you will call Render(). + * @param aPaintTransform The transform to apply to convert to + * aTargetFrame's SVG user space. Only used when painting. + * @param aPostFilterDirtyRegion [optional] The post-filter area + * that has to be repainted, in app units. Only required if you will + * call ComputeSourceNeededRect() or Render(). + * @param aPreFilterDirtyRegion [optional] The pre-filter area of + * the filtered element that changed, in app units. Only required if you + * will call ComputePostFilterDirtyRegion(). + * @param aPreFilterInkOverflowRectOverride [optional] Use a different + * ink overflow rect for the target element. + * @param aOverrideBBox [optional] Use a different SVG bbox for the target + * element. Must be non-null if aTargetFrame is null. + */ + FilterInstance( + nsIFrame* aTargetFrame, nsIContent* aTargetContent, + const UserSpaceMetrics& aMetrics, Span<const StyleFilter> aFilterChain, + bool aFilterInputIsTainted, + const SVGIntegrationUtils::SVGFilterPaintCallback& aPaintCallback, + const gfxMatrix& aPaintTransform, + const nsRegion* aPostFilterDirtyRegion = nullptr, + const nsRegion* aPreFilterDirtyRegion = nullptr, + const nsRect* aPreFilterInkOverflowRectOverride = nullptr, + const gfxRect* aOverrideBBox = nullptr); + + static bool BuildWebRenderFiltersImpl( + nsIFrame* aFilteredFrame, + mozilla::Span<const mozilla::StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, bool& aInitialized); + + /** + * Returns true if the filter instance was created successfully. + */ + bool IsInitialized() const { return mInitialized; } + + /** + * Draws the filter output into aDrawTarget. The area that + * needs to be painted must have been specified before calling this method + * by passing it as the aPostFilterDirtyRegion argument to the + * FilterInstance constructor. + */ + void Render(gfxContext* aCtx, imgDrawingParams& aImgParams, + float aOpacity = 1.0f); + + const FilterDescription& ExtractDescriptionAndAdditionalImages( + nsTArray<RefPtr<SourceSurface>>& aOutAdditionalImages) { + aOutAdditionalImages = std::move(mInputImages); + return mFilterDescription; + } + + /** + * Sets the aPostFilterDirtyRegion outparam to the post-filter area in frame + * space that would be dirtied by mTargetFrame when a given + * pre-filter area of mTargetFrame is dirtied. The pre-filter area must have + * been specified before calling this method by passing it as the + * aPreFilterDirtyRegion argument to the FilterInstance constructor. + */ + nsRegion ComputePostFilterDirtyRegion(); + + /** + * Sets the aPostFilterExtents outparam to the post-filter bounds in frame + * space for the whole filter output. This is not necessarily equivalent to + * the area that would be dirtied in the result when the entire pre-filter + * area is dirtied, because some filter primitives can generate output + * without any input. + */ + nsRect ComputePostFilterExtents(); + + /** + * Sets the aDirty outparam to the pre-filter bounds in frame space of the + * area of mTargetFrame that is needed in order to paint the filtered output + * for a given post-filter dirtied area. The post-filter area must have been + * specified before calling this method by passing it as the + * aPostFilterDirtyRegion argument to the FilterInstance constructor. + */ + nsRect ComputeSourceNeededRect(); + + struct SourceInfo { + // Specifies which parts of the source need to be rendered. + // Set by ComputeNeededBoxes(). + nsIntRect mNeededBounds; + + // The surface that contains the input rendering. + // Set by BuildSourceImage / BuildSourcePaint. + RefPtr<SourceSurface> mSourceSurface; + + // The position and size of mSourceSurface in filter space. + // Set by BuildSourceImage / BuildSourcePaint. + IntRect mSurfaceRect; + }; + + /** + * Creates a SourceSurface for either the FillPaint or StrokePaint graph + * nodes + */ + void BuildSourcePaint(SourceInfo* aSource, imgDrawingParams& aImgParams); + + /** + * Creates a SourceSurface for either the FillPaint and StrokePaint graph + * nodes, fills its contents and assigns it to mFillPaint.mSourceSurface and + * mStrokePaint.mSourceSurface respectively. + */ + void BuildSourcePaints(imgDrawingParams& aImgParams); + + /** + * Creates the SourceSurface for the SourceGraphic graph node, paints its + * contents, and assigns it to mSourceGraphic.mSourceSurface. + */ + void BuildSourceImage(DrawTarget* aDest, imgDrawingParams& aImgParams, + mozilla::gfx::FilterNode* aFilter, + mozilla::gfx::FilterNode* aSource, + const mozilla::gfx::Rect& aSourceRect); + + /** + * Build the list of FilterPrimitiveDescriptions that describes the filter's + * filter primitives and their connections. This populates + * mPrimitiveDescriptions and mInputImages. aFilterInputIsTainted describes + * whether the SourceGraphic is tainted. + */ + nsresult BuildPrimitives(Span<const StyleFilter> aFilterChain, + nsIFrame* aTargetFrame, bool aFilterInputIsTainted); + + /** + * Add to the list of FilterPrimitiveDescriptions for a particular SVG + * reference filter or CSS filter. This populates mPrimitiveDescriptions and + * mInputImages. aInputIsTainted describes whether the input to aFilter is + * tainted. + */ + nsresult BuildPrimitivesForFilter( + const StyleFilter& aFilter, nsIFrame* aTargetFrame, bool aInputIsTainted, + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescriptions); + + /** + * Computes the filter space bounds of the areas that we actually *need* from + * the filter sources, based on the value of mPostFilterDirtyRegion. + * This sets mNeededBounds on the corresponding SourceInfo structs. + */ + void ComputeNeededBoxes(); + + /** + * Returns the output bounds of the final FilterPrimitiveDescription. + */ + nsIntRect OutputFilterSpaceBounds() const; + + /** + * Compute the scale factors between user space and filter space. + */ + bool ComputeUserSpaceToFilterSpaceScale(); + + /** + * Transform a rect between user space and filter space. + */ + gfxRect UserSpaceToFilterSpace(const gfxRect& aUserSpace) const; + gfxRect FilterSpaceToUserSpace(const gfxRect& aFilterSpaceRect) const; + + /** + * Converts an nsRect or an nsRegion that is relative to a filtered frame's + * origin (i.e. the top-left corner of its border box) into filter space, + * rounding out. + * Returns the entire filter region if aRect / aRegion is null, or if the + * result is too large to be stored in an nsIntRect. + */ + nsIntRect FrameSpaceToFilterSpace(const nsRect* aRect) const; + nsIntRegion FrameSpaceToFilterSpace(const nsRegion* aRegion) const; + + /** + * Converts an nsIntRect or an nsIntRegion from filter space into the space + * that is relative to a filtered frame's origin (i.e. the top-left corner + * of its border box) in app units, rounding out. + */ + nsRect FilterSpaceToFrameSpace(const nsIntRect& aRect) const; + nsRegion FilterSpaceToFrameSpace(const nsIntRegion& aRegion) const; + + /** + * Returns the transform from frame space to the coordinate space that + * GetCanvasTM transforms to. "Frame space" is the origin of a frame, aka the + * top-left corner of its border box, aka the top left corner of its mRect. + */ + gfxMatrix GetUserSpaceToFrameSpaceInCSSPxTransform() const; + + bool ComputeTargetBBoxInFilterSpace(); + + /** + * The frame for the element that is currently being filtered. + */ + nsIFrame* mTargetFrame; + + /** + * The filtered element. + */ + nsIContent* mTargetContent; + + /** + * The user space metrics of the filtered frame. + */ + const UserSpaceMetrics& mMetrics; + + const SVGFilterPaintCallback& mPaintCallback; + + /** + * The SVG bbox of the element that is being filtered, in user space. + */ + gfxRect mTargetBBox; + + /** + * The SVG bbox of the element that is being filtered, in filter space. + */ + nsIntRect mTargetBBoxInFilterSpace; + + /** + * Transform rects between filter space and frame space in CSS pixels. + */ + gfxMatrix mFilterSpaceToFrameSpaceInCSSPxTransform; + gfxMatrix mFrameSpaceInCSSPxToFilterSpaceTransform; + + /** + * The scale factors between user space and filter space. + */ + gfx::MatrixScalesDouble mUserSpaceToFilterSpaceScale; + gfx::MatrixScalesDouble mFilterSpaceToUserSpaceScale; + + /** + * Pre-filter paint bounds of the element that is being filtered, in filter + * space. + */ + nsIntRect mTargetBounds; + + /** + * The dirty area that needs to be repainted, in filter space. + */ + nsIntRegion mPostFilterDirtyRegion; + + /** + * The pre-filter area of the filtered element that changed, in filter space. + */ + nsIntRegion mPreFilterDirtyRegion; + + SourceInfo mSourceGraphic; + SourceInfo mFillPaint; + SourceInfo mStrokePaint; + + /** + * The transform to the SVG user space of mTargetFrame. + */ + gfxMatrix mPaintTransform; + + nsTArray<RefPtr<SourceSurface>> mInputImages; + FilterDescription mFilterDescription; + bool mInitialized; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_FILTERINSTANCE_H_ diff --git a/layout/svg/ISVGDisplayableFrame.h b/layout/svg/ISVGDisplayableFrame.h new file mode 100644 index 0000000000..f29a51b6aa --- /dev/null +++ b/layout/svg/ISVGDisplayableFrame.h @@ -0,0 +1,151 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_ISVGDISPLAYABLEFRAME_H_ +#define LAYOUT_SVG_ISVGDISPLAYABLEFRAME_H_ + +#include "gfxMatrix.h" +#include "gfxPoint.h" +#include "gfxRect.h" +#include "nsQueryFrame.h" +#include "nsRect.h" +#include "mozilla/gfx/MatrixFwd.h" + +class gfxContext; +class nsIFrame; + +namespace mozilla { +class SVGAnimatedLengthList; +class SVGAnimatedNumberList; +class SVGBBox; +class SVGLengthList; +class SVGNumberList; +class SVGUserUnitList; + +namespace image { +struct imgDrawingParams; +} // namespace image + +/** + * This class is used for elements that can be part of a directly displayable + * section of a document. This includes SVGGeometryFrame and SVGGFrame. + * (Even though the latter doesn't display anything itself, if it contains + * SVGGeometryFrame descendants it is can still be part of a displayable + * section of a document) This class is not used for elements that can never + * display directly, including SVGGradientFrame and SVGPatternFrame. (The + * latter may contain displayable content, but it and its content are never + * *directly* displayed in a document. It can only end up being displayed by + * means of a reference from other content.) + * + * Note specifically that SVG frames that inherit SVGContainerFrame do *not* + * implement this class (only those that inherit SVGDisplayContainerFrame + * do.) + */ +class ISVGDisplayableFrame : public nsQueryFrame { + public: + using imgDrawingParams = image::imgDrawingParams; + + NS_DECL_QUERYFRAME_TARGET(ISVGDisplayableFrame) + + /** + * Paint this frame. + * + * SVG is painted using a combination of display lists (trees of + * nsDisplayItem built by BuildDisplayList() implementations) and recursive + * PaintSVG calls. SVG frames with the NS_FRAME_IS_NONDISPLAY bit set are + * always painted using recursive PaintSVG calls since display list painting + * would provide no advantages (they wouldn't be retained for invalidation). + * Displayed SVG is normally painted via a display list tree created under + * SVGOuterSVGFrame::BuildDisplayList, In future we may use a PaintSVG() call + * that recurses over the entire SVG frame tree on SVG container frames to + * avoid display list construction when it is expensive and unnecessary (see + * bug 934411). + * + * @param aTransform The transform that has to be multiplied onto the + * DrawTarget in order for drawing to be in this frame's SVG user space. + * Implementations of this method should avoid multiplying aTransform onto + * the DrawTarget when possible and instead just pass a transform down to + * their children. This is preferable because changing the transform is + * very expensive for certain DrawTarget backends so it is best to minimize + * the number of transform changes. + * + * @param aImgParams imagelib parameters that may be used when painting + * feImage. + */ + virtual void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) = 0; + + /** + * Returns the frame that should handle pointer events at aPoint. aPoint is + * expected to be in the SVG user space of the frame on which this method is + * called. The frame returned may be the frame on which this method is + * called, any of its descendants or else nullptr. + */ + virtual nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) = 0; + + // Called on SVG child frames (except NS_FRAME_IS_NONDISPLAY frames) + // to update and then invalidate their cached bounds. This method is not + // called until after the SVGOuterSVGFrame has had its initial reflow + // (i.e. once the SVG viewport dimensions are known). It should also only + // be called by SVGOuterSVGFrame during its reflow. + virtual void ReflowSVG() = 0; + + // Flags to pass to NotifySVGChange: + // + // TRANSFORM_CHANGED: + // the current transform matrix for this frame has changed + // COORD_CONTEXT_CHANGED: + // the dimensions of this frame's coordinate context has changed (percentage + // lengths must be reevaluated) + // FULL_ZOOM_CHANGED: + // the page's zoom level has changed + enum SVGChangedFlags { + TRANSFORM_CHANGED = 0x01, + COORD_CONTEXT_CHANGED = 0x02, + FULL_ZOOM_CHANGED = 0x04 + }; + /** + * This is called on a frame when there has been a change to one of its + * ancestors that might affect the frame too. SVGChangedFlags are passed + * to indicate what changed. + * + * Implementations do not need to invalidate, since the caller will + * invalidate the entire area of the ancestor that changed. However, they + * may need to update their bounds. + */ + virtual void NotifySVGChanged(uint32_t aFlags) = 0; + + /** + * Get this frame's contribution to the rect returned by a GetBBox() call + * that occurred either on this element, or on one of its ancestors. + * + * SVG defines an element's bbox to be the element's fill bounds in the + * userspace established by that element. By allowing callers to pass in the + * transform from the userspace established by this element to the userspace + * established by an an ancestor, this method allows callers to obtain this + * element's fill bounds in the userspace established by that ancestor + * instead. In that case, since we return the bounds in a different userspace + * (the ancestor's), the bounds we return are not this element's bbox, but + * rather this element's contribution to the bbox of the ancestor. + * + * @param aToBBoxUserspace The transform from the userspace established by + * this element to the userspace established by the ancestor on which + * getBBox was called. This will be the identity matrix if we are the + * element on which getBBox was called. + * + * @param aFlags Flags indicating whether, stroke, for example, should be + * included in the bbox calculation. + */ + virtual SVGBBox GetBBoxContribution(const gfx::Matrix& aToBBoxUserspace, + uint32_t aFlags) = 0; + + // Are we a container frame? + virtual bool IsDisplayContainer() = 0; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_ISVGDISPLAYABLEFRAME_H_ diff --git a/layout/svg/ISVGSVGFrame.h b/layout/svg/ISVGSVGFrame.h new file mode 100644 index 0000000000..ce034b5338 --- /dev/null +++ b/layout/svg/ISVGSVGFrame.h @@ -0,0 +1,30 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_ISVGSVGFRAME_H_ +#define LAYOUT_SVG_ISVGSVGFRAME_H_ + +#include "nsQueryFrame.h" + +namespace mozilla { + +class ISVGSVGFrame { + public: + NS_DECL_QUERYFRAME_TARGET(ISVGSVGFrame) + + /** + * Called when non-attribute changes have caused the element's width/height + * or its for-children transform to change, and to get the element to notify + * its children appropriately. aFlags must be set to + * ISVGDisplayableFrame::COORD_CONTEXT_CHANGED and/or + * ISVGDisplayableFrame::TRANSFORM_CHANGED. + */ + virtual void NotifyViewportOrTransformChanged(uint32_t aFlags) = 0; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_ISVGSVGFRAME_H_ diff --git a/layout/svg/SVGAFrame.cpp b/layout/svg/SVGAFrame.cpp new file mode 100644 index 0000000000..cbfdf69e62 --- /dev/null +++ b/layout/svg/SVGAFrame.cpp @@ -0,0 +1,102 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "gfxMatrix.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/dom/SVGAElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "SVGLengthList.h" + +nsIFrame* NS_NewSVGAFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGAFrame final : public SVGDisplayContainerFrame { + friend nsIFrame* ::NS_NewSVGAFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGAFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGDisplayContainerFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGAFrame) + +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + // nsIFrame: + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGA"_ns, aResult); + } +#endif +}; + +} // namespace mozilla + +//---------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGAFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGAFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGAFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods +#ifdef DEBUG +void SVGAFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::a), + "Trying to construct an SVGAFrame for a " + "content element that doesn't support the right interfaces"); + + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGAFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::transform) { + // 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. + NotifySVGChanged(TRANSFORM_CHANGED); + } + + // Currently our SMIL implementation does not modify the DOM attributes. Once + // we implement the SVG 2 SMIL behaviour this can be removed + // SVGAElement::SetAttr/UnsetAttr's ResetLinkState() call will be sufficient. + if (aModType == dom::MutationEvent_Binding::SMIL && + aAttribute == nsGkAtoms::href && + (aNameSpaceID == kNameSpaceID_None || + aNameSpaceID == kNameSpaceID_XLink)) { + auto* content = static_cast<dom::SVGAElement*>(GetContent()); + + // SMIL may change whether an <a> element is a link, in which case we will + // need to update the link state. + content->ResetLinkState(true, content->ElementHasHref()); + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/layout/svg/SVGClipPathFrame.cpp b/layout/svg/SVGClipPathFrame.cpp new file mode 100644 index 0000000000..644420b5ce --- /dev/null +++ b/layout/svg/SVGClipPathFrame.cpp @@ -0,0 +1,482 @@ +/* -*- 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 "SVGClipPathFrame.h" + +// Keep others in (case-insensitive) order: +#include "AutoReferenceChainGuard.h" +#include "ImgDrawResult.h" +#include "gfxContext.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGGeometryFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGClipPathElement.h" +#include "mozilla/dom/SVGGeometryElement.h" +#include "nsGkAtoms.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +//---------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGClipPathFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGClipPathFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGClipPathFrame) + +void SVGClipPathFrame::ApplyClipPath(gfxContext& aContext, + nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix) { + ISVGDisplayableFrame* singleClipPathChild = nullptr; + DebugOnly<bool> trivial = IsTrivial(&singleClipPathChild); + MOZ_ASSERT(trivial, "Caller needs to use GetClipMask"); + + const DrawTarget* drawTarget = aContext.GetDrawTarget(); + + // No need for AutoReferenceChainGuard since simple clip paths by definition + // don't reference another clip path. + + // Restore current transform after applying clip path: + gfxContextMatrixAutoSaveRestore autoRestoreTransform(&aContext); + + RefPtr<Path> clipPath; + + if (singleClipPathChild) { + SVGGeometryFrame* pathFrame = do_QueryFrame(singleClipPathChild); + if (pathFrame && pathFrame->StyleVisibility()->IsVisible()) { + SVGGeometryElement* pathElement = + static_cast<SVGGeometryElement*>(pathFrame->GetContent()); + + gfxMatrix toChildsUserSpace = + SVGUtils::GetTransformMatrixInUserSpace(pathFrame) * + (GetClipPathTransform(aClippedFrame) * aMatrix); + + gfxMatrix newMatrix = aContext.CurrentMatrixDouble() + .PreMultiply(toChildsUserSpace) + .NudgeToIntegers(); + if (!newMatrix.IsSingular()) { + aContext.SetMatrixDouble(newMatrix); + FillRule clipRule = + SVGUtils::ToFillRule(pathFrame->StyleSVG()->mClipRule); + clipPath = pathElement->GetOrBuildPath(drawTarget, clipRule); + } + } + } + + if (clipPath) { + aContext.Clip(clipPath); + } else { + // The spec says clip away everything if we have no children or the + // clipping path otherwise can't be resolved: + aContext.Clip(Rect()); + } +} + +static void ComposeExtraMask(DrawTarget* aTarget, SourceSurface* aExtraMask) { + MOZ_ASSERT(aExtraMask); + + Matrix origin = aTarget->GetTransform(); + aTarget->SetTransform(Matrix()); + aTarget->MaskSurface(ColorPattern(DeviceColor(0.0, 0.0, 0.0, 1.0)), + aExtraMask, Point(0, 0), + DrawOptions(1.0, CompositionOp::OP_IN)); + aTarget->SetTransform(origin); +} + +void SVGClipPathFrame::PaintChildren(gfxContext& aMaskContext, + nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix) { + // Check if this clipPath is itself clipped by another clipPath: + SVGClipPathFrame* clipPathThatClipsClipPath; + // XXX check return value? + SVGObserverUtils::GetAndObserveClipPath(this, &clipPathThatClipsClipPath); + SVGUtils::MaskUsage maskUsage; + SVGUtils::DetermineMaskUsage(this, true, maskUsage); + + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aMaskContext); + if (maskUsage.shouldApplyClipPath) { + clipPathThatClipsClipPath->ApplyClipPath(aMaskContext, aClippedFrame, + aMatrix); + } else if (maskUsage.shouldGenerateClipMaskLayer) { + RefPtr<SourceSurface> maskSurface = clipPathThatClipsClipPath->GetClipMask( + aMaskContext, aClippedFrame, aMatrix); + // We want the mask to be untransformed so use the inverse of the current + // transform as the maskTransform to compensate. + Matrix maskTransform = aMaskContext.CurrentMatrix(); + maskTransform.Invert(); + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::ALPHA, 1.0f, + maskSurface, maskTransform); + } + + // Paint our children into the mask: + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + PaintFrameIntoMask(kid, aClippedFrame, aMaskContext); + } + + if (maskUsage.shouldApplyClipPath) { + aMaskContext.PopClip(); + } +} + +void SVGClipPathFrame::PaintClipMask(gfxContext& aMaskContext, + nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix, + SourceSurface* aExtraMask) { + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + + // A clipPath can reference another clipPath, creating a chain of clipPaths + // that must all be applied. We re-enter this method for each clipPath in a + // chain, so we need to protect against reference chain related crashes etc.: + AutoReferenceChainGuard refChainGuard(this, &mIsBeingProcessed, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + return; // Break reference chain + } + + DrawTarget* maskDT = aMaskContext.GetDrawTarget(); + MOZ_ASSERT(maskDT->GetFormat() == SurfaceFormat::A8); + + // Paint this clipPath's contents into aMaskDT: + // We need to set mMatrixForChildren here so that under the PaintSVG calls + // on our children (below) our GetCanvasTM() method will return the correct + // transform. + mMatrixForChildren = GetClipPathTransform(aClippedFrame) * aMatrix; + + PaintChildren(aMaskContext, aClippedFrame, aMatrix); + + if (aExtraMask) { + ComposeExtraMask(maskDT, aExtraMask); + } +} + +void SVGClipPathFrame::PaintFrameIntoMask(nsIFrame* aFrame, + nsIFrame* aClippedFrame, + gfxContext& aTarget) { + ISVGDisplayableFrame* frame = do_QueryFrame(aFrame); + if (!frame) { + return; + } + + // The CTM of each frame referencing us can be different. + frame->NotifySVGChanged(ISVGDisplayableFrame::TRANSFORM_CHANGED); + + // Children of this clipPath may themselves be clipped. + SVGClipPathFrame* clipPathThatClipsChild; + // XXX check return value? + if (SVGObserverUtils::GetAndObserveClipPath(aFrame, + &clipPathThatClipsChild) == + SVGObserverUtils::eHasRefsSomeInvalid) { + return; + } + + SVGUtils::MaskUsage maskUsage; + SVGUtils::DetermineMaskUsage(aFrame, true, maskUsage); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aTarget); + if (maskUsage.shouldApplyClipPath) { + clipPathThatClipsChild->ApplyClipPath( + aTarget, aClippedFrame, + SVGUtils::GetTransformMatrixInUserSpace(aFrame) * mMatrixForChildren); + } else if (maskUsage.shouldGenerateClipMaskLayer) { + RefPtr<SourceSurface> maskSurface = clipPathThatClipsChild->GetClipMask( + aTarget, aClippedFrame, + SVGUtils::GetTransformMatrixInUserSpace(aFrame) * mMatrixForChildren); + + // We want the mask to be untransformed so use the inverse of the current + // transform as the maskTransform to compensate. + Matrix maskTransform = aTarget.CurrentMatrix(); + maskTransform.Invert(); + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::ALPHA, 1.0f, + maskSurface, maskTransform); + } + + gfxMatrix toChildsUserSpace = mMatrixForChildren; + nsIFrame* child = do_QueryFrame(frame); + nsIContent* childContent = child->GetContent(); + if (childContent->IsSVGElement()) { + toChildsUserSpace = + SVGUtils::GetTransformMatrixInUserSpace(child) * mMatrixForChildren; + } + + // clipPath does not result in any image rendering, so we just use a dummy + // imgDrawingParams instead of requiring our caller to pass one. + image::imgDrawingParams imgParams; + + // Our children have NS_STATE_SVG_CLIPPATH_CHILD set on them, and + // SVGGeometryFrame::Render checks for that state bit and paints + // only the geometry (opaque black) if set. + frame->PaintSVG(aTarget, toChildsUserSpace, imgParams); + + if (maskUsage.shouldApplyClipPath) { + aTarget.PopClip(); + } +} + +already_AddRefed<SourceSurface> SVGClipPathFrame::GetClipMask( + gfxContext& aReferenceContext, nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix, SourceSurface* aExtraMask) { + RefPtr<DrawTarget> maskDT = + aReferenceContext.GetDrawTarget()->CreateClippedDrawTarget( + Rect(), SurfaceFormat::A8); + if (!maskDT) { + return nullptr; + } + + gfxContext maskContext(maskDT, /* aPreserveTransform */ true); + PaintClipMask(maskContext, aClippedFrame, aMatrix, aExtraMask); + + RefPtr<SourceSurface> surface = maskDT->Snapshot(); + return surface.forget(); +} + +bool SVGClipPathFrame::PointIsInsideClipPath(nsIFrame* aClippedFrame, + const gfxPoint& aPoint) { + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + + // A clipPath can reference another clipPath, creating a chain of clipPaths + // that must all be applied. We re-enter this method for each clipPath in a + // chain, so we need to protect against reference chain related crashes etc.: + AutoReferenceChainGuard refChainGuard(this, &mIsBeingProcessed, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + return false; // Break reference chain + } + + gfxMatrix matrix = GetClipPathTransform(aClippedFrame); + if (!matrix.Invert()) { + return false; + } + gfxPoint point = matrix.TransformPoint(aPoint); + + // clipPath elements can themselves be clipped by a different clip path. In + // that case the other clip path further clips away the element that is being + // clipped by the original clipPath. If this clipPath is being clipped by a + // different clip path we need to check if it prevents the original element + // from receiving events at aPoint: + SVGClipPathFrame* clipPathFrame; + // XXX check return value? + SVGObserverUtils::GetAndObserveClipPath(this, &clipPathFrame); + if (clipPathFrame && + !clipPathFrame->PointIsInsideClipPath(aClippedFrame, aPoint)) { + return false; + } + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + gfxPoint pointForChild = point; + + gfxMatrix m = SVGUtils::GetTransformMatrixInUserSpace(kid); + if (!m.IsIdentity()) { + if (!m.Invert()) { + return false; + } + pointForChild = m.TransformPoint(point); + } + if (SVGFrame->GetFrameForPoint(pointForChild)) { + return true; + } + } + } + + return false; +} + +bool SVGClipPathFrame::IsTrivial(ISVGDisplayableFrame** aSingleChild) { + // If the clip path is clipped then it's non-trivial + if (SVGObserverUtils::GetAndObserveClipPath(this, nullptr) == + SVGObserverUtils::eHasRefsAllValid) { + return false; + } + + if (aSingleChild) { + *aSingleChild = nullptr; + } + + ISVGDisplayableFrame* foundChild = nullptr; + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + ISVGDisplayableFrame* svgChild = do_QueryFrame(kid); + if (svgChild) { + // We consider a non-trivial clipPath to be one containing + // either more than one svg child and/or a svg container + if (foundChild || svgChild->IsDisplayContainer()) { + return false; + } + + // or where the child is itself clipped + if (SVGObserverUtils::GetAndObserveClipPath(kid, nullptr) == + SVGObserverUtils::eHasRefsAllValid) { + return false; + } + + foundChild = svgChild; + } + } + if (aSingleChild) { + *aSingleChild = foundChild; + } + return true; +} + +bool SVGClipPathFrame::IsValid() { + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + + // A clipPath can reference another clipPath, creating a chain of clipPaths + // that must all be applied. We re-enter this method for each clipPath in a + // chain, so we need to protect against reference chain related crashes etc.: + AutoReferenceChainGuard refChainGuard(this, &mIsBeingProcessed, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + return false; // Break reference chain + } + + if (SVGObserverUtils::GetAndObserveClipPath(this, nullptr) == + SVGObserverUtils::eHasRefsSomeInvalid) { + return false; + } + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + LayoutFrameType kidType = kid->Type(); + + if (kidType == LayoutFrameType::SVGUse) { + for (nsIFrame* grandKid : kid->PrincipalChildList()) { + LayoutFrameType grandKidType = grandKid->Type(); + + if (grandKidType != LayoutFrameType::SVGGeometry && + grandKidType != LayoutFrameType::SVGText) { + return false; + } + } + continue; + } + + if (kidType != LayoutFrameType::SVGGeometry && + kidType != LayoutFrameType::SVGText) { + return false; + } + } + + return true; +} + +nsresult SVGClipPathFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::transform) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + SVGUtils::NotifyChildrenOfSVGChange( + this, ISVGDisplayableFrame::TRANSFORM_CHANGED); + } + if (aAttribute == nsGkAtoms::clipPathUnits) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + } + + return SVGContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +#ifdef DEBUG +void SVGClipPathFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::clipPath), + "Content is not an SVG clipPath!"); + + SVGContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif + +gfxMatrix SVGClipPathFrame::GetCanvasTM() { return mMatrixForChildren; } + +gfxMatrix SVGClipPathFrame::GetClipPathTransform(nsIFrame* aClippedFrame) { + SVGClipPathElement* content = static_cast<SVGClipPathElement*>(GetContent()); + + gfxMatrix tm = content->PrependLocalTransformsTo({}, eChildToUserSpace) * + SVGUtils::GetTransformMatrixInUserSpace(this); + + SVGAnimatedEnumeration* clipPathUnits = + &content->mEnumAttributes[SVGClipPathElement::CLIPPATHUNITS]; + + uint32_t flags = SVGUtils::eBBoxIncludeFillGeometry | + (aClippedFrame->StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Clone + ? SVGUtils::eIncludeOnlyCurrentFrameForNonSVGElement + : 0); + + return SVGUtils::AdjustMatrixForUnits(tm, clipPathUnits, aClippedFrame, + flags); +} + +SVGBBox SVGClipPathFrame::GetBBoxForClipPathFrame(const SVGBBox& aBBox, + const gfxMatrix& aMatrix, + uint32_t aFlags) { + SVGClipPathFrame* clipPathThatClipsClipPath; + if (SVGObserverUtils::GetAndObserveClipPath(this, + &clipPathThatClipsClipPath) == + SVGObserverUtils::eHasRefsSomeInvalid) { + return SVGBBox(); + } + + nsIContent* node = GetContent()->GetFirstChild(); + SVGBBox unionBBox, tmpBBox; + for (; node; node = node->GetNextSibling()) { + if (nsIFrame* frame = node->GetPrimaryFrame()) { + ISVGDisplayableFrame* svg = do_QueryFrame(frame); + if (svg) { + gfxMatrix matrix = + SVGUtils::GetTransformMatrixInUserSpace(frame) * aMatrix; + tmpBBox = svg->GetBBoxContribution(gfx::ToMatrix(matrix), + SVGUtils::eBBoxIncludeFill); + SVGClipPathFrame* clipPathFrame; + if (SVGObserverUtils::GetAndObserveClipPath(frame, &clipPathFrame) != + SVGObserverUtils::eHasRefsSomeInvalid && + clipPathFrame) { + tmpBBox = + clipPathFrame->GetBBoxForClipPathFrame(tmpBBox, aMatrix, aFlags); + } + if (!(aFlags & SVGUtils::eDoNotClipToBBoxOfContentInsideClipPath)) { + tmpBBox.Intersect(aBBox); + } + unionBBox.UnionEdges(tmpBBox); + } + } + } + + if (clipPathThatClipsClipPath) { + tmpBBox = clipPathThatClipsClipPath->GetBBoxForClipPathFrame(aBBox, aMatrix, + aFlags); + unionBBox.Intersect(tmpBBox); + } + return unionBBox; +} + +bool SVGClipPathFrame::IsSVGTransformed(Matrix* aOwnTransforms, + Matrix* aFromParentTransforms) const { + const auto* e = static_cast<SVGElement const*>(GetContent()); + Matrix m = ToMatrix(e->PrependLocalTransformsTo({}, eUserSpaceToParent)); + + if (m.IsIdentity()) { + return false; + } + + if (aOwnTransforms) { + *aOwnTransforms = m; + } + + return true; +} + +} // namespace mozilla diff --git a/layout/svg/SVGClipPathFrame.h b/layout/svg/SVGClipPathFrame.h new file mode 100644 index 0000000000..40d73f036b --- /dev/null +++ b/layout/svg/SVGClipPathFrame.h @@ -0,0 +1,176 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGCLIPPATHFRAME_H_ +#define LAYOUT_SVG_SVGCLIPPATHFRAME_H_ + +#include "gfxMatrix.h" +#include "mozilla/Attributes.h" +#include "mozilla/SVGContainerFrame.h" + +class gfxContext; + +namespace mozilla { +class ISVGDisplayableFrame; +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGClipPathFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGClipPathFrame final : public SVGContainerFrame { + friend nsIFrame* ::NS_NewSVGClipPathFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + using Matrix = gfx::Matrix; + using SourceSurface = gfx::SourceSurface; + using imgDrawingParams = image::imgDrawingParams; + + protected: + explicit SVGClipPathFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGContainerFrame(aStyle, aPresContext, kClassID), + mIsBeingProcessed(false) { + AddStateBits(NS_FRAME_IS_NONDISPLAY | NS_STATE_SVG_CLIPPATH_CHILD | + NS_FRAME_MAY_BE_TRANSFORMED); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGClipPathFrame) + + // nsIFrame methods: + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + bool IsSVGTransformed(Matrix* aOwnTransforms, + Matrix* aFromParentTransforms) const override; + + // SVGClipPathFrame methods: + + /** + * Applies the clipPath by pushing a clip path onto the DrawTarget. + * + * This method must only be used if IsTrivial() returns true, otherwise use + * GetClipMask. + * + * @param aContext The context that the clip path is to be applied to. + * @param aClippedFrame The/an nsIFrame of the element that references this + * clipPath that is currently being processed. + * @param aMatrix The transform from aClippedFrame's user space to aContext's + * current transform. + */ + void ApplyClipPath(gfxContext& aContext, nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix); + + /** + * Returns an alpha mask surface containing the clipping geometry. + * + * This method must only be used if IsTrivial() returns false, otherwise use + * ApplyClipPath. + * + * @param aReferenceContext Used to determine the backend for and size of the + * returned SourceSurface, the size being limited to the device space clip + * extents on the context. + * @param aClippedFrame The/an nsIFrame of the element that references this + * clipPath that is currently being processed. + * @param aMatrix The transform from aClippedFrame's user space to aContext's + * current transform. + * @param [in, optional] aExtraMask An extra surface that the returned + * surface should be masked with. + */ + already_AddRefed<SourceSurface> GetClipMask( + gfxContext& aReferenceContext, nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix, SourceSurface* aExtraMask = nullptr); + + /** + * Paint mask directly onto a given context(aMaskContext). + * + * @param aMaskContext The target of mask been painting on. + * @param aClippedFrame The/an nsIFrame of the element that references this + * clipPath that is currently being processed. + * @param aMatrix The transform from aClippedFrame's user space to + * current transform. + * @param [in, optional] aExtraMask An extra surface that the returned + * surface should be masked with. + */ + void PaintClipMask(gfxContext& aMaskContext, nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix, SourceSurface* aExtraMask); + + /** + * aPoint is expected to be in aClippedFrame's SVG user space. + */ + bool PointIsInsideClipPath(nsIFrame* aClippedFrame, const gfxPoint& aPoint); + + // Check if this clipPath is made up of more than one geometry object. + // If so, the clipping API in cairo isn't enough and we need to use + // mask based clipping. + bool IsTrivial(ISVGDisplayableFrame** aSingleChild = nullptr); + + bool IsValid(); + + // nsIFrame interface: + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGClipPath"_ns, aResult); + } +#endif + + SVGBBox GetBBoxForClipPathFrame(const SVGBBox& aBBox, + const gfxMatrix& aMatrix, uint32_t aFlags); + + /** + * If the clipPath element transforms its children due to + * clipPathUnits="objectBoundingBox" being set on it and/or due to the + * 'transform' attribute being set on it, this function returns the resulting + * transform. + */ + gfxMatrix GetClipPathTransform(nsIFrame* aClippedFrame); + + private: + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override; + + already_AddRefed<DrawTarget> CreateClipMask(gfxContext& aReferenceContext, + gfx::IntPoint& aOffset); + + void PaintFrameIntoMask(nsIFrame* aFrame, nsIFrame* aClippedFrame, + gfxContext& aTarget); + + void PaintChildren(gfxContext& aMaskContext, nsIFrame* aClippedFrame, + const gfxMatrix& aMatrix); + + // Set, during a GetClipMask() call, to the transform that still needs to be + // concatenated to the transform of the DrawTarget that was passed to + // GetClipMask in order to establish the coordinate space that the clipPath + // establishes for its contents (i.e. including applying 'clipPathUnits' and + // any 'transform' attribute set on the clipPath) specifically for clipping + // the frame that was passed to GetClipMask at that moment in time. This is + // set so that if our GetCanvasTM method is called while GetClipMask is + // painting its children, the returned matrix will include the transforms + // that should be used when creating the mask for the frame passed to + // GetClipMask. + // + // Note: The removal of GetCanvasTM is nearly complete, so our GetCanvasTM + // may not even be called soon/any more. + gfxMatrix mMatrixForChildren; + + // Flag used to indicate whether a methods that may reenter due to + // following a reference to another instance is currently executing. + bool mIsBeingProcessed; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGCLIPPATHFRAME_H_ diff --git a/layout/svg/SVGContainerFrame.cpp b/layout/svg/SVGContainerFrame.cpp new file mode 100644 index 0000000000..1f2025eb2f --- /dev/null +++ b/layout/svg/SVGContainerFrame.cpp @@ -0,0 +1,446 @@ +/* -*- 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 "SVGContainerFrame.h" + +// Keep others in (case-insensitive) order: +#include "ImgDrawResult.h" +#include "mozilla/PresShell.h" +#include "mozilla/RestyleManager.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGTextFrame.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGElement.h" +#include "nsCSSFrameConstructor.h" +#include "SVGAnimatedTransformList.h" + +using namespace mozilla::dom; +using namespace mozilla::image; + +nsIFrame* NS_NewSVGContainerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + nsIFrame* frame = new (aPresShell) + mozilla::SVGContainerFrame(aStyle, aPresShell->GetPresContext(), + mozilla::SVGContainerFrame::kClassID); + // If we were called directly, then the frame is for a <defs> or + // an unknown element type. In both cases we prevent the content + // from displaying directly. + frame->AddStateBits(NS_FRAME_IS_NONDISPLAY); + return frame; +} + +namespace mozilla { + +NS_QUERYFRAME_HEAD(SVGContainerFrame) + NS_QUERYFRAME_ENTRY(SVGContainerFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +NS_QUERYFRAME_HEAD(SVGDisplayContainerFrame) + NS_QUERYFRAME_ENTRY(SVGDisplayContainerFrame) + NS_QUERYFRAME_ENTRY(ISVGDisplayableFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGContainerFrame) + +NS_IMPL_FRAMEARENA_HELPERS(SVGContainerFrame) + +void SVGContainerFrame::AppendFrames(ChildListID aListID, + nsFrameList&& aFrameList) { + InsertFrames(aListID, mFrames.LastChild(), nullptr, std::move(aFrameList)); +} + +void SVGContainerFrame::InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) { + NS_ASSERTION(aListID == FrameChildListID::Principal, "unexpected child list"); + NS_ASSERTION(!aPrevFrame || aPrevFrame->GetParent() == this, + "inserting after sibling frame with different parent"); + + mFrames.InsertFrames(this, aPrevFrame, std::move(aFrameList)); +} + +void SVGContainerFrame::RemoveFrame(DestroyContext& aContext, + ChildListID aListID, nsIFrame* aOldFrame) { + NS_ASSERTION(aListID == FrameChildListID::Principal, "unexpected child list"); + mFrames.DestroyFrame(aContext, aOldFrame); +} + +bool SVGContainerFrame::ComputeCustomOverflow(OverflowAreas& aOverflowAreas) { + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // We don't maintain overflow rects. + // XXX It would have be better if the restyle request hadn't even happened. + return false; + } + return nsContainerFrame::ComputeCustomOverflow(aOverflowAreas); +} + +/** + * Traverses a frame tree, marking any SVGTextFrame frames as dirty + * and calling InvalidateRenderingObservers() on it. + * + * The reason that this helper exists is because SVGTextFrame is special. + * None of the other SVG frames ever need to be reflowed when they have the + * NS_FRAME_IS_NONDISPLAY bit set on them because their PaintSVG methods + * (and those of any containers that they can validly be contained within) do + * not make use of mRect or overflow rects. "em" lengths, etc., are resolved + * as those elements are painted. + * + * SVGTextFrame is different because its anonymous block and inline frames + * need to be reflowed in order to get the correct metrics when things like + * inherited font-size of an ancestor changes, or a delayed webfont loads and + * applies. + * + * However, we only need to do this work if we were reflowed with + * NS_FRAME_IS_DIRTY, which implies that all descendants are dirty. When + * that reflow reaches an NS_FRAME_IS_NONDISPLAY frame it would normally + * stop, but this helper looks for any SVGTextFrame descendants of such + * frames and marks them NS_FRAME_IS_DIRTY so that the next time that they + * are painted their anonymous kid will first get the necessary reflow. + */ +/* static */ +void SVGContainerFrame::ReflowSVGNonDisplayText(nsIFrame* aContainer) { + if (!aContainer->HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + return; + } + MOZ_ASSERT(aContainer->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) || + !aContainer->IsFrameOfType(nsIFrame::eSVG), + "it is wasteful to call ReflowSVGNonDisplayText on a container " + "frame that is not NS_FRAME_IS_NONDISPLAY or not SVG"); + for (nsIFrame* kid : aContainer->PrincipalChildList()) { + LayoutFrameType type = kid->Type(); + if (type == LayoutFrameType::SVGText) { + static_cast<SVGTextFrame*>(kid)->ReflowSVGNonDisplayText(); + } else { + if (kid->IsFrameOfType(nsIFrame::eSVG | nsIFrame::eSVGContainer) || + type == LayoutFrameType::SVGForeignObject || + !kid->IsFrameOfType(nsIFrame::eSVG)) { + ReflowSVGNonDisplayText(kid); + } + } + } +} + +void SVGDisplayContainerFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + if (!IsSVGOuterSVGFrame()) { + AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD); + } + SVGContainerFrame::Init(aContent, aParent, aPrevInFlow); +} + +void SVGDisplayContainerFrame::BuildDisplayList( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + // content could be a XUL element so check for an SVG element + if (auto* svg = SVGElement::FromNode(GetContent())) { + if (!svg->HasValidDimensions()) { + return; + } + } + DisplayOutline(aBuilder, aLists); + return BuildDisplayListForNonBlockChildren(aBuilder, aLists); +} + +void SVGDisplayContainerFrame::InsertFrames( + ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, nsFrameList&& aFrameList) { + // memorize first old frame after insertion point + // XXXbz once again, this would work a lot better if the nsIFrame + // methods returned framelist iterators.... + nsIFrame* nextFrame = aPrevFrame ? aPrevFrame->GetNextSibling() + : GetChildList(aListID).FirstChild(); + nsIFrame* firstNewFrame = aFrameList.FirstChild(); + + // Insert the new frames + SVGContainerFrame::InsertFrames(aListID, aPrevFrame, aPrevFrameLine, + std::move(aFrameList)); + + // If we are not a non-display SVG frame and we do not have a bounds update + // pending, then we need to schedule one for our new children: + if (!HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN | + NS_FRAME_IS_NONDISPLAY)) { + for (nsIFrame* kid = firstNewFrame; kid != nextFrame; + kid = kid->GetNextSibling()) { + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + MOZ_ASSERT(!kid->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Check for this explicitly in the |if|, then"); + bool isFirstReflow = kid->HasAnyStateBits(NS_FRAME_FIRST_REFLOW); + // Remove bits so that ScheduleBoundsUpdate will work: + kid->RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | + NS_FRAME_HAS_DIRTY_CHILDREN); + // No need to invalidate the new kid's old bounds, so we just use + // SVGUtils::ScheduleBoundsUpdate. + SVGUtils::ScheduleReflowSVG(kid); + if (isFirstReflow) { + // Add back the NS_FRAME_FIRST_REFLOW bit: + kid->AddStateBits(NS_FRAME_FIRST_REFLOW); + } + } + } + } +} + +void SVGDisplayContainerFrame::RemoveFrame(DestroyContext& aContext, + ChildListID aListID, + nsIFrame* aOldFrame) { + SVGObserverUtils::InvalidateRenderingObservers(aOldFrame); + + // SVGContainerFrame::RemoveFrame doesn't call down into + // nsContainerFrame::RemoveFrame, so it doesn't call FrameNeedsReflow. We + // need to schedule a repaint and schedule an update to our overflow rects. + SchedulePaint(); + PresContext()->RestyleManager()->PostRestyleEvent( + mContent->AsElement(), RestyleHint{0}, nsChangeHint_UpdateOverflow); + + SVGContainerFrame::RemoveFrame(aContext, aListID, aOldFrame); +} + +bool SVGDisplayContainerFrame::IsSVGTransformed( + gfx::Matrix* aOwnTransform, gfx::Matrix* aFromParentTransform) const { + return SVGUtils::IsSVGTransformed(this, aOwnTransform, aFromParentTransform); +} + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +void SVGDisplayContainerFrame::PaintSVG(gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) || + PresContext()->Document()->IsSVGGlyphsDocument(), + "Only painting of non-display SVG should take this code path"); + + if (StyleEffects()->IsTransparent()) { + return; + } + + gfxMatrix matrix = aTransform; + if (auto* svg = SVGElement::FromNode(GetContent())) { + matrix = svg->PrependLocalTransformsTo(matrix, eChildToUserSpace); + if (matrix.IsSingular()) { + return; + } + } + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + gfxMatrix m = matrix; + // PaintFrameWithEffects() expects the transform that is passed to it to + // include the transform to the passed frame's user space, so add it: + const nsIContent* content = kid->GetContent(); + if (const SVGElement* element = SVGElement::FromNode(content)) { + if (!element->HasValidDimensions()) { + continue; // nothing to paint for kid + } + + m = SVGUtils::GetTransformMatrixInUserSpace(kid) * m; + if (m.IsSingular()) { + continue; + } + } + SVGUtils::PaintFrameWithEffects(kid, aContext, m, aImgParams); + } +} + +nsIFrame* SVGDisplayContainerFrame::GetFrameForPoint(const gfxPoint& aPoint) { + NS_ASSERTION(HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD), + "Only hit-testing of a clipPath's contents should take this " + "code path"); + // First we transform aPoint into the coordinate space established by aFrame + // for its children (e.g. take account of any 'viewBox' attribute): + gfxPoint point = aPoint; + if (const auto* svg = SVGElement::FromNode(GetContent())) { + gfxMatrix m = svg->PrependLocalTransformsTo({}, eChildToUserSpace); + if (!m.IsIdentity()) { + if (!m.Invert()) { + return nullptr; + } + point = m.TransformPoint(point); + } + } + + // Traverse the list in reverse order, so that if we get a hit we know that's + // the topmost frame that intersects the point; then we can just return it. + nsIFrame* result = nullptr; + for (nsIFrame* current = PrincipalChildList().LastChild(); current; + current = current->GetPrevSibling()) { + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(current); + if (!SVGFrame) { + continue; + } + const nsIContent* content = current->GetContent(); + if (const auto* svg = SVGElement::FromNode(content)) { + if (!svg->HasValidDimensions()) { + continue; + } + } + // GetFrameForPoint() expects a point in its frame's SVG user space, so + // we need to convert to that space: + gfxPoint p = point; + if (const auto* svg = SVGElement::FromNode(content)) { + gfxMatrix m = svg->PrependLocalTransformsTo({}, eUserSpaceToParent); + if (!m.IsIdentity()) { + if (!m.Invert()) { + continue; + } + p = m.TransformPoint(p); + } + } + result = SVGFrame->GetFrameForPoint(p); + if (result) { + break; + } + } + + if (result && !SVGUtils::HitTestClip(this, aPoint)) { + result = nullptr; + } + + return result; +} + +void SVGDisplayContainerFrame::ReflowSVG() { + MOZ_ASSERT(SVGUtils::AnyOuterSVGIsCallingReflowSVG(this), + "This call is probably a wasteful mistake"); + + MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "ReflowSVG mechanism not designed for this"); + + MOZ_ASSERT(!IsSVGOuterSVGFrame(), "Do not call on outer-<svg>"); + + if (!SVGUtils::NeedsReflowSVG(this)) { + return; + } + + // If the NS_FRAME_FIRST_REFLOW bit has been removed from our parent frame, + // then our outer-<svg> has previously had its initial reflow. In that case + // we need to make sure that that bit has been removed from ourself _before_ + // recursing over our children to ensure that they know too. Otherwise, we + // need to remove it _after_ recursing over our children so that they know + // the initial reflow is currently underway. + + bool isFirstReflow = HasAnyStateBits(NS_FRAME_FIRST_REFLOW); + + bool outerSVGHasHadFirstReflow = + !GetParent()->HasAnyStateBits(NS_FRAME_FIRST_REFLOW); + + if (outerSVGHasHadFirstReflow) { + RemoveStateBits(NS_FRAME_FIRST_REFLOW); // tell our children + } + + OverflowAreas overflowRects; + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + MOZ_ASSERT(!kid->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Check for this explicitly in the |if|, then"); + SVGFrame->ReflowSVG(); + + // We build up our child frame overflows here instead of using + // nsLayoutUtils::UnionChildOverflow since SVG frame's all use the same + // frame list, and we're iterating over that list now anyway. + ConsiderChildOverflow(overflowRects, kid); + } else { + // Inside a non-display container frame, we might have some + // SVGTextFrames. We need to cause those to get reflowed in + // case they are the target of a rendering observer. + MOZ_ASSERT( + kid->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) || + !kid->IsFrameOfType(nsIFrame::eSVG), + "expected kid to be a NS_FRAME_IS_NONDISPLAY frame or not SVG"); + if (kid->HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + SVGContainerFrame* container = do_QueryFrame(kid); + if (container && container->GetContent()->IsSVGElement()) { + ReflowSVGNonDisplayText(container); + } + } + } + } + + // <svg> can create an SVG viewport with an offset due to its + // x/y/width/height attributes, and <use> can introduce an offset with an + // empty mRect (any width/height is copied to an anonymous <svg> child). + // Other than that containers should not set mRect since all other offsets + // come from transforms, which are accounted for by nsDisplayTransform. + // Note that we rely on |overflow:visible| to allow display list items to be + // created for our children. + MOZ_ASSERT(mContent->IsAnyOfSVGElements(nsGkAtoms::svg, nsGkAtoms::symbol) || + (mContent->IsSVGElement(nsGkAtoms::use) && + mRect.Size() == nsSize(0, 0)) || + mRect.IsEqualEdges(nsRect()), + "Only inner-<svg>/<use> is expected to have mRect set"); + + if (isFirstReflow) { + // Make sure we have our filter property (if any) before calling + // FinishAndStoreOverflow (subsequent filter changes are handled off + // nsChangeHint_UpdateEffects): + SVGObserverUtils::UpdateEffects(this); + } + + FinishAndStoreOverflow(overflowRects, mRect.Size()); + + // Remove state bits after FinishAndStoreOverflow so that it doesn't + // invalidate on first reflow: + RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | + NS_FRAME_HAS_DIRTY_CHILDREN); +} + +void SVGDisplayContainerFrame::NotifySVGChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), + "Invalidation logic may need adjusting"); + + if (aFlags & TRANSFORM_CHANGED) { + // make sure our cached transform matrix gets (lazily) updated + mCanvasTM = nullptr; + } + + SVGUtils::NotifyChildrenOfSVGChange(this, aFlags); +} + +SVGBBox SVGDisplayContainerFrame::GetBBoxContribution( + const Matrix& aToBBoxUserspace, uint32_t aFlags) { + SVGBBox bboxUnion; + + for (nsIFrame* kid : mFrames) { + ISVGDisplayableFrame* svgKid = do_QueryFrame(kid); + if (!svgKid) { + continue; + } + // content could be a XUL element + auto* svg = SVGElement::FromNode(kid->GetContent()); + if (svg && !svg->HasValidDimensions()) { + continue; + } + gfxMatrix transform = gfx::ThebesMatrix(aToBBoxUserspace); + if (svg) { + transform = svg->PrependLocalTransformsTo({}, eChildToUserSpace) * + SVGUtils::GetTransformMatrixInUserSpace(kid) * transform; + } + // We need to include zero width/height vertical/horizontal lines, so we + // have to use UnionEdges. + bboxUnion.UnionEdges( + svgKid->GetBBoxContribution(gfx::ToMatrix(transform), aFlags)); + } + + return bboxUnion; +} + +gfxMatrix SVGDisplayContainerFrame::GetCanvasTM() { + if (!mCanvasTM) { + NS_ASSERTION(GetParent(), "null parent"); + + auto* parent = static_cast<SVGContainerFrame*>(GetParent()); + auto* content = static_cast<SVGElement*>(GetContent()); + + gfxMatrix tm = content->PrependLocalTransformsTo(parent->GetCanvasTM()); + + mCanvasTM = MakeUnique<gfxMatrix>(tm); + } + + return *mCanvasTM; +} + +} // namespace mozilla diff --git a/layout/svg/SVGContainerFrame.h b/layout/svg/SVGContainerFrame.h new file mode 100644 index 0000000000..9df6f6b0c7 --- /dev/null +++ b/layout/svg/SVGContainerFrame.h @@ -0,0 +1,162 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGCONTAINERFRAME_H_ +#define LAYOUT_SVG_SVGCONTAINERFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/UniquePtr.h" +#include "nsContainerFrame.h" +#include "nsIFrame.h" +#include "nsQueryFrame.h" +#include "nsRect.h" + +class gfxContext; +class nsFrameList; +class nsIContent; + +struct nsRect; + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGContainerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +/** + * Base class for SVG container frames. Frame sub-classes that do not + * display their contents directly (such as the frames for <marker> or + * <pattern>) just inherit this class. Frame sub-classes that do or can + * display their contents directly (such as the frames for inner-<svg> or + * <g>) inherit our SVGDisplayContainerFrame sub-class. + * + * *** WARNING *** + * + * Do *not* blindly cast to SVG element types in this class's methods (see the + * warning comment for SVGDisplayContainerFrame below). + */ +class SVGContainerFrame : public nsContainerFrame { + friend nsIFrame* ::NS_NewSVGContainerFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + SVGContainerFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + ClassID aID) + : nsContainerFrame(aStyle, aPresContext, aID) { + AddStateBits(NS_FRAME_SVG_LAYOUT); + } + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGContainerFrame) + + // Returns the transform to our gfxContext (to device pixels, not CSS px) + virtual gfxMatrix GetCanvasTM() { return gfxMatrix(); } + + /** + * Returns true if the frame's content has a transform that applies only to + * its children, and not to the frame itself. For example, an implicit + * transform introduced by a 'viewBox' attribute, or an explicit transform + * due to a root-<svg> having its currentScale/currentTransform properties + * set. If aTransform is non-null, then it will be set to the transform. + */ + virtual bool HasChildrenOnlyTransform(Matrix* aTransform) const { + return false; + } + + // nsIFrame: + void AppendFrames(ChildListID aListID, nsFrameList&& aFrameList) override; + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; + void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsContainerFrame::IsFrameOfType( + aFlags & ~(nsIFrame::eSVG | nsIFrame::eSVGContainer)); + } + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + bool ComputeCustomOverflow(mozilla::OverflowAreas& aOverflowAreas) override; + + protected: + /** + * Traverses a frame tree, marking any SVGTextFrame frames as dirty + * and calling InvalidateRenderingObservers() on it. + */ + static void ReflowSVGNonDisplayText(nsIFrame* aContainer); +}; + +/** + * Frame class or base-class for SVG containers that can or do display their + * contents directly. + * + * *** WARNING *** + * + * This class's methods can *not* assume that mContent points to an instance of + * an SVG element class since this class is inherited by + * SVGGenericContainerFrame which is used for unrecognized elements in the + * SVG namespace. Do *not* blindly cast to SVG element types. + */ +class SVGDisplayContainerFrame : public SVGContainerFrame, + public ISVGDisplayableFrame { + protected: + SVGDisplayContainerFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID) + : SVGContainerFrame(aStyle, aPresContext, aID) { + AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED); + } + + public: + NS_DECL_QUERYFRAME + NS_DECL_QUERYFRAME_TARGET(SVGDisplayContainerFrame) + NS_DECL_ABSTRACT_FRAME(SVGDisplayContainerFrame) + + // nsIFrame: + void InsertFrames(ChildListID aListID, nsIFrame* aPrevFrame, + const nsLineList::iterator* aPrevFrameLine, + nsFrameList&& aFrameList) override; + void RemoveFrame(DestroyContext&, ChildListID, nsIFrame*) override; + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + bool IsSVGTransformed(Matrix* aOwnTransform = nullptr, + Matrix* aFromParentTransform = nullptr) const override; + + // ISVGDisplayableFrame interface: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + bool IsDisplayContainer() override { return true; } + gfxMatrix GetCanvasTM() override; + + protected: + /** + * Cached canvasTM value. + */ + UniquePtr<gfxMatrix> mCanvasTM; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGCONTAINERFRAME_H_ diff --git a/layout/svg/SVGContextPaint.cpp b/layout/svg/SVGContextPaint.cpp new file mode 100644 index 0000000000..181b637400 --- /dev/null +++ b/layout/svg/SVGContextPaint.cpp @@ -0,0 +1,364 @@ +/* -*- 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/. */ + +#include "SVGContextPaint.h" + +#include "gfxContext.h" +#include "gfxUtils.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/Document.h" +#include "mozilla/extensions/WebExtensionPolicy.h" +#include "mozilla/StaticPrefs_svg.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "SVGPaintServerFrame.h" + +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +using image::imgDrawingParams; + +/* static */ +bool SVGContextPaint::IsAllowedForImageFromURI(nsIURI* aURI) { + if (StaticPrefs::svg_context_properties_content_enabled()) { + return true; + } + + // Context paint is pref'ed off for Web content. Ideally we'd have some + // easy means to determine whether the frame that has linked to the image + // is a frame for a content node that originated from Web content. + // Unfortunately different types of anonymous content, about: documents + // such as about:reader, etc. that are "our" code that we ship are + // sometimes hard to distinguish from real Web content. As a result, + // instead of trying to figure out what content is "ours" we instead let + // any content provide image context paint, but only if the image is + // chrome:// or resource:// do we return true. This should be sufficient + // to stop the image context paint feature being useful to (and therefore + // used by and relied upon by) Web content. (We don't want Web content to + // use this feature because we're not sure that image context paint is a + // good mechanism for wider use, or suitable for specification.) + // + // Because the default favicon used in the browser UI needs context paint, we + // also allow it for page-icon:<page-url>. This exposes context paint to + // 3rd-party favicons, but only for history and bookmark items. Other places + // such as the tab bar don't use the page-icon protocol to load favicons. + // + // One case that is not covered by chrome:// or resource:// are WebExtensions, + // specifically ones that are "ours". WebExtensions are moz-extension:// + // regardless if the extension is in-tree or not. Since we don't want + // extension developers coming to rely on image context paint either, we only + // enable context-paint for extensions that are owned by Mozilla + // (based on the extension permission "internal:svgContextPropertiesAllowed"). + // + // We also allow this for browser UI icons that are served up from + // Mozilla-controlled domains listed in the + // svg.context-properties.content.allowed-domains pref. + // + nsAutoCString scheme; + if (NS_SUCCEEDED(aURI->GetScheme(scheme)) && + (scheme.EqualsLiteral("chrome") || scheme.EqualsLiteral("resource") || + scheme.EqualsLiteral("page-icon"))) { + return true; + } + RefPtr<BasePrincipal> principal = + BasePrincipal::CreateContentPrincipal(aURI, OriginAttributes()); + + RefPtr<extensions::WebExtensionPolicy> addonPolicy = principal->AddonPolicy(); + if (addonPolicy) { + // Only allowed for extensions that have the + // internal:svgContextPropertiesAllowed permission (added internally from + // to Mozilla-owned extensions, see `isMozillaExtension` function + // defined in Extension.jsm for the exact criteria). + return addonPolicy->HasPermission( + nsGkAtoms::svgContextPropertiesAllowedPermission); + } + + bool isInAllowList = false; + principal->IsURIInPrefList("svg.context-properties.content.allowed-domains", + &isInAllowList); + return isInAllowList; +} + +/** + * Stores in |aTargetPaint| information on how to reconstruct the current + * fill or stroke pattern. Will also set the paint opacity to transparent if + * the paint is set to "none". + * @param aOuterContextPaint pattern information from the outer text context + * @param aTargetPaint where to store the current pattern information + * @param aFillOrStroke member pointer to the paint we are setting up + */ +static void SetupInheritablePaint(const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, + nsIFrame* aFrame, float& aOpacity, + SVGContextPaint* aOuterContextPaint, + SVGContextPaintImpl::Paint& aTargetPaint, + StyleSVGPaint nsStyleSVG::*aFillOrStroke, + nscolor aDefaultFallbackColor, + imgDrawingParams& aImgParams) { + const nsStyleSVG* style = aFrame->StyleSVG(); + SVGPaintServerFrame* ps = + SVGObserverUtils::GetAndObservePaintServer(aFrame, aFillOrStroke); + + if (ps) { + RefPtr<gfxPattern> pattern = + ps->GetPaintServerPattern(aFrame, aDrawTarget, aContextMatrix, + aFillOrStroke, aOpacity, aImgParams); + + if (pattern) { + aTargetPaint.SetPaintServer(aFrame, aContextMatrix, ps); + return; + } + } + + if (aOuterContextPaint) { + RefPtr<gfxPattern> pattern; + auto tag = SVGContextPaintImpl::Paint::Tag::None; + switch ((style->*aFillOrStroke).kind.tag) { + case StyleSVGPaintKind::Tag::ContextFill: + tag = SVGContextPaintImpl::Paint::Tag::ContextFill; + pattern = aOuterContextPaint->GetFillPattern( + aDrawTarget, aOpacity, aContextMatrix, aImgParams); + break; + case StyleSVGPaintKind::Tag::ContextStroke: + tag = SVGContextPaintImpl::Paint::Tag::ContextStroke; + pattern = aOuterContextPaint->GetStrokePattern( + aDrawTarget, aOpacity, aContextMatrix, aImgParams); + break; + default:; + } + if (pattern) { + aTargetPaint.SetContextPaint(aOuterContextPaint, tag); + return; + } + } + + nscolor color = SVGUtils::GetFallbackOrPaintColor( + *aFrame->Style(), aFillOrStroke, aDefaultFallbackColor); + aTargetPaint.SetColor(color); +} + +DrawMode SVGContextPaintImpl::Init(const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, + nsIFrame* aFrame, + SVGContextPaint* aOuterContextPaint, + imgDrawingParams& aImgParams) { + DrawMode toDraw = DrawMode(0); + + const nsStyleSVG* style = aFrame->StyleSVG(); + + // fill: + if (style->mFill.kind.IsNone()) { + SetFillOpacity(0.0f); + } else { + float opacity = + SVGUtils::GetOpacity(style->mFillOpacity, aOuterContextPaint); + + SetupInheritablePaint(aDrawTarget, aContextMatrix, aFrame, opacity, + aOuterContextPaint, mFillPaint, &nsStyleSVG::mFill, + NS_RGB(0, 0, 0), aImgParams); + + SetFillOpacity(opacity); + + toDraw |= DrawMode::GLYPH_FILL; + } + + // stroke: + if (style->mStroke.kind.IsNone()) { + SetStrokeOpacity(0.0f); + } else { + float opacity = + SVGUtils::GetOpacity(style->mStrokeOpacity, aOuterContextPaint); + + SetupInheritablePaint( + aDrawTarget, aContextMatrix, aFrame, opacity, aOuterContextPaint, + mStrokePaint, &nsStyleSVG::mStroke, NS_RGBA(0, 0, 0, 0), aImgParams); + + SetStrokeOpacity(opacity); + + toDraw |= DrawMode::GLYPH_STROKE; + } + + return toDraw; +} + +void SVGContextPaint::InitStrokeGeometry(gfxContext* aContext, + float devUnitsPerSVGUnit) { + mStrokeWidth = aContext->CurrentLineWidth() / devUnitsPerSVGUnit; + aContext->CurrentDash(mDashes, &mDashOffset); + for (uint32_t i = 0; i < mDashes.Length(); i++) { + mDashes[i] /= devUnitsPerSVGUnit; + } + mDashOffset /= devUnitsPerSVGUnit; +} + +SVGContextPaint* SVGContextPaint::GetContextPaint(nsIContent* aContent) { + dom::Document* ownerDoc = aContent->OwnerDoc(); + + const auto* contextPaint = ownerDoc->GetCurrentContextPaint(); + + // XXX The SVGContextPaint that Document keeps around is const. We could + // and should keep that constness to the SVGContextPaint that we get here + // (SVGImageContext is never changed after it is initialized). + // + // Unfortunately lazy initialization of SVGContextPaint (which is a member of + // SVGImageContext, and also conceptually never changes after construction) + // prevents some of SVGContextPaint's conceptually const methods from being + // const. Trying to fix SVGContextPaint (perhaps by using |mutable|) is a + // bit of a headache so for now we punt on that, don't reapply the constness + // to the SVGContextPaint here, and trust that no one will add code that + // actually modifies the object. + return const_cast<SVGContextPaint*>(contextPaint); +} + +already_AddRefed<gfxPattern> SVGContextPaintImpl::GetFillPattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + return mFillPaint.GetPattern(aDrawTarget, aOpacity, &nsStyleSVG::mFill, aCTM, + aImgParams); +} + +already_AddRefed<gfxPattern> SVGContextPaintImpl::GetStrokePattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + return mStrokePaint.GetPattern(aDrawTarget, aOpacity, &nsStyleSVG::mStroke, + aCTM, aImgParams); +} + +already_AddRefed<gfxPattern> SVGContextPaintImpl::Paint::GetPattern( + const DrawTarget* aDrawTarget, float aOpacity, + StyleSVGPaint nsStyleSVG::*aFillOrStroke, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + RefPtr<gfxPattern> pattern; + if (mPatternCache.Get(aOpacity, getter_AddRefs(pattern))) { + // Set the pattern matrix just in case it was messed with by a previous + // caller. We should get the same matrix each time a pattern is constructed + // so this should be fine. + pattern->SetMatrix(aCTM * mPatternMatrix); + return pattern.forget(); + } + + switch (mPaintType) { + case Tag::None: + pattern = new gfxPattern(DeviceColor()); + mPatternMatrix = gfxMatrix(); + break; + case Tag::Color: { + DeviceColor color = ToDeviceColor(mPaintDefinition.mColor); + color.a *= aOpacity; + pattern = new gfxPattern(color); + mPatternMatrix = gfxMatrix(); + break; + } + case Tag::PaintServer: + pattern = mPaintDefinition.mPaintServerFrame->GetPaintServerPattern( + mFrame, aDrawTarget, mContextMatrix, aFillOrStroke, aOpacity, + aImgParams); + if (!pattern) { + return nullptr; + } + { + // m maps original-user-space to pattern space + gfxMatrix m = pattern->GetMatrix(); + gfxMatrix deviceToOriginalUserSpace = mContextMatrix; + if (!deviceToOriginalUserSpace.Invert()) { + return nullptr; + } + // mPatternMatrix maps device space to pattern space via original user + // space + mPatternMatrix = deviceToOriginalUserSpace * m; + } + pattern->SetMatrix(aCTM * mPatternMatrix); + break; + case Tag::ContextFill: + pattern = mPaintDefinition.mContextPaint->GetFillPattern( + aDrawTarget, aOpacity, aCTM, aImgParams); + // Don't cache this. mContextPaint will have cached it anyway. If we + // cache it, we'll have to compute mPatternMatrix, which is annoying. + return pattern.forget(); + case Tag::ContextStroke: + pattern = mPaintDefinition.mContextPaint->GetStrokePattern( + aDrawTarget, aOpacity, aCTM, aImgParams); + // Don't cache this. mContextPaint will have cached it anyway. If we + // cache it, we'll have to compute mPatternMatrix, which is annoying. + return pattern.forget(); + default: + MOZ_ASSERT(false, "invalid paint type"); + return nullptr; + } + + mPatternCache.InsertOrUpdate(aOpacity, RefPtr{pattern}); + return pattern.forget(); +} + +AutoSetRestoreSVGContextPaint::AutoSetRestoreSVGContextPaint( + const SVGContextPaint* aContextPaint, dom::Document* aDocument) + : mDocument(aDocument), + mOuterContextPaint(aDocument->GetCurrentContextPaint()) { + mDocument->SetCurrentContextPaint(aContextPaint); +} + +AutoSetRestoreSVGContextPaint::~AutoSetRestoreSVGContextPaint() { + mDocument->SetCurrentContextPaint(mOuterContextPaint); +} + +// SVGEmbeddingContextPaint + +already_AddRefed<gfxPattern> SVGEmbeddingContextPaint::GetFillPattern( + const DrawTarget* aDrawTarget, float aFillOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + if (!mFill) { + return nullptr; + } + // The gfxPattern that we create below depends on aFillOpacity, and since + // different elements in the SVG image may pass in different values for + // fill opacities we don't try to cache the gfxPattern that we create. + DeviceColor fill = *mFill; + fill.a *= aFillOpacity; + return do_AddRef(new gfxPattern(fill)); +} + +already_AddRefed<gfxPattern> SVGEmbeddingContextPaint::GetStrokePattern( + const DrawTarget* aDrawTarget, float aStrokeOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + if (!mStroke) { + return nullptr; + } + DeviceColor stroke = *mStroke; + stroke.a *= aStrokeOpacity; + return do_AddRef(new gfxPattern(stroke)); +} + +uint32_t SVGEmbeddingContextPaint::Hash() const { + uint32_t hash = 0; + + if (mFill) { + hash = HashGeneric(hash, mFill->ToABGR()); + } else { + // Arbitrary number, just to avoid trivial hash collisions between pairs of + // instances where one embedding context has fill set to the same value as + // another context has stroke set to. + hash = 1; + } + + if (mStroke) { + hash = HashGeneric(hash, mStroke->ToABGR()); + } + + if (mFillOpacity != 1.0f) { + hash = HashGeneric(hash, mFillOpacity); + } + + if (mStrokeOpacity != 1.0f) { + hash = HashGeneric(hash, mStrokeOpacity); + } + + return hash; +} + +} // namespace mozilla diff --git a/layout/svg/SVGContextPaint.h b/layout/svg/SVGContextPaint.h new file mode 100644 index 0000000000..1264bf706f --- /dev/null +++ b/layout/svg/SVGContextPaint.h @@ -0,0 +1,287 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGCONTEXTPAINT_H_ +#define LAYOUT_SVG_SVGCONTEXTPAINT_H_ + +#include "DrawMode.h" +#include "gfxMatrix.h" +#include "gfxPattern.h" +#include "gfxTypes.h" +#include "gfxUtils.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/gfx/2D.h" +#include "nsColor.h" +#include "nsStyleStruct.h" +#include "nsTArray.h" +#include "ImgDrawResult.h" +#include "nsRefPtrHashtable.h" + +class gfxContext; + +namespace mozilla { +class SVGPaintServerFrame; + +namespace dom { +class Document; +} + +/** + * This class is used to pass information about a context element through to + * SVG painting code in order to resolve the 'context-fill' and related + * keywords. See: + * + * https://www.w3.org/TR/SVG2/painting.html#context-paint + * + * This feature allows the color in an SVG-in-OpenType glyph to come from the + * computed style for the text that is being drawn, for example, or for color + * in an SVG embedded by an <img> element to come from the embedding <img> + * element. + * + * This class is reference counted so that it can be shared among many similar + * SVGImageContext objects. (SVGImageContext objects are frequently + * copy-constructed with small modifications, and we'd like for those copies to + * be able to share their context-paint data cheaply.) However, in most cases, + * SVGContextPaint instances are stored in a local RefPtr and only last for the + * duration of a function call. + * XXX Note: SVGImageContext doesn't actually have a SVGContextPaint member yet, + * but it will in a later patch in the patch series that added this comment. + */ +class SVGContextPaint : public RefCounted<SVGContextPaint> { + protected: + using DrawTarget = mozilla::gfx::DrawTarget; + using Float = mozilla::gfx::Float; + using imgDrawingParams = mozilla::image::imgDrawingParams; + + SVGContextPaint() : mDashOffset(0.0f), mStrokeWidth(0.0f) {} + + public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(SVGContextPaint) + + virtual ~SVGContextPaint() = default; + + virtual already_AddRefed<gfxPattern> GetFillPattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) = 0; + virtual already_AddRefed<gfxPattern> GetStrokePattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) = 0; + virtual float GetFillOpacity() const = 0; + virtual float GetStrokeOpacity() const = 0; + + already_AddRefed<gfxPattern> GetFillPattern(const DrawTarget* aDrawTarget, + const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + return GetFillPattern(aDrawTarget, GetFillOpacity(), aCTM, aImgParams); + } + + already_AddRefed<gfxPattern> GetStrokePattern(const DrawTarget* aDrawTarget, + const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) { + return GetStrokePattern(aDrawTarget, GetStrokeOpacity(), aCTM, aImgParams); + } + + static SVGContextPaint* GetContextPaint(nsIContent* aContent); + + // XXX This gets the geometry params from the gfxContext. We should get that + // information from the actual paint context! + void InitStrokeGeometry(gfxContext* aContext, float devUnitsPerSVGUnit); + + const FallibleTArray<Float>& GetStrokeDashArray() const { return mDashes; } + + Float GetStrokeDashOffset() const { return mDashOffset; } + + Float GetStrokeWidth() const { return mStrokeWidth; } + + virtual uint32_t Hash() const { + MOZ_ASSERT_UNREACHABLE( + "Only VectorImage needs to hash, and that should " + "only be operating on our SVGEmbeddingContextPaint " + "subclass"); + return 0; + } + + /** + * Returns true if image context paint is allowed to be used in an image that + * has the given URI, else returns false. + */ + static bool IsAllowedForImageFromURI(nsIURI* aURI); + + private: + // Member-vars are initialized in InitStrokeGeometry. + FallibleTArray<Float> mDashes; + MOZ_INIT_OUTSIDE_CTOR Float mDashOffset; + MOZ_INIT_OUTSIDE_CTOR Float mStrokeWidth; +}; + +/** + * RAII class used to temporarily set and remove an SVGContextPaint while a + * piece of SVG is being painted. The context paint is set on the SVG's owner + * document, as expected by SVGContextPaint::GetContextPaint. Any pre-existing + * context paint is restored after this class removes the context paint that it + * set. + */ +class MOZ_RAII AutoSetRestoreSVGContextPaint { + public: + AutoSetRestoreSVGContextPaint(const SVGContextPaint* aContextPaint, + dom::Document* aDocument); + ~AutoSetRestoreSVGContextPaint(); + + private: + dom::Document* mDocument; + // The context paint that needs to be restored by our dtor after it removes + // aContextPaint: + const SVGContextPaint* mOuterContextPaint; +}; + +/** + * This class should be flattened into SVGContextPaint once we get rid of the + * other sub-class (SimpleTextContextPaint). + */ +struct SVGContextPaintImpl : public SVGContextPaint { + protected: + using DrawTarget = mozilla::gfx::DrawTarget; + + public: + DrawMode Init(const DrawTarget* aDrawTarget, const gfxMatrix& aContextMatrix, + nsIFrame* aFrame, SVGContextPaint* aOuterContextPaint, + imgDrawingParams& aImgParams); + + already_AddRefed<gfxPattern> GetFillPattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) override; + already_AddRefed<gfxPattern> GetStrokePattern( + const DrawTarget* aDrawTarget, float aOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) override; + + void SetFillOpacity(float aOpacity) { mFillOpacity = aOpacity; } + float GetFillOpacity() const override { return mFillOpacity; } + + void SetStrokeOpacity(float aOpacity) { mStrokeOpacity = aOpacity; } + float GetStrokeOpacity() const override { return mStrokeOpacity; } + + struct Paint { + enum class Tag : uint8_t { + None, + Color, + PaintServer, + ContextFill, + ContextStroke, + }; + + Paint() : mPaintDefinition{}, mPaintType(Tag::None) {} + + void SetPaintServer(nsIFrame* aFrame, const gfxMatrix& aContextMatrix, + SVGPaintServerFrame* aPaintServerFrame) { + mPaintType = Tag::PaintServer; + mPaintDefinition.mPaintServerFrame = aPaintServerFrame; + mFrame = aFrame; + mContextMatrix = aContextMatrix; + } + + void SetColor(const nscolor& aColor) { + mPaintType = Tag::Color; + mPaintDefinition.mColor = aColor; + } + + void SetContextPaint(SVGContextPaint* aContextPaint, Tag aTag) { + MOZ_ASSERT(aTag == Tag::ContextFill || aTag == Tag::ContextStroke); + mPaintType = aTag; + mPaintDefinition.mContextPaint = aContextPaint; + } + + union { + SVGPaintServerFrame* mPaintServerFrame; + SVGContextPaint* mContextPaint; + nscolor mColor; + } mPaintDefinition; + + // Initialized (if needed) in SetPaintServer(): + MOZ_INIT_OUTSIDE_CTOR nsIFrame* mFrame; + // CTM defining the user space for the pattern we will use. + gfxMatrix mContextMatrix; + Tag mPaintType; + + // Device-space-to-pattern-space + gfxMatrix mPatternMatrix; + nsRefPtrHashtable<nsFloatHashKey, gfxPattern> mPatternCache; + + already_AddRefed<gfxPattern> GetPattern( + const DrawTarget* aDrawTarget, float aOpacity, + StyleSVGPaint nsStyleSVG::*aFillOrStroke, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams); + }; + + Paint mFillPaint; + Paint mStrokePaint; + + float mFillOpacity; + float mStrokeOpacity; +}; + +/** + * This class is used to pass context paint to an SVG image when an element + * references that image (e.g. via HTML <img> or SVG <image>, or by referencing + * it from a CSS property such as 'background-image'). In this case we only + * support context colors and not paint servers. + */ +class SVGEmbeddingContextPaint : public SVGContextPaint { + using DeviceColor = gfx::DeviceColor; + + public: + SVGEmbeddingContextPaint() : mFillOpacity(1.0f), mStrokeOpacity(1.0f) {} + + bool operator==(const SVGEmbeddingContextPaint& aOther) const { + MOZ_ASSERT(GetStrokeWidth() == aOther.GetStrokeWidth() && + GetStrokeDashOffset() == aOther.GetStrokeDashOffset() && + GetStrokeDashArray() == aOther.GetStrokeDashArray(), + "We don't currently include these in the context information " + "from an embedding element"); + return mFill == aOther.mFill && mStroke == aOther.mStroke && + mFillOpacity == aOther.mFillOpacity && + mStrokeOpacity == aOther.mStrokeOpacity; + } + + void SetFill(nscolor aFill) { mFill.emplace(gfx::ToDeviceColor(aFill)); } + const Maybe<DeviceColor>& GetFill() const { return mFill; } + void SetStroke(nscolor aStroke) { + mStroke.emplace(gfx::ToDeviceColor(aStroke)); + } + const Maybe<DeviceColor>& GetStroke() const { return mStroke; } + + /** + * Returns a pattern of type PatternType::COLOR, or else nullptr. + */ + already_AddRefed<gfxPattern> GetFillPattern( + const DrawTarget* aDrawTarget, float aFillOpacity, const gfxMatrix& aCTM, + imgDrawingParams& aImgParams) override; + + /** + * Returns a pattern of type PatternType::COLOR, or else nullptr. + */ + already_AddRefed<gfxPattern> GetStrokePattern( + const DrawTarget* aDrawTarget, float aStrokeOpacity, + const gfxMatrix& aCTM, imgDrawingParams& aImgParams) override; + + void SetFillOpacity(float aOpacity) { mFillOpacity = aOpacity; } + float GetFillOpacity() const override { return mFillOpacity; }; + + void SetStrokeOpacity(float aOpacity) { mStrokeOpacity = aOpacity; } + float GetStrokeOpacity() const override { return mStrokeOpacity; }; + + uint32_t Hash() const override; + + private: + Maybe<DeviceColor> mFill; + Maybe<DeviceColor> mStroke; + float mFillOpacity; + float mStrokeOpacity; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGCONTEXTPAINT_H_ diff --git a/layout/svg/SVGFEContainerFrame.cpp b/layout/svg/SVGFEContainerFrame.cpp new file mode 100644 index 0000000000..36d018c657 --- /dev/null +++ b/layout/svg/SVGFEContainerFrame.cpp @@ -0,0 +1,107 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/dom/SVGFilters.h" +#include "nsContainerFrame.h" +#include "nsGkAtoms.h" +#include "nsIFrame.h" +#include "nsLiteralString.h" + +using namespace mozilla::dom; + +nsIFrame* NS_NewSVGFEContainerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +/* + * This frame is used by filter primitive elements that + * have special child elements that provide parameters. + */ +class SVGFEContainerFrame final : public nsContainerFrame { + friend nsIFrame* ::NS_NewSVGFEContainerFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGFEContainerFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGFEContainerFrame) + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsContainerFrame::IsFrameOfType(aFlags & ~nsIFrame::eSVG); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGFEContainer"_ns, aResult); + } +#endif + +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + bool ComputeCustomOverflow(OverflowAreas& aOverflowAreas) override { + // We don't maintain a ink overflow rect + return false; + } +}; + +} // namespace mozilla + +nsIFrame* NS_NewSVGFEContainerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGFEContainerFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGFEContainerFrame) + +#ifdef DEBUG +void SVGFEContainerFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsCOMPtr<SVGFE> filterPrimitive = do_QueryInterface(aContent); + NS_ASSERTION(filterPrimitive, + "Trying to construct an SVGFEContainerFrame for a " + "content element that doesn't support the right interfaces"); + + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGFEContainerFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + dom::SVGFE* element = static_cast<dom::SVGFE*>(GetContent()); + if (element->AttributeAffectsRendering(aNameSpaceID, aAttribute)) { + MOZ_ASSERT( + GetParent()->IsSVGFilterFrame(), + "Observers observe the filter, so that's what we must invalidate"); + SVGObserverUtils::InvalidateDirectRenderingObservers(GetParent()); + } + + return nsContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +} // namespace mozilla diff --git a/layout/svg/SVGFEImageFrame.cpp b/layout/svg/SVGFEImageFrame.cpp new file mode 100644 index 0000000000..2070348db5 --- /dev/null +++ b/layout/svg/SVGFEImageFrame.cpp @@ -0,0 +1,170 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/dom/SVGFEImageElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "nsContainerFrame.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" +#include "nsLiteralString.h" + +using namespace mozilla::dom; + +nsIFrame* NS_NewSVGFEImageFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGFEImageFrame final : public nsIFrame { + friend nsIFrame* ::NS_NewSVGFEImageFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGFEImageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + + // This frame isn't actually displayed, but it contains an image and we want + // to use the nsImageLoadingContent machinery for managing images, which + // requires visibility tracking, so we enable visibility tracking and + // forcibly mark it visible below. + EnableVisibilityTracking(); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGFEImageFrame) + + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + void Destroy(DestroyContext&) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~(nsIFrame::eSVG)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGFEImage"_ns, aResult); + } +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + void OnVisibilityChange( + Visibility aNewVisibility, + const Maybe<OnNonvisible>& aNonvisibleAction = Nothing()) override; + + bool ComputeCustomOverflow(OverflowAreas& aOverflowAreas) override { + // We don't maintain a ink overflow rect + return false; + } +}; + +} // namespace mozilla + +nsIFrame* NS_NewSVGFEImageFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGFEImageFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGFEImageFrame) + +/* virtual */ +void SVGFEImageFrame::Destroy(DestroyContext& aContext) { + DecApproximateVisibleCount(); + + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(nsIFrame::mContent); + if (imageLoader) { + imageLoader->FrameDestroyed(this); + } + + nsIFrame::Destroy(aContext); +} + +void SVGFEImageFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::feImage), + "Trying to construct an SVGFEImageFrame for a " + "content element that doesn't support the right interfaces"); + + nsIFrame::Init(aContent, aParent, aPrevInFlow); + + // We assume that feImage's are always visible. + // This call must happen before the FrameCreated. This is because the + // primary frame pointer on our content node isn't set until after this + // function ends, so there is no way for the resulting OnVisibilityChange + // notification to get a frame. FrameCreated has a workaround for this in + // that it passes our frame around so it can be accessed. OnVisibilityChange + // doesn't have that workaround. + IncApproximateVisibleCount(); + + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(nsIFrame::mContent); + if (imageLoader) { + imageLoader->FrameCreated(this); + } +} + +nsresult SVGFEImageFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + SVGFEImageElement* element = static_cast<SVGFEImageElement*>(GetContent()); + if (element->AttributeAffectsRendering(aNameSpaceID, aAttribute)) { + MOZ_ASSERT( + GetParent()->IsSVGFilterFrame(), + "Observers observe the filter, so that's what we must invalidate"); + SVGObserverUtils::InvalidateDirectRenderingObservers(GetParent()); + } + + // Currently our SMIL implementation does not modify the DOM attributes. Once + // we implement the SVG 2 SMIL behaviour this can be removed + // SVGFEImageElement::AfterSetAttr's implementation will be sufficient. + if (aModType == MutationEvent_Binding::SMIL && + aAttribute == nsGkAtoms::href && + (aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None)) { + bool hrefIsSet = + element->mStringAttributes[SVGFEImageElement::HREF].IsExplicitlySet() || + element->mStringAttributes[SVGFEImageElement::XLINK_HREF] + .IsExplicitlySet(); + if (hrefIsSet) { + element->LoadSVGImage(true, true); + } else { + element->CancelImageRequests(true); + } + } + + return nsIFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +void SVGFEImageFrame::OnVisibilityChange( + Visibility aNewVisibility, const Maybe<OnNonvisible>& aNonvisibleAction) { + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(nsIFrame::mContent); + if (!imageLoader) { + MOZ_ASSERT_UNREACHABLE("Should have an nsIImageLoadingContent"); + nsIFrame::OnVisibilityChange(aNewVisibility, aNonvisibleAction); + return; + } + + imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction); + + nsIFrame::OnVisibilityChange(aNewVisibility, aNonvisibleAction); +} + +} // namespace mozilla diff --git a/layout/svg/SVGFELeafFrame.cpp b/layout/svg/SVGFELeafFrame.cpp new file mode 100644 index 0000000000..c49ae429b4 --- /dev/null +++ b/layout/svg/SVGFELeafFrame.cpp @@ -0,0 +1,105 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/dom/SVGFilters.h" +#include "ComputedStyle.h" +#include "nsContainerFrame.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" + +using namespace mozilla::dom; + +nsIFrame* NS_NewSVGFELeafFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); +namespace mozilla { + +/* + * This frame is used by filter primitive elements that don't + * have special child elements that provide parameters. + */ +class SVGFELeafFrame final : public nsIFrame { + friend nsIFrame* ::NS_NewSVGFELeafFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGFELeafFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGFELeafFrame) + +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~(nsIFrame::eSVG)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGFELeaf"_ns, aResult); + } +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + bool ComputeCustomOverflow(OverflowAreas& aOverflowAreas) override { + // We don't maintain a ink overflow rect + return false; + } +}; + +} // namespace mozilla + +nsIFrame* NS_NewSVGFELeafFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGFELeafFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGFELeafFrame) + +#ifdef DEBUG +void SVGFELeafFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + nsCOMPtr<SVGFE> filterPrimitive = do_QueryInterface(aContent); + NS_ASSERTION(filterPrimitive, + "Trying to construct an SVGFELeafFrame for a " + "content element that doesn't support the right interfaces"); + + nsIFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGFELeafFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + auto* element = static_cast<mozilla::dom::SVGFE*>(GetContent()); + if (element->AttributeAffectsRendering(aNameSpaceID, aAttribute)) { + MOZ_ASSERT( + GetParent()->IsSVGFilterFrame(), + "Observers observe the filter, so that's what we must invalidate"); + SVGObserverUtils::InvalidateDirectRenderingObservers(GetParent()); + } + + return nsIFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +} // namespace mozilla diff --git a/layout/svg/SVGFEUnstyledLeafFrame.cpp b/layout/svg/SVGFEUnstyledLeafFrame.cpp new file mode 100644 index 0000000000..3d103cb920 --- /dev/null +++ b/layout/svg/SVGFEUnstyledLeafFrame.cpp @@ -0,0 +1,88 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "mozilla/dom/SVGFilters.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "nsContainerFrame.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" + +nsIFrame* NS_NewSVGFEUnstyledLeafFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGFEUnstyledLeafFrame final : public nsIFrame { + friend nsIFrame* ::NS_NewSVGFEUnstyledLeafFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + protected: + explicit SVGFEUnstyledLeafFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGFEUnstyledLeafFrame) + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~(nsIFrame::eSVG)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGFEUnstyledLeaf"_ns, aResult); + } +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + bool ComputeCustomOverflow(OverflowAreas& aOverflowAreas) override { + // We don't maintain a ink overflow rect + return false; + } +}; + +} // namespace mozilla + +nsIFrame* NS_NewSVGFEUnstyledLeafFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGFEUnstyledLeafFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGFEUnstyledLeafFrame) + +nsresult SVGFEUnstyledLeafFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + auto* element = + static_cast<mozilla::dom::SVGFEUnstyledElement*>(GetContent()); + if (element->AttributeAffectsRendering(aNameSpaceID, aAttribute)) { + MOZ_ASSERT( + GetParent()->GetParent()->IsSVGFilterFrame(), + "Observers observe the filter, so that's what we must invalidate"); + SVGObserverUtils::InvalidateDirectRenderingObservers( + GetParent()->GetParent()); + } + + return nsIFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +} // namespace mozilla diff --git a/layout/svg/SVGFilterFrame.cpp b/layout/svg/SVGFilterFrame.cpp new file mode 100644 index 0000000000..d134e16e30 --- /dev/null +++ b/layout/svg/SVGFilterFrame.cpp @@ -0,0 +1,177 @@ +/* -*- 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 "SVGFilterFrame.h" + +// Keep others in (case-insensitive) order: +#include "AutoReferenceChainGuard.h" +#include "gfxUtils.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/SVGFilterElement.h" +#include "nsGkAtoms.h" +#include "SVGObserverUtils.h" +#include "SVGElement.h" +#include "SVGFilterInstance.h" +#include "nsContentUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +nsIFrame* NS_NewSVGFilterFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGFilterFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGFilterFrame) + +uint16_t SVGFilterFrame::GetEnumValue(uint32_t aIndex, nsIContent* aDefault) { + SVGAnimatedEnumeration& thisEnum = + static_cast<SVGFilterElement*>(GetContent())->mEnumAttributes[aIndex]; + + if (thisEnum.IsExplicitlySet()) { + return thisEnum.GetAnimValue(); + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGFilterElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); + } + + SVGFilterFrame* next = GetReferencedFilter(); + + return next ? next->GetEnumValue(aIndex, aDefault) + : static_cast<SVGFilterElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); +} + +const SVGAnimatedLength* SVGFilterFrame::GetLengthValue(uint32_t aIndex, + nsIContent* aDefault) { + const SVGAnimatedLength* thisLength = + &static_cast<SVGFilterElement*>(GetContent())->mLengthAttributes[aIndex]; + + if (thisLength->IsExplicitlySet()) { + return thisLength; + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return &static_cast<SVGFilterElement*>(aDefault)->mLengthAttributes[aIndex]; + } + + SVGFilterFrame* next = GetReferencedFilter(); + + return next ? next->GetLengthValue(aIndex, aDefault) + : &static_cast<SVGFilterElement*>(aDefault) + ->mLengthAttributes[aIndex]; +} + +const SVGFilterElement* SVGFilterFrame::GetFilterContent(nsIContent* aDefault) { + for (nsIContent* child = mContent->GetFirstChild(); child; + child = child->GetNextSibling()) { + RefPtr<SVGFE> primitive; + CallQueryInterface(child, (SVGFE**)getter_AddRefs(primitive)); + if (primitive) { + return static_cast<SVGFilterElement*>(GetContent()); + } + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGFilterElement*>(aDefault); + } + + SVGFilterFrame* next = GetReferencedFilter(); + + return next ? next->GetFilterContent(aDefault) + : static_cast<SVGFilterElement*>(aDefault); +} + +SVGFilterFrame* SVGFilterFrame::GetReferencedFilter() { + if (mNoHRefURI) { + return nullptr; + } + + auto GetHref = [this](nsAString& aHref) { + SVGFilterElement* filter = static_cast<SVGFilterElement*>(GetContent()); + if (filter->mStringAttributes[SVGFilterElement::HREF].IsExplicitlySet()) { + filter->mStringAttributes[SVGFilterElement::HREF].GetAnimValue(aHref, + filter); + } else { + filter->mStringAttributes[SVGFilterElement::XLINK_HREF].GetAnimValue( + aHref, filter); + } + this->mNoHRefURI = aHref.IsEmpty(); + }; + + nsIFrame* tframe = SVGObserverUtils::GetAndObserveTemplate(this, GetHref); + if (tframe) { + LayoutFrameType frameType = tframe->Type(); + if (frameType == LayoutFrameType::SVGFilter) { + return static_cast<SVGFilterFrame*>(tframe); + } + // We don't call SVGObserverUtils::RemoveTemplateObserver and set + // `mNoHRefURI = false` here since we want to be invalidated if the ID + // specified by our href starts resolving to a different/valid element. + } + + return nullptr; +} + +nsresult SVGFilterFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::x || aAttribute == nsGkAtoms::y || + aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::filterUnits || + aAttribute == nsGkAtoms::primitiveUnits)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } else if ((aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None) && + aAttribute == nsGkAtoms::href) { + // Blow away our reference, if any + SVGObserverUtils::RemoveTemplateObserver(this); + mNoHRefURI = false; + // And update whoever references us + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + return SVGContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +#ifdef DEBUG +void SVGFilterFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::filter), + "Content is not an SVG filter"); + + SVGContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +} // namespace mozilla diff --git a/layout/svg/SVGFilterFrame.h b/layout/svg/SVGFilterFrame.h new file mode 100644 index 0000000000..d6839c3ff4 --- /dev/null +++ b/layout/svg/SVGFilterFrame.h @@ -0,0 +1,93 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGFILTERFRAME_H_ +#define LAYOUT_SVG_SVGFILTERFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SVGContainerFrame.h" +#include "nsQueryFrame.h" + +class nsAtom; +class nsIContent; +class nsIFrame; + +struct nsRect; + +namespace mozilla { +class SVGAnimatedLength; +class SVGFilterInstance; +class PresShell; + +namespace dom { +class SVGFilterElement; +} // namespace dom +} // namespace mozilla + +nsIFrame* NS_NewSVGFilterFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGFilterFrame final : public SVGContainerFrame { + friend nsIFrame* ::NS_NewSVGFilterFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGFilterFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGContainerFrame(aStyle, aPresContext, kClassID), + mLoopFlag(false), + mNoHRefURI(false) { + AddStateBits(NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGFilterFrame) + + // nsIFrame methods: + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + private: + friend class SVGFilterInstance; + + /** + * Parses this frame's href and - if it references another filter - returns + * it. It also makes this frame a rendering observer of the specified ID. + */ + SVGFilterFrame* GetReferencedFilter(); + + // Accessors to lookup filter attributes + uint16_t GetEnumValue(uint32_t aIndex, nsIContent* aDefault); + uint16_t GetEnumValue(uint32_t aIndex) { + return GetEnumValue(aIndex, mContent); + } + const mozilla::SVGAnimatedLength* GetLengthValue(uint32_t aIndex, + nsIContent* aDefault); + const mozilla::SVGAnimatedLength* GetLengthValue(uint32_t aIndex) { + return GetLengthValue(aIndex, mContent); + } + const mozilla::dom::SVGFilterElement* GetFilterContent(nsIContent* aDefault); + const mozilla::dom::SVGFilterElement* GetFilterContent() { + return GetFilterContent(mContent); + } + + // This flag is used to detect loops in xlink:href processing + bool mLoopFlag; + bool mNoHRefURI; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGFILTERFRAME_H_ diff --git a/layout/svg/SVGFilterInstance.cpp b/layout/svg/SVGFilterInstance.cpp new file mode 100644 index 0000000000..67288d59e9 --- /dev/null +++ b/layout/svg/SVGFilterInstance.cpp @@ -0,0 +1,447 @@ +/* -*- 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 "SVGFilterInstance.h" + +// Keep others in (case-insensitive) order: +#include "gfxPlatform.h" +#include "gfxUtils.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/IDTracker.h" +#include "mozilla/dom/SVGLengthBinding.h" +#include "mozilla/dom/SVGUnitTypesBinding.h" +#include "mozilla/dom/SVGFilterElement.h" +#include "SVGFilterFrame.h" +#include "FilterSupport.h" +#include "gfx2DGlue.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGUnitTypes_Binding; +using namespace mozilla::gfx; + +namespace mozilla { + +SVGFilterInstance::SVGFilterInstance( + const StyleFilter& aFilter, nsIFrame* aTargetFrame, + nsIContent* aTargetContent, const UserSpaceMetrics& aMetrics, + const gfxRect& aTargetBBox, + const MatrixScalesDouble& aUserSpaceToFilterSpaceScale) + : mFilter(aFilter), + mTargetContent(aTargetContent), + mMetrics(aMetrics), + mTargetBBox(aTargetBBox), + mUserSpaceToFilterSpaceScale(aUserSpaceToFilterSpaceScale), + mSourceAlphaAvailable(false), + mInitialized(false) { + // Get the filter frame. + mFilterFrame = GetFilterFrame(aTargetFrame); + if (!mFilterFrame) { + return; + } + + // Get the filter element. + mFilterElement = mFilterFrame->GetFilterContent(); + if (!mFilterElement) { + MOZ_ASSERT_UNREACHABLE("filter frame should have a related element"); + return; + } + + mPrimitiveUnits = + mFilterFrame->GetEnumValue(SVGFilterElement::PRIMITIVEUNITS); + + if (!ComputeBounds()) { + return; + } + + mInitialized = true; +} + +bool SVGFilterInstance::ComputeBounds() { + // XXX if filterUnits is set (or has defaulted) to objectBoundingBox, we + // should send a warning to the error console if the author has used lengths + // with units. This is a common mistake and can result in the filter region + // being *massive* below (because we ignore the units and interpret the number + // as a factor of the bbox width/height). We should also send a warning if the + // user uses a number without units (a future SVG spec should really + // deprecate that, since it's too confusing for a bare number to be sometimes + // interpreted as a fraction of the bounding box and sometimes as user-space + // units). So really only percentage values should be used in this case. + + // Set the user space bounds (i.e. the filter region in user space). + SVGAnimatedLength XYWH[4]; + static_assert(sizeof(mFilterElement->mLengthAttributes) == sizeof(XYWH), + "XYWH size incorrect"); + memcpy(XYWH, mFilterElement->mLengthAttributes, + sizeof(mFilterElement->mLengthAttributes)); + XYWH[0] = *mFilterFrame->GetLengthValue(SVGFilterElement::ATTR_X); + XYWH[1] = *mFilterFrame->GetLengthValue(SVGFilterElement::ATTR_Y); + XYWH[2] = *mFilterFrame->GetLengthValue(SVGFilterElement::ATTR_WIDTH); + XYWH[3] = *mFilterFrame->GetLengthValue(SVGFilterElement::ATTR_HEIGHT); + uint16_t filterUnits = + mFilterFrame->GetEnumValue(SVGFilterElement::FILTERUNITS); + gfxRect userSpaceBounds = + SVGUtils::GetRelativeRect(filterUnits, XYWH, mTargetBBox, mMetrics); + + // Transform the user space bounds to filter space, so we + // can align them with the pixel boundaries of the offscreen surface. + // The offscreen surface has the same scale as filter space. + gfxRect filterSpaceBounds = UserSpaceToFilterSpace(userSpaceBounds); + filterSpaceBounds.RoundOut(); + if (filterSpaceBounds.width <= 0 || filterSpaceBounds.height <= 0) { + // 0 disables rendering, < 0 is error. dispatch error console warning + // or error as appropriate. + return false; + } + + // Set the filter space bounds. + if (!gfxUtils::GfxRectToIntRect(filterSpaceBounds, &mFilterSpaceBounds)) { + // The filter region is way too big if there is float -> int overflow. + return false; + } + + return true; +} + +SVGFilterFrame* SVGFilterInstance::GetFilterFrame(nsIFrame* aTargetFrame) { + if (!mFilter.IsUrl()) { + // The filter is not an SVG reference filter. + return nullptr; + } + + // Get the target element to use as a point of reference for looking up the + // filter element. + if (!mTargetContent) { + return nullptr; + } + + // aTargetFrame can be null if this filter belongs to a + // CanvasRenderingContext2D. + nsCOMPtr<nsIURI> url; + if (aTargetFrame) { + RefPtr<URLAndReferrerInfo> urlExtraReferrer = + SVGObserverUtils::GetFilterURI(aTargetFrame, mFilter); + + // urlExtraReferrer might be null when mFilter has an invalid url + if (!urlExtraReferrer) { + return nullptr; + } + + url = urlExtraReferrer->GetURI(); + } else { + url = mFilter.AsUrl().ResolveLocalRef(mTargetContent); + } + + if (!url) { + return nullptr; + } + + // Look up the filter element by URL. + IDTracker idTracker; + bool watch = false; + idTracker.ResetToURIFragmentID( + mTargetContent, url, mFilter.AsUrl().ExtraData().ReferrerInfo(), watch); + Element* element = idTracker.get(); + if (!element) { + // The URL points to no element. + return nullptr; + } + + // Get the frame of the filter element. + nsIFrame* frame = element->GetPrimaryFrame(); + if (!frame || !frame->IsSVGFilterFrame()) { + // The URL points to an element that's not an SVG filter element, or to an + // element that hasn't had its frame constructed yet. + return nullptr; + } + + return static_cast<SVGFilterFrame*>(frame); +} + +float SVGFilterInstance::GetPrimitiveNumber(uint8_t aCtxType, + float aValue) const { + SVGAnimatedLength val; + val.Init(aCtxType, 0xff, aValue, SVGLength_Binding::SVG_LENGTHTYPE_NUMBER); + + float value; + if (mPrimitiveUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + value = SVGUtils::ObjectSpace(mTargetBBox, &val); + } else { + value = SVGUtils::UserSpace(mMetrics, &val); + } + + switch (aCtxType) { + case SVGContentUtils::X: + return value * static_cast<float>(mUserSpaceToFilterSpaceScale.xScale); + case SVGContentUtils::Y: + return value * static_cast<float>(mUserSpaceToFilterSpaceScale.yScale); + case SVGContentUtils::XY: + default: + return value * SVGContentUtils::ComputeNormalizedHypotenuse( + mUserSpaceToFilterSpaceScale.xScale, + mUserSpaceToFilterSpaceScale.yScale); + } +} + +Point3D SVGFilterInstance::ConvertLocation(const Point3D& aPoint) const { + SVGAnimatedLength val[4]; + val[0].Init(SVGContentUtils::X, 0xff, aPoint.x, + SVGLength_Binding::SVG_LENGTHTYPE_NUMBER); + val[1].Init(SVGContentUtils::Y, 0xff, aPoint.y, + SVGLength_Binding::SVG_LENGTHTYPE_NUMBER); + // Dummy width/height values + val[2].Init(SVGContentUtils::X, 0xff, 0, + SVGLength_Binding::SVG_LENGTHTYPE_NUMBER); + val[3].Init(SVGContentUtils::Y, 0xff, 0, + SVGLength_Binding::SVG_LENGTHTYPE_NUMBER); + + gfxRect feArea = + SVGUtils::GetRelativeRect(mPrimitiveUnits, val, mTargetBBox, mMetrics); + gfxRect r = UserSpaceToFilterSpace(feArea); + return Point3D(r.x, r.y, GetPrimitiveNumber(SVGContentUtils::XY, aPoint.z)); +} + +gfxRect SVGFilterInstance::UserSpaceToFilterSpace( + const gfxRect& aUserSpaceRect) const { + gfxRect filterSpaceRect = aUserSpaceRect; + filterSpaceRect.Scale(mUserSpaceToFilterSpaceScale); + return filterSpaceRect; +} + +IntRect SVGFilterInstance::ComputeFilterPrimitiveSubregion( + SVGFE* aFilterElement, + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTArray<int32_t>& aInputIndices) { + SVGFE* fE = aFilterElement; + + IntRect defaultFilterSubregion(0, 0, 0, 0); + if (fE->SubregionIsUnionOfRegions()) { + for (uint32_t i = 0; i < aInputIndices.Length(); ++i) { + int32_t inputIndex = aInputIndices[i]; + bool isStandardInput = + inputIndex < 0 || inputIndex == mSourceGraphicIndex; + IntRect inputSubregion = + isStandardInput ? mFilterSpaceBounds + : aPrimitiveDescrs[inputIndex].PrimitiveSubregion(); + + defaultFilterSubregion = defaultFilterSubregion.Union(inputSubregion); + } + } else { + defaultFilterSubregion = mFilterSpaceBounds; + } + + gfxRect feArea = SVGUtils::GetRelativeRect( + mPrimitiveUnits, &fE->mLengthAttributes[SVGFE::ATTR_X], mTargetBBox, + mMetrics); + Rect region = ToRect(UserSpaceToFilterSpace(feArea)); + + if (!fE->mLengthAttributes[SVGFE::ATTR_X].IsExplicitlySet()) + region.x = defaultFilterSubregion.X(); + if (!fE->mLengthAttributes[SVGFE::ATTR_Y].IsExplicitlySet()) + region.y = defaultFilterSubregion.Y(); + if (!fE->mLengthAttributes[SVGFE::ATTR_WIDTH].IsExplicitlySet()) + region.width = defaultFilterSubregion.Width(); + if (!fE->mLengthAttributes[SVGFE::ATTR_HEIGHT].IsExplicitlySet()) + region.height = defaultFilterSubregion.Height(); + + // We currently require filter primitive subregions to be pixel-aligned. + // Following the spec, any pixel partially in the region is included + // in the region. + region.RoundOut(); + return RoundedToInt(region); +} + +void SVGFilterInstance::GetInputsAreTainted( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTArray<int32_t>& aInputIndices, bool aFilterInputIsTainted, + nsTArray<bool>& aOutInputsAreTainted) { + for (uint32_t i = 0; i < aInputIndices.Length(); i++) { + int32_t inputIndex = aInputIndices[i]; + if (inputIndex < 0) { + aOutInputsAreTainted.AppendElement(aFilterInputIsTainted); + } else { + aOutInputsAreTainted.AppendElement( + aPrimitiveDescrs[inputIndex].IsTainted()); + } + } +} + +static int32_t GetLastResultIndex( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs) { + uint32_t numPrimitiveDescrs = aPrimitiveDescrs.Length(); + return !numPrimitiveDescrs + ? FilterPrimitiveDescription::kPrimitiveIndexSourceGraphic + : numPrimitiveDescrs - 1; +} + +int32_t SVGFilterInstance::GetOrCreateSourceAlphaIndex( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs) { + // If the SourceAlpha index has already been determined or created for this + // SVG filter, just return it. + if (mSourceAlphaAvailable) { + return mSourceAlphaIndex; + } + + // If this is the first filter in the chain, we can just use the + // kPrimitiveIndexSourceAlpha keyword to refer to the SourceAlpha of the + // original image. + if (mSourceGraphicIndex < 0) { + mSourceAlphaIndex = FilterPrimitiveDescription::kPrimitiveIndexSourceAlpha; + mSourceAlphaAvailable = true; + return mSourceAlphaIndex; + } + + // Otherwise, create a primitive description to turn the previous filter's + // output into a SourceAlpha input. + FilterPrimitiveDescription descr(AsVariant(ToAlphaAttributes())); + descr.SetInputPrimitive(0, mSourceGraphicIndex); + + const FilterPrimitiveDescription& sourcePrimitiveDescr = + aPrimitiveDescrs[mSourceGraphicIndex]; + descr.SetPrimitiveSubregion(sourcePrimitiveDescr.PrimitiveSubregion()); + descr.SetIsTainted(sourcePrimitiveDescr.IsTainted()); + + ColorSpace colorSpace = sourcePrimitiveDescr.OutputColorSpace(); + descr.SetInputColorSpace(0, colorSpace); + descr.SetOutputColorSpace(colorSpace); + + aPrimitiveDescrs.AppendElement(std::move(descr)); + mSourceAlphaIndex = aPrimitiveDescrs.Length() - 1; + mSourceAlphaAvailable = true; + return mSourceAlphaIndex; +} + +nsresult SVGFilterInstance::GetSourceIndices( + SVGFE* aPrimitiveElement, + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTHashMap<nsStringHashKey, int32_t>& aImageTable, + nsTArray<int32_t>& aSourceIndices) { + AutoTArray<SVGStringInfo, 2> sources; + aPrimitiveElement->GetSourceImageNames(sources); + + for (uint32_t j = 0; j < sources.Length(); j++) { + nsAutoString str; + sources[j].mString->GetAnimValue(str, sources[j].mElement); + + int32_t sourceIndex = 0; + if (str.EqualsLiteral("SourceGraphic")) { + sourceIndex = mSourceGraphicIndex; + } else if (str.EqualsLiteral("SourceAlpha")) { + sourceIndex = GetOrCreateSourceAlphaIndex(aPrimitiveDescrs); + } else if (str.EqualsLiteral("FillPaint")) { + sourceIndex = FilterPrimitiveDescription::kPrimitiveIndexFillPaint; + } else if (str.EqualsLiteral("StrokePaint")) { + sourceIndex = FilterPrimitiveDescription::kPrimitiveIndexStrokePaint; + } else if (str.EqualsLiteral("BackgroundImage") || + str.EqualsLiteral("BackgroundAlpha")) { + return NS_ERROR_NOT_IMPLEMENTED; + } else if (str.EqualsLiteral("")) { + sourceIndex = GetLastResultIndex(aPrimitiveDescrs); + } else { + bool inputExists = aImageTable.Get(str, &sourceIndex); + if (!inputExists) { + sourceIndex = GetLastResultIndex(aPrimitiveDescrs); + } + } + + aSourceIndices.AppendElement(sourceIndex); + } + return NS_OK; +} + +nsresult SVGFilterInstance::BuildPrimitives( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + nsTArray<RefPtr<SourceSurface>>& aInputImages, bool aInputIsTainted) { + mSourceGraphicIndex = GetLastResultIndex(aPrimitiveDescrs); + + // Clip previous filter's output to this filter's filter region. + if (mSourceGraphicIndex >= 0) { + FilterPrimitiveDescription& sourceDescr = + aPrimitiveDescrs[mSourceGraphicIndex]; + sourceDescr.SetPrimitiveSubregion( + sourceDescr.PrimitiveSubregion().Intersect(mFilterSpaceBounds)); + } + + // Get the filter primitive elements. + AutoTArray<RefPtr<SVGFE>, 8> primitives; + for (nsIContent* child = mFilterElement->nsINode::GetFirstChild(); child; + child = child->GetNextSibling()) { + RefPtr<SVGFE> primitive; + CallQueryInterface(child, (SVGFE**)getter_AddRefs(primitive)); + if (primitive) { + primitives.AppendElement(primitive); + } + } + + // Maps source image name to source index. + nsTHashMap<nsStringHashKey, int32_t> imageTable(8); + + // The principal that we check principals of any loaded images against. + nsCOMPtr<nsIPrincipal> principal = mTargetContent->NodePrincipal(); + + for (uint32_t primitiveElementIndex = 0; + primitiveElementIndex < primitives.Length(); ++primitiveElementIndex) { + SVGFE* filter = primitives[primitiveElementIndex]; + + AutoTArray<int32_t, 2> sourceIndices; + nsresult rv = + GetSourceIndices(filter, aPrimitiveDescrs, imageTable, sourceIndices); + if (NS_FAILED(rv)) { + return rv; + } + + IntRect primitiveSubregion = ComputeFilterPrimitiveSubregion( + filter, aPrimitiveDescrs, sourceIndices); + + AutoTArray<bool, 8> sourcesAreTainted; + GetInputsAreTainted(aPrimitiveDescrs, sourceIndices, aInputIsTainted, + sourcesAreTainted); + + FilterPrimitiveDescription descr = filter->GetPrimitiveDescription( + this, primitiveSubregion, sourcesAreTainted, aInputImages); + + descr.SetIsTainted(filter->OutputIsTainted(sourcesAreTainted, principal)); + descr.SetFilterSpaceBounds(mFilterSpaceBounds); + descr.SetPrimitiveSubregion( + primitiveSubregion.Intersect(descr.FilterSpaceBounds())); + + for (uint32_t i = 0; i < sourceIndices.Length(); i++) { + int32_t inputIndex = sourceIndices[i]; + descr.SetInputPrimitive(i, inputIndex); + + ColorSpace inputColorSpace = + inputIndex >= 0 ? aPrimitiveDescrs[inputIndex].OutputColorSpace() + : ColorSpace(ColorSpace::SRGB); + + ColorSpace desiredInputColorSpace = + filter->GetInputColorSpace(i, inputColorSpace); + descr.SetInputColorSpace(i, desiredInputColorSpace); + if (i == 0) { + // the output color space is whatever in1 is if there is an in1 + descr.SetOutputColorSpace(desiredInputColorSpace); + } + } + + if (sourceIndices.Length() == 0) { + descr.SetOutputColorSpace(filter->GetOutputColorSpace()); + } + + aPrimitiveDescrs.AppendElement(std::move(descr)); + uint32_t primitiveDescrIndex = aPrimitiveDescrs.Length() - 1; + + nsAutoString str; + filter->GetResultImageName().GetAnimValue(str, filter); + imageTable.InsertOrUpdate(str, primitiveDescrIndex); + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/layout/svg/SVGFilterInstance.h b/layout/svg/SVGFilterInstance.h new file mode 100644 index 0000000000..d7cda4ae87 --- /dev/null +++ b/layout/svg/SVGFilterInstance.h @@ -0,0 +1,267 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGFILTERINSTANCE_H_ +#define LAYOUT_SVG_SVGFILTERINSTANCE_H_ + +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "SVGAnimatedNumber.h" +#include "SVGAnimatedNumberPair.h" +#include "SVGFilters.h" +#include "mozilla/ServoStyleConsts.h" + +namespace mozilla { +class SVGFilterFrame; + +namespace dom { +class SVGFilterElement; +} // namespace dom + +/** + * This class helps FilterInstance build its filter graph by processing a + * single SVG reference filter. + * + * In BuildPrimitives, this class iterates through the referenced <filter> + * element's primitive elements, creating a FilterPrimitiveDescription for + * each one. + * + * This class uses several different coordinate spaces, defined as follows: + * + * "user space" + * The filtered SVG element's user space or the filtered HTML element's + * CSS pixel space. The origin for an HTML element is the top left corner of + * its border box. + * + * "filter space" + * User space scaled to device pixels. Shares the same origin as user space. + * This space is the same across chained SVG and CSS filters. To compute the + * overall filter space for a chain, we first need to build each filter's + * FilterPrimitiveDescriptions in some common space. That space is + * filter space. + * + * To understand the spaces better, let's take an example filter: + * <filter id="f">...</filter> + * + * And apply the filter to a div element: + * <div style="filter: url(#f); ...">...</div> + * + * And let's say there are 2 device pixels for every 1 CSS pixel. + * + * Finally, let's define an arbitrary point in user space: + * "user space point" = (10, 10) + * + * The point will be inset 10 CSS pixels from both the top and left edges of the + * div element's border box. + * + * Now, let's transform the point from user space to filter space: + * "filter space point" = "user space point" * "device pixels per CSS pixel" + * "filter space point" = (10, 10) * 2 + * "filter space point" = (20, 20) + */ +class SVGFilterInstance { + using Point3D = gfx::Point3D; + using IntRect = gfx::IntRect; + using SourceSurface = gfx::SourceSurface; + using FilterPrimitiveDescription = gfx::FilterPrimitiveDescription; + using SVGFE = dom::SVGFE; + using UserSpaceMetrics = dom::UserSpaceMetrics; + + public: + /** + * @param aFilter The SVG filter reference from the style system. This class + * stores aFilter by reference, so callers should avoid modifying or + * deleting aFilter during the lifetime of SVGFilterInstance. + * @param aTargetContent The filtered element. + * @param aTargetBBox The SVG bbox to use for the target frame, computed by + * the caller. The caller may decide to override the actual SVG bbox. + */ + SVGFilterInstance( + const StyleFilter& aFilter, nsIFrame* aTargetFrame, + nsIContent* aTargetContent, const UserSpaceMetrics& aMetrics, + const gfxRect& aTargetBBox, + const gfx::MatrixScalesDouble& aUserSpaceToFilterSpaceScale); + + /** + * Returns true if the filter instance was created successfully. + */ + bool IsInitialized() const { return mInitialized; } + + /** + * Iterates through the <filter> element's primitive elements, creating a + * FilterPrimitiveDescription for each one. Appends the new + * FilterPrimitiveDescription(s) to the aPrimitiveDescrs list. Also, appends + * new images from feImage filter primitive elements to the aInputImages list. + * aInputIsTainted describes whether the input to this filter is tainted, i.e. + * whether it contains security-sensitive content. This is needed to propagate + * taintedness to the FilterPrimitive that take tainted inputs. Something + * being tainted means that it contains security sensitive content. The input + * to this filter is the previous filter's output, i.e. the last element in + * aPrimitiveDescrs, or the SourceGraphic input if this is the first filter in + * the filter chain. + */ + nsresult BuildPrimitives( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + nsTArray<RefPtr<SourceSurface>>& aInputImages, bool aInputIsTainted); + + float GetPrimitiveNumber(uint8_t aCtxType, + const SVGAnimatedNumber* aNumber) const { + return GetPrimitiveNumber(aCtxType, aNumber->GetAnimValue()); + } + float GetPrimitiveNumber(uint8_t aCtxType, + const SVGAnimatedNumberPair* aNumberPair, + SVGAnimatedNumberPair::PairIndex aIndex) const { + return GetPrimitiveNumber(aCtxType, aNumberPair->GetAnimValue(aIndex)); + } + + /** + * Converts a userSpaceOnUse/objectBoundingBoxUnits unitless point + * into filter space, depending on the value of mPrimitiveUnits. (For + * objectBoundingBoxUnits, the bounding box offset is applied to the point.) + */ + Point3D ConvertLocation(const Point3D& aPoint) const; + + /** + * Transform a rect between user space and filter space. + */ + gfxRect UserSpaceToFilterSpace(const gfxRect& aUserSpaceRect) const; + + private: + /** + * Finds the filter frame associated with this SVG filter. + */ + SVGFilterFrame* GetFilterFrame(nsIFrame* aTargetFrame); + + /** + * Computes the filter primitive subregion for the given primitive. + */ + IntRect ComputeFilterPrimitiveSubregion( + SVGFE* aFilterElement, + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTArray<int32_t>& aInputIndices); + + /** + * Takes the input indices of a filter primitive and returns for each input + * whether the input's output is tainted. + */ + void GetInputsAreTainted( + const nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTArray<int32_t>& aInputIndices, bool aFilterInputIsTainted, + nsTArray<bool>& aOutInputsAreTainted); + + /** + * Scales a numeric filter primitive length in the X, Y or "XY" directions + * into a length in filter space (no offset is applied). + */ + float GetPrimitiveNumber(uint8_t aCtxType, float aValue) const; + + /** + * Returns the transform from frame space to the coordinate space that + * GetCanvasTM transforms to. "Frame space" is the origin of a frame, aka the + * top-left corner of its border box, aka the top left corner of its mRect. + */ + gfxMatrix GetUserSpaceToFrameSpaceInCSSPxTransform() const; + + /** + * Appends a new FilterPrimitiveDescription to aPrimitiveDescrs that + * converts the FilterPrimitiveDescription at mSourceGraphicIndex into + * a SourceAlpha input for the next FilterPrimitiveDescription. + * + * The new FilterPrimitiveDescription zeros out the SourceGraphic's RGB + * channels and keeps the alpha channel intact. + */ + int32_t GetOrCreateSourceAlphaIndex( + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs); + + /** + * Finds the index in aPrimitiveDescrs of each input to aPrimitiveElement. + * For example, if aPrimitiveElement is: + * <feGaussianBlur in="another-primitive" .../> + * Then, the resulting aSourceIndices will contain the index of the + * FilterPrimitiveDescription representing "another-primitive". + */ + nsresult GetSourceIndices( + SVGFE* aPrimitiveElement, + nsTArray<FilterPrimitiveDescription>& aPrimitiveDescrs, + const nsTHashMap<nsStringHashKey, int32_t>& aImageTable, + nsTArray<int32_t>& aSourceIndices); + + /** + * Compute the filter region in user space, filter space, and filter + * space. + */ + bool ComputeBounds(); + + /** + * The SVG reference filter originally from the style system. + */ + const StyleFilter& mFilter; + + /** + * The filtered element. + */ + nsIContent* mTargetContent; + + /** + * The SVG user space metrics that SVG lengths are resolved against. + */ + const UserSpaceMetrics& mMetrics; + + /** + * The filter element referenced by mTargetFrame's element. + */ + const dom::SVGFilterElement* mFilterElement; + + /** + * The frame for the SVG filter element. + */ + SVGFilterFrame* mFilterFrame; + + /** + * The SVG bbox of the element that is being filtered, in user space. + */ + gfxRect mTargetBBox; + + /** + * The "filter region" in various spaces. + */ + nsIntRect mFilterSpaceBounds; + + /** + * The scale factors between user space and filter space. + */ + gfx::MatrixScalesDouble mUserSpaceToFilterSpaceScale; + + /** + * The 'primitiveUnits' attribute value (objectBoundingBox or userSpaceOnUse). + */ + uint16_t mPrimitiveUnits; + + /** + * The index of the FilterPrimitiveDescription that this SVG filter should use + * as its SourceGraphic, or the SourceGraphic keyword index if this is the + * first filter in a chain. Initialized in BuildPrimitives + */ + MOZ_INIT_OUTSIDE_CTOR int32_t mSourceGraphicIndex; + + /** + * The index of the FilterPrimitiveDescription that this SVG filter should use + * as its SourceAlpha, or the SourceAlpha keyword index if this is the first + * filter in a chain. Initialized in BuildPrimitives + */ + MOZ_INIT_OUTSIDE_CTOR int32_t mSourceAlphaIndex; + + /** + * SourceAlpha is available if GetOrCreateSourceAlphaIndex has been called. + */ + int32_t mSourceAlphaAvailable; + + bool mInitialized; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGFILTERINSTANCE_H_ diff --git a/layout/svg/SVGForeignObjectFrame.cpp b/layout/svg/SVGForeignObjectFrame.cpp new file mode 100644 index 0000000000..576d6a1367 --- /dev/null +++ b/layout/svg/SVGForeignObjectFrame.cpp @@ -0,0 +1,451 @@ +/* -*- 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 "SVGForeignObjectFrame.h" + +// Keep others in (case-insensitive) order: +#include "ImgDrawResult.h" +#include "gfxContext.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGForeignObjectElement.h" +#include "nsDisplayList.h" +#include "nsGkAtoms.h" +#include "nsNameSpaceManager.h" +#include "nsLayoutUtils.h" +#include "nsRegion.h" +#include "SVGGeometryProperty.h" + +using namespace mozilla::dom; +using namespace mozilla::image; +namespace SVGT = SVGGeometryProperty::Tags; + +//---------------------------------------------------------------------- +// Implementation + +nsContainerFrame* NS_NewSVGForeignObjectFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGForeignObjectFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGForeignObjectFrame) + +SVGForeignObjectFrame::SVGForeignObjectFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : nsContainerFrame(aStyle, aPresContext, kClassID), mInReflow(false) { + AddStateBits(NS_FRAME_REFLOW_ROOT | NS_FRAME_MAY_BE_TRANSFORMED | + NS_FRAME_SVG_LAYOUT | NS_FRAME_FONT_INFLATION_CONTAINER | + NS_FRAME_FONT_INFLATION_FLOW_ROOT); +} + +//---------------------------------------------------------------------- +// nsIFrame methods + +NS_QUERYFRAME_HEAD(SVGForeignObjectFrame) + NS_QUERYFRAME_ENTRY(ISVGDisplayableFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) + +void SVGForeignObjectFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::foreignObject), + "Content is not an SVG foreignObject!"); + + nsContainerFrame::Init(aContent, aParent, aPrevInFlow); + AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD); +} + +nsresult SVGForeignObjectFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::transform) { + // 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. + mCanvasTM = nullptr; + } else if (aAttribute == nsGkAtoms::viewBox || + aAttribute == nsGkAtoms::preserveAspectRatio) { + nsLayoutUtils::PostRestyleEvent( + mContent->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + } + } + + return NS_OK; +} + +void SVGForeignObjectFrame::DidSetComputedStyle( + ComputedStyle* aOldComputedStyle) { + nsContainerFrame::DidSetComputedStyle(aOldComputedStyle); + + if (aOldComputedStyle) { + if (StyleSVGReset()->mX != aOldComputedStyle->StyleSVGReset()->mX || + StyleSVGReset()->mY != aOldComputedStyle->StyleSVGReset()->mY) { + // Invalidate cached transform matrix. + mCanvasTM = nullptr; + SVGUtils::ScheduleReflowSVG(this); + } + } +} + +void SVGForeignObjectFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Should not have been called"); + + // Only InvalidateAndScheduleBoundsUpdate marks us with NS_FRAME_IS_DIRTY, + // so if that bit is still set we still have a resize pending. If we hit + // this assertion, then we should get the presShell to skip reflow roots + // that have a dirty parent since a reflow is going to come via the + // reflow root's parent anyway. + NS_ASSERTION(!HasAnyStateBits(NS_FRAME_IS_DIRTY), + "Reflowing while a resize is pending is wasteful"); + + // ReflowSVG makes sure mRect is up to date before we're called. + + NS_ASSERTION(!aReflowInput.mParentReflowInput, + "should only get reflow from being reflow root"); + NS_ASSERTION(aReflowInput.ComputedWidth() == GetSize().width && + aReflowInput.ComputedHeight() == GetSize().height, + "reflow roots should be reflowed at existing size and " + "svg.css should ensure we have no padding/border/margin"); + + DoReflow(); + + WritingMode wm = aReflowInput.GetWritingMode(); + LogicalSize finalSize(wm, aReflowInput.ComputedISize(), + aReflowInput.ComputedBSize()); + aDesiredSize.SetSize(wm, finalSize); + aDesiredSize.SetOverflowAreasToDesiredBounds(); +} + +void SVGForeignObjectFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!static_cast<const SVGElement*>(GetContent())->HasValidDimensions()) { + return; + } + nsDisplayList newList(aBuilder); + nsDisplayListSet set(&newList, &newList, &newList, &newList, &newList, + &newList); + DisplayOutline(aBuilder, set); + BuildDisplayListForNonBlockChildren(aBuilder, set); + aLists.Content()->AppendNewToTop<nsDisplayForeignObject>(aBuilder, this, + &newList); +} + +bool SVGForeignObjectFrame::IsSVGTransformed( + Matrix* aOwnTransform, Matrix* aFromParentTransform) const { + return SVGUtils::IsSVGTransformed(this, aOwnTransform, aFromParentTransform); +} + +void SVGForeignObjectFrame::PaintSVG(gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Only painting of non-display SVG should take this code path"); + + if (IsDisabled()) { + return; + } + + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + if (aTransform.IsSingular()) { + NS_WARNING("Can't render foreignObject element!"); + return; + } + + gfxClipAutoSaveRestore autoSaveClip(&aContext); + + if (StyleDisplay()->IsScrollableOverflow()) { + float x, y, width, height; + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, + SVGT::Height>( + static_cast<SVGElement*>(GetContent()), &x, &y, &width, &height); + + gfxRect clipRect = + SVGUtils::GetClipRectForFrame(this, 0.0f, 0.0f, width, height); + autoSaveClip.TransformedClip(aTransform, clipRect); + } + + // SVG paints in CSS px, but normally frames paint in dev pixels. Here we + // multiply a CSS-px-to-dev-pixel factor onto aTransform so our children + // paint correctly. + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + PresContext()->AppUnitsPerDevPixel()); + gfxMatrix canvasTMForChildren = aTransform; + canvasTMForChildren.PreScale(cssPxPerDevPx, cssPxPerDevPx); + + aContext.Multiply(canvasTMForChildren); + + using PaintFrameFlags = nsLayoutUtils::PaintFrameFlags; + PaintFrameFlags flags = PaintFrameFlags::InTransform; + if (SVGAutoRenderState::IsPaintingToWindow(aContext.GetDrawTarget())) { + flags |= PaintFrameFlags::ToWindow; + } + if (aImgParams.imageFlags & imgIContainer::FLAG_SYNC_DECODE) { + flags |= PaintFrameFlags::SyncDecodeImages; + } + if (aImgParams.imageFlags & imgIContainer::FLAG_HIGH_QUALITY_SCALING) { + flags |= PaintFrameFlags::UseHighQualityScaling; + } + nsLayoutUtils::PaintFrame(&aContext, kid, nsRegion(kid->InkOverflowRect()), + NS_RGBA(0, 0, 0, 0), + nsDisplayListBuilderMode::Painting, flags); +} + +nsIFrame* SVGForeignObjectFrame::GetFrameForPoint(const gfxPoint& aPoint) { + MOZ_ASSERT_UNREACHABLE( + "A clipPath cannot contain an SVGForeignObject element"); + return nullptr; +} + +void SVGForeignObjectFrame::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; + } + + // We update mRect before the DoReflow call so that DoReflow uses the + // correct dimensions: + + float x, y, w, h; + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + static_cast<SVGElement*>(GetContent()), &x, &y, &w, &h); + + // If mRect's width or height are negative, reflow blows up! We must clamp! + if (w < 0.0f) w = 0.0f; + if (h < 0.0f) h = 0.0f; + + mRect = nsLayoutUtils::RoundGfxRectToAppRect(gfxRect(x, y, w, h), + AppUnitsPerCSSPixel()); + + // Fully mark our kid dirty so that it gets resized if necessary + // (NS_FRAME_HAS_DIRTY_CHILDREN isn't enough in that case): + nsIFrame* kid = PrincipalChildList().FirstChild(); + kid->MarkSubtreeDirty(); + + // Make sure to not allow interrupts if we're not being reflown as a root: + nsPresContext::InterruptPreventer noInterrupts(PresContext()); + + DoReflow(); + + 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); + } + + // If we have a filter, we need to invalidate ourselves because filter + // output can change even if none of our descendants need repainting. + if (StyleEffects()->HasFilters()) { + InvalidateFrame(); + } + + auto* anonKid = PrincipalChildList().FirstChild(); + nsRect overflow = anonKid->InkOverflowRect(); + + OverflowAreas overflowAreas(overflow, overflow); + FinishAndStoreOverflow(overflowAreas, mRect.Size()); + + // Now unset the various reflow bits: + RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | + NS_FRAME_HAS_DIRTY_CHILDREN); +} + +void SVGForeignObjectFrame::NotifySVGChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), + "Invalidation logic may need adjusting"); + + bool needNewBounds = false; // i.e. mRect or ink overflow rect + bool needReflow = false; + bool needNewCanvasTM = false; + + if (aFlags & COORD_CONTEXT_CHANGED) { + // Coordinate context changes affect mCanvasTM if we have a + // percentage 'x' or 'y' + if (StyleSVGReset()->mX.HasPercent() || StyleSVGReset()->mY.HasPercent()) { + needNewBounds = true; + needNewCanvasTM = true; + } + + // Our coordinate context's width/height has changed. If we have a + // percentage width/height our dimensions will change so we must reflow. + if (StylePosition()->mWidth.HasPercent() || + StylePosition()->mHeight.HasPercent()) { + needNewBounds = true; + needReflow = true; + } + } + + if (aFlags & TRANSFORM_CHANGED) { + if (mCanvasTM && mCanvasTM->IsSingular()) { + needNewBounds = true; // old bounds are bogus + } + needNewCanvasTM = true; + // In an ideal world we would reflow when our CTM changes. This is because + // glyph metrics do not necessarily scale uniformly with change in scale + // and, as a result, CTM changes may require text to break at different + // points. The problem would be how to keep performance acceptable when + // e.g. the transform of an ancestor is animated. + // We also seem to get some sort of infinite loop post bug 421584 if we + // reflow. + } + + if (needNewBounds) { + // Ancestor changes can't affect how we render from the perspective of + // any rendering observers that we may have, so we don't need to + // invalidate them. We also don't need to invalidate ourself, since our + // changed ancestor will have invalidated its entire area, which includes + // our area. + SVGUtils::ScheduleReflowSVG(this); + } + + // If we're called while the PresShell is handling reflow events then we + // must have been called as a result of the NotifyViewportChange() call in + // our SVGOuterSVGFrame's Reflow() method. We must not call RequestReflow + // at this point (i.e. during reflow) because it could confuse the + // PresShell and prevent it from reflowing us properly in future. Besides + // that, SVGOuterSVGFrame::DidReflow will take care of reflowing us + // synchronously, so there's no need. + if (needReflow && !PresShell()->IsReflowLocked()) { + RequestReflow(IntrinsicDirty::None); + } + + if (needNewCanvasTM) { + // Do this after calling InvalidateAndScheduleBoundsUpdate in case we + // change the code and it needs to use it. + mCanvasTM = nullptr; + } +} + +SVGBBox SVGForeignObjectFrame::GetBBoxContribution( + const Matrix& aToBBoxUserspace, uint32_t aFlags) { + SVGForeignObjectElement* content = + static_cast<SVGForeignObjectElement*>(GetContent()); + + float x, y, w, h; + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + content, &x, &y, &w, &h); + + if (w < 0.0f) w = 0.0f; + if (h < 0.0f) h = 0.0f; + + if (aToBBoxUserspace.IsSingular()) { + // XXX ReportToConsole + return SVGBBox(); + } + return aToBBoxUserspace.TransformBounds(gfx::Rect(0.0, 0.0, w, h)); +} + +//---------------------------------------------------------------------- + +gfxMatrix SVGForeignObjectFrame::GetCanvasTM() { + if (!mCanvasTM) { + NS_ASSERTION(GetParent(), "null parent"); + + auto* parent = static_cast<SVGContainerFrame*>(GetParent()); + auto* content = static_cast<SVGForeignObjectElement*>(GetContent()); + + gfxMatrix tm = content->PrependLocalTransformsTo(parent->GetCanvasTM()); + + mCanvasTM = MakeUnique<gfxMatrix>(tm); + } + return *mCanvasTM; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +void SVGForeignObjectFrame::RequestReflow(IntrinsicDirty aType) { + if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + // If we haven't had a ReflowSVG() yet, nothing to do. + return; + } + + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + PresShell()->FrameNeedsReflow(kid, aType, NS_FRAME_IS_DIRTY); +} + +void SVGForeignObjectFrame::DoReflow() { + MarkInReflow(); + // Skip reflow if we're zero-sized, unless this is our first reflow. + if (IsDisabled() && !HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + return; + } + + nsPresContext* presContext = PresContext(); + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + // initiate a synchronous reflow here and now: + UniquePtr<gfxContext> renderingContext = + presContext->PresShell()->CreateReferenceRenderingContext(); + + mInReflow = true; + + WritingMode wm = kid->GetWritingMode(); + ReflowInput reflowInput(presContext, kid, renderingContext.get(), + LogicalSize(wm, ISize(wm), NS_UNCONSTRAINEDSIZE)); + ReflowOutput desiredSize(reflowInput); + nsReflowStatus status; + + // We don't use mRect.height above because that tells the child to do + // page/column breaking at that height. + NS_ASSERTION( + reflowInput.ComputedPhysicalBorderPadding() == nsMargin(0, 0, 0, 0) && + reflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0), + "style system should ensure that :-moz-svg-foreign-content " + "does not get styled"); + NS_ASSERTION(reflowInput.ComputedISize() == ISize(wm), + "reflow input made child wrong size"); + reflowInput.SetComputedBSize(BSize(wm)); + + ReflowChild(kid, presContext, desiredSize, reflowInput, 0, 0, + ReflowChildFlags::NoMoveFrame, status); + NS_ASSERTION(mRect.width == desiredSize.Width() && + mRect.height == desiredSize.Height(), + "unexpected size"); + FinishReflowChild(kid, presContext, desiredSize, &reflowInput, 0, 0, + ReflowChildFlags::NoMoveFrame); + + mInReflow = false; +} + +void SVGForeignObjectFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + MOZ_ASSERT(PrincipalChildList().FirstChild(), "Must have our anon box"); + aResult.AppendElement(OwnedAnonBox(PrincipalChildList().FirstChild())); +} + +} // namespace mozilla diff --git a/layout/svg/SVGForeignObjectFrame.h b/layout/svg/SVGForeignObjectFrame.h new file mode 100644 index 0000000000..313145abd3 --- /dev/null +++ b/layout/svg/SVGForeignObjectFrame.h @@ -0,0 +1,102 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGFOREIGNOBJECTFRAME_H_ +#define LAYOUT_SVG_SVGFOREIGNOBJECTFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/PresShellForwards.h" +#include "mozilla/UniquePtr.h" +#include "nsContainerFrame.h" + +class gfxContext; + +nsContainerFrame* NS_NewSVGForeignObjectFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGForeignObjectFrame final : public nsContainerFrame, + public ISVGDisplayableFrame { + friend nsContainerFrame* ::NS_NewSVGForeignObjectFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + protected: + explicit SVGForeignObjectFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGForeignObjectFrame) + + // nsIFrame: + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + nsContainerFrame* GetContentInsertionFrame() override { + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsContainerFrame::IsFrameOfType(aFlags & ~nsIFrame::eSVG); + } + + bool IsSVGTransformed(Matrix* aOwnTransform, + Matrix* aFromParentTransform) const override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGForeignObject"_ns, aResult); + } +#endif + + // ISVGDisplayableFrame interface: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + bool IsDisplayContainer() override { return true; } + + gfxMatrix GetCanvasTM(); + + // Return our ::-moz-svg-foreign-content anonymous box. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + protected: + // implementation helpers: + void DoReflow(); + void RequestReflow(IntrinsicDirty aType); + + // If width or height is less than or equal to zero we must disable rendering + bool IsDisabled() const { return mRect.width <= 0 || mRect.height <= 0; } + + UniquePtr<gfxMatrix> mCanvasTM; + + bool mInReflow; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGFOREIGNOBJECTFRAME_H_ diff --git a/layout/svg/SVGGFrame.cpp b/layout/svg/SVGGFrame.cpp new file mode 100644 index 0000000000..92df721ffb --- /dev/null +++ b/layout/svg/SVGGFrame.cpp @@ -0,0 +1,58 @@ +/* -*- 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 "SVGGFrame.h" + +// Keep others in (case-insensitive) order: +#include "mozilla/dom/SVGElement.h" +#include "mozilla/PresShell.h" +#include "nsGkAtoms.h" +#include "nsIFrame.h" + +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGGFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGGFrame) + +#ifdef DEBUG +void SVGGFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement() && + static_cast<SVGElement*>(aContent)->IsTransformable(), + "The element is not transformable"); + + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +nsresult SVGGFrame::AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::transform) { + // 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. + NotifySVGChanged(TRANSFORM_CHANGED); + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/layout/svg/SVGGFrame.h b/layout/svg/SVGGFrame.h new file mode 100644 index 0000000000..68e01ec8b9 --- /dev/null +++ b/layout/svg/SVGGFrame.h @@ -0,0 +1,55 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGGFRAME_H_ +#define LAYOUT_SVG_SVGGFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SVGContainerFrame.h" +#include "gfxMatrix.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGGFrame : public SVGDisplayContainerFrame { + friend nsIFrame* ::NS_NewSVGGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + explicit SVGGFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGGFrame(aStyle, aPresContext, kClassID) {} + + protected: + SVGGFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID) + : SVGDisplayContainerFrame(aStyle, aPresContext, aID) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGGFrame) + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGG"_ns, aResult); + } +#endif + + // nsIFrame interface: + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGGFRAME_H_ diff --git a/layout/svg/SVGGeometryFrame.cpp b/layout/svg/SVGGeometryFrame.cpp new file mode 100644 index 0000000000..7d0bd7cc4c --- /dev/null +++ b/layout/svg/SVGGeometryFrame.cpp @@ -0,0 +1,823 @@ +/* -*- 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<SVGGeometryElement*>(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<SVGGeometryElement*>(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<const SVGElement*>(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<DisplaySVGGeometry>(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<SVGGeometryElement*>(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> drawTarget = + gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + RefPtr<Path> 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<PathBuilder> 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<SVGGeometryElement*>(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-<svg> 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<SVGGeometryElement*>(GetContent()); + + bool getFill = (aFlags & SVGUtils::eBBoxIncludeFillGeometry) || + ((aFlags & SVGUtils::eBBoxIncludeFill) && + !StyleSVG()->mFill.kind.IsNone()); + + bool getStroke = + (aFlags & SVGUtils::eBBoxIncludeStrokeGeometry) || + ((aFlags & SVGUtils::eBBoxIncludeStroke) && SVGUtils::HasStroke(this)); + + 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<DrawTarget> tmpDT; + tmpDT = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); + + FillRule fillRule = SVGUtils::ToFillRule( + HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD) ? StyleSVG()->mClipRule + : StyleSVG()->mFillRule); + RefPtr<Path> pathInUserSpace = element->GetOrBuildPath(tmpDT, fillRule); + if (!pathInUserSpace) { + return bbox; + } + RefPtr<Path> pathInBBoxSpace; + if (aToBBoxUserspace.IsIdentity()) { + pathInBBoxSpace = pathInUserSpace; + } else { + RefPtr<PathBuilder> 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-<svg> 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<PathBuilder> builder = + pathInUserSpace->TransformedCopyToBuilder(ToMatrix(userToOuterSVG)); + RefPtr<Path> 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<SVGMark> 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<SVGContainerFrame*>(GetParent()); + auto* content = static_cast<SVGGraphicsElement*>(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<SVGGeometryElement*>(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> 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> 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<PathBuilder> 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<SVGElement*>(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<SVGGeometryElement*>(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<SVGGeometryElement*>(GetContent()); + if (!element->IsMarkable()) { + return; + } + SVGMarkerFrame* markerFrames[SVGMark::eTypeCount]; + if (!SVGObserverUtils::GetAndObserveMarkers(this, &markerFrames)) { + return; + } + nsTArray<SVGMark> 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 diff --git a/layout/svg/SVGGeometryFrame.h b/layout/svg/SVGGeometryFrame.h new file mode 100644 index 0000000000..ab45e6fbaa --- /dev/null +++ b/layout/svg/SVGGeometryFrame.h @@ -0,0 +1,189 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGGEOMETRYFRAME_H_ +#define LAYOUT_SVG_SVGGEOMETRYFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/DisplaySVGItem.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "nsIFrame.h" + +namespace mozilla { + +class DisplaySVGGeometry; +class PresShell; +class SVGGeometryFrame; +class SVGMarkerObserver; + +namespace gfx { +class DrawTarget; +} // namespace gfx + +namespace image { +struct imgDrawingParams; +} // namespace image + +} // namespace mozilla + +class gfxContext; +class nsAtom; +class nsIFrame; + +struct nsRect; + +nsIFrame* NS_NewSVGGeometryFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGGeometryFrame final : public nsIFrame, public ISVGDisplayableFrame { + using DrawTarget = gfx::DrawTarget; + + friend nsIFrame* ::NS_NewSVGGeometryFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + friend class DisplaySVGGeometry; + + protected: + SVGGeometryFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID = kClassID) + : nsIFrame(aStyle, aPresContext, aID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_MAY_BE_TRANSFORMED); + } + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGGeometryFrame) + + // nsIFrame interface: + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~nsIFrame::eSVG); + } + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + bool IsSVGTransformed(Matrix* aOwnTransforms = nullptr, + Matrix* aFromParentTransforms = nullptr) const override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGGeometry"_ns, aResult); + } +#endif + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + // SVGGeometryFrame methods + gfxMatrix GetCanvasTM(); + + bool IsInvisible() const; + + private: + // ISVGDisplayableFrame interface: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + bool IsDisplayContainer() override { return false; } + + enum { eRenderFill = 1, eRenderStroke = 2 }; + void Render(gfxContext* aContext, uint32_t aRenderComponents, + const gfxMatrix& aTransform, imgDrawingParams& aImgParams); + + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder, DisplaySVGGeometry* aItem, + bool aDryRun); + /** + * @param aMatrix The transform that must be multiplied onto aContext to + * establish this frame's SVG user space. + */ + void PaintMarkers(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams); + + /* + * Get the stroke width that markers should use, accounting for + * non-scaling stroke. + */ + float GetStrokeWidthForMarkers(); +}; + +//---------------------------------------------------------------------- +// Display list item: + +class DisplaySVGGeometry final : public DisplaySVGItem { + public: + DisplaySVGGeometry(nsDisplayListBuilder* aBuilder, SVGGeometryFrame* aFrame) + : DisplaySVGItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(DisplaySVGGeometry); + } + + MOZ_COUNTED_DTOR_OVERRIDE(DisplaySVGGeometry) + + NS_DISPLAY_DECL_NAME("DisplaySVGGeometry", TYPE_SVG_GEOMETRY) + + // Whether this part of the SVG should be natively handled by webrender, + // potentially becoming an "active layer" inside a blob image. + bool ShouldBeActive(mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + // We delegate this question to the parent frame to take advantage of + // the SVGGeometryFrame inheritance hierarchy which provides actual + // implementation details. The dryRun flag prevents serious side-effects. + auto* frame = static_cast<SVGGeometryFrame*>(mFrame); + return frame->CreateWebRenderCommands(aBuilder, aResources, aSc, aManager, + aDisplayListBuilder, this, + /*aDryRun=*/true); + } + + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override { + // We delegate this question to the parent frame to take advantage of + // the SVGGeometryFrame inheritance hierarchy which provides actual + // implementation details. + auto* frame = static_cast<SVGGeometryFrame*>(mFrame); + bool result = frame->CreateWebRenderCommands(aBuilder, aResources, aSc, + aManager, aDisplayListBuilder, + this, /*aDryRun=*/false); + MOZ_ASSERT(result, "ShouldBeActive inconsistent with CreateWRCommands?"); + return result; + } + + bool IsInvisible() const override { + auto* frame = static_cast<SVGGeometryFrame*>(mFrame); + return frame->IsInvisible(); + } +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGGEOMETRYFRAME_H_ diff --git a/layout/svg/SVGGradientFrame.cpp b/layout/svg/SVGGradientFrame.cpp new file mode 100644 index 0000000000..cc91b68f1c --- /dev/null +++ b/layout/svg/SVGGradientFrame.cpp @@ -0,0 +1,605 @@ +/* -*- 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 "SVGGradientFrame.h" +#include <algorithm> + +// Keep others in (case-insensitive) order: +#include "AutoReferenceChainGuard.h" +#include "gfxPattern.h" +#include "gfxUtils.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGGradientElement.h" +#include "mozilla/dom/SVGGradientElementBinding.h" +#include "mozilla/dom/SVGStopElement.h" +#include "mozilla/dom/SVGUnitTypesBinding.h" +#include "nsContentUtils.h" +#include "SVGAnimatedTransformList.h" + +// XXX Tight coupling with content classes ahead! + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGGradientElement_Binding; +using namespace mozilla::dom::SVGUnitTypes_Binding; +using namespace mozilla::gfx; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Implementation + +SVGGradientFrame::SVGGradientFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext, ClassID aID) + : SVGPaintServerFrame(aStyle, aPresContext, aID), + mSource(nullptr), + mLoopFlag(false), + mNoHRefURI(false) {} + +NS_QUERYFRAME_HEAD(SVGGradientFrame) + NS_QUERYFRAME_ENTRY(SVGGradientFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGPaintServerFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods: + +nsresult SVGGradientFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::gradientUnits || + aAttribute == nsGkAtoms::gradientTransform || + aAttribute == nsGkAtoms::spreadMethod)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } else if ((aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None) && + aAttribute == nsGkAtoms::href) { + // Blow away our reference, if any + SVGObserverUtils::RemoveTemplateObserver(this); + mNoHRefURI = false; + // And update whoever references us + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGPaintServerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +//---------------------------------------------------------------------- + +uint16_t SVGGradientFrame::GetEnumValue(uint32_t aIndex, nsIContent* aDefault) { + const SVGAnimatedEnumeration& thisEnum = + static_cast<dom::SVGGradientElement*>(GetContent()) + ->mEnumAttributes[aIndex]; + + if (thisEnum.IsExplicitlySet()) { + return thisEnum.GetAnimValue(); + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<dom::SVGGradientElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); + } + + SVGGradientFrame* next = GetReferencedGradient(); + + return next ? next->GetEnumValue(aIndex, aDefault) + : static_cast<dom::SVGGradientElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); +} + +uint16_t SVGGradientFrame::GetGradientUnits() { + // This getter is called every time the others are called - maybe cache it? + return GetEnumValue(dom::SVGGradientElement::GRADIENTUNITS); +} + +uint16_t SVGGradientFrame::GetSpreadMethod() { + return GetEnumValue(dom::SVGGradientElement::SPREADMETHOD); +} + +const SVGAnimatedTransformList* SVGGradientFrame::GetGradientTransformList( + nsIContent* aDefault) { + SVGAnimatedTransformList* thisTransformList = + static_cast<dom::SVGGradientElement*>(GetContent()) + ->GetAnimatedTransformList(); + + if (thisTransformList && thisTransformList->IsExplicitlySet()) + return thisTransformList; + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<const dom::SVGGradientElement*>(aDefault) + ->mGradientTransform.get(); + } + + SVGGradientFrame* next = GetReferencedGradient(); + + return next ? next->GetGradientTransformList(aDefault) + : static_cast<const dom::SVGGradientElement*>(aDefault) + ->mGradientTransform.get(); +} + +gfxMatrix SVGGradientFrame::GetGradientTransform( + nsIFrame* aSource, const gfxRect* aOverrideBounds) { + gfxMatrix bboxMatrix; + + uint16_t gradientUnits = GetGradientUnits(); + if (gradientUnits != SVG_UNIT_TYPE_USERSPACEONUSE) { + NS_ASSERTION(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, + "Unknown gradientUnits type"); + // objectBoundingBox is the default anyway + + gfxRect bbox = aOverrideBounds + ? *aOverrideBounds + : SVGUtils::GetBBox( + aSource, SVGUtils::eUseFrameBoundsForOuterSVG | + SVGUtils::eBBoxIncludeFillGeometry); + bboxMatrix = + gfxMatrix(bbox.Width(), 0, 0, bbox.Height(), bbox.X(), bbox.Y()); + } + + const SVGAnimatedTransformList* animTransformList = + GetGradientTransformList(GetContent()); + if (!animTransformList) { + return bboxMatrix; + } + + gfxMatrix gradientTransform = + animTransformList->GetAnimValue().GetConsolidationMatrix(); + return bboxMatrix.PreMultiply(gradientTransform); +} + +dom::SVGLinearGradientElement* SVGGradientFrame::GetLinearGradientWithLength( + uint32_t aIndex, dom::SVGLinearGradientElement* aDefault) { + // If this was a linear gradient with the required length, we would have + // already found it in SVGLinearGradientFrame::GetLinearGradientWithLength. + // Since we didn't find the length, continue looking down the chain. + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return aDefault; + } + + SVGGradientFrame* next = GetReferencedGradient(); + return next ? next->GetLinearGradientWithLength(aIndex, aDefault) : aDefault; +} + +dom::SVGRadialGradientElement* SVGGradientFrame::GetRadialGradientWithLength( + uint32_t aIndex, dom::SVGRadialGradientElement* aDefault) { + // If this was a radial gradient with the required length, we would have + // already found it in SVGRadialGradientFrame::GetRadialGradientWithLength. + // Since we didn't find the length, continue looking down the chain. + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return aDefault; + } + + SVGGradientFrame* next = GetReferencedGradient(); + return next ? next->GetRadialGradientWithLength(aIndex, aDefault) : aDefault; +} + +//---------------------------------------------------------------------- +// SVGPaintServerFrame methods: + +// helper +static void GetStopInformation(nsIFrame* aStopFrame, float* aOffset, + nscolor* aStopColor, float* aStopOpacity) { + nsIContent* stopContent = aStopFrame->GetContent(); + MOZ_ASSERT(stopContent && stopContent->IsSVGElement(nsGkAtoms::stop)); + + static_cast<SVGStopElement*>(stopContent) + ->GetAnimatedNumberValues(aOffset, nullptr); + + const nsStyleSVGReset* styleSVGReset = aStopFrame->StyleSVGReset(); + *aOffset = mozilla::clamped(*aOffset, 0.0f, 1.0f); + *aStopColor = styleSVGReset->mStopColor.CalcColor(aStopFrame); + *aStopOpacity = styleSVGReset->mStopOpacity; +} + +already_AddRefed<gfxPattern> SVGGradientFrame::GetPaintServerPattern( + nsIFrame* aSource, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + float aGraphicOpacity, imgDrawingParams& aImgParams, + const gfxRect* aOverrideBounds) { + uint16_t gradientUnits = GetGradientUnits(); + MOZ_ASSERT(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX || + gradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE); + if (gradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE) { + // Set mSource for this consumer. + // If this gradient is applied to text, our caller will be the glyph, which + // is not an element, so we need to get the parent + mSource = aSource->IsTextFrame() ? aSource->GetParent() : aSource; + } + + AutoTArray<nsIFrame*, 8> stopFrames; + GetStopFrames(&stopFrames); + + uint32_t nStops = stopFrames.Length(); + + // SVG specification says that no stops should be treated like + // the corresponding fill or stroke had "none" specified. + if (nStops == 0) { + RefPtr<gfxPattern> pattern = new gfxPattern(DeviceColor()); + return do_AddRef(new gfxPattern(DeviceColor())); + } + + if (nStops == 1 || GradientVectorLengthIsZero()) { + auto* lastStopFrame = stopFrames[nStops - 1]; + const auto* svgReset = lastStopFrame->StyleSVGReset(); + // The gradient paints a single colour, using the stop-color of the last + // gradient step if there are more than one. + float stopOpacity = svgReset->mStopOpacity; + nscolor stopColor = svgReset->mStopColor.CalcColor(lastStopFrame); + + sRGBColor stopColor2 = sRGBColor::FromABGR(stopColor); + stopColor2.a *= stopOpacity * aGraphicOpacity; + return do_AddRef(new gfxPattern(ToDeviceColor(stopColor2))); + } + + // Get the transform list (if there is one). We do this after the returns + // above since this call can be expensive when "gradientUnits" is set to + // "objectBoundingBox" (since that requiring a GetBBox() call). + gfxMatrix patternMatrix = GetGradientTransform(aSource, aOverrideBounds); + + if (patternMatrix.IsSingular()) { + return nullptr; + } + + // revert any vector effect transform so that the gradient appears unchanged + if (aFillOrStroke == &nsStyleSVG::mStroke) { + gfxMatrix userToOuterSVG; + if (SVGUtils::GetNonScalingStrokeTransform(aSource, &userToOuterSVG)) { + patternMatrix *= userToOuterSVG; + } + } + + if (!patternMatrix.Invert()) { + return nullptr; + } + + RefPtr<gfxPattern> gradient = CreateGradient(); + if (!gradient) { + return nullptr; + } + + uint16_t aSpread = GetSpreadMethod(); + if (aSpread == SVG_SPREADMETHOD_PAD) + gradient->SetExtend(ExtendMode::CLAMP); + else if (aSpread == SVG_SPREADMETHOD_REFLECT) + gradient->SetExtend(ExtendMode::REFLECT); + else if (aSpread == SVG_SPREADMETHOD_REPEAT) + gradient->SetExtend(ExtendMode::REPEAT); + + gradient->SetMatrix(patternMatrix); + + // setup stops + float lastOffset = 0.0f; + + for (uint32_t i = 0; i < nStops; i++) { + float offset, stopOpacity; + nscolor stopColor; + + GetStopInformation(stopFrames[i], &offset, &stopColor, &stopOpacity); + + if (offset < lastOffset) + offset = lastOffset; + else + lastOffset = offset; + + sRGBColor stopColor2 = sRGBColor::FromABGR(stopColor); + stopColor2.a *= stopOpacity * aGraphicOpacity; + gradient->AddColorStop(offset, ToDeviceColor(stopColor2)); + } + + return gradient.forget(); +} + +// Private (helper) methods + +SVGGradientFrame* SVGGradientFrame::GetReferencedGradient() { + if (mNoHRefURI) { + return nullptr; + } + + auto GetHref = [this](nsAString& aHref) { + dom::SVGGradientElement* grad = + static_cast<dom::SVGGradientElement*>(this->GetContent()); + if (grad->mStringAttributes[dom::SVGGradientElement::HREF] + .IsExplicitlySet()) { + grad->mStringAttributes[dom::SVGGradientElement::HREF].GetAnimValue(aHref, + grad); + } else { + grad->mStringAttributes[dom::SVGGradientElement::XLINK_HREF].GetAnimValue( + aHref, grad); + } + this->mNoHRefURI = aHref.IsEmpty(); + }; + + // We don't call SVGObserverUtils::RemoveTemplateObserver and set + // `mNoHRefURI = false` on failure since we want to be invalidated if the ID + // specified by our href starts resolving to a different/valid element. + + return do_QueryFrame(SVGObserverUtils::GetAndObserveTemplate(this, GetHref)); +} + +void SVGGradientFrame::GetStopFrames(nsTArray<nsIFrame*>* aStopFrames) { + nsIFrame* stopFrame = nullptr; + for (stopFrame = mFrames.FirstChild(); stopFrame; + stopFrame = stopFrame->GetNextSibling()) { + if (stopFrame->IsSVGStopFrame()) { + aStopFrames->AppendElement(stopFrame); + } + } + if (aStopFrames->Length() > 0) { + return; + } + + // Our gradient element doesn't have stops - try to "inherit" them + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return; + } + + SVGGradientFrame* next = GetReferencedGradient(); + if (next) { + next->GetStopFrames(aStopFrames); + } +} + +// ------------------------------------------------------------------------- +// Linear Gradients +// ------------------------------------------------------------------------- + +NS_QUERYFRAME_HEAD(SVGLinearGradientFrame) + NS_QUERYFRAME_ENTRY(SVGLinearGradientFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGGradientFrame) + +#ifdef DEBUG +void SVGLinearGradientFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::linearGradient), + "Content is not an SVG linearGradient"); + + SVGGradientFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGLinearGradientFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::x1 || aAttribute == nsGkAtoms::y1 || + aAttribute == nsGkAtoms::x2 || aAttribute == nsGkAtoms::y2)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGGradientFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +//---------------------------------------------------------------------- + +float SVGLinearGradientFrame::GetLengthValue(uint32_t aIndex) { + dom::SVGLinearGradientElement* lengthElement = GetLinearGradientWithLength( + aIndex, static_cast<dom::SVGLinearGradientElement*>(GetContent())); + // We passed in mContent as a fallback, so, assuming mContent is non-null, the + // return value should also be non-null. + MOZ_ASSERT(lengthElement, + "Got unexpected null element from GetLinearGradientWithLength"); + const SVGAnimatedLength& length = lengthElement->mLengthAttributes[aIndex]; + + // Object bounding box units are handled by setting the appropriate + // transform in GetGradientTransform, but we need to handle user + // space units as part of the individual Get* routines. Fixes 323669. + + uint16_t gradientUnits = GetGradientUnits(); + if (gradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE) { + return SVGUtils::UserSpace(mSource, &length); + } + + NS_ASSERTION(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, + "Unknown gradientUnits type"); + + return length.GetAnimValue(static_cast<SVGViewportElement*>(nullptr)); +} + +dom::SVGLinearGradientElement* +SVGLinearGradientFrame::GetLinearGradientWithLength( + uint32_t aIndex, dom::SVGLinearGradientElement* aDefault) { + dom::SVGLinearGradientElement* thisElement = + static_cast<dom::SVGLinearGradientElement*>(GetContent()); + const SVGAnimatedLength& length = thisElement->mLengthAttributes[aIndex]; + + if (length.IsExplicitlySet()) { + return thisElement; + } + + return SVGGradientFrame::GetLinearGradientWithLength(aIndex, aDefault); +} + +bool SVGLinearGradientFrame::GradientVectorLengthIsZero() { + return GetLengthValue(dom::SVGLinearGradientElement::ATTR_X1) == + GetLengthValue(dom::SVGLinearGradientElement::ATTR_X2) && + GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y1) == + GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y2); +} + +already_AddRefed<gfxPattern> SVGLinearGradientFrame::CreateGradient() { + float x1 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_X1); + float y1 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y1); + float x2 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_X2); + float y2 = GetLengthValue(dom::SVGLinearGradientElement::ATTR_Y2); + + RefPtr<gfxPattern> pattern = new gfxPattern(x1, y1, x2, y2); + return pattern.forget(); +} + +// ------------------------------------------------------------------------- +// Radial Gradients +// ------------------------------------------------------------------------- + +NS_QUERYFRAME_HEAD(SVGRadialGradientFrame) + NS_QUERYFRAME_ENTRY(SVGRadialGradientFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGGradientFrame) + +#ifdef DEBUG +void SVGRadialGradientFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::radialGradient), + "Content is not an SVG radialGradient"); + + SVGGradientFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGRadialGradientFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::r || aAttribute == nsGkAtoms::cx || + aAttribute == nsGkAtoms::cy || aAttribute == nsGkAtoms::fx || + aAttribute == nsGkAtoms::fy)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGGradientFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +//---------------------------------------------------------------------- + +float SVGRadialGradientFrame::GetLengthValue(uint32_t aIndex) { + dom::SVGRadialGradientElement* lengthElement = GetRadialGradientWithLength( + aIndex, static_cast<dom::SVGRadialGradientElement*>(GetContent())); + // We passed in mContent as a fallback, so, assuming mContent is non-null, + // the return value should also be non-null. + MOZ_ASSERT(lengthElement, + "Got unexpected null element from GetRadialGradientWithLength"); + return GetLengthValueFromElement(aIndex, *lengthElement); +} + +float SVGRadialGradientFrame::GetLengthValue(uint32_t aIndex, + float aDefaultValue) { + dom::SVGRadialGradientElement* lengthElement = + GetRadialGradientWithLength(aIndex, nullptr); + + return lengthElement ? GetLengthValueFromElement(aIndex, *lengthElement) + : aDefaultValue; +} + +float SVGRadialGradientFrame::GetLengthValueFromElement( + uint32_t aIndex, dom::SVGRadialGradientElement& aElement) { + const SVGAnimatedLength& length = aElement.mLengthAttributes[aIndex]; + + // Object bounding box units are handled by setting the appropriate + // transform in GetGradientTransform, but we need to handle user + // space units as part of the individual Get* routines. Fixes 323669. + + uint16_t gradientUnits = GetGradientUnits(); + if (gradientUnits == SVG_UNIT_TYPE_USERSPACEONUSE) { + return SVGUtils::UserSpace(mSource, &length); + } + + NS_ASSERTION(gradientUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX, + "Unknown gradientUnits type"); + + return length.GetAnimValue(static_cast<SVGViewportElement*>(nullptr)); +} + +dom::SVGRadialGradientElement* +SVGRadialGradientFrame::GetRadialGradientWithLength( + uint32_t aIndex, dom::SVGRadialGradientElement* aDefault) { + dom::SVGRadialGradientElement* thisElement = + static_cast<dom::SVGRadialGradientElement*>(GetContent()); + const SVGAnimatedLength& length = thisElement->mLengthAttributes[aIndex]; + + if (length.IsExplicitlySet()) { + return thisElement; + } + + return SVGGradientFrame::GetRadialGradientWithLength(aIndex, aDefault); +} + +bool SVGRadialGradientFrame::GradientVectorLengthIsZero() { + float cx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CX); + float cy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CY); + float r = GetLengthValue(dom::SVGRadialGradientElement::ATTR_R); + // If fx or fy are not set, use cx/cy instead + float fx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FX, cx); + float fy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FY, cy); + float fr = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FR); + return cx == fx && cy == fy && r == fr; +} + +already_AddRefed<gfxPattern> SVGRadialGradientFrame::CreateGradient() { + float cx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CX); + float cy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_CY); + float r = GetLengthValue(dom::SVGRadialGradientElement::ATTR_R); + // If fx or fy are not set, use cx/cy instead + float fx = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FX, cx); + float fy = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FY, cy); + float fr = GetLengthValue(dom::SVGRadialGradientElement::ATTR_FR); + + RefPtr<gfxPattern> pattern = new gfxPattern(fx, fy, fr, cx, cy, r); + return pattern.forget(); +} + +} // namespace mozilla + +// ------------------------------------------------------------------------- +// Public functions +// ------------------------------------------------------------------------- + +nsIFrame* NS_NewSVGLinearGradientFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGLinearGradientFrame(aStyle, aPresShell->GetPresContext()); +} + +nsIFrame* NS_NewSVGRadialGradientFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGRadialGradientFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGLinearGradientFrame) +NS_IMPL_FRAMEARENA_HELPERS(SVGRadialGradientFrame) + +} // namespace mozilla diff --git a/layout/svg/SVGGradientFrame.h b/layout/svg/SVGGradientFrame.h new file mode 100644 index 0000000000..50b78fb06c --- /dev/null +++ b/layout/svg/SVGGradientFrame.h @@ -0,0 +1,203 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGGRADIENTFRAME_H_ +#define LAYOUT_SVG_SVGGRADIENTFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SVGPaintServerFrame.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "nsCOMPtr.h" +#include "nsIFrame.h" +#include "nsLiteralString.h" + +class gfxPattern; +class nsAtom; +class nsIContent; + +namespace mozilla { +class PresShell; +class SVGAnimatedTransformList; + +namespace dom { +class SVGLinearGradientElement; +class SVGRadialGradientElement; +} // namespace dom +} // namespace mozilla + +nsIFrame* NS_NewSVGLinearGradientFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); +nsIFrame* NS_NewSVGRadialGradientFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGGradientFrame : public SVGPaintServerFrame { + using ExtendMode = gfx::ExtendMode; + + protected: + SVGGradientFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + ClassID aID); + + public: + NS_DECL_ABSTRACT_FRAME(SVGGradientFrame) + NS_DECL_QUERYFRAME + NS_DECL_QUERYFRAME_TARGET(SVGGradientFrame) + + // SVGPaintServerFrame methods: + already_AddRefed<gfxPattern> GetPaintServerPattern( + nsIFrame* aSource, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + float aGraphicOpacity, imgDrawingParams& aImgParams, + const gfxRect* aOverrideBounds) override; + + // nsIFrame interface: + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGGradient"_ns, aResult); + } +#endif // DEBUG + + private: + /** + * Parses this frame's href and - if it references another gradient - returns + * it. It also makes this frame a rendering observer of the specified ID. + */ + SVGGradientFrame* GetReferencedGradient(); + + // Optionally get a stop frame (returns stop index/count) + void GetStopFrames(nsTArray<nsIFrame*>* aStopFrames); + + const SVGAnimatedTransformList* GetGradientTransformList( + nsIContent* aDefault); + // Will be singular for gradientUnits="objectBoundingBox" with an empty bbox. + gfxMatrix GetGradientTransform(nsIFrame* aSource, + const gfxRect* aOverrideBounds); + + protected: + virtual bool GradientVectorLengthIsZero() = 0; + virtual already_AddRefed<gfxPattern> CreateGradient() = 0; + + // Accessors to lookup gradient attributes + uint16_t GetEnumValue(uint32_t aIndex, nsIContent* aDefault); + uint16_t GetEnumValue(uint32_t aIndex) { + return GetEnumValue(aIndex, mContent); + } + uint16_t GetGradientUnits(); + uint16_t GetSpreadMethod(); + + // Gradient-type-specific lookups since the length values differ between + // linear and radial gradients + virtual dom::SVGLinearGradientElement* GetLinearGradientWithLength( + uint32_t aIndex, dom::SVGLinearGradientElement* aDefault); + virtual dom::SVGRadialGradientElement* GetRadialGradientWithLength( + uint32_t aIndex, dom::SVGRadialGradientElement* aDefault); + + // The frame our gradient is (currently) being applied to + nsIFrame* mSource; + + private: + // Flag to mark this frame as "in use" during recursive calls along our + // gradient's reference chain so we can detect reference loops. See: + // http://www.w3.org/TR/SVG11/pservers.html#LinearGradientElementHrefAttribute + bool mLoopFlag; + // Gradients often don't reference other gradients, so here we cache + // the fact that that isn't happening. + bool mNoHRefURI; +}; + +// ------------------------------------------------------------------------- +// Linear Gradients +// ------------------------------------------------------------------------- + +class SVGLinearGradientFrame final : public SVGGradientFrame { + friend nsIFrame* ::NS_NewSVGLinearGradientFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + protected: + explicit SVGLinearGradientFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGGradientFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGLinearGradientFrame) + + // nsIFrame interface: +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGLinearGradient"_ns, aResult); + } +#endif // DEBUG + + protected: + float GetLengthValue(uint32_t aIndex); + mozilla::dom::SVGLinearGradientElement* GetLinearGradientWithLength( + uint32_t aIndex, + mozilla::dom::SVGLinearGradientElement* aDefault) override; + bool GradientVectorLengthIsZero() override; + already_AddRefed<gfxPattern> CreateGradient() override; +}; + +// ------------------------------------------------------------------------- +// Radial Gradients +// ------------------------------------------------------------------------- + +class SVGRadialGradientFrame final : public SVGGradientFrame { + friend nsIFrame* ::NS_NewSVGRadialGradientFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + protected: + explicit SVGRadialGradientFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGGradientFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGRadialGradientFrame) + + // nsIFrame interface: +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGRadialGradient"_ns, aResult); + } +#endif // DEBUG + + protected: + float GetLengthValue(uint32_t aIndex); + float GetLengthValue(uint32_t aIndex, float aDefaultValue); + float GetLengthValueFromElement( + uint32_t aIndex, mozilla::dom::SVGRadialGradientElement& aElement); + mozilla::dom::SVGRadialGradientElement* GetRadialGradientWithLength( + uint32_t aIndex, + mozilla::dom::SVGRadialGradientElement* aDefault) override; + bool GradientVectorLengthIsZero() override; + already_AddRefed<gfxPattern> CreateGradient() override; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGGRADIENTFRAME_H_ diff --git a/layout/svg/SVGImageContext.cpp b/layout/svg/SVGImageContext.cpp new file mode 100644 index 0000000000..b256d3cf29 --- /dev/null +++ b/layout/svg/SVGImageContext.cpp @@ -0,0 +1,88 @@ +/* -*- 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 "SVGImageContext.h" + +// Keep others in (case-insensitive) order: +#include "gfxUtils.h" +#include "mozilla/LookAndFeel.h" +#include "mozilla/StaticPrefs_svg.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsPresContext.h" +#include "nsStyleStruct.h" + +namespace mozilla { + +/* static */ +void SVGImageContext::MaybeStoreContextPaint(SVGImageContext& aContext, + nsIFrame* aFromFrame, + imgIContainer* aImgContainer) { + return MaybeStoreContextPaint(aContext, *aFromFrame->PresContext(), + *aFromFrame->Style(), aImgContainer); +} + +/* static */ +void SVGImageContext::MaybeStoreContextPaint(SVGImageContext& aContext, + const nsPresContext& aPresContext, + const ComputedStyle& aStyle, + imgIContainer* aImgContainer) { + if (aImgContainer->GetType() != imgIContainer::TYPE_VECTOR) { + // Avoid this overhead for raster images. + return; + } + + if (StaticPrefs::svg_embedder_prefers_color_scheme_content_enabled() || + aPresContext.Document()->ChromeRulesEnabled()) { + auto scheme = LookAndFeel::ColorSchemeForStyle( + *aPresContext.Document(), aStyle.StyleUI()->mColorScheme.bits, + ColorSchemeMode::Preferred); + aContext.SetColorScheme(Some(scheme)); + } + + const nsStyleSVG* style = aStyle.StyleSVG(); + if (!style->ExposesContextProperties()) { + // Content must have '-moz-context-properties' set to the names of the + // properties it wants to expose to images it links to. + return; + } + + bool haveContextPaint = false; + + auto contextPaint = MakeRefPtr<SVGEmbeddingContextPaint>(); + + if ((style->mMozContextProperties.bits & StyleContextPropertyBits::FILL) && + style->mFill.kind.IsColor()) { + haveContextPaint = true; + contextPaint->SetFill(style->mFill.kind.AsColor().CalcColor(aStyle)); + } + if ((style->mMozContextProperties.bits & StyleContextPropertyBits::STROKE) && + style->mStroke.kind.IsColor()) { + haveContextPaint = true; + contextPaint->SetStroke(style->mStroke.kind.AsColor().CalcColor(aStyle)); + } + if (style->mMozContextProperties.bits & + StyleContextPropertyBits::FILL_OPACITY) { + haveContextPaint = true; + contextPaint->SetFillOpacity(style->mFillOpacity.IsOpacity() + ? style->mFillOpacity.AsOpacity() + : 1.0f); + } + if (style->mMozContextProperties.bits & + StyleContextPropertyBits::STROKE_OPACITY) { + haveContextPaint = true; + contextPaint->SetStrokeOpacity(style->mStrokeOpacity.IsOpacity() + ? style->mStrokeOpacity.AsOpacity() + : 1.0f); + } + + if (haveContextPaint) { + aContext.mContextPaint = std::move(contextPaint); + } +} + +} // namespace mozilla diff --git a/layout/svg/SVGImageContext.h b/layout/svg/SVGImageContext.h new file mode 100644 index 0000000000..e40e41d16b --- /dev/null +++ b/layout/svg/SVGImageContext.h @@ -0,0 +1,136 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGIMAGECONTEXT_H_ +#define LAYOUT_SVG_SVGIMAGECONTEXT_H_ + +#include "mozilla/Maybe.h" +#include "mozilla/SVGContextPaint.h" +#include "mozilla/SVGPreserveAspectRatio.h" +#include "Units.h" + +class nsIFrame; + +namespace mozilla { + +enum class ColorScheme : uint8_t; +class ComputedStyle; + +// SVG image-specific rendering context. For imgIContainer::Draw. +// Used to pass information such as +// - viewport and color-scheme information from CSS, and +// - overridden attributes from an SVG <image> element +// to the image's internal SVG document when it's drawn. +class SVGImageContext { + public: + SVGImageContext() = default; + + /** + * Currently it seems that the aViewportSize parameter ends up being used + * for different things by different pieces of code, and probably in some + * cases being used incorrectly (specifically in the case of pixel snapping + * under the nsLayoutUtils::Draw*Image() methods). An unfortunate result of + * the messy code is that aViewportSize is currently a Maybe<T> since it + * is difficult to create a utility function that consumers can use up + * front to get the "correct" viewport size (i.e. which for compatibility + * with the current code (bugs and all) would mean the size including all + * the snapping and resizing magic that happens in various places under the + * nsLayoutUtils::Draw*Image() methods on the way to DrawImageInternal + * creating |fallbackContext|). Using Maybe<T> allows code to pass Nothing() + * in order to get the size that's created for |fallbackContext|. At some + * point we need to clean this code up, make our abstractions clear, create + * that utility and stop using Maybe for this parameter. + */ + explicit SVGImageContext( + const Maybe<CSSIntSize>& aViewportSize, + const Maybe<SVGPreserveAspectRatio>& aPreserveAspectRatio = Nothing(), + const Maybe<ColorScheme>& aColorScheme = Nothing()) + : mViewportSize(aViewportSize), + mPreserveAspectRatio(aPreserveAspectRatio), + mColorScheme(aColorScheme) {} + + static void MaybeStoreContextPaint(SVGImageContext& aContext, + nsIFrame* aFromFrame, + imgIContainer* aImgContainer); + + static void MaybeStoreContextPaint(SVGImageContext& aContext, + const nsPresContext&, const ComputedStyle&, + imgIContainer*); + + const Maybe<CSSIntSize>& GetViewportSize() const { return mViewportSize; } + + void SetViewportSize(const Maybe<CSSIntSize>& aSize) { + mViewportSize = aSize; + } + + const Maybe<ColorScheme>& GetColorScheme() const { return mColorScheme; } + + void SetColorScheme(const Maybe<ColorScheme>& aScheme) { + mColorScheme = aScheme; + } + + const Maybe<SVGPreserveAspectRatio>& GetPreserveAspectRatio() const { + return mPreserveAspectRatio; + } + + void SetPreserveAspectRatio(const Maybe<SVGPreserveAspectRatio>& aPAR) { + mPreserveAspectRatio = aPAR; + } + + const SVGEmbeddingContextPaint* GetContextPaint() const { + return mContextPaint.get(); + } + + void ClearContextPaint() { mContextPaint = nullptr; } + + bool operator==(const SVGImageContext& aOther) const { + bool contextPaintIsEqual = + // neither have context paint, or they have the same object: + (mContextPaint == aOther.mContextPaint) || + // or both have context paint that are different but equivalent objects: + (mContextPaint && aOther.mContextPaint && + *mContextPaint == *aOther.mContextPaint); + + return contextPaintIsEqual && mViewportSize == aOther.mViewportSize && + mPreserveAspectRatio == aOther.mPreserveAspectRatio && + mColorScheme == aOther.mColorScheme; + } + + bool operator!=(const SVGImageContext& aOther) const { + return !(*this == aOther); + } + + PLDHashNumber Hash() const { + PLDHashNumber hash = 0; + if (mContextPaint) { + hash = HashGeneric(hash, mContextPaint->Hash()); + } + return HashGeneric(hash, mViewportSize.map(HashSize).valueOr(0), + mPreserveAspectRatio.map(HashPAR).valueOr(0), + mColorScheme.map(HashColorScheme).valueOr(0)); + } + + private: + static PLDHashNumber HashSize(const CSSIntSize& aSize) { + return HashGeneric(aSize.width, aSize.height); + } + static PLDHashNumber HashPAR(const SVGPreserveAspectRatio& aPAR) { + return aPAR.Hash(); + } + static PLDHashNumber HashColorScheme(ColorScheme aScheme) { + return HashGeneric(uint8_t(aScheme)); + } + + // NOTE: When adding new member-vars, remember to update Hash() & operator==. + RefPtr<SVGEmbeddingContextPaint> mContextPaint; + Maybe<CSSIntSize> mViewportSize; + Maybe<SVGPreserveAspectRatio> mPreserveAspectRatio; + Maybe<ColorScheme> mColorScheme; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGIMAGECONTEXT_H_ diff --git a/layout/svg/SVGImageFrame.cpp b/layout/svg/SVGImageFrame.cpp new file mode 100644 index 0000000000..da04beee71 --- /dev/null +++ b/layout/svg/SVGImageFrame.cpp @@ -0,0 +1,891 @@ +/* -*- 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/. */ + +#include "SVGImageFrame.h" + +// Keep in (case-insensitive) order: +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "mozilla/ComputedStyleInlines.h" +#include "mozilla/image/WebRenderImageProvider.h" +#include "mozilla/layers/RenderRootStateManager.h" +#include "mozilla/layers/WebRenderLayerManager.h" +#include "imgIContainer.h" +#include "ImageRegion.h" +#include "nsContainerFrame.h" +#include "nsIImageLoadingContent.h" +#include "nsLayoutUtils.h" +#include "imgINotificationObserver.h" +#include "SVGGeometryProperty.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_image.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/SVGImageContext.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/SVGImageElement.h" +#include "nsIReflowCallback.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::dom::SVGPreserveAspectRatio_Binding; +namespace SVGT = SVGGeometryProperty::Tags; + +namespace mozilla { + +class SVGImageListener final : public imgINotificationObserver { + public: + explicit SVGImageListener(SVGImageFrame* aFrame); + + NS_DECL_ISUPPORTS + NS_DECL_IMGINOTIFICATIONOBSERVER + + void SetFrame(SVGImageFrame* frame) { mFrame = frame; } + + private: + ~SVGImageListener() = default; + + SVGImageFrame* mFrame; +}; + +// --------------------------------------------------------------------- +// nsQueryFrame methods + +NS_QUERYFRAME_HEAD(SVGImageFrame) + NS_QUERYFRAME_ENTRY(ISVGDisplayableFrame) + NS_QUERYFRAME_ENTRY(SVGImageFrame) +NS_QUERYFRAME_TAIL_INHERITING(nsIFrame) + +} // namespace mozilla + +nsIFrame* NS_NewSVGImageFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGImageFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGImageFrame) + +SVGImageFrame::~SVGImageFrame() { + // set the frame to null so we don't send messages to a dead object. + if (mListener) { + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) { + imageLoader->RemoveNativeObserver(mListener); + } + reinterpret_cast<SVGImageListener*>(mListener.get())->SetFrame(nullptr); + } + mListener = nullptr; +} + +void SVGImageFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::image), + "Content is not an SVG image!"); + + AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD); + nsIFrame::Init(aContent, aParent, aPrevInFlow); + + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // Non-display frames are likely to be patterns, masks or the like. + // Treat them as always visible. + // This call must happen before the FrameCreated. This is because the + // primary frame pointer on our content node isn't set until after this + // function ends, so there is no way for the resulting OnVisibilityChange + // notification to get a frame. FrameCreated has a workaround for this in + // that it passes our frame around so it can be accessed. OnVisibilityChange + // doesn't have that workaround. + IncApproximateVisibleCount(); + } + + mListener = new SVGImageListener(this); + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (!imageLoader) { + MOZ_CRASH("Why is this not an image loading content?"); + } + + // We should have a PresContext now, so let's notify our image loader that + // we need to register any image animations with the refresh driver. + imageLoader->FrameCreated(this); + + imageLoader->AddNativeObserver(mListener); +} + +/* virtual */ +void SVGImageFrame::Destroy(DestroyContext& aContext) { + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + DecApproximateVisibleCount(); + } + + if (mReflowCallbackPosted) { + PresShell()->CancelReflowCallback(this); + mReflowCallbackPosted = false; + } + + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(nsIFrame::mContent); + + if (imageLoader) { + imageLoader->FrameDestroyed(this); + } + + nsIFrame::Destroy(aContext); +} + +/* virtual */ +void SVGImageFrame::DidSetComputedStyle(ComputedStyle* aOldStyle) { + nsIFrame::DidSetComputedStyle(aOldStyle); + + if (!mImageContainer || !aOldStyle) { + return; + } + + nsCOMPtr<imgIRequest> currentRequest; + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) { + imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(currentRequest)); + } + + StyleImageOrientation newOrientation = + StyleVisibility()->UsedImageOrientation(currentRequest); + StyleImageOrientation oldOrientation = + aOldStyle->StyleVisibility()->UsedImageOrientation(currentRequest); + + if (oldOrientation != newOrientation) { + nsCOMPtr<imgIContainer> image(mImageContainer->Unwrap()); + mImageContainer = nsLayoutUtils::OrientImage(image, newOrientation); + } + + // TODO(heycam): We should handle aspect-ratio, like nsImageFrame does. +} + +bool SVGImageFrame::IsSVGTransformed(gfx::Matrix* aOwnTransform, + gfx::Matrix* aFromParentTransform) const { + return SVGUtils::IsSVGTransformed(this, aOwnTransform, aFromParentTransform); +} + +//---------------------------------------------------------------------- +// nsIFrame methods: + +nsresult SVGImageFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::preserveAspectRatio) { + // We don't paint the content of the image using display lists, therefore + // we have to invalidate for this children-only transform changes since + // there is no layer tree to notice that the transform changed and + // recomposite. + InvalidateFrame(); + return NS_OK; + } + } + + // Currently our SMIL implementation does not modify the DOM attributes. Once + // we implement the SVG 2 SMIL behaviour this can be removed + // SVGImageElement::AfterSetAttr's implementation will be sufficient. + if (aModType == MutationEvent_Binding::SMIL && + aAttribute == nsGkAtoms::href && + (aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None)) { + SVGImageElement* element = static_cast<SVGImageElement*>(GetContent()); + + bool hrefIsSet = + element->mStringAttributes[SVGImageElement::HREF].IsExplicitlySet() || + element->mStringAttributes[SVGImageElement::XLINK_HREF] + .IsExplicitlySet(); + if (hrefIsSet) { + element->LoadSVGImage(true, true); + } else { + element->CancelImageRequests(true); + } + } + + return NS_OK; +} + +void SVGImageFrame::OnVisibilityChange( + Visibility aNewVisibility, const Maybe<OnNonvisible>& aNonvisibleAction) { + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) { + imageLoader->OnVisibilityChange(aNewVisibility, aNonvisibleAction); + } + + nsIFrame::OnVisibilityChange(aNewVisibility, aNonvisibleAction); +} + +gfx::Matrix SVGImageFrame::GetRasterImageTransform(int32_t aNativeWidth, + int32_t aNativeHeight) { + float x, y, width, height; + SVGImageElement* element = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + element, &x, &y, &width, &height); + + Matrix viewBoxTM = SVGContentUtils::GetViewBoxTransform( + width, height, 0, 0, aNativeWidth, aNativeHeight, + element->mPreserveAspectRatio); + + return viewBoxTM * gfx::Matrix::Translation(x, y); +} + +gfx::Matrix SVGImageFrame::GetVectorImageTransform() { + float x, y; + SVGImageElement* element = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y>(element, &x, &y); + + // No viewBoxTM needed here -- our height/width overrides any concept of + // "native size" that the SVG image has, and it will handle viewBox and + // preserveAspectRatio on its own once we give it a region to draw into. + + return gfx::Matrix::Translation(x, y); +} + +bool SVGImageFrame::GetIntrinsicImageDimensions( + mozilla::gfx::Size& aSize, mozilla::AspectRatio& aAspectRatio) const { + if (!mImageContainer) { + return false; + } + + ImageResolution resolution = mImageContainer->GetResolution(); + + int32_t width, height; + if (NS_FAILED(mImageContainer->GetWidth(&width))) { + aSize.width = -1; + } else { + aSize.width = width; + resolution.ApplyXTo(aSize.width); + } + + if (NS_FAILED(mImageContainer->GetHeight(&height))) { + aSize.height = -1; + } else { + aSize.height = height; + resolution.ApplyYTo(aSize.height); + } + + Maybe<AspectRatio> asp = mImageContainer->GetIntrinsicRatio(); + aAspectRatio = asp.valueOr(AspectRatio{}); + + return true; +} + +bool SVGImageFrame::TransformContextForPainting(gfxContext* aGfxContext, + const gfxMatrix& aTransform) { + gfx::Matrix imageTransform; + if (mImageContainer->GetType() == imgIContainer::TYPE_VECTOR) { + imageTransform = GetVectorImageTransform() * ToMatrix(aTransform); + } else { + int32_t nativeWidth, nativeHeight; + if (NS_FAILED(mImageContainer->GetWidth(&nativeWidth)) || + NS_FAILED(mImageContainer->GetHeight(&nativeHeight)) || + nativeWidth == 0 || nativeHeight == 0) { + return false; + } + mImageContainer->GetResolution().ApplyTo(nativeWidth, nativeHeight); + imageTransform = GetRasterImageTransform(nativeWidth, nativeHeight) * + ToMatrix(aTransform); + + // NOTE: We need to cancel out the effects of Full-Page-Zoom, or else + // it'll get applied an extra time by DrawSingleUnscaledImage. + nscoord appUnitsPerDevPx = PresContext()->AppUnitsPerDevPixel(); + gfxFloat pageZoomFactor = + nsPresContext::AppUnitsToFloatCSSPixels(appUnitsPerDevPx); + imageTransform.PreScale(pageZoomFactor, pageZoomFactor); + } + + if (imageTransform.IsSingular()) { + return false; + } + + aGfxContext->Multiply(ThebesMatrix(imageTransform)); + return true; +} + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +void SVGImageFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + if (!StyleVisibility()->IsVisible()) { + return; + } + + float x, y, width, height; + SVGImageElement* imgElem = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + imgElem, &x, &y, &width, &height); + NS_ASSERTION(width > 0 && height > 0, + "Should only be painting things with valid width/height"); + + if (!mImageContainer) { + nsCOMPtr<imgIRequest> currentRequest; + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) + imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(currentRequest)); + + if (currentRequest) + currentRequest->GetImage(getter_AddRefs(mImageContainer)); + } + + if (mImageContainer) { + gfxClipAutoSaveRestore autoSaveClip(&aContext); + + if (StyleDisplay()->IsScrollableOverflow()) { + gfxRect clipRect = + SVGUtils::GetClipRectForFrame(this, x, y, width, height); + autoSaveClip.TransformedClip(aTransform, clipRect); + } + + gfxContextMatrixAutoSaveRestore autoSaveMatrix(&aContext); + + if (!TransformContextForPainting(&aContext, aTransform)) { + return; + } + + // fill-opacity doesn't affect <image>, so if we're allowed to + // optimize group opacity, the opacity used for compositing the + // image into the current canvas is just the group opacity. + float opacity = 1.0f; + if (SVGUtils::CanOptimizeOpacity(this)) { + opacity = StyleEffects()->mOpacity; + } + + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aContext); + if (opacity != 1.0f || StyleEffects()->HasMixBlendMode()) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + opacity); + } + + nscoord appUnitsPerDevPx = PresContext()->AppUnitsPerDevPixel(); + uint32_t flags = aImgParams.imageFlags; + if (mForceSyncDecoding) { + flags |= imgIContainer::FLAG_SYNC_DECODE; + } + + if (mImageContainer->GetType() == imgIContainer::TYPE_VECTOR) { + // Package up the attributes of this image element which can override the + // attributes of mImageContainer's internal SVG document. The 'width' & + // 'height' values we're passing in here are in CSS units (though they + // come from width/height *attributes* in SVG). They influence the region + // of the SVG image's internal document that is visible, in combination + // with preserveAspectRatio and viewBox. + const SVGImageContext context( + Some(CSSIntSize::Ceil(width, height)), + Some(imgElem->mPreserveAspectRatio.GetAnimValue())); + + // For the actual draw operation to draw crisply (and at the right size), + // our destination rect needs to be |width|x|height|, *in dev pixels*. + LayoutDeviceSize devPxSize(width, height); + nsRect destRect(nsPoint(), LayoutDevicePixel::ToAppUnits( + devPxSize, appUnitsPerDevPx)); + + // Note: Can't use DrawSingleUnscaledImage for the TYPE_VECTOR case. + // That method needs our image to have a fixed native width & height, + // and that's not always true for TYPE_VECTOR images. + aImgParams.result &= nsLayoutUtils::DrawSingleImage( + aContext, PresContext(), mImageContainer, + nsLayoutUtils::GetSamplingFilterForFrame(this), destRect, destRect, + context, flags); + } else { // mImageContainer->GetType() == TYPE_RASTER + aImgParams.result &= nsLayoutUtils::DrawSingleUnscaledImage( + aContext, PresContext(), mImageContainer, + nsLayoutUtils::GetSamplingFilterForFrame(this), nsPoint(0, 0), + nullptr, SVGImageContext(), flags); + } + } +} + +void SVGImageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (!static_cast<const SVGElement*>(GetContent())->HasValidDimensions()) { + return; + } + + if (aBuilder->IsForPainting()) { + if (!IsVisibleForPainting()) { + return; + } + if (StyleEffects()->IsTransparent()) { + return; + } + aBuilder->BuildCompositorHitTestInfoIfNeeded(this, + aLists.BorderBackground()); + } + + DisplayOutline(aBuilder, aLists); + aLists.Content()->AppendNewToTop<DisplaySVGImage>(aBuilder, this); +} + +bool SVGImageFrame::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; + + return StyleEffects()->mOpacity <= opacity_threshold; +} + +bool SVGImageFrame::CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder, DisplaySVGImage* aItem, + bool aDryRun) { + if (!StyleVisibility()->IsVisible()) { + return true; + } + + float opacity = 1.0f; + if (SVGUtils::CanOptimizeOpacity(this)) { + opacity = StyleEffects()->mOpacity; + } + + if (opacity != 1.0f) { + // FIXME: not implemented, might be trivial + return false; + } + if (StyleEffects()->HasMixBlendMode()) { + // FIXME: not implemented + return false; + } + + // try to setup the image + if (!mImageContainer) { + nsCOMPtr<imgIRequest> currentRequest; + nsCOMPtr<nsIImageLoadingContent> imageLoader = + do_QueryInterface(GetContent()); + if (imageLoader) { + imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(currentRequest)); + } + + if (currentRequest) { + currentRequest->GetImage(getter_AddRefs(mImageContainer)); + } + } + + if (!mImageContainer) { + // nothing to draw (yet) + return true; + } + + uint32_t flags = aDisplayListBuilder->GetImageDecodeFlags(); + if (mForceSyncDecoding) { + flags |= imgIContainer::FLAG_SYNC_DECODE; + } + + // Compute bounds of the image + nscoord appUnitsPerDevPx = PresContext()->AppUnitsPerDevPixel(); + int32_t appUnitsPerCSSPixel = AppUnitsPerCSSPixel(); + + float x, y, width, height; + SVGImageElement* imgElem = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + imgElem, &x, &y, &width, &height); + NS_ASSERTION(width > 0 && height > 0, + "Should only be painting things with valid width/height"); + + auto toReferenceFrame = aItem->ToReferenceFrame(); + auto appRect = nsLayoutUtils::RoundGfxRectToAppRect(Rect(0, 0, width, height), + appUnitsPerCSSPixel); + appRect += toReferenceFrame; + auto destRect = LayoutDeviceRect::FromAppUnits(appRect, appUnitsPerDevPx); + auto clipRect = destRect; + + if (StyleDisplay()->IsScrollableOverflow()) { + // Apply potential non-trivial clip + auto cssClip = SVGUtils::GetClipRectForFrame(this, 0, 0, width, height); + auto appClip = + nsLayoutUtils::RoundGfxRectToAppRect(cssClip, appUnitsPerCSSPixel); + appClip += toReferenceFrame; + clipRect = LayoutDeviceRect::FromAppUnits(appClip, appUnitsPerDevPx); + + // Apply preserveAspectRatio + if (mImageContainer->GetType() == imgIContainer::TYPE_RASTER) { + int32_t nativeWidth, nativeHeight; + if (NS_FAILED(mImageContainer->GetWidth(&nativeWidth)) || + NS_FAILED(mImageContainer->GetHeight(&nativeHeight)) || + nativeWidth == 0 || nativeHeight == 0) { + // Image has no size; nothing to draw + return true; + } + + mImageContainer->GetResolution().ApplyTo(nativeWidth, nativeHeight); + + auto preserveAspectRatio = imgElem->mPreserveAspectRatio.GetAnimValue(); + uint16_t align = preserveAspectRatio.GetAlign(); + uint16_t meetOrSlice = preserveAspectRatio.GetMeetOrSlice(); + + // default to the defaults + if (align == SVG_PRESERVEASPECTRATIO_UNKNOWN) { + align = SVG_PRESERVEASPECTRATIO_XMIDYMID; + } + if (meetOrSlice == SVG_MEETORSLICE_UNKNOWN) { + meetOrSlice = SVG_MEETORSLICE_MEET; + } + + // aspect > 1 is horizontal + // aspect < 1 is vertical + float nativeAspect = ((float)nativeWidth) / ((float)nativeHeight); + float viewAspect = width / height; + + // "Meet" is "fit image to view"; "Slice" is "cover view with image". + // + // Whether we meet or slice, one side of the destRect will always be + // perfectly spanned by our image. The only questions to answer are + // "which side won't span perfectly" and "should that side be grown + // or shrunk". + // + // Because we fit our image to the destRect, this all just reduces to: + // "if meet, shrink to fit. if slice, grow to fit." + if (align != SVG_PRESERVEASPECTRATIO_NONE && nativeAspect != viewAspect) { + // Slightly redundant bools, but they make the conditions clearer + bool tooTall = nativeAspect > viewAspect; + bool tooWide = nativeAspect < viewAspect; + if ((meetOrSlice == SVG_MEETORSLICE_MEET && tooTall) || + (meetOrSlice == SVG_MEETORSLICE_SLICE && tooWide)) { + // Adjust height and realign y + auto oldHeight = destRect.height; + destRect.height = destRect.width / nativeAspect; + auto heightChange = oldHeight - destRect.height; + switch (align) { + case SVG_PRESERVEASPECTRATIO_XMINYMIN: + case SVG_PRESERVEASPECTRATIO_XMIDYMIN: + case SVG_PRESERVEASPECTRATIO_XMAXYMIN: + // align to top (no-op) + break; + case SVG_PRESERVEASPECTRATIO_XMINYMID: + case SVG_PRESERVEASPECTRATIO_XMIDYMID: + case SVG_PRESERVEASPECTRATIO_XMAXYMID: + // align to center + destRect.y += heightChange / 2.0f; + break; + case SVG_PRESERVEASPECTRATIO_XMINYMAX: + case SVG_PRESERVEASPECTRATIO_XMIDYMAX: + case SVG_PRESERVEASPECTRATIO_XMAXYMAX: + // align to bottom + destRect.y += heightChange; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown value for align"); + } + } else if ((meetOrSlice == SVG_MEETORSLICE_MEET && tooWide) || + (meetOrSlice == SVG_MEETORSLICE_SLICE && tooTall)) { + // Adjust width and realign x + auto oldWidth = destRect.width; + destRect.width = destRect.height * nativeAspect; + auto widthChange = oldWidth - destRect.width; + switch (align) { + case SVG_PRESERVEASPECTRATIO_XMINYMIN: + case SVG_PRESERVEASPECTRATIO_XMINYMID: + case SVG_PRESERVEASPECTRATIO_XMINYMAX: + // align to left (no-op) + break; + case SVG_PRESERVEASPECTRATIO_XMIDYMIN: + case SVG_PRESERVEASPECTRATIO_XMIDYMID: + case SVG_PRESERVEASPECTRATIO_XMIDYMAX: + // align to center + destRect.x += widthChange / 2.0f; + break; + case SVG_PRESERVEASPECTRATIO_XMAXYMIN: + case SVG_PRESERVEASPECTRATIO_XMAXYMID: + case SVG_PRESERVEASPECTRATIO_XMAXYMAX: + // align to right + destRect.x += widthChange; + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown value for align"); + } + } + } + } + } + + SVGImageContext svgContext; + if (mImageContainer->GetType() == imgIContainer::TYPE_VECTOR) { + if (StaticPrefs::image_svg_blob_image()) { + flags |= imgIContainer::FLAG_RECORD_BLOB; + } + // Forward preserveAspectRatio to inner SVGs + svgContext.SetViewportSize(Some(CSSIntSize::Ceil(width, height))); + svgContext.SetPreserveAspectRatio( + Some(imgElem->mPreserveAspectRatio.GetAnimValue())); + } + + Maybe<ImageIntRegion> region; + IntSize decodeSize = nsLayoutUtils::ComputeImageContainerDrawingParameters( + mImageContainer, this, destRect, clipRect, aSc, flags, svgContext, + region); + + RefPtr<image::WebRenderImageProvider> provider; + ImgDrawResult drawResult = mImageContainer->GetImageProvider( + aManager->LayerManager(), decodeSize, svgContext, region, flags, + getter_AddRefs(provider)); + + // While we got a container, it may not contain a fully decoded surface. If + // that is the case, and we have an image we were previously displaying which + // has a fully decoded surface, then we should prefer the previous image. + switch (drawResult) { + case ImgDrawResult::NOT_READY: + case ImgDrawResult::TEMPORARY_ERROR: + // nothing to draw (yet) + return true; + case ImgDrawResult::NOT_SUPPORTED: + // things we haven't implemented for WR yet + return false; + default: + // image is ready to draw + break; + } + + // Don't do any actual mutations to state if we're doing a dry run + // (used to decide if we're making this into an active layer) + if (!aDryRun) { + // If the image container is empty, we don't want to fallback. Any other + // failure will be due to resource constraints and fallback is unlikely to + // help us. Hence we can ignore the return value from PushImage. + if (provider) { + aManager->CommandBuilder().PushImageProvider(aItem, provider, drawResult, + aBuilder, aResources, + destRect, clipRect); + } + } + + return true; +} + +nsIFrame* SVGImageFrame::GetFrameForPoint(const gfxPoint& aPoint) { + if (!HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD) && IgnoreHitTest()) { + return nullptr; + } + + Rect rect; + SVGImageElement* element = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + element, &rect.x, &rect.y, &rect.width, &rect.height); + + if (!rect.Contains(ToPoint(aPoint))) { + return nullptr; + } + + // Special case for raster images -- we only want to accept points that fall + // in the underlying image's (scaled to fit) native bounds. That region + // doesn't necessarily map to our <image> element's [x,y,width,height] if the + // raster image's aspect ratio is being preserved. We have to look up the + // native image size & our viewBox transform in order to filter out points + // that fall outside that area. (This special case doesn't apply to vector + // images because they don't limit their drawing to explicit "native + // bounds" -- they have an infinite canvas on which to place content.) + if (StyleDisplay()->IsScrollableOverflow() && mImageContainer) { + if (mImageContainer->GetType() == imgIContainer::TYPE_RASTER) { + int32_t nativeWidth, nativeHeight; + if (NS_FAILED(mImageContainer->GetWidth(&nativeWidth)) || + NS_FAILED(mImageContainer->GetHeight(&nativeHeight)) || + nativeWidth == 0 || nativeHeight == 0) { + return nullptr; + } + mImageContainer->GetResolution().ApplyTo(nativeWidth, nativeHeight); + Matrix viewBoxTM = SVGContentUtils::GetViewBoxTransform( + rect.width, rect.height, 0, 0, nativeWidth, nativeHeight, + element->mPreserveAspectRatio); + if (!SVGUtils::HitTestRect(viewBoxTM, 0, 0, nativeWidth, nativeHeight, + aPoint.x - rect.x, aPoint.y - rect.y)) { + return nullptr; + } + } + } + + return this; +} + +void SVGImageFrame::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; + } + + float x, y, width, height; + SVGImageElement* element = static_cast<SVGImageElement*>(GetContent()); + SVGGeometryProperty::ResolveAll<SVGT::X, SVGT::Y, SVGT::Width, SVGT::Height>( + element, &x, &y, &width, &height); + + Rect extent(x, y, width, height); + + if (!extent.IsEmpty()) { + mRect = nsLayoutUtils::RoundGfxRectToAppRect(extent, AppUnitsPerCSSPixel()); + } else { + mRect.SetEmpty(); + } + + 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); + + if (!mReflowCallbackPosted) { + mReflowCallbackPosted = true; + PresShell()->PostReflowCallback(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(); + } +} + +bool SVGImageFrame::ReflowFinished() { + mReflowCallbackPosted = false; + + // XXX(seth): We don't need this. The purpose of updating visibility + // synchronously is to ensure that animated images start animating + // immediately. In the short term, however, + // nsImageLoadingContent::OnUnlockedDraw() is enough to ensure that + // animations start as soon as the image is painted for the first time, and in + // the long term we want to update visibility information from the display + // list whenever we paint, so we don't actually need to do this. However, to + // avoid behavior changes during the transition from the old image visibility + // code, we'll leave it in for now. + UpdateVisibilitySynchronously(); + + return false; +} + +void SVGImageFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; } + +bool SVGImageFrame::IgnoreHitTest() const { + switch (Style()->PointerEvents()) { + case StylePointerEvents::None: + break; + case StylePointerEvents::Visiblepainted: + case StylePointerEvents::Auto: + if (StyleVisibility()->IsVisible()) { + /* XXX: should check pixel transparency */ + return false; + } + break; + case StylePointerEvents::Visiblefill: + case StylePointerEvents::Visiblestroke: + case StylePointerEvents::Visible: + if (StyleVisibility()->IsVisible()) { + return false; + } + break; + case StylePointerEvents::Painted: + /* XXX: should check pixel transparency */ + return false; + case StylePointerEvents::Fill: + case StylePointerEvents::Stroke: + case StylePointerEvents::All: + return false; + default: + NS_ERROR("not reached"); + break; + } + + return true; +} + +void SVGImageFrame::NotifySVGChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), + "Invalidation logic may need adjusting"); +} + +SVGBBox SVGImageFrame::GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) { + if (aToBBoxUserspace.IsSingular()) { + // XXX ReportToConsole + return {}; + } + + if ((aFlags & SVGUtils::eForGetClientRects) && + aToBBoxUserspace.PreservesAxisAlignedRectangles()) { + Rect rect = NSRectToRect(mRect, AppUnitsPerCSSPixel()); + return aToBBoxUserspace.TransformBounds(rect); + } + + auto* element = static_cast<SVGImageElement*>(GetContent()); + + return element->GeometryBounds(aToBBoxUserspace); +} + +//---------------------------------------------------------------------- +// SVGImageListener implementation + +NS_IMPL_ISUPPORTS(SVGImageListener, imgINotificationObserver) + +SVGImageListener::SVGImageListener(SVGImageFrame* aFrame) : mFrame(aFrame) {} + +void SVGImageListener::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (!mFrame) { + return; + } + + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + mFrame->InvalidateFrame(); + nsLayoutUtils::PostRestyleEvent(mFrame->GetContent()->AsElement(), + RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(mFrame); + } + + if (aType == imgINotificationObserver::FRAME_UPDATE) { + // No new dimensions, so we don't need to call + // SVGUtils::InvalidateAndScheduleBoundsUpdate. + nsLayoutUtils::PostRestyleEvent(mFrame->GetContent()->AsElement(), + RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + mFrame->InvalidateFrame(); + } + + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + // Called once the resource's dimensions have been obtained. + nsCOMPtr<imgIContainer> image; + aRequest->GetImage(getter_AddRefs(image)); + if (image) { + StyleImageOrientation orientation = + mFrame->StyleVisibility()->UsedImageOrientation(aRequest); + image = nsLayoutUtils::OrientImage(image, orientation); + image->SetAnimationMode(mFrame->PresContext()->ImageAnimationMode()); + mFrame->mImageContainer = std::move(image); + } + mFrame->InvalidateFrame(); + nsLayoutUtils::PostRestyleEvent(mFrame->GetContent()->AsElement(), + RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(mFrame); + } +} + +} // namespace mozilla diff --git a/layout/svg/SVGImageFrame.h b/layout/svg/SVGImageFrame.h new file mode 100644 index 0000000000..71e291e579 --- /dev/null +++ b/layout/svg/SVGImageFrame.h @@ -0,0 +1,186 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGIMAGEFRAME_H_ +#define LAYOUT_SVG_SVGIMAGEFRAME_H_ + +// Keep in (case-insensitive) order: +#include "mozilla/gfx/2D.h" +#include "mozilla/DisplaySVGItem.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "imgIContainer.h" +#include "nsContainerFrame.h" +#include "imgINotificationObserver.h" +#include "nsIReflowCallback.h" + +namespace mozilla { +class DisplaySVGImage; +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGImageFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGImageFrame final : public nsIFrame, + public ISVGDisplayableFrame, + public nsIReflowCallback { + friend nsIFrame* ::NS_NewSVGImageFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + friend class DisplaySVGImage; + + bool CreateWebRenderCommands(wr::DisplayListBuilder& aBuilder, + wr::IpcResourceUpdateQueue& aResources, + const layers::StackingContextHelper& aSc, + layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder, + DisplaySVGImage* aItem, bool aDryRun); + + private: + explicit SVGImageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID), + mReflowCallbackPosted(false), + mForceSyncDecoding(false) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_MAY_BE_TRANSFORMED); + EnableVisibilityTracking(); + } + + virtual ~SVGImageFrame(); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGImageFrame) + + // ISVGDisplayableFrame interface: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + bool IsDisplayContainer() override { return false; } + + // nsIFrame interface: + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~nsIFrame::eSVG); + } + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + void OnVisibilityChange( + Visibility aNewVisibility, + const Maybe<OnNonvisible>& aNonvisibleAction = Nothing()) override; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + void Destroy(DestroyContext&) override; + + void DidSetComputedStyle(ComputedStyle* aOldStyle) final; + + bool IsSVGTransformed(Matrix* aOwnTransforms = nullptr, + Matrix* aFromParentTransforms = nullptr) const override; + + bool GetIntrinsicImageDimensions(gfx::Size& aSize, + AspectRatio& aAspectRatio) const; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGImage"_ns, aResult); + } +#endif + + // nsIReflowCallback + bool ReflowFinished() override; + void ReflowCallbackCanceled() override; + + /// Always sync decode our image when painting if @aForce is true. + void SetForceSyncDecoding(bool aForce) { mForceSyncDecoding = aForce; } + + // SVGImageFrame methods: + bool IsInvisible() const; + + private: + bool IgnoreHitTest() const; + + gfx::Matrix GetRasterImageTransform(int32_t aNativeWidth, + int32_t aNativeHeight); + gfx::Matrix GetVectorImageTransform(); + bool TransformContextForPainting(gfxContext* aGfxContext, + const gfxMatrix& aTransform); + + nsCOMPtr<imgINotificationObserver> mListener; + + nsCOMPtr<imgIContainer> mImageContainer; + + bool mReflowCallbackPosted; + bool mForceSyncDecoding; + + friend class SVGImageListener; +}; + +//---------------------------------------------------------------------- +// Display list item: + +class DisplaySVGImage final : public DisplaySVGItem { + public: + DisplaySVGImage(nsDisplayListBuilder* aBuilder, SVGImageFrame* aFrame) + : DisplaySVGItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(DisplaySVGImage); + } + + MOZ_COUNTED_DTOR_OVERRIDE(DisplaySVGImage) + + NS_DISPLAY_DECL_NAME("DisplaySVGImage", TYPE_SVG_IMAGE) + + // Whether this part of the SVG should be natively handled by webrender, + // potentially becoming an "active layer" inside a blob image. + bool ShouldBeActive(mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) { + auto* frame = static_cast<SVGImageFrame*>(mFrame); + return frame->CreateWebRenderCommands(aBuilder, aResources, aSc, aManager, + aDisplayListBuilder, this, + /*aDryRun=*/true); + } + + bool CreateWebRenderCommands( + mozilla::wr::DisplayListBuilder& aBuilder, + mozilla::wr::IpcResourceUpdateQueue& aResources, + const mozilla::layers::StackingContextHelper& aSc, + mozilla::layers::RenderRootStateManager* aManager, + nsDisplayListBuilder* aDisplayListBuilder) override { + auto* frame = static_cast<SVGImageFrame*>(mFrame); + bool result = frame->CreateWebRenderCommands(aBuilder, aResources, aSc, + aManager, aDisplayListBuilder, + this, /*aDryRun=*/false); + MOZ_ASSERT(result, "ShouldBeActive inconsistent with CreateWRCommands?"); + return result; + } + + bool IsInvisible() const override { + auto* frame = static_cast<SVGImageFrame*>(mFrame); + return frame->IsInvisible(); + } +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGIMAGEFRAME_H_ diff --git a/layout/svg/SVGInnerSVGFrame.cpp b/layout/svg/SVGInnerSVGFrame.cpp new file mode 100644 index 0000000000..0a547ccc94 --- /dev/null +++ b/layout/svg/SVGInnerSVGFrame.cpp @@ -0,0 +1,40 @@ +/* -*- 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 "SVGInnerSVGFrame.h" + +#include "mozilla/PresShell.h" + +nsIFrame* NS_NewSVGInnerSVGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGInnerSVGFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGInnerSVGFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods + +NS_QUERYFRAME_HEAD(SVGInnerSVGFrame) + NS_QUERYFRAME_ENTRY(SVGInnerSVGFrame) + NS_QUERYFRAME_ENTRY(ISVGSVGFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGViewportFrame) + +#ifdef DEBUG +void SVGInnerSVGFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::svg), + "Content is not an SVG 'svg' element!"); + + SVGViewportFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +} // namespace mozilla diff --git a/layout/svg/SVGInnerSVGFrame.h b/layout/svg/SVGInnerSVGFrame.h new file mode 100644 index 0000000000..1db386f344 --- /dev/null +++ b/layout/svg/SVGInnerSVGFrame.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGINNERSVGFRAME_H_ +#define LAYOUT_SVG_SVGINNERSVGFRAME_H_ + +#include "SVGViewportFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGInnerSVGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGInnerSVGFrame final : public SVGViewportFrame { + friend nsIFrame* ::NS_NewSVGInnerSVGFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGInnerSVGFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGViewportFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGInnerSVGFrame) + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGInnerSVG"_ns, aResult); + } +#endif +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGINNERSVGFRAME_H_ diff --git a/layout/svg/SVGIntegrationUtils.cpp b/layout/svg/SVGIntegrationUtils.cpp new file mode 100644 index 0000000000..99e634d49d --- /dev/null +++ b/layout/svg/SVGIntegrationUtils.cpp @@ -0,0 +1,1235 @@ +/* -*- 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(); +} + +bool SVGIntegrationUtils::UsingMaskOrClipPathForFrame(const nsIFrame* aFrame) { + const nsStyleSVGReset* style = aFrame->StyleSVGReset(); + return style->HasClipPath() || style->HasMask(); +} + +bool SVGIntegrationUtils::UsingSimpleClipPathForFrame(const nsIFrame* aFrame) { + const nsStyleSVGReset* style = aFrame->StyleSVGReset(); + if (!style->HasClipPath() || style->HasMask()) { + return false; + } + + const auto& clipPath = style->mClipPath; + if (!clipPath.IsShape()) { + return false; + } + + return !clipPath.AsShape()._0->IsPolygon(); +} + +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->IsFrameOfType(nsIFrame::eSVG)) { + /* 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->IsFrameOfType(nsIFrame::eSVG), + "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->IsFrameOfType(nsIFrame::eSVG), + "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 currently pass nullptr instead of an nsTArray* here, but we + // actually should get the filter frames and then pass them into + // GetPostFilterBounds below! See bug 1494263. + // TODO: we should really return an empty rect for eHasRefsSomeInvalid since + // in that case we disable painting of the element. + if (SVGObserverUtils::GetAndObserveFilters(firstFrame, nullptr) == + 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<nsRect> overflowRect = + FilterInstance::GetPostFilterBounds(firstFrame, &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). + if (!aFrame->StyleEffects()->HasFilters() || + SVGObserverUtils::GetFiltersIfObserving(firstFrame, nullptr) == + 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, postEffectsRect) + .GetBounds() - + toUserSpace; +} + +bool SVGIntegrationUtils::HitTestFrameForEffects(nsIFrame* aFrame, + const nsPoint& aPt) { + nsIFrame* firstFrame = + nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame); + // Convert aPt to user space: + nsPoint toUserSpace; + if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + // XXXmstange Isn't this wrong for svg:use and innerSVG frames? + toUserSpace = aFrame->GetPosition(); + } else { + toUserSpace = + aFrame->GetOffsetTo(firstFrame) + GetOffsetToBoundingBox(firstFrame); + } + nsPoint pt = aPt + toUserSpace; + gfxPoint userSpacePt = gfxPoint(pt.x, pt.y) / AppUnitsPerCSSPixel(); + return SVGUtils::HitTestClip(firstFrame, userSpacePt); +} + +using PaintFramesParams = SVGIntegrationUtils::PaintFramesParams; + +/** + * Paint css-positioned-mask onto a given target(aMaskDT). + * Return value indicates if mask is complete. + */ +static bool PaintMaskSurface(const PaintFramesParams& aParams, + DrawTarget* aMaskDT, float aOpacity, + const ComputedStyle* aSC, + const nsTArray<SVGMaskFrame*>& aMaskFrames, + const nsPoint& aOffsetToUserSpace) { + MOZ_ASSERT(aMaskFrames.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<SourceSurface> svgMask = maskFrame->GetMaskForMaskedFrame(params); + if (svgMask) { + Matrix tmp = aMaskDT->GetTransform(); + aMaskDT->SetTransform(Matrix()); + aMaskDT->MaskSurface(ColorPattern(DeviceColor(0.0, 0.0, 0.0, 1.0)), + svgMask, Point(0, 0), + DrawOptions(1.0, compositionOp)); + aMaskDT->SetTransform(tmp); + } + } else if (svgReset->mMask.mLayers[i].mImage.IsResolved()) { + gfxContextMatrixAutoSaveRestore matRestore(&maskContext); + + maskContext.Multiply(gfxMatrix::Translation(-devPixelOffsetToUserSpace)); + nsCSSRendering::PaintBGParams params = + nsCSSRendering::PaintBGParams::ForSingleLayer( + *presContext, aParams.dirtyRect, aParams.borderArea, + aParams.frame, + aParams.builder->GetBackgroundPaintFlags() | + nsCSSRendering::PAINTBG_MASK_IMAGE, + i, compositionOp, aOpacity); + + aParams.imgParams.result &= nsCSSRendering::PaintStyleImageLayerWithSC( + params, maskContext, aSC, *aParams.frame->StyleBorder()); + } else { + isMaskComplete = false; + } + } + + return isMaskComplete; +} + +struct MaskPaintResult { + RefPtr<SourceSurface> maskSurface; + Matrix maskTransform; + bool transparentBlackMask; + bool opacityApplied; + + MaskPaintResult() : transparentBlackMask(false), opacityApplied(false) {} +}; + +static MaskPaintResult CreateAndPaintMaskSurface( + const PaintFramesParams& aParams, float aOpacity, const ComputedStyle* aSC, + const nsTArray<SVGMaskFrame*>& aMaskFrames, + const nsPoint& aOffsetToUserSpace) { + const nsStyleSVGReset* svgReset = aSC->StyleSVGReset(); + MOZ_ASSERT(aMaskFrames.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<DrawTarget> maskDT = ctx.GetDrawTarget()->CreateClippedDrawTarget( + maskSurfaceRect.ToUnknownRect(), SurfaceFormat::A8); + if (!maskDT || !maskDT->IsValid()) { + return paintResult; + } + + // We can paint mask along with opacity only if + // 1. There is only one mask, or + // 2. No overlap among masks. + // Collision detect in #2 is not that trivial, we only accept #1 here. + paintResult.opacityApplied = (aMaskFrames.Length() == 1); + + // Set context's matrix on maskContext, offset by the maskSurfaceRect's + // position. This makes sure that we combine the masks in device space. + Matrix maskSurfaceMatrix = ctx.CurrentMatrix(); + + bool isMaskComplete = PaintMaskSurface( + aParams, maskDT, paintResult.opacityApplied ? aOpacity : 1.0, aSC, + aMaskFrames, aOffsetToUserSpace); + + if (!isMaskComplete || + (aParams.imgParams.result != ImgDrawResult::SUCCESS && + aParams.imgParams.result != ImgDrawResult::SUCCESS_NOT_COMPLETE && + aParams.imgParams.result != ImgDrawResult::WRONG_SIZE)) { + // Now we know the status of mask resource since we used it while painting. + // According to the return value of PaintMaskSurface, we know whether mask + // resource is resolvable or not. + // + // For a HTML doc: + // According to css-masking spec, always create a mask surface when + // we have any item in maskFrame even if all of those items are + // non-resolvable <mask-sources> or <images>. + // Set paintResult.transparentBlackMask as true, the caller should stop + // painting masked content as if this mask is a transparent black one. + // For a SVG doc: + // SVG 1.1 say that if we fail to resolve a mask, we should draw the + // object unmasked. + // Left paintResult.maskSurface empty, the caller should paint all + // masked content as if this mask is an opaque white one(no mask). + paintResult.transparentBlackMask = + !aParams.frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT); + + MOZ_ASSERT(!paintResult.maskSurface); + return paintResult; + } + + paintResult.maskTransform = maskSurfaceMatrix; + if (!paintResult.maskTransform.Invert()) { + return paintResult; + } + + paintResult.maskSurface = maskDT->Snapshot(); + return paintResult; +} + +static bool ValidateSVGFrame(nsIFrame* aFrame) { + NS_ASSERTION( + !aFrame->HasAllStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY), + "Should not use SVGIntegrationUtils on this SVG frame"); + + if (aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { +#ifdef DEBUG + ISVGDisplayableFrame* svgFrame = do_QueryFrame(aFrame); + MOZ_ASSERT(svgFrame && aFrame->GetContent()->IsSVGElement(), + "A non-SVG frame carries NS_FRAME_SVG_LAYOUT flag?"); +#endif + + const nsIContent* content = aFrame->GetContent(); + if (!static_cast<const SVGElement*>(content)->HasValidDimensions()) { + // The SVG spec says not to draw _anything_ + return false; + } + } + + return true; +} + +bool SVGIntegrationUtils::PaintMask(const PaintFramesParams& aParams, + bool& aOutIsMaskComplete) { + aOutIsMaskComplete = true; + + SVGUtils::MaskUsage maskUsage; + SVGUtils::DetermineMaskUsage(aParams.frame, aParams.handleOpacity, maskUsage); + if (!maskUsage.shouldDoSomething()) { + return false; + } + + nsIFrame* frame = aParams.frame; + if (!ValidateSVGFrame(frame)) { + return false; + } + + gfxContext& ctx = aParams.ctx; + RefPtr<DrawTarget> maskTarget = ctx.GetDrawTarget(); + + if (maskUsage.shouldGenerateMaskLayer && + (maskUsage.shouldGenerateClipMaskLayer || + maskUsage.shouldApplyClipPath)) { + // We will paint both mask of positioned mask and clip-path into + // maskTarget. + // + // Create one extra draw target for drawing positioned mask, so that we do + // not have to copy the content of maskTarget before painting + // clip-path into it. + maskTarget = maskTarget->CreateClippedDrawTarget(Rect(), SurfaceFormat::A8); + } + + nsIFrame* firstFrame = + nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame); + nsTArray<SVGMaskFrame*> maskFrames; + // XXX check return value? + SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames); + + gfxGroupForBlendAutoSaveRestore autoPop(&ctx); + bool shouldPushOpacity = + (maskUsage.opacity != 1.0) && (maskFrames.Length() != 1); + if (shouldPushOpacity) { + autoPop.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + maskUsage.opacity); + } + + gfxContextMatrixAutoSaveRestore matSR; + + // Paint clip-path-basic-shape onto ctx + gfxContextAutoSaveRestore basicShapeSR; + if (maskUsage.shouldApplyBasicShapeOrPath) { + matSR.SetContext(&ctx); + + MoveContextOriginToUserSpace(firstFrame, aParams); + + basicShapeSR.SetContext(&ctx); + gfxMatrix mat = SVGUtils::GetCSSPxToDevPxMatrix(frame); + if (!maskUsage.shouldGenerateMaskLayer) { + // Only have basic-shape clip-path effect. Fill clipped region by + // opaque white. + ctx.SetDeviceColor(DeviceColor::MaskOpaqueWhite()); + RefPtr<Path> path = CSSClipPathInstance::CreateClipPathForFrame( + ctx.GetDrawTarget(), frame, mat); + if (path) { + ctx.SetPath(path); + ctx.Fill(); + } + + return true; + } + CSSClipPathInstance::ApplyBasicShapeOrPathClip(ctx, frame, mat); + } + + // Paint mask into maskTarget. + if (maskUsage.shouldGenerateMaskLayer) { + matSR.Restore(); + matSR.SetContext(&ctx); + + EffectOffsets offsets = ComputeEffectOffset(frame, aParams); + maskTarget->SetTransform(maskTarget->GetTransform().PreTranslate( + ToPoint(offsets.offsetToUserSpaceInDevPx))); + aOutIsMaskComplete = PaintMaskSurface( + aParams, maskTarget, shouldPushOpacity ? 1.0 : maskUsage.opacity, + firstFrame->Style(), maskFrames, offsets.offsetToUserSpace); + } + + // Paint clip-path onto ctx. + if (maskUsage.shouldGenerateClipMaskLayer || maskUsage.shouldApplyClipPath) { + matSR.Restore(); + matSR.SetContext(&ctx); + + MoveContextOriginToUserSpace(firstFrame, aParams); + Matrix clipMaskTransform; + gfxMatrix cssPxToDevPxMatrix = SVGUtils::GetCSSPxToDevPxMatrix(frame); + + SVGClipPathFrame* clipPathFrame; + // XXX check return value? + SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame); + RefPtr<SourceSurface> maskSurface = + maskUsage.shouldGenerateMaskLayer ? maskTarget->Snapshot() : nullptr; + clipPathFrame->PaintClipMask(ctx, frame, cssPxToDevPxMatrix, maskSurface); + } + + return true; +} + +template <class T> +void PaintMaskAndClipPathInternal(const PaintFramesParams& aParams, + const T& aPaintChild) { + MOZ_ASSERT(SVGIntegrationUtils::UsingMaskOrClipPathForFrame(aParams.frame), + "Should not use this method when no mask or clipPath effect" + "on this frame"); + + /* 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, maskUsage); + + if (maskUsage.opacity == 0.0f) { + return; + } + + gfxContext& context = aParams.ctx; + gfxContextMatrixAutoSaveRestore matrixAutoSaveRestore(&context); + + nsIFrame* firstFrame = + nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame); + + SVGClipPathFrame* clipPathFrame; + // XXX check return value? + SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame); + + nsTArray<SVGMaskFrame*> maskFrames; + // XXX check return value? + SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames); + + gfxMatrix cssPxToDevPxMatrix = SVGUtils::GetCSSPxToDevPxMatrix(frame); + + bool shouldGenerateMask = + (maskUsage.opacity != 1.0f || maskUsage.shouldGenerateClipMaskLayer || + maskUsage.shouldGenerateMaskLayer); + 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 (shouldGenerateMask) { + gfxContextMatrixAutoSaveRestore matSR; + + RefPtr<SourceSurface> maskSurface; + bool opacityApplied = false; + + if (maskUsage.shouldGenerateMaskLayer) { + matSR.SetContext(&context); + + // For css-mask, we want to generate a mask for each continuation frame, + // so we setup context matrix by the position of the current frame, + // instead of the first continuation frame. + EffectOffsets offsets = MoveContextOriginToUserSpace(frame, aParams); + MaskPaintResult paintResult = CreateAndPaintMaskSurface( + aParams, maskUsage.opacity, firstFrame->Style(), maskFrames, + offsets.offsetToUserSpace); + + if (paintResult.transparentBlackMask) { + return; + } + + maskSurface = paintResult.maskSurface; + if (maskSurface) { + shouldPushMask = true; + + opacityApplied = paintResult.opacityApplied; + } + } + + if (maskUsage.shouldGenerateClipMaskLayer) { + matSR.Restore(); + matSR.SetContext(&context); + + MoveContextOriginToUserSpace(firstFrame, aParams); + RefPtr<SourceSurface> clipMaskSurface = clipPathFrame->GetClipMask( + context, frame, cssPxToDevPxMatrix, maskSurface); + + if (clipMaskSurface) { + maskSurface = clipMaskSurface; + } else { + // Either entire surface is clipped out, or gfx buffer allocation + // failure in SVGClipPathFrame::GetClipMask. + return; + } + + shouldPushMask = true; + } + + // opacity != 1.0f. + if (!maskUsage.shouldGenerateClipMaskLayer && + !maskUsage.shouldGenerateMaskLayer) { + MOZ_ASSERT(maskUsage.opacity != 1.0f); + + 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.shouldApplyClipPath || + maskUsage.shouldGenerateClipMaskLayer) { + overlayColor.g = 1.0f; // green represents clip-path:<clip-source>. + } + if (maskUsage.shouldApplyBasicShapeOrPath) { + overlayColor.b = 1.0f; // blue represents + // clip-path:<basic-shape>||<geometry-box>. + } + + context.SetColor(overlayColor); + context.Fill(); + } + + if (maskUsage.shouldApplyClipPath || maskUsage.shouldApplyBasicShapeOrPath) { + context.PopClip(); + } +} + +void SVGIntegrationUtils::PaintMaskAndClipPath( + const PaintFramesParams& aParams, + const std::function<void()>& aPaintChild) { + PaintMaskAndClipPathInternal(aParams, aPaintChild); +} + +void SVGIntegrationUtils::PaintFilter(const PaintFramesParams& aParams, + Span<const StyleFilter> aFilters, + const SVGFilterPaintCallback& aCallback) { + MOZ_ASSERT(!aParams.builder->IsForGenerateGlyphMask(), + "Filter effect is discarded while generating glyph mask."); + MOZ_ASSERT(!aFilters.IsEmpty(), + "Should not use this method when no filter effect on this frame"); + + nsIFrame* frame = aParams.frame; + if (!ValidateSVGFrame(frame)) { + return; + } + + float opacity = SVGUtils::ComputeOpacity(frame, aParams.handleOpacity); + if (opacity == 0.0f) { + return; + } + + // Properties are added lazily and may have been removed by a restyle, so make + // sure all applicable ones are set again. + nsIFrame* firstFrame = + nsLayoutUtils::FirstContinuationOrIBSplitSibling(frame); + // Note: we do not return here for eHasNoRefs since we must still handle any + // CSS filter functions. + // TODO: We currently pass nullptr instead of an nsTArray* here, but we + // actually should get the filter frames and then pass them into + // PaintFilteredFrame below! See bug 1494263. + // XXX: Do we need to check for eHasRefsSomeInvalid here given that + // nsDisplayFilter::BuildLayer returns nullptr for eHasRefsSomeInvalid? + // Or can we just assert !eHasRefsSomeInvalid? + if (SVGObserverUtils::GetAndObserveFilters(firstFrame, nullptr) == + 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, &context, aCallback, + &dirtyRegion, aParams.imgParams, opacity); +} + +bool SVGIntegrationUtils::CreateWebRenderCSSFilters( + Span<const StyleFilter> 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<float>::Value(opacity), opacity)); + break; + } + case StyleFilter::Tag::Saturate: + wrFilters.AppendElement(wr::FilterOp::Saturate(filter.AsSaturate())); + break; + case StyleFilter::Tag::Sepia: + wrFilters.AppendElement(wr::FilterOp::Sepia(filter.AsSepia())); + break; + case StyleFilter::Tag::HueRotate: { + wrFilters.AppendElement( + wr::FilterOp::HueRotate(filter.AsHueRotate().ToDegrees())); + break; + } + case StyleFilter::Tag::Blur: { + // TODO(emilio): we should go directly from css pixels -> device pixels. + float appUnitsPerDevPixel = + aFrame->PresContext()->AppUnitsPerDevPixel(); + float radius = NSAppUnitsToFloatPixels(filter.AsBlur().ToAppUnits(), + appUnitsPerDevPixel); + wrFilters.AppendElement(wr::FilterOp::Blur(radius, radius)); + break; + } + case StyleFilter::Tag::DropShadow: { + float appUnitsPerDevPixel = + aFrame->PresContext()->AppUnitsPerDevPixel(); + const StyleSimpleShadow& shadow = filter.AsDropShadow(); + nscolor color = shadow.color.CalcColor(aFrame); + + wr::Shadow wrShadow; + wrShadow.offset = { + NSAppUnitsToFloatPixels(shadow.horizontal.ToAppUnits(), + appUnitsPerDevPixel), + NSAppUnitsToFloatPixels(shadow.vertical.ToAppUnits(), + appUnitsPerDevPixel)}; + wrShadow.blur_radius = NSAppUnitsToFloatPixels(shadow.blur.ToAppUnits(), + appUnitsPerDevPixel); + wrShadow.color = {NS_GET_R(color) / 255.0f, NS_GET_G(color) / 255.0f, + NS_GET_B(color) / 255.0f, NS_GET_A(color) / 255.0f}; + wrFilters.AppendElement(wr::FilterOp::DropShadow(wrShadow)); + break; + } + default: + return false; + } + } + + return true; +} + +bool SVGIntegrationUtils::BuildWebRenderFilters( + nsIFrame* aFilteredFrame, Span<const StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, bool& aInitialized) { + return FilterInstance::BuildWebRenderFilters(aFilteredFrame, aFilters, + 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, 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<gfxDrawable> 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<gfxPattern> pattern = server->GetPaintServerPattern( + aTarget, aDrawTarget, aContextMatrix, &nsStyleSVG::mFill, 1.0, + imgParams, &overrideBounds); + + if (!pattern) { + return nullptr; + } + + // pattern is now set up to fill aPaintServerSize. But we want it to + // fill aRenderSize, so we need to add a scaling transform. + // We couldn't just have set overrideBounds to aRenderSize - it would have + // worked for gradients, but for patterns it would result in a different + // pattern size. + gfxFloat scaleX = overrideBounds.Width() / aRenderSize.width; + gfxFloat scaleY = overrideBounds.Height() / aRenderSize.height; + gfxMatrix scaleMatrix = gfxMatrix::Scaling(scaleX, scaleY); + pattern->SetMatrix(scaleMatrix * pattern->GetMatrix()); + RefPtr<gfxDrawable> drawable = new gfxPatternDrawable(pattern, aRenderSize); + return drawable.forget(); + } + + if (aFrame->IsFrameOfType(nsIFrame::eSVG) && + !static_cast<ISVGDisplayableFrame*>(do_QueryFrame(aFrame))) { + MOZ_ASSERT_UNREACHABLE( + "We should prevent painting of unpaintable SVG " + "before we get here"); + return nullptr; + } + + // We don't want to paint into a surface as long as we don't need to, so we + // set up a drawing callback. + RefPtr<gfxDrawingCallback> cb = + new PaintFrameCallback(aFrame, aPaintServerSize, aRenderSize, aFlags); + RefPtr<gfxDrawable> drawable = new gfxCallbackDrawable(cb, aRenderSize); + return drawable.forget(); +} + +} // namespace mozilla diff --git a/layout/svg/SVGIntegrationUtils.h b/layout/svg/SVGIntegrationUtils.h new file mode 100644 index 0000000000..c736727d2b --- /dev/null +++ b/layout/svg/SVGIntegrationUtils.h @@ -0,0 +1,269 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGINTEGRATIONUTILS_H_ +#define LAYOUT_SVG_SVGINTEGRATIONUTILS_H_ + +#include "ImgDrawResult.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "nsRegionFwd.h" +#include "mozilla/gfx/Rect.h" +#include "mozilla/ServoStyleConsts.h" +#include "mozilla/webrender/WebRenderTypes.h" + +class gfxContext; +class gfxDrawable; +class nsIFrame; +struct nsPoint; +struct nsRect; +struct nsSize; + +struct WrFiltersHolder { + nsTArray<mozilla::wr::FilterOp> filters; + nsTArray<mozilla::wr::WrFilterData> filter_datas; + mozilla::Maybe<nsRect> post_filters_clip; + // This exists just to own the values long enough for them to be copied into + // rust. + nsTArray<nsTArray<float>> values; +}; + +namespace mozilla { +class nsDisplayList; +class nsDisplayListBuilder; + +namespace gfx { +class DrawTarget; +} // namespace gfx + +/** + * Integration of SVG effects (clipPath clipping, masking and filters) into + * regular display list based painting and hit-testing. + */ +class SVGIntegrationUtils final { + using DrawTarget = gfx::DrawTarget; + using IntRect = gfx::IntRect; + using imgDrawingParams = image::imgDrawingParams; + + public: + /** + * Returns true if SVG effects that affect the overflow of the given frame + * are currently applied to the frame. + */ + static bool UsingOverflowAffectingEffects(const nsIFrame* aFrame); + + /** + * Returns true if SVG effects are currently applied to this frame. + */ + static bool UsingEffectsForFrame(const nsIFrame* aFrame); + + /** + * Returns true if mask or clippath are currently applied to this frame. + */ + static bool UsingMaskOrClipPathForFrame(const nsIFrame* aFrame); + + /** + * Returns true if the element has a clippath that is simple enough to + * be represented without a mask in WebRender. + */ + static bool UsingSimpleClipPathForFrame(const nsIFrame* aFrame); + + /** + * Returns the size of the union of the border-box rects of all of + * aNonSVGFrame's continuations. + */ + static nsSize GetContinuationUnionSize(nsIFrame* aNonSVGFrame); + + /** + * When SVG effects need to resolve percentage, userSpaceOnUse lengths, they + * need a coordinate context to resolve them against. This method provides + * that coordinate context for non-SVG frames with SVG effects applied to + * them. The gfxSize returned is the size of the union of all of the given + * frame's continuations' border boxes, converted to SVG user units (equal to + * CSS px units), as required by the SVG code. + */ + static gfx::Size GetSVGCoordContextForNonSVGFrame(nsIFrame* aNonSVGFrame); + + /** + * SVG effects such as SVG filters, masking and clipPath may require an SVG + * "bbox" for the element they're being applied to in order to make decisions + * about positioning, and to resolve various lengths against. This method + * provides the "bbox" for non-SVG frames. The bbox returned is in CSS px + * units, and aUnionContinuations decide whether bbox contains the area of + * current frame only or the union of all aNonSVGFrame's continuations' + * overflow areas, relative to the top-left of the union of all aNonSVGFrame's + * continuations' border box rects. + */ + static gfxRect GetSVGBBoxForNonSVGFrame(nsIFrame* aNonSVGFrame, + bool aUnionContinuations); + + /** + * Used to adjust a frame's pre-effects ink overflow rect to take account + * of SVG effects. + * + * XXX This method will not do the right thing for frames with continuations. + * It really needs all the continuations to have been reflowed before being + * called, but we currently call it on each continuation as its overflow + * rects are set during the reflow of each particular continuation. Gecko's + * current reflow architecture does not allow us to set the overflow rects + * for a whole chain of continuations for a given element at the point when + * the last continuation is reflowed. See: + * http://groups.google.com/group/mozilla.dev.tech.layout/msg/6b179066f3051f65 + */ + static nsRect ComputePostEffectsInkOverflowRect( + nsIFrame* aFrame, const nsRect& aPreEffectsOverflowRect); + + /** + * Figure out which area of the source is needed given an area to + * repaint + */ + static nsRect GetRequiredSourceForInvalidArea(nsIFrame* aFrame, + const nsRect& aDirtyRect); + + /** + * Returns true if the given point is not clipped out by effects. + * @param aPt in appunits relative to aFrame + */ + static bool HitTestFrameForEffects(nsIFrame* aFrame, const nsPoint& aPt); + + struct MOZ_STACK_CLASS PaintFramesParams { + gfxContext& ctx; + nsIFrame* frame; + nsRect dirtyRect; + nsRect borderArea; + nsDisplayListBuilder* builder; + bool handleOpacity; // If true, PaintMaskAndClipPath/ PaintFilter should + // apply css opacity. + Maybe<LayoutDeviceRect> maskRect; + imgDrawingParams& imgParams; + + explicit PaintFramesParams(gfxContext& aCtx, nsIFrame* aFrame, + const nsRect& aDirtyRect, + const nsRect& aBorderArea, + nsDisplayListBuilder* aBuilder, + bool aHandleOpacity, + imgDrawingParams& aImgParams) + : ctx(aCtx), + frame(aFrame), + dirtyRect(aDirtyRect), + borderArea(aBorderArea), + builder(aBuilder), + handleOpacity(aHandleOpacity), + imgParams(aImgParams) {} + }; + + // This should use FunctionRef instead of std::function because we don't need + // to take ownership of the function. See bug 1490781. + static void PaintMaskAndClipPath(const PaintFramesParams& aParams, + const std::function<void()>& aPaintChild); + + /** + * Paint mask of frame onto a given context, aParams.ctx. + * aParams.ctx must contain an A8 surface. Returns false if the mask + * didn't get painted and should be ignored at the call site. + * isMaskComplete is an outparameter returning whether the mask is complete. + * Incomplete masks should not be drawn and the proper fallback behaviour + * depends on if the masked element is html or svg. + */ + static bool PaintMask(const PaintFramesParams& aParams, + bool& aOutIsMaskComplete); + + /** + * Paint the frame contents. + * SVG frames will have had matrix propagation set to false already. + * Non-SVG frames have to do their own thing. + * The caller will do a Save()/Restore() as necessary so feel free + * to mess with context state. + * The context will be configured to use the "user space" coordinate + * system if passing aTransform/aDirtyRect, or untouched otherwise. + * @param aImgParams the params to draw with. + * @param aTransform the user-to-device space matrix, if painting with + * filters. + * @param aDirtyRect the dirty rect *in user space pixels* + */ + using SVGFilterPaintCallback = std::function<void( + gfxContext& aContext, imgDrawingParams&, const gfxMatrix* aTransform, + const nsIntRect* aDirtyRect)>; + + /** + * Paint non-SVG frame with filter and opacity effect. + */ + static void PaintFilter(const PaintFramesParams& aParams, + Span<const StyleFilter> aFilters, + const SVGFilterPaintCallback& aCallback); + + /** + * Build WebRender filters for a frame with CSS filters applied to it. + */ + static bool CreateWebRenderCSSFilters(Span<const StyleFilter> aFilters, + nsIFrame* aFrame, + WrFiltersHolder& aWrFilters); + + /** + * Try to build WebRender filters for a frame with SVG filters applied to it + * if the filters are supported. + */ + static bool BuildWebRenderFilters(nsIFrame* aFilteredFrame, + Span<const StyleFilter> aFilters, + WrFiltersHolder& aWrFilters, + bool& aInitialized); + + /** + * Check if the filters present on |aFrame| are supported by WebRender. + */ + static bool CanCreateWebRenderFiltersForFrame(nsIFrame* aFrame); + + /** + * Check if |aFrame| uses any SVG effects that cannot be rendered in the + * compositor. + */ + static bool UsesSVGEffectsNotSupportedInCompositor(nsIFrame* aFrame); + + /** + * @param aRenderingContext the target rendering context in which the paint + * server will be rendered + * @param aTarget the target frame onto which the paint server will be + * rendered + * @param aPaintServer a first-continuation frame to use as the source + * @param aFilter a filter to be applied when scaling + * @param aDest the area the paint server image should be mapped to + * @param aFill the area to be filled with copies of the paint server image + * @param aAnchor a point in aFill which we will ensure is pixel-aligned in + * the output + * @param aDirty pixels outside this area may be skipped + * @param aPaintServerSize 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. + * @param aFlags pass FLAG_SYNC_DECODE_IMAGES and any images in the paint + * server will be decoding synchronously if they are not decoded already. + */ + enum { + FLAG_SYNC_DECODE_IMAGES = 0x01, + }; + + static already_AddRefed<gfxDrawable> DrawableFromPaintServer( + nsIFrame* aFrame, nsIFrame* aTarget, const nsSize& aPaintServerSize, + const gfx::IntSize& aRenderSize, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, uint32_t aFlags); + + /** + * For non-SVG frames, this gives the offset to the frame's "user space". + * For SVG frames, this returns a zero offset. + */ + static nsPoint GetOffsetToBoundingBox(nsIFrame* aFrame); + + /** + * The offset between the reference frame and the bounding box of the + * target frame in device units. + */ + static gfxPoint GetOffsetToUserSpaceInDevPx(nsIFrame* aFrame, + const PaintFramesParams& aParams); +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGINTEGRATIONUTILS_H_ diff --git a/layout/svg/SVGMarkerFrame.cpp b/layout/svg/SVGMarkerFrame.cpp new file mode 100644 index 0000000000..16c8869fa0 --- /dev/null +++ b/layout/svg/SVGMarkerFrame.cpp @@ -0,0 +1,245 @@ +/* -*- 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 "SVGMarkerFrame.h" + +// Keep others in (case-insensitive) order: +#include "gfxContext.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGContextPaint.h" +#include "mozilla/SVGGeometryFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGGeometryElement.h" +#include "mozilla/dom/SVGMarkerElement.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +nsContainerFrame* NS_NewSVGMarkerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGMarkerFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGMarkerFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods: + +nsresult SVGMarkerFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::markerUnits || aAttribute == nsGkAtoms::refX || + aAttribute == nsGkAtoms::refY || aAttribute == nsGkAtoms::markerWidth || + aAttribute == nsGkAtoms::markerHeight || + aAttribute == nsGkAtoms::orient || + aAttribute == nsGkAtoms::preserveAspectRatio || + aAttribute == nsGkAtoms::viewBox)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +#ifdef DEBUG +void SVGMarkerFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::marker), + "Content is not an SVG marker"); + + SVGContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +//---------------------------------------------------------------------- +// SVGContainerFrame methods: + +gfxMatrix SVGMarkerFrame::GetCanvasTM() { + NS_ASSERTION(mMarkedFrame, "null SVGGeometry frame"); + + if (mInUse2) { + // We're going to be bailing drawing the marker, so return an identity. + return gfxMatrix(); + } + + SVGMarkerElement* content = static_cast<SVGMarkerElement*>(GetContent()); + + mInUse2 = true; + gfxMatrix markedTM = mMarkedFrame->GetCanvasTM(); + mInUse2 = false; + + Matrix viewBoxTM = content->GetViewBoxTransform(); + + return ThebesMatrix(viewBoxTM * mMarkerTM) * markedTM; +} + +static nsIFrame* GetAnonymousChildFrame(nsIFrame* aFrame) { + nsIFrame* kid = aFrame->PrincipalChildList().FirstChild(); + MOZ_ASSERT(kid && kid->IsSVGMarkerAnonChildFrame(), + "expected to find anonymous child of marker frame"); + return kid; +} + +void SVGMarkerFrame::PaintMark(gfxContext& aContext, + const gfxMatrix& aToMarkedFrameUserSpace, + SVGGeometryFrame* aMarkedFrame, + const SVGMark& aMark, float aStrokeWidth, + imgDrawingParams& aImgParams) { + // If the flag is set when we get here, it means this marker frame + // has already been used painting the current mark, and the document + // has a marker reference loop. + if (mInUse) { + return; + } + + AutoMarkerReferencer markerRef(this, aMarkedFrame); + + SVGMarkerElement* marker = static_cast<SVGMarkerElement*>(GetContent()); + if (!marker->HasValidDimensions()) { + return; + } + + const SVGViewBox viewBox = marker->GetViewBox(); + + if (viewBox.width <= 0.0f || viewBox.height <= 0.0f) { + // We must disable rendering if the viewBox width or height are zero. + return; + } + + Matrix viewBoxTM = marker->GetViewBoxTransform(); + + mMarkerTM = marker->GetMarkerTransform(aStrokeWidth, aMark); + + gfxMatrix markTM = ThebesMatrix(viewBoxTM) * ThebesMatrix(mMarkerTM) * + aToMarkedFrameUserSpace; + + gfxClipAutoSaveRestore autoSaveClip(&aContext); + if (StyleDisplay()->IsScrollableOverflow()) { + gfxRect clipRect = SVGUtils::GetClipRectForFrame( + this, viewBox.x, viewBox.y, viewBox.width, viewBox.height); + autoSaveClip.TransformedClip(markTM, clipRect); + } + + nsIFrame* kid = GetAnonymousChildFrame(this); + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + // The CTM of each frame referencing us may be different. + SVGFrame->NotifySVGChanged(ISVGDisplayableFrame::TRANSFORM_CHANGED); + RefPtr<SVGContextPaintImpl> contextPaint = new SVGContextPaintImpl(); + contextPaint->Init(aContext.GetDrawTarget(), aContext.CurrentMatrixDouble(), + aMarkedFrame, SVGContextPaint::GetContextPaint(marker), + aImgParams); + AutoSetRestoreSVGContextPaint autoSetRestore(contextPaint, + marker->OwnerDoc()); + SVGUtils::PaintFrameWithEffects(kid, aContext, markTM, aImgParams); +} + +SVGBBox SVGMarkerFrame::GetMarkBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags, + SVGGeometryFrame* aMarkedFrame, + const SVGMark& aMark, + float aStrokeWidth) { + SVGBBox bbox; + + // If the flag is set when we get here, it means this marker frame + // has already been used in calculating the current mark bbox, and + // the document has a marker reference loop. + if (mInUse) { + return bbox; + } + + AutoMarkerReferencer markerRef(this, aMarkedFrame); + + SVGMarkerElement* content = static_cast<SVGMarkerElement*>(GetContent()); + if (!content->HasValidDimensions()) { + return bbox; + } + + const SVGViewBox viewBox = content->GetViewBox(); + + if (viewBox.width <= 0.0f || viewBox.height <= 0.0f) { + return bbox; + } + + mMarkerTM = content->GetMarkerTransform(aStrokeWidth, aMark); + Matrix viewBoxTM = content->GetViewBoxTransform(); + + Matrix tm = viewBoxTM * mMarkerTM * aToBBoxUserspace; + + ISVGDisplayableFrame* child = do_QueryFrame(GetAnonymousChildFrame(this)); + // When we're being called to obtain the invalidation area, we need to + // pass down all the flags so that stroke is included. However, once DOM + // getBBox() accepts flags, maybe we should strip some of those here? + + // We need to include zero width/height vertical/horizontal lines, so we have + // to use UnionEdges. + bbox.UnionEdges(child->GetBBoxContribution(tm, aFlags)); + + return bbox; +} + +void SVGMarkerFrame::SetParentCoordCtxProvider(SVGViewportElement* aContext) { + SVGMarkerElement* marker = static_cast<SVGMarkerElement*>(GetContent()); + marker->SetParentCoordCtxProvider(aContext); +} + +void SVGMarkerFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + aResult.AppendElement(OwnedAnonBox(GetAnonymousChildFrame(this))); +} + +//---------------------------------------------------------------------- +// helper class + +SVGMarkerFrame::AutoMarkerReferencer::AutoMarkerReferencer( + SVGMarkerFrame* aFrame, SVGGeometryFrame* aMarkedFrame) + : mFrame(aFrame) { + mFrame->mInUse = true; + mFrame->mMarkedFrame = aMarkedFrame; + + SVGViewportElement* ctx = + static_cast<SVGElement*>(aMarkedFrame->GetContent())->GetCtx(); + mFrame->SetParentCoordCtxProvider(ctx); +} + +SVGMarkerFrame::AutoMarkerReferencer::~AutoMarkerReferencer() { + mFrame->SetParentCoordCtxProvider(nullptr); + + mFrame->mMarkedFrame = nullptr; + mFrame->mInUse = false; +} + +} // namespace mozilla + +//---------------------------------------------------------------------- +// Implementation of SVGMarkerAnonChildFrame + +nsContainerFrame* NS_NewSVGMarkerAnonChildFrame( + mozilla::PresShell* aPresShell, mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGMarkerAnonChildFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGMarkerAnonChildFrame) + +#ifdef DEBUG +void SVGMarkerAnonChildFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + MOZ_ASSERT(aParent->IsSVGMarkerFrame(), "Unexpected parent"); + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif + +} // namespace mozilla diff --git a/layout/svg/SVGMarkerFrame.h b/layout/svg/SVGMarkerFrame.h new file mode 100644 index 0000000000..7f4da9c5a5 --- /dev/null +++ b/layout/svg/SVGMarkerFrame.h @@ -0,0 +1,164 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGMARKERFRAME_H_ +#define LAYOUT_SVG_SVGMARKERFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SVGContainerFrame.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "nsIFrame.h" +#include "nsLiteralString.h" +#include "nsQueryFrame.h" + +class gfxContext; + +namespace mozilla { + +class PresShell; +class SVGGeometryFrame; + +struct SVGMark; + +namespace dom { +class SVGViewportElement; +} // namespace dom +} // namespace mozilla + +nsContainerFrame* NS_NewSVGMarkerFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); +nsContainerFrame* NS_NewSVGMarkerAnonChildFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGMarkerFrame final : public SVGContainerFrame { + using imgDrawingParams = image::imgDrawingParams; + + friend class SVGMarkerAnonChildFrame; + friend nsContainerFrame* ::NS_NewSVGMarkerFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + protected: + explicit SVGMarkerFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGContainerFrame(aStyle, aPresContext, kClassID), + mMarkedFrame(nullptr), + mInUse(false), + mInUse2(false) { + AddStateBits(NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGMarkerFrame) + + // nsIFrame interface: +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGMarker"_ns, aResult); + } +#endif + + nsContainerFrame* GetContentInsertionFrame() override { + // Any children must be added to our single anonymous inner frame kid. + MOZ_ASSERT( + PrincipalChildList().FirstChild() && + PrincipalChildList().FirstChild()->IsSVGMarkerAnonChildFrame(), + "Where is our anonymous child?"); + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + // SVGMarkerFrame methods: + void PaintMark(gfxContext& aContext, const gfxMatrix& aToMarkedFrameUserSpace, + SVGGeometryFrame* aMarkedFrame, const SVGMark& aMark, + float aStrokeWidth, imgDrawingParams& aImgParams); + + SVGBBox GetMarkBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags, + SVGGeometryFrame* aMarkedFrame, + const SVGMark& aMark, float aStrokeWidth); + + // Return our anonymous box child. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + + private: + // stuff needed for callback + SVGGeometryFrame* mMarkedFrame; + Matrix mMarkerTM; + + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override; + + // A helper class to allow us to paint markers safely. The helper + // automatically sets and clears the mInUse flag on the marker frame (to + // prevent nasty reference loops) as well as the reference to the marked + // frame and its coordinate context. It's easy to mess this up + // and break things, so this helper makes the code far more robust. + class MOZ_RAII AutoMarkerReferencer { + public: + AutoMarkerReferencer(SVGMarkerFrame* aFrame, + SVGGeometryFrame* aMarkedFrame); + ~AutoMarkerReferencer(); + + private: + SVGMarkerFrame* mFrame; + }; + + // SVGMarkerFrame methods: + void SetParentCoordCtxProvider(dom::SVGViewportElement* aContext); + + // recursion prevention flag + bool mInUse; + + // second recursion prevention flag, for GetCanvasTM() + bool mInUse2; +}; + +//////////////////////////////////////////////////////////////////////// +// nsMarkerAnonChildFrame class + +class SVGMarkerAnonChildFrame final : public SVGDisplayContainerFrame { + friend nsContainerFrame* ::NS_NewSVGMarkerAnonChildFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + explicit SVGMarkerAnonChildFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGDisplayContainerFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGMarkerAnonChildFrame) + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGMarkerAnonChild"_ns, aResult); + } +#endif + + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override { + return static_cast<SVGMarkerFrame*>(GetParent())->GetCanvasTM(); + } +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGMARKERFRAME_H_ diff --git a/layout/svg/SVGMaskFrame.cpp b/layout/svg/SVGMaskFrame.cpp new file mode 100644 index 0000000000..e38ec2922f --- /dev/null +++ b/layout/svg/SVGMaskFrame.cpp @@ -0,0 +1,188 @@ +/* -*- 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 "SVGMaskFrame.h" + +// Keep others in (case-insensitive) order: +#include "AutoReferenceChainGuard.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "mozilla/PresShell.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGMaskElement.h" +#include "mozilla/dom/SVGUnitTypesBinding.h" +#include "mozilla/gfx/2D.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGUnitTypes_Binding; +using namespace mozilla::gfx; +using namespace mozilla::image; + +nsIFrame* NS_NewSVGMaskFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGMaskFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGMaskFrame) + +already_AddRefed<SourceSurface> SVGMaskFrame::GetMaskForMaskedFrame( + MaskParams& aParams) { + // Make sure we break reference loops and over long reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mInUse, &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return nullptr; + } + + gfxRect maskArea = GetMaskArea(aParams.maskedFrame); + if (maskArea.IsEmpty()) { + return nullptr; + } + // Get the clip extents in device space: + // Minimizing the mask surface extents (using both the current clip extents + // and maskArea) is important for performance. + // + gfxRect maskSurfaceRectDouble = aParams.toUserSpace.TransformBounds(maskArea); + Rect maskSurfaceRect = ToRect(maskSurfaceRectDouble); + maskSurfaceRect.RoundOut(); + + StyleMaskType maskType; + if (aParams.maskMode == StyleMaskMode::MatchSource) { + maskType = StyleSVGReset()->mMaskType; + } else { + maskType = aParams.maskMode == StyleMaskMode::Luminance + ? StyleMaskType::Luminance + : StyleMaskType::Alpha; + } + + RefPtr<DrawTarget> maskDT; + if (maskType == StyleMaskType::Luminance) { + maskDT = aParams.dt->CreateClippedDrawTarget(maskSurfaceRect, + SurfaceFormat::B8G8R8A8); + } else { + maskDT = + aParams.dt->CreateClippedDrawTarget(maskSurfaceRect, SurfaceFormat::A8); + } + + if (!maskDT || !maskDT->IsValid()) { + return nullptr; + } + + gfxContext tmpCtx(maskDT, /* aPreserveTransform */ true); + + mMatrixForChildren = + GetMaskTransform(aParams.maskedFrame) * aParams.toUserSpace; + + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + gfxMatrix m = mMatrixForChildren; + + // The CTM of each frame referencing us can be different + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + SVGFrame->NotifySVGChanged(ISVGDisplayableFrame::TRANSFORM_CHANGED); + m = SVGUtils::GetTransformMatrixInUserSpace(kid) * m; + } + + SVGUtils::PaintFrameWithEffects(kid, tmpCtx, m, aParams.imgParams); + } + + RefPtr<SourceSurface> surface; + if (maskType == StyleMaskType::Luminance) { + auto luminanceType = LuminanceType::LUMINANCE; + if (StyleSVG()->mColorInterpolation == StyleColorInterpolation::Linearrgb) { + luminanceType = LuminanceType::LINEARRGB; + } + + RefPtr<SourceSurface> maskSnapshot = + maskDT->IntoLuminanceSource(luminanceType, aParams.opacity); + if (!maskSnapshot) { + return nullptr; + } + surface = std::move(maskSnapshot); + } else { + maskDT->FillRect(maskSurfaceRect, + ColorPattern(DeviceColor::MaskWhite(aParams.opacity)), + DrawOptions(1, CompositionOp::OP_IN)); + RefPtr<SourceSurface> maskSnapshot = maskDT->Snapshot(); + if (!maskSnapshot) { + return nullptr; + } + surface = std::move(maskSnapshot); + } + + return surface.forget(); +} + +gfxRect SVGMaskFrame::GetMaskArea(nsIFrame* aMaskedFrame) { + SVGMaskElement* maskElem = static_cast<SVGMaskElement*>(GetContent()); + + uint16_t units = + maskElem->mEnumAttributes[SVGMaskElement::MASKUNITS].GetAnimValue(); + gfxRect bbox; + if (units == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + bbox = + SVGUtils::GetBBox(aMaskedFrame, SVGUtils::eUseFrameBoundsForOuterSVG | + SVGUtils::eBBoxIncludeFillGeometry); + } + + // Bounds in the user space of aMaskedFrame + gfxRect maskArea = SVGUtils::GetRelativeRect( + units, &maskElem->mLengthAttributes[SVGMaskElement::ATTR_X], bbox, + aMaskedFrame); + + return maskArea; +} + +nsresult SVGMaskFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::x || aAttribute == nsGkAtoms::y || + aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::maskUnits || + aAttribute == nsGkAtoms::maskContentUnits)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGContainerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +#ifdef DEBUG +void SVGMaskFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::mask), + "Content is not an SVG mask"); + + SVGContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +gfxMatrix SVGMaskFrame::GetCanvasTM() { return mMatrixForChildren; } + +gfxMatrix SVGMaskFrame::GetMaskTransform(nsIFrame* aMaskedFrame) { + SVGMaskElement* content = static_cast<SVGMaskElement*>(GetContent()); + + SVGAnimatedEnumeration* maskContentUnits = + &content->mEnumAttributes[SVGMaskElement::MASKCONTENTUNITS]; + + uint32_t flags = SVGUtils::eBBoxIncludeFillGeometry | + (aMaskedFrame->StyleBorder()->mBoxDecorationBreak == + StyleBoxDecorationBreak::Clone + ? SVGUtils::eIncludeOnlyCurrentFrameForNonSVGElement + : 0); + + return SVGUtils::AdjustMatrixForUnits(gfxMatrix(), maskContentUnits, + aMaskedFrame, flags); +} + +} // namespace mozilla diff --git a/layout/svg/SVGMaskFrame.h b/layout/svg/SVGMaskFrame.h new file mode 100644 index 0000000000..ee97415459 --- /dev/null +++ b/layout/svg/SVGMaskFrame.h @@ -0,0 +1,111 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGMASKFRAME_H_ +#define LAYOUT_SVG_SVGMASKFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/gfx/2D.h" +#include "gfxPattern.h" +#include "gfxMatrix.h" + +class gfxContext; + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGMaskFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGMaskFrame final : public SVGContainerFrame { + friend nsIFrame* ::NS_NewSVGMaskFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + using Matrix = gfx::Matrix; + using SourceSurface = gfx::SourceSurface; + using imgDrawingParams = image::imgDrawingParams; + + protected: + explicit SVGMaskFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGContainerFrame(aStyle, aPresContext, kClassID), mInUse(false) { + AddStateBits(NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGMaskFrame) + + struct MaskParams { + gfx::DrawTarget* dt; + nsIFrame* maskedFrame; + const gfxMatrix& toUserSpace; + float opacity; + StyleMaskMode maskMode; + imgDrawingParams& imgParams; + + explicit MaskParams(gfx::DrawTarget* aDt, nsIFrame* aMaskedFrame, + const gfxMatrix& aToUserSpace, float aOpacity, + StyleMaskMode aMaskMode, imgDrawingParams& aImgParams) + : dt(aDt), + maskedFrame(aMaskedFrame), + toUserSpace(aToUserSpace), + opacity(aOpacity), + maskMode(aMaskMode), + imgParams(aImgParams) {} + }; + + // SVGMaskFrame method: + + /** + * Generate a mask surface for the target frame. + * + * The return surface can be null, it's the caller's responsibility to + * null-check before dereferencing. + */ + already_AddRefed<SourceSurface> GetMaskForMaskedFrame(MaskParams& aParams); + + gfxRect GetMaskArea(nsIFrame* aMaskedFrame); + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGMask"_ns, aResult); + } +#endif + + private: + /** + * If the mask element transforms its children due to + * maskContentUnits="objectBoundingBox" being set on it, this function + * returns the resulting transform. + */ + gfxMatrix GetMaskTransform(nsIFrame* aMaskedFrame); + + gfxMatrix mMatrixForChildren; + // recursion prevention flag + bool mInUse; + + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGMASKFRAME_H_ diff --git a/layout/svg/SVGObserverUtils.cpp b/layout/svg/SVGObserverUtils.cpp new file mode 100644 index 0000000000..654f3e78cb --- /dev/null +++ b/layout/svg/SVGObserverUtils.cpp @@ -0,0 +1,1750 @@ +/* -*- 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 "SVGObserverUtils.h" + +// Keep others in (case-insensitive) order: +#include "mozilla/css/ImageLoader.h" +#include "mozilla/dom/CanvasRenderingContext2D.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/SVGGeometryElement.h" +#include "mozilla/dom/SVGTextPathElement.h" +#include "mozilla/dom/SVGUseElement.h" +#include "mozilla/PresShell.h" +#include "mozilla/RestyleManager.h" +#include "mozilla/SVGClipPathFrame.h" +#include "mozilla/SVGMaskFrame.h" +#include "mozilla/SVGTextFrame.h" +#include "mozilla/SVGUtils.h" +#include "nsCSSFrameConstructor.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsIContent.h" +#include "nsIContentInlines.h" +#include "nsInterfaceHashtable.h" +#include "nsIReflowCallback.h" +#include "nsISupportsImpl.h" +#include "nsLayoutUtils.h" +#include "nsNetUtil.h" +#include "nsTHashtable.h" +#include "nsURIHashKey.h" +#include "SVGFilterFrame.h" +#include "SVGMarkerFrame.h" +#include "SVGPaintServerFrame.h" + +using namespace mozilla::dom; + +namespace mozilla { + +bool URLAndReferrerInfo::operator==(const URLAndReferrerInfo& aRHS) const { + bool uriEqual = false, referrerEqual = false; + this->mURI->Equals(aRHS.mURI, &uriEqual); + this->mReferrerInfo->Equals(aRHS.mReferrerInfo, &referrerEqual); + + return uriEqual && referrerEqual; +} + +class URLAndReferrerInfoHashKey : public PLDHashEntryHdr { + public: + using KeyType = const URLAndReferrerInfo*; + using KeyTypePointer = const URLAndReferrerInfo*; + + explicit URLAndReferrerInfoHashKey(const URLAndReferrerInfo* aKey) noexcept + : mKey(aKey) { + MOZ_COUNT_CTOR(URLAndReferrerInfoHashKey); + } + URLAndReferrerInfoHashKey(URLAndReferrerInfoHashKey&& aToMove) noexcept + : PLDHashEntryHdr(std::move(aToMove)), mKey(std::move(aToMove.mKey)) { + MOZ_COUNT_CTOR(URLAndReferrerInfoHashKey); + } + MOZ_COUNTED_DTOR(URLAndReferrerInfoHashKey) + + const URLAndReferrerInfo* GetKey() const { return mKey; } + + bool KeyEquals(const URLAndReferrerInfo* aKey) const { + if (!mKey) { + return !aKey; + } + return *mKey == *aKey; + } + + static const URLAndReferrerInfo* KeyToPointer( + const URLAndReferrerInfo* aKey) { + return aKey; + } + + static PLDHashNumber HashKey(const URLAndReferrerInfo* aKey) { + if (!aKey) { + // If the key is null, return hash for empty string. + return HashString(""_ns); + } + nsAutoCString urlSpec, referrerSpec; + // nsURIHashKey ignores GetSpec() failures, so we do too: + Unused << aKey->GetURI()->GetSpec(urlSpec); + return AddToHash( + HashString(urlSpec), + static_cast<ReferrerInfo*>(aKey->GetReferrerInfo())->Hash()); + } + + enum { ALLOW_MEMMOVE = true }; + + protected: + RefPtr<const URLAndReferrerInfo> mKey; +}; + +static already_AddRefed<URLAndReferrerInfo> ResolveURLUsingLocalRef( + nsIFrame* aFrame, const StyleComputedImageUrl& aURL) { + MOZ_ASSERT(aFrame); + + nsCOMPtr<nsIURI> uri = aURL.GetURI(); + + if (aURL.IsLocalRef()) { + uri = SVGObserverUtils::GetBaseURLForLocalRef(aFrame->GetContent(), uri); + uri = aURL.ResolveLocalRef(uri); + } + + if (!uri) { + return nullptr; + } + + RefPtr<URLAndReferrerInfo> info = + new URLAndReferrerInfo(uri, aURL.ExtraData()); + return info.forget(); +} + +static already_AddRefed<URLAndReferrerInfo> ResolveURLUsingLocalRef( + nsIFrame* aFrame, const nsAString& aURL, nsIReferrerInfo* aReferrerInfo) { + MOZ_ASSERT(aFrame); + + nsIContent* content = aFrame->GetContent(); + + // Like SVGObserverUtils::GetBaseURLForLocalRef, we want to resolve the + // URL against any <use> element shadow tree's source document. + // + // Unlike GetBaseURLForLocalRef, we are assuming that the URL was specified + // directly on mFrame's content (because this ResolveURLUsingLocalRef + // overload is used for href="" attributes and not CSS URL values), so there + // is no need to check whether the URL was specified / inherited from + // outside the shadow tree. + nsIURI* base = nullptr; + const Encoding* encoding = nullptr; + if (SVGUseElement* use = content->GetContainingSVGUseShadowHost()) { + base = use->GetSourceDocURI(); + encoding = use->GetSourceDocCharacterSet(); + } + + if (!base) { + base = content->OwnerDoc()->GetDocumentURI(); + encoding = content->OwnerDoc()->GetDocumentCharacterSet(); + } + + nsCOMPtr<nsIURI> uri; + Unused << NS_NewURI(getter_AddRefs(uri), aURL, WrapNotNull(encoding), base); + + if (!uri) { + return nullptr; + } + + RefPtr<URLAndReferrerInfo> info = new URLAndReferrerInfo(uri, aReferrerInfo); + return info.forget(); +} + +class SVGFilterObserverList; + +/** + * A class used as a member of the "observer" classes below to help them + * avoid dereferencing their frame during presshell teardown when their frame + * may have been destroyed (leaving their pointer to their frame dangling). + * + * When a presshell is torn down, the properties for each frame may not be + * deleted until after the frames are destroyed. "Observer" objects (attached + * as frame properties) must therefore check whether the presshell is being + * torn down before using their pointer to their frame. + * + * mFramePresShell may be null, but when mFrame is non-null, mFramePresShell + * is guaranteed to be non-null, too. + */ +struct SVGFrameReferenceFromProperty { + explicit SVGFrameReferenceFromProperty(nsIFrame* aFrame) + : mFrame(aFrame), mFramePresShell(aFrame->PresShell()) {} + + // Clear our reference to the frame. + void Detach() { + mFrame = nullptr; + mFramePresShell = nullptr; + } + + // null if the frame has become invalid + nsIFrame* Get() { + if (mFramePresShell && mFramePresShell->IsDestroying()) { + Detach(); // mFrame is no longer valid. + } + return mFrame; + } + + private: + // The frame that our property is attached to (may be null). + nsIFrame* mFrame; + PresShell* mFramePresShell; +}; + +void SVGRenderingObserver::StartObserving() { + Element* target = GetReferencedElementWithoutObserving(); + if (target) { + target->AddMutationObserver(this); + } +} + +void SVGRenderingObserver::StopObserving() { + Element* target = GetReferencedElementWithoutObserving(); + + if (target) { + target->RemoveMutationObserver(this); + if (mInObserverSet) { + SVGObserverUtils::RemoveRenderingObserver(target, this); + mInObserverSet = false; + } + } + NS_ASSERTION(!mInObserverSet, "still in an observer set?"); +} + +Element* SVGRenderingObserver::GetAndObserveReferencedElement() { +#ifdef DEBUG + DebugObserverSet(); +#endif + Element* referencedElement = GetReferencedElementWithoutObserving(); + if (referencedElement && !mInObserverSet) { + SVGObserverUtils::AddRenderingObserver(referencedElement, this); + mInObserverSet = true; + } + return referencedElement; +} + +nsIFrame* SVGRenderingObserver::GetAndObserveReferencedFrame() { + Element* referencedElement = GetAndObserveReferencedElement(); + return referencedElement ? referencedElement->GetPrimaryFrame() : nullptr; +} + +nsIFrame* SVGRenderingObserver::GetAndObserveReferencedFrame( + LayoutFrameType aFrameType, bool* aOK) { + nsIFrame* frame = GetAndObserveReferencedFrame(); + if (frame) { + if (frame->Type() == aFrameType) { + return frame; + } + if (aOK) { + *aOK = false; + } + } + return nullptr; +} + +void SVGRenderingObserver::OnNonDOMMutationRenderingChange() { + mInObserverSet = false; + OnRenderingChange(); +} + +void SVGRenderingObserver::NotifyEvictedFromRenderingObserverSet() { + mInObserverSet = false; // We've been removed from rendering-obs. set. + StopObserving(); // Stop observing mutations too. +} + +void SVGRenderingObserver::AttributeChanged(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) { + if (aElement->IsInNativeAnonymousSubtree()) { + // Don't observe attribute changes in native-anonymous subtrees like + // scrollbars. + return; + } + + // An attribute belonging to the element that we are observing *or one of its + // descendants* has changed. + // + // In the case of observing a gradient element, say, we want to know if any + // of its 'stop' element children change, but we don't actually want to do + // anything for changes to SMIL element children, for example. Maybe it's not + // worth having logic to optimize for that, but in most cases it could be a + // small check? + // + // XXXjwatt: do we really want to blindly break the link between our + // observers and ourselves for all attribute changes? For non-ID changes + // surely that is unnecessary. + + OnRenderingChange(); +} + +void SVGRenderingObserver::ContentAppended(nsIContent* aFirstNewContent) { + OnRenderingChange(); +} + +void SVGRenderingObserver::ContentInserted(nsIContent* aChild) { + OnRenderingChange(); +} + +void SVGRenderingObserver::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + OnRenderingChange(); +} + +/** + * SVG elements reference supporting resources by element ID. We need to + * track when those resources change and when the document changes in ways + * that affect which element is referenced by a given ID (e.g., when + * element IDs change). The code here is responsible for that. + * + * When a frame references a supporting resource, we create a property + * object derived from SVGIDRenderingObserver to manage the relationship. The + * property object is attached to the referencing frame. + */ +class SVGIDRenderingObserver : public SVGRenderingObserver { + public: + // Callback for checking if the element being observed is valid for this + // observer. Note that this may be called during construction, before the + // deriving class is fully constructed. + using TargetIsValidCallback = bool (*)(const Element&); + SVGIDRenderingObserver( + URLAndReferrerInfo* aURI, nsIContent* aObservingContent, + bool aReferenceImage, + TargetIsValidCallback aTargetIsValidCallback = nullptr); + + protected: + virtual ~SVGIDRenderingObserver() { + // This needs to call our GetReferencedElementWithoutObserving override, + // so must be called here rather than in our base class's dtor. + StopObserving(); + } + + void TargetChanged() { + mTargetIsValid = ([this] { + Element* observed = mObservedElementTracker.get(); + if (!observed) { + return false; + } + // If the content is observing an ancestor, then return the target is not + // valid. + // + // TODO(emilio): Should we allow content observing its own descendants? + // That seems potentially-bad as well. + if (observed->OwnerDoc() == mObservingContent->OwnerDoc() && + nsContentUtils::ContentIsHostIncludingDescendantOf(mObservingContent, + observed)) { + return false; + } + if (mTargetIsValidCallback) { + return mTargetIsValidCallback(*observed); + } + return true; + }()); + } + + Element* GetReferencedElementWithoutObserving() final { + return mTargetIsValid ? mObservedElementTracker.get() : nullptr; + } + + void OnRenderingChange() override; + + /** + * Helper that provides a reference to the element with the ID that our + * observer wants to observe, and that will invalidate our observer if the + * element that that ID identifies changes to a different element (or none). + */ + class ElementTracker final : public IDTracker { + public: + explicit ElementTracker(SVGIDRenderingObserver* aOwningObserver) + : mOwningObserver(aOwningObserver) {} + + protected: + void ElementChanged(Element* aFrom, Element* aTo) override { + // Call OnRenderingChange() before the target changes, so that + // mIsTargetValid reflects the right state. + mOwningObserver->OnRenderingChange(); + mOwningObserver->StopObserving(); + IDTracker::ElementChanged(aFrom, aTo); + mOwningObserver->TargetChanged(); + mOwningObserver->StartObserving(); + // And same after the target changes, for the same reason. + mOwningObserver->OnRenderingChange(); + } + /** + * Override IsPersistent because we want to keep tracking the element + * for the ID even when it changes. + */ + bool IsPersistent() override { return true; } + + private: + SVGIDRenderingObserver* mOwningObserver; + }; + + ElementTracker mObservedElementTracker; + RefPtr<Element> mObservingContent; + bool mTargetIsValid = false; + TargetIsValidCallback mTargetIsValidCallback; +}; + +/** + * Note that in the current setup there are two separate observer lists. + * + * In SVGIDRenderingObserver's ctor, the new object adds itself to the + * mutation observer list maintained by the referenced element. In this way the + * SVGIDRenderingObserver is notified if there are any attribute or content + * tree changes to the element or any of its *descendants*. + * + * In SVGIDRenderingObserver::GetAndObserveReferencedElement() the + * SVGIDRenderingObserver object also adds itself to an + * SVGRenderingObserverSet object belonging to the referenced + * element. + * + * XXX: it would be nice to have a clear and concise executive summary of the + * benefits/necessity of maintaining a second observer list. + */ +SVGIDRenderingObserver::SVGIDRenderingObserver( + URLAndReferrerInfo* aURI, nsIContent* aObservingContent, + bool aReferenceImage, TargetIsValidCallback aTargetIsValidCallback) + : mObservedElementTracker(this), + mObservingContent(aObservingContent->AsElement()), + mTargetIsValidCallback(aTargetIsValidCallback) { + // Start watching the target element + nsIURI* uri = nullptr; + nsIReferrerInfo* referrerInfo = nullptr; + if (aURI) { + uri = aURI->GetURI(); + referrerInfo = aURI->GetReferrerInfo(); + } + + mObservedElementTracker.ResetToURIFragmentID( + aObservingContent, uri, referrerInfo, true, aReferenceImage); + TargetChanged(); + StartObserving(); +} + +void SVGIDRenderingObserver::OnRenderingChange() { + if (mObservedElementTracker.get() && mInObserverSet) { + SVGObserverUtils::RemoveRenderingObserver(mObservedElementTracker.get(), + this); + mInObserverSet = false; + } +} + +class SVGRenderingObserverProperty : public SVGIDRenderingObserver { + public: + NS_DECL_ISUPPORTS + + SVGRenderingObserverProperty(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGIDRenderingObserver(aURI, aFrame->GetContent(), aReferenceImage), + mFrameReference(aFrame) {} + + protected: + virtual ~SVGRenderingObserverProperty() = default; // non-public + + void OnRenderingChange() override; + + SVGFrameReferenceFromProperty mFrameReference; +}; + +NS_IMPL_ISUPPORTS(SVGRenderingObserverProperty, nsIMutationObserver) + +void SVGRenderingObserverProperty::OnRenderingChange() { + SVGIDRenderingObserver::OnRenderingChange(); + + if (!mTargetIsValid) { + return; + } + + nsIFrame* frame = mFrameReference.Get(); + + if (frame && frame->HasAllStateBits(NS_FRAME_SVG_LAYOUT)) { + // We need to notify anything that is observing the referencing frame or + // any of its ancestors that the referencing frame has been invalidated. + // Since walking the parent chain checking for observers is expensive we + // do that using a change hint (multiple change hints of the same type are + // coalesced). + nsLayoutUtils::PostRestyleEvent(frame->GetContent()->AsElement(), + RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + } +} + +class SVGTextPathObserver final : public SVGRenderingObserverProperty { + public: + SVGTextPathObserver(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGRenderingObserverProperty(aURI, aFrame, aReferenceImage) {} + + protected: + void OnRenderingChange() override; +}; + +void SVGTextPathObserver::OnRenderingChange() { + SVGRenderingObserverProperty::OnRenderingChange(); + + if (!mTargetIsValid) { + return; + } + + nsIFrame* frame = mFrameReference.Get(); + if (!frame) { + return; + } + + MOZ_ASSERT( + frame->IsFrameOfType(nsIFrame::eSVG) || frame->IsInSVGTextSubtree(), + "SVG frame expected"); + + MOZ_ASSERT(frame->GetContent()->IsSVGElement(nsGkAtoms::textPath), + "expected frame for a <textPath> element"); + + auto* text = static_cast<SVGTextFrame*>( + nsLayoutUtils::GetClosestFrameOfType(frame, LayoutFrameType::SVGText)); + MOZ_ASSERT(text, "expected to find an ancestor SVGTextFrame"); + if (text) { + text->AddStateBits(NS_STATE_SVG_POSITIONING_DIRTY); + + if (SVGUtils::AnyOuterSVGIsCallingReflowSVG(text)) { + text->AddStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN); + if (text->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + text->ReflowSVGNonDisplayText(); + } else { + text->ReflowSVG(); + } + } else { + text->ScheduleReflowSVG(); + } + } +} + +class SVGMarkerObserver final : public SVGRenderingObserverProperty { + public: + SVGMarkerObserver(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGRenderingObserverProperty(aURI, aFrame, aReferenceImage) {} + + protected: + void OnRenderingChange() override; +}; + +void SVGMarkerObserver::OnRenderingChange() { + SVGRenderingObserverProperty::OnRenderingChange(); + + nsIFrame* frame = mFrameReference.Get(); + if (!frame) { + return; + } + + MOZ_ASSERT(frame->IsFrameOfType(nsIFrame::eSVG), "SVG frame expected"); + + // Don't need to request ReflowFrame if we're being reflowed. + // Because mRect for SVG frames includes the bounds of any markers + // (see the comment for nsIFrame::GetRect), the referencing frame must be + // reflowed for any marker changes. + if (!frame->HasAnyStateBits(NS_FRAME_IN_REFLOW)) { + // XXXjwatt: We need to unify SVG into standard reflow so we can just use + // nsChangeHint_NeedReflow | nsChangeHint_NeedDirtyReflow here. + // XXXSDL KILL THIS!!! + SVGUtils::ScheduleReflowSVG(frame); + } + frame->PresContext()->RestyleManager()->PostRestyleEvent( + frame->GetContent()->AsElement(), RestyleHint{0}, + nsChangeHint_RepaintFrame); +} + +class SVGPaintingProperty : public SVGRenderingObserverProperty { + public: + SVGPaintingProperty(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGRenderingObserverProperty(aURI, aFrame, aReferenceImage) {} + + protected: + void OnRenderingChange() override; +}; + +void SVGPaintingProperty::OnRenderingChange() { + SVGRenderingObserverProperty::OnRenderingChange(); + + nsIFrame* frame = mFrameReference.Get(); + if (!frame) { + return; + } + + if (frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + frame->InvalidateFrameSubtree(); + } else { + for (nsIFrame* f = frame; f; + f = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(f)) { + f->InvalidateFrame(); + } + } +} + +// Observer for -moz-element(#element). Note that the observed element does not +// have to be an SVG element. +class SVGMozElementObserver final : public SVGPaintingProperty { + public: + SVGMozElementObserver(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGPaintingProperty(aURI, aFrame, aReferenceImage) {} + + // We only return true here because GetAndObserveBackgroundImage uses us + // to implement observing of arbitrary elements (including HTML elements) + // that may require us to repaint if the referenced element is reflowed. + // Bug 1496065 has been filed to remove that support though. + bool ObservesReflow() override { return true; } +}; + +/** + * For content with `background-clip: text`. + * + * This observer is unusual in that the observing frame and observed frame are + * the same frame. This is because the observing frame is observing for reflow + * of its descendant text nodes, since such reflows may not result in the + * frame's nsDisplayBackground changing. In other words, Display List Based + * Invalidation may not invalidate the frame's background, so we need this + * observer to make sure that happens. + * + * XXX: It's questionable whether we should even be [ab]using the SVG observer + * mechanism for `background-clip:text`. Since we know that the observed frame + * is the frame we need to invalidate, we could just check the computed style + * in the (one) place where we pass INVALIDATE_REFLOW and invalidate there... + */ +class BackgroundClipRenderingObserver : public SVGRenderingObserver { + public: + explicit BackgroundClipRenderingObserver(nsIFrame* aFrame) : mFrame(aFrame) {} + + NS_DECL_ISUPPORTS + + private: + // We do not call StopObserving() since the observing and observed element + // are the same element (and because we could crash - see bug 1556441). + virtual ~BackgroundClipRenderingObserver() = default; + + Element* GetReferencedElementWithoutObserving() final { + return mFrame->GetContent()->AsElement(); + } + + void OnRenderingChange() final; + + /** + * Observing for mutations is not enough. A new font loading and applying + * to the text content could cause it to reflow, and we need to invalidate + * for that. + */ + bool ObservesReflow() final { return true; } + + // The observer and observee! + nsIFrame* mFrame; +}; + +NS_IMPL_ISUPPORTS(BackgroundClipRenderingObserver, nsIMutationObserver) + +void BackgroundClipRenderingObserver::OnRenderingChange() { + for (nsIFrame* f = mFrame; f; + f = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(f)) { + f->InvalidateFrame(); + } +} + +static bool IsSVGFilterElement(const Element& aObserved) { + return aObserved.IsSVGElement(nsGkAtoms::filter); +} + +/** + * In a filter chain, there can be multiple SVG reference filters. + * e.g. filter: url(#svg-filter-1) blur(10px) url(#svg-filter-2); + * + * This class keeps track of one SVG reference filter in a filter chain. + * e.g. url(#svg-filter-1) + * + * It fires invalidations when the SVG filter element's id changes or when + * the SVG filter element's content changes. + * + * The SVGFilterObserverList class manages a list of SVGFilterObservers. + */ +class SVGFilterObserver final : public SVGIDRenderingObserver { + public: + SVGFilterObserver(URLAndReferrerInfo* aURI, nsIContent* aObservingContent, + SVGFilterObserverList* aFilterChainObserver) + : SVGIDRenderingObserver(aURI, aObservingContent, false, + IsSVGFilterElement), + mFilterObserverList(aFilterChainObserver) {} + + void DetachFromChainObserver() { mFilterObserverList = nullptr; } + + /** + * @return the filter frame, or null if there is no filter frame + */ + SVGFilterFrame* GetAndObserveFilterFrame(); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(SVGFilterObserver) + + // SVGIDRenderingObserver + void OnRenderingChange() override; + + protected: + virtual ~SVGFilterObserver() = default; // non-public + + SVGFilterObserverList* mFilterObserverList; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SVGFilterObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SVGFilterObserver) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SVGFilterObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(SVGFilterObserver) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SVGFilterObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservedElementTracker) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservingContent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SVGFilterObserver) + tmp->StopObserving(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservedElementTracker); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservingContent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +SVGFilterFrame* SVGFilterObserver::GetAndObserveFilterFrame() { + return static_cast<SVGFilterFrame*>( + GetAndObserveReferencedFrame(LayoutFrameType::SVGFilter, nullptr)); +} + +/** + * This class manages a list of SVGFilterObservers, which correspond to + * reference to SVG filters in a list of filters in a given 'filter' property. + * e.g. filter: url(#svg-filter-1) blur(10px) url(#svg-filter-2); + * + * In the above example, the SVGFilterObserverList will manage two + * SVGFilterObservers, one for each of the references to SVG filters. CSS + * filters like "blur(10px)" don't reference filter elements, so they don't + * need an SVGFilterObserver. The style system invalidates changes to CSS + * filters. + * + * FIXME(emilio): Why do we need this as opposed to the individual observers we + * create in the constructor? + */ +class SVGFilterObserverList : public nsISupports { + public: + SVGFilterObserverList(Span<const StyleFilter> aFilters, + nsIContent* aFilteredElement, + nsIFrame* aFilteredFrame = nullptr); + + const nsTArray<RefPtr<SVGFilterObserver>>& GetObservers() const { + return mObservers; + } + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(SVGFilterObserverList) + + virtual void OnRenderingChange() = 0; + + protected: + virtual ~SVGFilterObserverList(); + + void DetachObservers() { + for (auto& observer : mObservers) { + observer->DetachFromChainObserver(); + } + } + + nsTArray<RefPtr<SVGFilterObserver>> mObservers; +}; + +void SVGFilterObserver::OnRenderingChange() { + SVGIDRenderingObserver::OnRenderingChange(); + + if (mFilterObserverList) { + mFilterObserverList->OnRenderingChange(); + } + + if (!mTargetIsValid) { + return; + } + + nsIFrame* frame = mObservingContent->GetPrimaryFrame(); + if (!frame) { + return; + } + + // Repaint asynchronously in case the filter frame is being torn down + nsChangeHint changeHint = nsChangeHint(nsChangeHint_RepaintFrame); + + // Since we don't call SVGRenderingObserverProperty:: + // OnRenderingChange, we have to add this bit ourselves. + if (frame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + // Changes should propagate out to things that might be observing + // the referencing frame or its ancestors. + changeHint |= nsChangeHint_InvalidateRenderingObservers; + } + + // Don't need to request UpdateOverflow if we're being reflowed. + if (!frame->HasAnyStateBits(NS_FRAME_IN_REFLOW)) { + changeHint |= nsChangeHint_UpdateOverflow; + } + frame->PresContext()->RestyleManager()->PostRestyleEvent( + mObservingContent, RestyleHint{0}, changeHint); +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SVGFilterObserverList) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SVGFilterObserverList) + +NS_IMPL_CYCLE_COLLECTION_CLASS(SVGFilterObserverList) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SVGFilterObserverList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mObservers) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SVGFilterObserverList) + tmp->DetachObservers(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mObservers); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SVGFilterObserverList) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +SVGFilterObserverList::SVGFilterObserverList(Span<const StyleFilter> aFilters, + nsIContent* aFilteredElement, + nsIFrame* aFilteredFrame) { + for (const auto& filter : aFilters) { + if (!filter.IsUrl()) { + continue; + } + + const auto& url = filter.AsUrl(); + + // aFilteredFrame can be null if this filter belongs to a + // CanvasRenderingContext2D. + RefPtr<URLAndReferrerInfo> filterURL; + if (aFilteredFrame) { + filterURL = ResolveURLUsingLocalRef(aFilteredFrame, url); + } else { + nsCOMPtr<nsIURI> resolvedURI = url.ResolveLocalRef(aFilteredElement); + if (resolvedURI) { + filterURL = new URLAndReferrerInfo(resolvedURI, url.ExtraData()); + } + } + + RefPtr<SVGFilterObserver> observer = + new SVGFilterObserver(filterURL, aFilteredElement, this); + mObservers.AppendElement(observer); + } +} + +SVGFilterObserverList::~SVGFilterObserverList() { DetachObservers(); } + +class SVGFilterObserverListForCSSProp final : public SVGFilterObserverList { + public: + SVGFilterObserverListForCSSProp(Span<const StyleFilter> aFilters, + nsIFrame* aFilteredFrame) + : SVGFilterObserverList(aFilters, aFilteredFrame->GetContent(), + aFilteredFrame) {} + + protected: + void OnRenderingChange() override; + bool mInvalidating = false; +}; + +void SVGFilterObserverListForCSSProp::OnRenderingChange() { + if (mInvalidating) { + return; + } + AutoRestore<bool> guard(mInvalidating); + mInvalidating = true; + for (auto& observer : mObservers) { + observer->OnRenderingChange(); + } +} + +class SVGFilterObserverListForCanvasContext final + : public SVGFilterObserverList { + public: + SVGFilterObserverListForCanvasContext(CanvasRenderingContext2D* aContext, + Element* aCanvasElement, + Span<const StyleFilter> aFilters) + : SVGFilterObserverList(aFilters, aCanvasElement), mContext(aContext) {} + + void OnRenderingChange() override; + void DetachFromContext() { mContext = nullptr; } + + private: + CanvasRenderingContext2D* mContext; +}; + +void SVGFilterObserverListForCanvasContext::OnRenderingChange() { + if (!mContext) { + NS_WARNING( + "GFX: This should never be called without a context, except during " + "cycle collection (when DetachFromContext has been called)"); + return; + } + // Refresh the cached FilterDescription in mContext->CurrentState().filter. + // If this filter is not at the top of the state stack, we'll refresh the + // wrong filter, but that's ok, because we'll refresh the right filter + // when we pop the state stack in CanvasRenderingContext2D::Restore(). + RefPtr<CanvasRenderingContext2D> kungFuDeathGrip(mContext); + kungFuDeathGrip->UpdateFilter(); +} + +class SVGMaskObserverList final : public nsISupports { + public: + explicit SVGMaskObserverList(nsIFrame* aFrame); + + // nsISupports + NS_DECL_ISUPPORTS + + const nsTArray<RefPtr<SVGPaintingProperty>>& GetObservers() const { + return mProperties; + } + + void ResolveImage(uint32_t aIndex); + + private: + virtual ~SVGMaskObserverList() = default; // non-public + nsTArray<RefPtr<SVGPaintingProperty>> mProperties; + nsIFrame* mFrame; +}; + +NS_IMPL_ISUPPORTS(SVGMaskObserverList, nsISupports) + +SVGMaskObserverList::SVGMaskObserverList(nsIFrame* aFrame) : mFrame(aFrame) { + const nsStyleSVGReset* svgReset = aFrame->StyleSVGReset(); + + for (uint32_t i = 0; i < svgReset->mMask.mImageCount; i++) { + const StyleComputedImageUrl* data = + svgReset->mMask.mLayers[i].mImage.GetImageRequestURLValue(); + RefPtr<URLAndReferrerInfo> maskUri; + if (data) { + maskUri = ResolveURLUsingLocalRef(aFrame, *data); + } + + bool hasRef = false; + if (maskUri) { + maskUri->GetURI()->GetHasRef(&hasRef); + } + + // Accrording to maskUri, SVGPaintingProperty's ctor may trigger an + // external SVG resource download, so we should pass maskUri in only if + // maskUri has a chance pointing to an SVG mask resource. + // + // And, an URL may refer to an SVG mask resource if it consists of + // a fragment. + SVGPaintingProperty* prop = new SVGPaintingProperty( + hasRef ? maskUri.get() : nullptr, aFrame, false); + mProperties.AppendElement(prop); + } +} + +void SVGMaskObserverList::ResolveImage(uint32_t aIndex) { + const nsStyleSVGReset* svgReset = mFrame->StyleSVGReset(); + MOZ_ASSERT(aIndex < svgReset->mMask.mImageCount); + + auto& image = const_cast<StyleImage&>(svgReset->mMask.mLayers[aIndex].mImage); + if (image.IsResolved()) { + return; + } + MOZ_ASSERT(image.IsImageRequestType()); + Document* doc = mFrame->PresContext()->Document(); + image.ResolveImage(*doc, nullptr); + if (imgRequestProxy* req = image.GetImageRequest()) { + // FIXME(emilio): What disassociates this request? + doc->StyleImageLoader()->AssociateRequestToFrame(req, mFrame); + } +} + +/** + * Used for gradient-to-gradient, pattern-to-pattern and filter-to-filter + * references to "template" elements (specified via the 'href' attributes). + * + * This is a special class for the case where we know we only want to call + * InvalidateDirectRenderingObservers (as opposed to + * InvalidateRenderingObservers). + * + * TODO(jwatt): If we added a new NS_FRAME_RENDERING_OBSERVER_CONTAINER state + * bit to clipPath, filter, gradients, marker, mask, pattern and symbol, and + * could have InvalidateRenderingObservers stop on reaching such an element, + * then we would no longer need this class (not to mention improving perf by + * significantly cutting down on ancestor traversal). + */ +class SVGTemplateElementObserver : public SVGIDRenderingObserver { + public: + NS_DECL_ISUPPORTS + + SVGTemplateElementObserver(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + bool aReferenceImage) + : SVGIDRenderingObserver(aURI, aFrame->GetContent(), aReferenceImage), + mFrameReference(aFrame) {} + + protected: + virtual ~SVGTemplateElementObserver() = default; // non-public + + void OnRenderingChange() override; + + SVGFrameReferenceFromProperty mFrameReference; +}; + +NS_IMPL_ISUPPORTS(SVGTemplateElementObserver, nsIMutationObserver) + +void SVGTemplateElementObserver::OnRenderingChange() { + SVGIDRenderingObserver::OnRenderingChange(); + + if (nsIFrame* frame = mFrameReference.Get()) { + // We know that we don't need to walk the parent chain notifying rendering + // observers since changes to a gradient etc. do not affect ancestor + // elements. So we only invalidate *direct* rendering observers here. + // Since we don't need to walk the parent chain, we don't need to worry + // about coalescing multiple invalidations by using a change hint as we do + // in SVGRenderingObserverProperty::OnRenderingChange. + SVGObserverUtils::InvalidateDirectRenderingObservers(frame); + } +} + +/** + * An instance of this class is stored on an observed frame (as a frame + * property) whenever the frame has active rendering observers. It is used to + * store pointers to the SVGRenderingObserver instances belonging to any + * observing frames, allowing invalidations from the observed frame to be sent + * to all observing frames. + * + * SVGRenderingObserver instances that are added are not strongly referenced, + * so they must remove themselves before they die. + * + * This class is "single-shot", which is to say that when something about the + * observed element changes, InvalidateAll() clears our hashtable of + * SVGRenderingObservers. SVGRenderingObserver objects will be added back + * again if/when the observing frame looks up our observed frame to use it. + * + * XXXjwatt: is this the best thing to do nowadays? Back when that mechanism + * landed in bug 330498 we had two pass, recursive invalidation up the frame + * tree, and I think reference loops were a problem. Nowadays maybe a flag + * on the SVGRenderingObserver objects to coalesce invalidations may work + * better? + * + * InvalidateAll must be called before this object is destroyed, i.e. + * before the referenced frame is destroyed. This should normally happen + * via SVGContainerFrame::RemoveFrame, since only frames in the frame + * tree should be referenced. + */ +class SVGRenderingObserverSet { + public: + SVGRenderingObserverSet() : mObservers(4) { + MOZ_COUNT_CTOR(SVGRenderingObserverSet); + } + + ~SVGRenderingObserverSet() { + InvalidateAll(); + MOZ_COUNT_DTOR(SVGRenderingObserverSet); + } + + void Add(SVGRenderingObserver* aObserver) { mObservers.Insert(aObserver); } + void Remove(SVGRenderingObserver* aObserver) { mObservers.Remove(aObserver); } +#ifdef DEBUG + bool Contains(SVGRenderingObserver* aObserver) { + return mObservers.Contains(aObserver); + } +#endif + bool IsEmpty() { return mObservers.IsEmpty(); } + + /** + * Drop all our observers, and notify them that we have changed and dropped + * our reference to them. + */ + void InvalidateAll(); + + /** + * Drop all observers that observe reflow, and notify them that we have + * changed and dropped our reference to them. + */ + void InvalidateAllForReflow(); + + /** + * Drop all our observers, and notify them that we have dropped our reference + * to them. + */ + void RemoveAll(); + + private: + nsTHashSet<SVGRenderingObserver*> mObservers; +}; + +void SVGRenderingObserverSet::InvalidateAll() { + if (mObservers.IsEmpty()) { + return; + } + + const auto observers = std::move(mObservers); + + for (const auto& observer : observers) { + observer->OnNonDOMMutationRenderingChange(); + } +} + +void SVGRenderingObserverSet::InvalidateAllForReflow() { + if (mObservers.IsEmpty()) { + return; + } + + AutoTArray<SVGRenderingObserver*, 10> observers; + + for (auto it = mObservers.cbegin(), end = mObservers.cend(); it != end; + ++it) { + SVGRenderingObserver* obs = *it; + if (obs->ObservesReflow()) { + observers.AppendElement(obs); + mObservers.Remove(it); + } + } + + for (uint32_t i = 0; i < observers.Length(); ++i) { + observers[i]->OnNonDOMMutationRenderingChange(); + } +} + +void SVGRenderingObserverSet::RemoveAll() { + const auto observers = std::move(mObservers); + + // Our list is now cleared. We need to notify the observers we've removed, + // so they can update their state & remove themselves as mutation-observers. + for (const auto& observer : observers) { + observer->NotifyEvictedFromRenderingObserverSet(); + } +} + +static SVGRenderingObserverSet* GetObserverSet(Element* aElement) { + return static_cast<SVGRenderingObserverSet*>( + aElement->GetProperty(nsGkAtoms::renderingobserverset)); +} + +#ifdef DEBUG +// Defined down here because we need SVGRenderingObserverSet's definition. +void SVGRenderingObserver::DebugObserverSet() { + Element* referencedElement = GetReferencedElementWithoutObserving(); + if (referencedElement) { + SVGRenderingObserverSet* observers = GetObserverSet(referencedElement); + bool inObserverSet = observers && observers->Contains(this); + MOZ_ASSERT(inObserverSet == mInObserverSet, + "failed to track whether we're in our referenced element's " + "observer set!"); + } else { + MOZ_ASSERT(!mInObserverSet, "In whose observer set are we, then?"); + } +} +#endif + +using URIObserverHashtable = + nsInterfaceHashtable<URLAndReferrerInfoHashKey, nsIMutationObserver>; + +using PaintingPropertyDescriptor = + const FramePropertyDescriptor<SVGPaintingProperty>*; + +static void DestroyFilterProperty(SVGFilterObserverListForCSSProp* aProp) { + aProp->Release(); +} + +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(HrefToTemplateProperty, + SVGTemplateElementObserver) +NS_DECLARE_FRAME_PROPERTY_WITH_DTOR(FilterProperty, + SVGFilterObserverListForCSSProp, + DestroyFilterProperty) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(MaskProperty, SVGMaskObserverList) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(ClipPathProperty, SVGPaintingProperty) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(MarkerStartProperty, SVGMarkerObserver) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(MarkerMidProperty, SVGMarkerObserver) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(MarkerEndProperty, SVGMarkerObserver) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(FillProperty, SVGPaintingProperty) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(StrokeProperty, SVGPaintingProperty) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(HrefAsTextPathProperty, + SVGTextPathObserver) +NS_DECLARE_FRAME_PROPERTY_DELETABLE(BackgroundImageProperty, + URIObserverHashtable) +NS_DECLARE_FRAME_PROPERTY_RELEASABLE(BackgroundClipObserverProperty, + BackgroundClipRenderingObserver) + +template <class T> +static T* GetEffectProperty(URLAndReferrerInfo* aURI, nsIFrame* aFrame, + const FramePropertyDescriptor<T>* aProperty) { + if (!aURI) { + return nullptr; + } + + bool found; + T* prop = aFrame->GetProperty(aProperty, &found); + if (found) { + MOZ_ASSERT(prop, "this property should only store non-null values"); + return prop; + } + prop = new T(aURI, aFrame, false); + NS_ADDREF(prop); + aFrame->AddProperty(aProperty, prop); + return prop; +} + +static SVGPaintingProperty* GetPaintingProperty( + URLAndReferrerInfo* aURI, nsIFrame* aFrame, + const FramePropertyDescriptor<SVGPaintingProperty>* aProperty) { + return GetEffectProperty(aURI, aFrame, aProperty); +} + +static already_AddRefed<URLAndReferrerInfo> GetMarkerURI( + nsIFrame* aFrame, const StyleUrlOrNone nsStyleSVG::*aMarker) { + const StyleUrlOrNone& url = aFrame->StyleSVG()->*aMarker; + if (url.IsNone()) { + return nullptr; + } + return ResolveURLUsingLocalRef(aFrame, url.AsUrl()); +} + +bool SVGObserverUtils::GetAndObserveMarkers(nsIFrame* aMarkedFrame, + SVGMarkerFrame* (*aFrames)[3]) { + MOZ_ASSERT(!aMarkedFrame->GetPrevContinuation() && + aMarkedFrame->IsSVGGeometryFrame() && + static_cast<SVGGeometryElement*>(aMarkedFrame->GetContent()) + ->IsMarkable(), + "Bad frame"); + + bool foundMarker = false; + RefPtr<URLAndReferrerInfo> markerURL; + SVGMarkerObserver* observer; + nsIFrame* marker; + +#define GET_MARKER(type) \ + markerURL = GetMarkerURI(aMarkedFrame, &nsStyleSVG::mMarker##type); \ + observer = \ + GetEffectProperty(markerURL, aMarkedFrame, Marker##type##Property()); \ + marker = observer ? observer->GetAndObserveReferencedFrame( \ + LayoutFrameType::SVGMarker, nullptr) \ + : nullptr; \ + foundMarker = foundMarker || bool(marker); \ + (*aFrames)[SVGMark::e##type] = static_cast<SVGMarkerFrame*>(marker); + + GET_MARKER(Start) + GET_MARKER(Mid) + GET_MARKER(End) + +#undef GET_MARKER + + return foundMarker; +} + +// Note that the returned list will be empty in the case of a 'filter' property +// that only specifies CSS filter functions (no url()'s to SVG filters). +static SVGFilterObserverListForCSSProp* GetOrCreateFilterObserverListForCSS( + nsIFrame* aFrame) { + MOZ_ASSERT(!aFrame->GetPrevContinuation(), "Require first continuation"); + + const nsStyleEffects* effects = aFrame->StyleEffects(); + if (!effects->HasFilters()) { + return nullptr; + } + + bool found; + SVGFilterObserverListForCSSProp* observers = + aFrame->GetProperty(FilterProperty(), &found); + if (found) { + MOZ_ASSERT(observers, "this property should only store non-null values"); + return observers; + } + observers = + new SVGFilterObserverListForCSSProp(effects->mFilters.AsSpan(), aFrame); + NS_ADDREF(observers); + aFrame->AddProperty(FilterProperty(), observers); + return observers; +} + +static SVGObserverUtils::ReferenceState GetAndObserveFilters( + SVGFilterObserverListForCSSProp* aObserverList, + nsTArray<SVGFilterFrame*>* aFilterFrames) { + if (!aObserverList) { + return SVGObserverUtils::eHasNoRefs; + } + + const nsTArray<RefPtr<SVGFilterObserver>>& observers = + aObserverList->GetObservers(); + if (observers.IsEmpty()) { + return SVGObserverUtils::eHasNoRefs; + } + + for (uint32_t i = 0; i < observers.Length(); i++) { + SVGFilterFrame* filter = observers[i]->GetAndObserveFilterFrame(); + if (!filter) { + if (aFilterFrames) { + aFilterFrames->Clear(); + } + return SVGObserverUtils::eHasRefsSomeInvalid; + } + if (aFilterFrames) { + aFilterFrames->AppendElement(filter); + } + } + + return SVGObserverUtils::eHasRefsAllValid; +} + +SVGObserverUtils::ReferenceState SVGObserverUtils::GetAndObserveFilters( + nsIFrame* aFilteredFrame, nsTArray<SVGFilterFrame*>* aFilterFrames) { + SVGFilterObserverListForCSSProp* observerList = + GetOrCreateFilterObserverListForCSS(aFilteredFrame); + return mozilla::GetAndObserveFilters(observerList, aFilterFrames); +} + +SVGObserverUtils::ReferenceState SVGObserverUtils::GetFiltersIfObserving( + nsIFrame* aFilteredFrame, nsTArray<SVGFilterFrame*>* aFilterFrames) { + SVGFilterObserverListForCSSProp* observerList = + aFilteredFrame->GetProperty(FilterProperty()); + return mozilla::GetAndObserveFilters(observerList, aFilterFrames); +} + +already_AddRefed<nsISupports> SVGObserverUtils::ObserveFiltersForCanvasContext( + CanvasRenderingContext2D* aContext, Element* aCanvasElement, + const Span<const StyleFilter> aFilters) { + return do_AddRef(new SVGFilterObserverListForCanvasContext( + aContext, aCanvasElement, aFilters)); +} + +void SVGObserverUtils::DetachFromCanvasContext(nsISupports* aAutoObserver) { + static_cast<SVGFilterObserverListForCanvasContext*>(aAutoObserver) + ->DetachFromContext(); +} + +static SVGPaintingProperty* GetOrCreateClipPathObserver( + nsIFrame* aClippedFrame) { + MOZ_ASSERT(!aClippedFrame->GetPrevContinuation(), + "Require first continuation"); + + const nsStyleSVGReset* svgStyleReset = aClippedFrame->StyleSVGReset(); + if (!svgStyleReset->mClipPath.IsUrl()) { + return nullptr; + } + const auto& url = svgStyleReset->mClipPath.AsUrl(); + RefPtr<URLAndReferrerInfo> pathURI = + ResolveURLUsingLocalRef(aClippedFrame, url); + return GetPaintingProperty(pathURI, aClippedFrame, ClipPathProperty()); +} + +SVGObserverUtils::ReferenceState SVGObserverUtils::GetAndObserveClipPath( + nsIFrame* aClippedFrame, SVGClipPathFrame** aClipPathFrame) { + if (aClipPathFrame) { + *aClipPathFrame = nullptr; + } + SVGPaintingProperty* observers = GetOrCreateClipPathObserver(aClippedFrame); + if (!observers) { + return eHasNoRefs; + } + bool frameTypeOK = true; + SVGClipPathFrame* frame = + static_cast<SVGClipPathFrame*>(observers->GetAndObserveReferencedFrame( + LayoutFrameType::SVGClipPath, &frameTypeOK)); + // Note that, unlike for filters, a reference to an ID that doesn't exist + // is not invalid for clip-path or mask. + if (!frameTypeOK || (frame && !frame->IsValid())) { + return eHasRefsSomeInvalid; + } + if (aClipPathFrame) { + *aClipPathFrame = frame; + } + return frame ? eHasRefsAllValid : eHasNoRefs; +} + +static SVGMaskObserverList* GetOrCreateMaskObserverList( + nsIFrame* aMaskedFrame) { + MOZ_ASSERT(!aMaskedFrame->GetPrevContinuation(), + "Require first continuation"); + + const nsStyleSVGReset* style = aMaskedFrame->StyleSVGReset(); + if (!style->HasMask()) { + return nullptr; + } + + MOZ_ASSERT(style->mMask.mImageCount > 0); + + bool found; + SVGMaskObserverList* prop = aMaskedFrame->GetProperty(MaskProperty(), &found); + if (found) { + MOZ_ASSERT(prop, "this property should only store non-null values"); + return prop; + } + prop = new SVGMaskObserverList(aMaskedFrame); + NS_ADDREF(prop); + aMaskedFrame->AddProperty(MaskProperty(), prop); + return prop; +} + +SVGObserverUtils::ReferenceState SVGObserverUtils::GetAndObserveMasks( + nsIFrame* aMaskedFrame, nsTArray<SVGMaskFrame*>* aMaskFrames) { + SVGMaskObserverList* observerList = GetOrCreateMaskObserverList(aMaskedFrame); + if (!observerList) { + return eHasNoRefs; + } + + const nsTArray<RefPtr<SVGPaintingProperty>>& observers = + observerList->GetObservers(); + if (observers.IsEmpty()) { + return eHasNoRefs; + } + + ReferenceState state = eHasRefsAllValid; + + for (size_t i = 0; i < observers.Length(); i++) { + bool frameTypeOK = true; + SVGMaskFrame* maskFrame = + static_cast<SVGMaskFrame*>(observers[i]->GetAndObserveReferencedFrame( + LayoutFrameType::SVGMask, &frameTypeOK)); + MOZ_ASSERT(!maskFrame || frameTypeOK); + // XXXjwatt: this looks fishy + if (!frameTypeOK) { + // We can not find the specific SVG mask resource in the downloaded SVG + // document. There are two possibilities: + // 1. The given resource id is invalid. + // 2. The given resource id refers to a viewbox. + // + // Hand it over to the style image. + observerList->ResolveImage(i); + state = eHasRefsSomeInvalid; + } + if (aMaskFrames) { + aMaskFrames->AppendElement(maskFrame); + } + } + + return state; +} + +SVGGeometryElement* SVGObserverUtils::GetAndObserveTextPathsPath( + nsIFrame* aTextPathFrame) { + // Continuations can come and go during reflow, and we don't need to observe + // the referenced element more than once for a given node. + aTextPathFrame = aTextPathFrame->FirstContinuation(); + + SVGTextPathObserver* property = + aTextPathFrame->GetProperty(HrefAsTextPathProperty()); + + if (!property) { + nsIContent* content = aTextPathFrame->GetContent(); + nsAutoString href; + static_cast<SVGTextPathElement*>(content)->HrefAsString(href); + if (href.IsEmpty()) { + return nullptr; // no URL + } + + // There's no clear refererer policy spec about non-CSS SVG resource + // references Bug 1415044 to investigate which referrer we should use + nsIReferrerInfo* referrerInfo = + content->OwnerDoc()->ReferrerInfoForInternalCSSAndSVGResources(); + RefPtr<URLAndReferrerInfo> target = + ResolveURLUsingLocalRef(aTextPathFrame, href, referrerInfo); + + property = + GetEffectProperty(target, aTextPathFrame, HrefAsTextPathProperty()); + if (!property) { + return nullptr; + } + } + + return SVGGeometryElement::FromNodeOrNull( + property->GetAndObserveReferencedElement()); +} + +void SVGObserverUtils::InitiateResourceDocLoads(nsIFrame* aFrame) { + // We create observer objects and attach them to aFrame, but we do not + // make aFrame start observing the referenced frames. + Unused << GetOrCreateFilterObserverListForCSS(aFrame); + Unused << GetOrCreateClipPathObserver(aFrame); + Unused << GetOrCreateMaskObserverList(aFrame); +} + +void SVGObserverUtils::RemoveTextPathObserver(nsIFrame* aTextPathFrame) { + aTextPathFrame->RemoveProperty(HrefAsTextPathProperty()); +} + +nsIFrame* SVGObserverUtils::GetAndObserveTemplate( + nsIFrame* aFrame, HrefToTemplateCallback aGetHref) { + SVGTemplateElementObserver* observer = + aFrame->GetProperty(HrefToTemplateProperty()); + + if (!observer) { + nsAutoString href; + aGetHref(href); + if (href.IsEmpty()) { + return nullptr; // no URL + } + + // Convert href to an nsIURI + nsIContent* content = aFrame->GetContent(); + + nsCOMPtr<nsIURI> baseURI = content->GetBaseURI(); + if (nsContentUtils::IsLocalRefURL(href)) { + baseURI = GetBaseURLForLocalRef(content, baseURI); + } + nsCOMPtr<nsIURI> targetURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(targetURI), href, content->GetUncomposedDoc(), baseURI); + + // There's no clear refererer policy spec about non-CSS SVG resource + // references. Bug 1415044 to investigate which referrer we should use. + nsIReferrerInfo* referrerInfo = + content->OwnerDoc()->ReferrerInfoForInternalCSSAndSVGResources(); + RefPtr<URLAndReferrerInfo> target = + new URLAndReferrerInfo(targetURI, referrerInfo); + + observer = GetEffectProperty(target, aFrame, HrefToTemplateProperty()); + } + + return observer ? observer->GetAndObserveReferencedFrame() : nullptr; +} + +void SVGObserverUtils::RemoveTemplateObserver(nsIFrame* aFrame) { + aFrame->RemoveProperty(HrefToTemplateProperty()); +} + +Element* SVGObserverUtils::GetAndObserveBackgroundImage(nsIFrame* aFrame, + const nsAtom* aHref) { + bool found; + URIObserverHashtable* hashtable = + aFrame->GetProperty(BackgroundImageProperty(), &found); + if (!found) { + hashtable = new URIObserverHashtable(); + aFrame->AddProperty(BackgroundImageProperty(), hashtable); + } else { + MOZ_ASSERT(hashtable, "this property should only store non-null values"); + } + + nsAutoString elementId = u"#"_ns + nsDependentAtomString(aHref); + nsCOMPtr<nsIURI> targetURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(targetURI), elementId, + aFrame->GetContent()->GetUncomposedDoc(), + aFrame->GetContent()->GetBaseURI()); + nsIReferrerInfo* referrerInfo = + aFrame->GetContent() + ->OwnerDoc() + ->ReferrerInfoForInternalCSSAndSVGResources(); + RefPtr<URLAndReferrerInfo> url = + new URLAndReferrerInfo(targetURI, referrerInfo); + + return static_cast<SVGMozElementObserver*>( + hashtable + ->LookupOrInsertWith( + url, + [&] { + return MakeRefPtr<SVGMozElementObserver>( + url, aFrame, + /* aWatchImage */ true); + }) + .get()) + ->GetAndObserveReferencedElement(); +} + +Element* SVGObserverUtils::GetAndObserveBackgroundClip(nsIFrame* aFrame) { + bool found; + BackgroundClipRenderingObserver* obs = + aFrame->GetProperty(BackgroundClipObserverProperty(), &found); + if (!found) { + obs = new BackgroundClipRenderingObserver(aFrame); + NS_ADDREF(obs); + aFrame->AddProperty(BackgroundClipObserverProperty(), obs); + } + + return obs->GetAndObserveReferencedElement(); +} + +SVGPaintServerFrame* SVGObserverUtils::GetAndObservePaintServer( + nsIFrame* aPaintedFrame, StyleSVGPaint nsStyleSVG::*aPaint) { + // If we're looking at a frame within SVG text, then we need to look up + // to find the right frame to get the painting property off. We should at + // least look up past a text frame, and if the text frame's parent is the + // anonymous block frame, then we look up to its parent (the SVGTextFrame). + nsIFrame* paintedFrame = aPaintedFrame; + if (paintedFrame->IsInSVGTextSubtree()) { + paintedFrame = paintedFrame->GetParent(); + nsIFrame* grandparent = paintedFrame->GetParent(); + if (grandparent && grandparent->IsSVGTextFrame()) { + paintedFrame = grandparent; + } + } + + const nsStyleSVG* svgStyle = paintedFrame->StyleSVG(); + if (!(svgStyle->*aPaint).kind.IsPaintServer()) { + return nullptr; + } + + RefPtr<URLAndReferrerInfo> paintServerURL = ResolveURLUsingLocalRef( + paintedFrame, (svgStyle->*aPaint).kind.AsPaintServer()); + + MOZ_ASSERT(aPaint == &nsStyleSVG::mFill || aPaint == &nsStyleSVG::mStroke); + PaintingPropertyDescriptor propDesc = + (aPaint == &nsStyleSVG::mFill) ? FillProperty() : StrokeProperty(); + if (auto* property = + GetPaintingProperty(paintServerURL, paintedFrame, propDesc)) { + return do_QueryFrame(property->GetAndObserveReferencedFrame()); + } + return nullptr; +} + +void SVGObserverUtils::UpdateEffects(nsIFrame* aFrame) { + NS_ASSERTION(aFrame->GetContent()->IsElement(), + "aFrame's content should be an element"); + + aFrame->RemoveProperty(FilterProperty()); + aFrame->RemoveProperty(MaskProperty()); + aFrame->RemoveProperty(ClipPathProperty()); + aFrame->RemoveProperty(MarkerStartProperty()); + aFrame->RemoveProperty(MarkerMidProperty()); + aFrame->RemoveProperty(MarkerEndProperty()); + aFrame->RemoveProperty(FillProperty()); + aFrame->RemoveProperty(StrokeProperty()); + aFrame->RemoveProperty(BackgroundImageProperty()); + + // Ensure that the filter is repainted correctly + // We can't do that in OnRenderingChange as the referenced frame may + // not be valid + GetOrCreateFilterObserverListForCSS(aFrame); + + if (aFrame->IsSVGGeometryFrame() && + static_cast<SVGGeometryElement*>(aFrame->GetContent())->IsMarkable()) { + // Set marker properties here to avoid reference loops + RefPtr<URLAndReferrerInfo> markerURL = + GetMarkerURI(aFrame, &nsStyleSVG::mMarkerStart); + GetEffectProperty(markerURL, aFrame, MarkerStartProperty()); + markerURL = GetMarkerURI(aFrame, &nsStyleSVG::mMarkerMid); + GetEffectProperty(markerURL, aFrame, MarkerMidProperty()); + markerURL = GetMarkerURI(aFrame, &nsStyleSVG::mMarkerEnd); + GetEffectProperty(markerURL, aFrame, MarkerEndProperty()); + } +} + +void SVGObserverUtils::AddRenderingObserver(Element* aElement, + SVGRenderingObserver* aObserver) { + SVGRenderingObserverSet* observers = GetObserverSet(aElement); + if (!observers) { + observers = new SVGRenderingObserverSet(); + aElement->SetProperty(nsGkAtoms::renderingobserverset, observers, + nsINode::DeleteProperty<SVGRenderingObserverSet>); + } + aElement->SetHasRenderingObservers(true); + observers->Add(aObserver); +} + +void SVGObserverUtils::RemoveRenderingObserver( + Element* aElement, SVGRenderingObserver* aObserver) { + SVGRenderingObserverSet* observers = GetObserverSet(aElement); + if (observers) { + NS_ASSERTION(observers->Contains(aObserver), + "removing observer from an element we're not observing?"); + observers->Remove(aObserver); + if (observers->IsEmpty()) { + aElement->SetHasRenderingObservers(false); + } + } +} + +void SVGObserverUtils::RemoveAllRenderingObservers(Element* aElement) { + SVGRenderingObserverSet* observers = GetObserverSet(aElement); + if (observers) { + observers->RemoveAll(); + aElement->SetHasRenderingObservers(false); + } +} + +void SVGObserverUtils::InvalidateRenderingObservers(nsIFrame* aFrame) { + NS_ASSERTION(!aFrame->GetPrevContinuation(), + "aFrame must be first continuation"); + + auto* element = Element::FromNodeOrNull(aFrame->GetContent()); + if (!element) { + return; + } + + // If the rendering has changed, the bounds may well have changed too: + aFrame->RemoveProperty(SVGUtils::ObjectBoundingBoxProperty()); + + if (auto* observers = GetObserverSet(element)) { + observers->InvalidateAll(); + return; + } + + // Check ancestor SVG containers. The root frame cannot be of type + // eSVGContainer so we don't have to check f for null here. + for (nsIFrame* f = aFrame->GetParent(); + f->IsFrameOfType(nsIFrame::eSVGContainer); f = f->GetParent()) { + if (auto* element = Element::FromNode(f->GetContent())) { + if (auto* observers = GetObserverSet(element)) { + observers->InvalidateAll(); + return; + } + } + } +} + +void SVGObserverUtils::InvalidateDirectRenderingObservers( + Element* aElement, uint32_t aFlags /* = 0 */) { + if (nsIFrame* frame = aElement->GetPrimaryFrame()) { + // If the rendering has changed, the bounds may well have changed too: + frame->RemoveProperty(SVGUtils::ObjectBoundingBoxProperty()); + } + + if (aElement->HasRenderingObservers()) { + SVGRenderingObserverSet* observers = GetObserverSet(aElement); + if (observers) { + if (aFlags & INVALIDATE_REFLOW) { + observers->InvalidateAllForReflow(); + } else { + observers->InvalidateAll(); + } + } + } +} + +void SVGObserverUtils::InvalidateDirectRenderingObservers( + nsIFrame* aFrame, uint32_t aFlags /* = 0 */) { + if (auto* element = Element::FromNodeOrNull(aFrame->GetContent())) { + InvalidateDirectRenderingObservers(element, aFlags); + } +} + +already_AddRefed<nsIURI> SVGObserverUtils::GetBaseURLForLocalRef( + nsIContent* content, nsIURI* aDocURI) { + MOZ_ASSERT(content); + + // Content is in a shadow tree. If this URL was specified in the subtree + // referenced by the <use>, element, and that subtree came from a separate + // resource document, then we want the fragment-only URL to resolve to an + // element from the resource document. Otherwise, the URL was specified + // somewhere in the document with the <use> element, and we want the + // fragment-only URL to resolve to an element in that document. + if (SVGUseElement* use = content->GetContainingSVGUseShadowHost()) { + if (nsIURI* originalURI = use->GetSourceDocURI()) { + bool isEqualsExceptRef = false; + aDocURI->EqualsExceptRef(originalURI, &isEqualsExceptRef); + if (isEqualsExceptRef) { + return do_AddRef(originalURI); + } + } + } + + // For a local-reference URL, resolve that fragment against the current + // document that relative URLs are resolved against. + return do_AddRef(content->OwnerDoc()->GetDocumentURI()); +} + +already_AddRefed<URLAndReferrerInfo> SVGObserverUtils::GetFilterURI( + nsIFrame* aFrame, const StyleFilter& aFilter) { + MOZ_ASSERT(!aFrame->StyleEffects()->mFilters.IsEmpty() || + !aFrame->StyleEffects()->mBackdropFilters.IsEmpty() || + !aFrame->GetContent()->GetParent()); + MOZ_ASSERT(aFilter.IsUrl()); + return ResolveURLUsingLocalRef(aFrame, aFilter.AsUrl()); +} + +} // namespace mozilla diff --git a/layout/svg/SVGObserverUtils.h b/layout/svg/SVGObserverUtils.h new file mode 100644 index 0000000000..17bd012078 --- /dev/null +++ b/layout/svg/SVGObserverUtils.h @@ -0,0 +1,420 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGOBSERVERUTILS_H_ +#define LAYOUT_SVG_SVGOBSERVERUTILS_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/dom/IDTracker.h" +#include "FrameProperties.h" +#include "nsID.h" +#include "nsIFrame.h" // only for LayoutFrameType +#include "nsIMutationObserver.h" +#include "nsISupports.h" +#include "nsISupportsImpl.h" +#include "nsIReferrerInfo.h" +#include "nsStringFwd.h" +#include "nsStubMutationObserver.h" +#include "nsStyleStruct.h" +#include "nsCycleCollectionParticipant.h" + +class nsAtom; +class nsIFrame; +class nsIURI; + +namespace mozilla { +class SVGClipPathFrame; +class SVGFilterFrame; +class SVGMarkerFrame; +class SVGMaskFrame; +class SVGPaintServerFrame; + +namespace dom { +class CanvasRenderingContext2D; +class Element; +class SVGGeometryElement; +} // namespace dom +} // namespace mozilla + +namespace mozilla { + +/* + * This class contains URL and referrer information (referrer and referrer + * policy). + * We use it to pass to svg system instead of nsIURI. The object brings referrer + * and referrer policy so we can send correct Referer headers. + */ +class URLAndReferrerInfo { + public: + URLAndReferrerInfo(nsIURI* aURI, nsIReferrerInfo* aReferrerInfo) + : mURI(aURI), mReferrerInfo(aReferrerInfo) { + MOZ_ASSERT(aURI); + } + + URLAndReferrerInfo(nsIURI* aURI, const URLExtraData& aExtraData) + : mURI(aURI), mReferrerInfo(aExtraData.ReferrerInfo()) { + MOZ_ASSERT(aURI); + } + + NS_INLINE_DECL_REFCOUNTING(URLAndReferrerInfo) + + nsIURI* GetURI() const { return mURI; } + nsIReferrerInfo* GetReferrerInfo() const { return mReferrerInfo; } + + bool operator==(const URLAndReferrerInfo& aRHS) const; + + private: + ~URLAndReferrerInfo() = default; + + nsCOMPtr<nsIURI> mURI; + nsCOMPtr<nsIReferrerInfo> mReferrerInfo; +}; + +/** + * This interface allows us to be notified when a piece of SVG content is + * re-rendered. + * + * Concrete implementations of this base class need to implement + * GetReferencedElementWithoutObserving to specify the SVG element that + * they'd like to monitor for rendering changes, and they need to implement + * OnRenderingChange to specify how we'll react when that content gets + * re-rendered. They also need to implement a constructor and destructor, + * which should call StartObserving and StopObserving, respectively. + * + * The referenced element is generally looked up and stored during + * construction. If the referenced element is in an extenal SVG resource + * document, the lookup code will initiate loading of the external resource and + * OnRenderingChange will be called once the element in the external resource + * is available. + * + * Although the referenced element may be found and stored during construction, + * observing for rendering changes does not start until requested. + */ +class SVGRenderingObserver : public nsStubMutationObserver { + protected: + virtual ~SVGRenderingObserver() = default; + + public: + using Element = dom::Element; + + SVGRenderingObserver() : mInObserverSet(false) {} + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + /** + * Called when non-DOM-mutation changes to the observed element should likely + * cause the rendering of our observer to change. This includes changes to + * CSS computed values, but also changes to rendering observers that the + * observed element itself may have (for example, when we're being used to + * observe an SVG pattern, and an element in that pattern references and + * observes a gradient that has changed). + */ + void OnNonDOMMutationRenderingChange(); + + // When a SVGRenderingObserver list gets forcibly cleared, it uses this + // callback to notify every observer that's cleared from it, so they can + // react. + void NotifyEvictedFromRenderingObserverSet(); + + nsIFrame* GetAndObserveReferencedFrame(); + /** + * @param aOK this is only for the convenience of callers. We set *aOK to + * false if the frame is the wrong type + */ + nsIFrame* GetAndObserveReferencedFrame(mozilla::LayoutFrameType aFrameType, + bool* aOK); + + Element* GetAndObserveReferencedElement(); + + virtual bool ObservesReflow() { return false; } + + protected: + void StartObserving(); + void StopObserving(); + + /** + * Called whenever the rendering of the observed element may have changed. + * + * More specifically, this method is called whenever DOM mutation occurs in + * the observed element's subtree, or whenever + * SVGObserverUtils::InvalidateRenderingObservers or + * SVGObserverUtils::InvalidateDirectRenderingObservers is called for the + * observed element's frame. + * + * Subclasses should override this method to handle rendering changes + * appropriately. + */ + virtual void OnRenderingChange() = 0; + + virtual Element* GetReferencedElementWithoutObserving() = 0; + +#ifdef DEBUG + void DebugObserverSet(); +#endif + + // Whether we're in our observed element's observer set at this time. + bool mInObserverSet; +}; + +class SVGObserverUtils { + public: + using CanvasRenderingContext2D = dom::CanvasRenderingContext2D; + using Element = dom::Element; + using SVGGeometryElement = dom::SVGGeometryElement; + using HrefToTemplateCallback = const std::function<void(nsAString&)>&; + + /** + * Ensures that that if the given frame requires any resources that are in + * SVG resource documents that the loading of those documents is initiated. + * This does not make aFrame start to observe any elements that it + * references. + */ + static void InitiateResourceDocLoads(nsIFrame* aFrame); + + /** + * Called when changes to an element (e.g. CSS property changes) cause its + * frame to start/stop referencing (or reference different) SVG resource + * elements. (_Not_ called for changes to referenced resource elements.) + * + * This function handles such changes by discarding _all_ the frame's SVG + * effects frame properties (causing those properties to stop watching their + * target element). It also synchronously (re)creates the filter and marker + * frame properties (XXX why not the other properties?), which makes it + * useful for initializing those properties during first reflow. + * + * XXX rename to something more meaningful like RefreshResourceReferences? + */ + static void UpdateEffects(nsIFrame* aFrame); + + /** + * @param aFrame must be a first-continuation. + */ + static void AddRenderingObserver(Element* aElement, + SVGRenderingObserver* aObserver); + /** + * @param aFrame must be a first-continuation. + */ + static void RemoveRenderingObserver(Element* aElement, + SVGRenderingObserver* aObserver); + + /** + * Removes all rendering observers from aElement. + */ + static void RemoveAllRenderingObservers(Element* aElement); + + /** + * This can be called on any frame. We invalidate the observers of aFrame's + * element, if any, or else walk up to the nearest observable SVG parent + * frame with observers and invalidate them instead. + * + * Note that this method is very different to e.g. + * MutationObservers::AttributeChanged which walks up the content node tree + * all the way to the root node (not stopping if it encounters a non-container + * SVG node) invalidating all mutation observers (not just + * SVGRenderingObservers) on all nodes along the way (not just the first + * node it finds with observers). In other words, by doing all the + * things in parentheses in the preceding sentence, this method uses + * knowledge about our implementation and what can be affected by SVG effects + * to make invalidation relatively lightweight when an SVG effect changes. + */ + static void InvalidateRenderingObservers(nsIFrame* aFrame); + + enum { INVALIDATE_REFLOW = 1 }; + + enum ReferenceState { + /// Has no references to SVG filters (may still have CSS filter functions!) + eHasNoRefs, + eHasRefsAllValid, + eHasRefsSomeInvalid, + }; + + /** + * This can be called on any element or frame. Only direct observers of this + * (frame's) element, if any, are invalidated. + */ + static void InvalidateDirectRenderingObservers(Element* aElement, + uint32_t aFlags = 0); + static void InvalidateDirectRenderingObservers(nsIFrame* aFrame, + uint32_t aFlags = 0); + + /** + * Get the paint server for aPaintedFrame. + */ + static SVGPaintServerFrame* GetAndObservePaintServer( + nsIFrame* aPaintedFrame, mozilla::StyleSVGPaint nsStyleSVG::*aPaint); + + /** + * Get the start/mid/end-markers for the given frame, and add the frame as + * an observer to those markers. Returns true if at least one marker type is + * found, false otherwise. + */ + static bool GetAndObserveMarkers(nsIFrame* aMarkedFrame, + SVGMarkerFrame* (*aFrames)[3]); + + /** + * Get the frames of the SVG filters applied to the given frame, and add the + * frame as an observer to those filter frames. + * + * NOTE! A return value of eHasNoRefs does NOT mean that there are no filters + * to be applied, only that there are no references to SVG filter elements. + * + * XXX Callers other than ComputePostEffectsInkOverflowRect and + * SVGUtils::GetPostFilterInkOverflowRect should not need to initiate + * observing. If we have a bug that causes invalidation (which would remove + * observers) between reflow and painting, then we don't really want to + * re-add abservers during painting. That has the potential to hide logic + * bugs, or cause later invalidation problems. However, let's not change + * that behavior just yet due to the regression potential. + */ + static ReferenceState GetAndObserveFilters( + nsIFrame* aFilteredFrame, nsTArray<SVGFilterFrame*>* aFilterFrames); + + /** + * If the given frame is already observing SVG filters, this function gets + * those filters. If the frame is not already observing filters this + * function assumes that it doesn't have anything to observe. + */ + static ReferenceState GetFiltersIfObserving( + nsIFrame* aFilteredFrame, nsTArray<SVGFilterFrame*>* aFilterFrames); + + /** + * Starts observing filters for a <canvas> element's CanvasRenderingContext2D. + * + * Returns a RAII object that the caller should make sure is released once + * the CanvasRenderingContext2D is no longer using them (that is, when the + * CanvasRenderingContext2D "drawing style state" on which the filters were + * set is destroyed or has its filter style reset). + * + * XXXjwatt: It's a bit unfortunate that both we and + * CanvasRenderingContext2D::UpdateFilter process the list of StyleFilter + * objects separately. It would be better to refactor things so that we only + * do that work once. + */ + static already_AddRefed<nsISupports> ObserveFiltersForCanvasContext( + CanvasRenderingContext2D* aContext, Element* aCanvasElement, + Span<const StyleFilter> aFilters); + + /** + * Called when cycle collecting CanvasRenderingContext2D, and requires the + * RAII object returned from ObserveFiltersForCanvasContext to be passed in. + * + * XXXjwatt: I don't think this is doing anything useful. All we do under + * this function is clear a raw C-style (i.e. not strong) pointer. That's + * clearly not helping in breaking any cycles. The fact that we MOZ_CRASH + * in OnRenderingChange if that pointer is null indicates that this isn't + * even doing anything useful in terms of preventing further invalidation + * from any observed filters. + */ + static void DetachFromCanvasContext(nsISupports* aAutoObserver); + + /** + * Get the frame of the SVG clipPath applied to aClippedFrame, if any, and + * set up aClippedFrame as a rendering observer of the clipPath's frame, to + * be invalidated if it changes. + * + * Currently we only have support for 'clip-path' with a single item, but the + * spec. now says 'clip-path' can be set to an arbitrary number of items. + * Once we support that, aClipPathFrame will need to be an nsTArray as it + * is for 'filter' and 'mask'. Currently a return value of eHasNoRefs means + * that there is no clipping at all, but once we support more than one item + * then - as for filter and mask - we could still have basic shape clipping + * to apply even if there are no references to SVG clipPath elements. + * + * Note that, unlike for filters, a reference to an ID that doesn't exist + * is not invalid for clip-path or mask. We will return eHasNoRefs in that + * case. + */ + static ReferenceState GetAndObserveClipPath( + nsIFrame* aClippedFrame, SVGClipPathFrame** aClipPathFrame); + + /** + * If masking is applied to aMaskedFrame, gets an array of any SVG masks + * that are referenced, setting up aMaskFrames as a rendering observer of + * those masks (if any). + * + * NOTE! A return value of eHasNoRefs does NOT mean that there are no masks + * to be applied, only that there are no references to SVG mask elements. + * + * Note that, unlike for filters, a reference to an ID that doesn't exist + * is not invalid for clip-path or mask. We will return eHasNoRefs in that + * case. + */ + static ReferenceState GetAndObserveMasks( + nsIFrame* aMaskedFrame, nsTArray<SVGMaskFrame*>* aMaskFrames); + + /** + * Get the SVGGeometryElement that is referenced by aTextPathFrame, and make + * aTextPathFrame start observing rendering changes to that element. + */ + static SVGGeometryElement* GetAndObserveTextPathsPath( + nsIFrame* aTextPathFrame); + + /** + * Make aTextPathFrame stop observing rendering changes to the + * SVGGeometryElement that it references, if any. + */ + static void RemoveTextPathObserver(nsIFrame* aTextPathFrame); + + /** + * Gets the nsIFrame of a referenced SVG "template" element, if any, and + * makes aFrame start observing rendering changes to the template element. + * + * Template elements: some elements like gradients, pattern or filter can + * reference another element of the same type using their 'href' attribute, + * and use that element as a template that provides attributes or content + * that is missing from the referring element. + * + * The frames that this function is called for do not have a common base + * class, which is why it is necessary to pass in a function that can be + * used as a callback to lazily get the href value, if necessary. + */ + static nsIFrame* GetAndObserveTemplate(nsIFrame* aFrame, + HrefToTemplateCallback aGetHref); + + static void RemoveTemplateObserver(nsIFrame* aFrame); + + /** + * Gets an arbitrary element and starts observing it. Used to implement + * '-moz-element'. + * + * Note that bug 1496065 has been filed to remove support for referencing + * arbitrary elements using '-moz-element'. + */ + static Element* GetAndObserveBackgroundImage(nsIFrame* aFrame, + const nsAtom* aHref); + + /** + * Gets an arbitrary element and starts observing it. Used to detect + * invalidation changes for background-clip:text. + */ + static Element* GetAndObserveBackgroundClip(nsIFrame* aFrame); + + /** + * A helper function to resolve filter URL. + */ + static already_AddRefed<URLAndReferrerInfo> GetFilterURI( + nsIFrame* aFrame, const StyleFilter& aFilter); + + /** + * Return a baseURL for resolving a local-ref URL. + * + * @param aContent an element which uses a local-ref property. Here are some + * examples: + * <rect fill=url(#foo)> + * <circle clip-path=url(#foo)> + * <use xlink:href="#foo"> + */ + static already_AddRefed<nsIURI> GetBaseURLForLocalRef(nsIContent* aContent, + nsIURI* aDocURI); +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGOBSERVERUTILS_H_ diff --git a/layout/svg/SVGOuterSVGFrame.cpp b/layout/svg/SVGOuterSVGFrame.cpp new file mode 100644 index 0000000000..a43e6815cd --- /dev/null +++ b/layout/svg/SVGOuterSVGFrame.cpp @@ -0,0 +1,880 @@ +/* -*- 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 "SVGOuterSVGFrame.h" + +// Keep others in (case-insensitive) order: +#include "gfxContext.h" +#include "nsDisplayList.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsLayoutUtils.h" +#include "nsObjectLoadingContent.h" +#include "nsSubDocumentFrame.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/SVGSVGElement.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +//---------------------------------------------------------------------- +// Implementation + +nsContainerFrame* NS_NewSVGOuterSVGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGOuterSVGFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGOuterSVGFrame) + +SVGOuterSVGFrame::SVGOuterSVGFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGDisplayContainerFrame(aStyle, aPresContext, kClassID) { + // Outer-<svg> has CSS layout, so remove this bit: + RemoveStateBits(NS_FRAME_SVG_LAYOUT); + AddStateBits(NS_FRAME_REFLOW_ROOT | NS_FRAME_FONT_INFLATION_CONTAINER | + NS_FRAME_FONT_INFLATION_FLOW_ROOT | NS_FRAME_MAY_BE_TRANSFORMED); +} + +// The CSS Containment spec says that size-contained replaced elements must be +// treated as having an intrinsic width and height of 0. That's applicable to +// outer SVG frames, unless they're the outermost element (in which case +// they're not really "replaced", and there's no outer context to contain sizes +// from leaking into). Hence, we check for a parent element before we bother +// testing for 'contain:size'. +static inline ContainSizeAxes ContainSizeAxesIfApplicable( + const SVGOuterSVGFrame* aFrame) { + if (!aFrame->GetContent()->GetParent()) { + return ContainSizeAxes(false, false); + } + return aFrame->GetContainSizeAxes(); +} + +// This should match ImageDocument::GetZoomLevel. +float SVGOuterSVGFrame::ComputeFullZoom() const { + MOZ_ASSERT(mIsRootContent); + MOZ_ASSERT(!mIsInIframe); + if (BrowsingContext* bc = PresContext()->Document()->GetBrowsingContext()) { + return bc->FullZoom(); + } + return 1.0f; +} + +void SVGOuterSVGFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::svg), + "Content is not an SVG 'svg' element!"); + + // Check for conditional processing attributes here rather than in + // nsCSSFrameConstructor::FindSVGData because we want to avoid + // simply giving failing outer <svg> elements an SVGContainerFrame. + // We don't create other SVG frames if PassesConditionalProcessingTests + // returns false, but since we do create SVGOuterSVGFrame frames we + // prevent them from painting by [ab]use NS_FRAME_IS_NONDISPLAY. The + // frame will be recreated via an nsChangeHint_ReconstructFrame restyle if + // the value returned by PassesConditionalProcessingTests changes. + auto* svg = static_cast<SVGSVGElement*>(aContent); + if (!svg->PassesConditionalProcessingTests()) { + AddStateBits(NS_FRAME_IS_NONDISPLAY); + } + + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); + + Document* doc = mContent->GetUncomposedDoc(); + mIsRootContent = doc && doc->GetRootElement() == mContent; + + if (mIsRootContent) { + if (nsCOMPtr<nsIDocShell> docShell = PresContext()->GetDocShell()) { + RefPtr<BrowsingContext> bc = docShell->GetBrowsingContext(); + if (const Maybe<nsString>& type = bc->GetEmbedderElementType()) { + mIsInObjectOrEmbed = + nsGkAtoms::object->Equals(*type) || nsGkAtoms::embed->Equals(*type); + mIsInIframe = nsGkAtoms::iframe->Equals(*type); + } + } + if (!mIsInIframe) { + mFullZoom = ComputeFullZoom(); + } + } + + MaybeSendIntrinsicSizeAndRatioToEmbedder(); +} + +//---------------------------------------------------------------------- +// nsQueryFrame methods + +NS_QUERYFRAME_HEAD(SVGOuterSVGFrame) + NS_QUERYFRAME_ENTRY(SVGOuterSVGFrame) + NS_QUERYFRAME_ENTRY(ISVGSVGFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGDisplayContainerFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods +//---------------------------------------------------------------------- +// reflowing + +/* virtual */ +nscoord SVGOuterSVGFrame::GetMinISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_MIN_INLINE_SIZE(this, result); + + // If this ever changes to return something other than zero, then + // nsSubDocumentFrame::GetMinISize will also need to change. + result = nscoord(0); + + return result; +} + +/* virtual */ +nscoord SVGOuterSVGFrame::GetPrefISize(gfxContext* aRenderingContext) { + nscoord result; + DISPLAY_PREF_INLINE_SIZE(this, result); + + SVGSVGElement* svg = static_cast<SVGSVGElement*>(GetContent()); + WritingMode wm = GetWritingMode(); + const SVGAnimatedLength& isize = + wm.IsVertical() ? svg->mLengthAttributes[SVGSVGElement::ATTR_HEIGHT] + : svg->mLengthAttributes[SVGSVGElement::ATTR_WIDTH]; + + if (Maybe<nscoord> containISize = + ContainSizeAxesIfApplicable(this).ContainIntrinsicISize(*this)) { + result = *containISize; + } else if (isize.IsPercentage()) { + // If we are here, our inline size attribute is a percentage either + // explicitly (via an attribute value) or implicitly (by being unset, which + // is treated as 100%). The following if-condition, deciding to return + // either the fallback intrinsic size or zero, is made to match blink and + // webkit's behavior for webcompat. + if (isize.IsExplicitlySet() || StylePosition()->ISize(wm).HasPercent() || + !GetAspectRatio()) { + result = wm.IsVertical() ? kFallbackIntrinsicSize.height + : kFallbackIntrinsicSize.width; + } else { + result = nscoord(0); + } + } else { + result = nsPresContext::CSSPixelsToAppUnits(isize.GetAnimValue(svg)); + if (result < 0) { + result = nscoord(0); + } + } + + return result; +} + +/* virtual */ +IntrinsicSize SVGOuterSVGFrame::GetIntrinsicSize() { + // XXXjwatt Note that here we want to return the CSS width/height if they're + // specified and we're embedded inside an nsIObjectLoadingContent. + + const auto containAxes = ContainSizeAxesIfApplicable(this); + if (containAxes.IsBoth()) { + // Intrinsic size of 'contain:size' replaced elements is determined by + // contain-intrinsic-size, defaulting to 0x0. + return containAxes.ContainIntrinsicSize(IntrinsicSize(0, 0), *this); + } + + SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent()); + const SVGAnimatedLength& width = + content->mLengthAttributes[SVGSVGElement::ATTR_WIDTH]; + const SVGAnimatedLength& height = + content->mLengthAttributes[SVGSVGElement::ATTR_HEIGHT]; + + IntrinsicSize intrinsicSize; + + if (!width.IsPercentage()) { + nscoord val = + nsPresContext::CSSPixelsToAppUnits(width.GetAnimValue(content)); + intrinsicSize.width.emplace(std::max(val, 0)); + } + + if (!height.IsPercentage()) { + nscoord val = + nsPresContext::CSSPixelsToAppUnits(height.GetAnimValue(content)); + intrinsicSize.height.emplace(std::max(val, 0)); + } + + return containAxes.ContainIntrinsicSize(intrinsicSize, *this); +} + +/* virtual */ +AspectRatio SVGOuterSVGFrame::GetIntrinsicRatio() const { + if (ContainSizeAxesIfApplicable(this).IsAny()) { + return AspectRatio(); + } + + // We only have an intrinsic size/ratio if our width and height attributes + // are both specified and set to non-percentage values, or we have a viewBox + // rect: https://svgwg.org/svg2-draft/coords.html#SizingSVGInCSS + + auto* content = static_cast<SVGSVGElement*>(GetContent()); + const SVGAnimatedLength& width = + content->mLengthAttributes[SVGSVGElement::ATTR_WIDTH]; + const SVGAnimatedLength& height = + content->mLengthAttributes[SVGSVGElement::ATTR_HEIGHT]; + if (!width.IsPercentage() && !height.IsPercentage()) { + // Use width/height ratio only if + // 1. it's not a degenerate ratio, and + // 2. width and height are non-negative numbers. + // Otherwise, we use the viewbox rect. + // https://github.com/w3c/csswg-drafts/issues/6286 + const float w = width.GetAnimValue(content); + const float h = height.GetAnimValue(content); + if (w > 0.0f && h > 0.0f) { + return AspectRatio::FromSize(w, h); + } + } + + const auto& viewBox = content->GetViewBoxInternal(); + if (viewBox.HasRect()) { + const auto& anim = viewBox.GetAnimValue(); + return AspectRatio::FromSize(anim.width, anim.height); + } + + return SVGDisplayContainerFrame::GetIntrinsicRatio(); +} + +/* virtual */ +nsIFrame::SizeComputationResult SVGOuterSVGFrame::ComputeSize( + gfxContext* aRenderingContext, WritingMode aWritingMode, + const LogicalSize& aCBSize, nscoord aAvailableISize, + const LogicalSize& aMargin, const LogicalSize& aBorderPadding, + const StyleSizeOverrides& aSizeOverrides, ComputeSizeFlags aFlags) { + if (IsRootOfImage() || mIsInObjectOrEmbed) { + // The embedding element has sized itself using the CSS replaced element + // sizing rules, using our intrinsic dimensions as necessary. The SVG spec + // says that the width and height of embedded SVG is overridden by the + // width and height of the embedding element, so we just need to size to + // the viewport that the embedding element has established for us. + return {aCBSize, AspectRatioUsage::None}; + } + + LogicalSize cbSize = aCBSize; + IntrinsicSize intrinsicSize = GetIntrinsicSize(); + + if (mIsRootContent) { + // We're the root of the outermost browsing context, so we need to scale + // cbSize by the full-zoom so that SVGs with percentage width/height zoom: + + NS_ASSERTION(aCBSize.ISize(aWritingMode) != NS_UNCONSTRAINEDSIZE && + aCBSize.BSize(aWritingMode) != NS_UNCONSTRAINEDSIZE, + "root should not have auto-width/height containing block"); + + if (!mIsInIframe) { + // NOTE: We can't just use mFullZoom because this can run before Reflow() + // updates it. + const float zoom = ComputeFullZoom(); + cbSize.ISize(aWritingMode) *= zoom; + cbSize.BSize(aWritingMode) *= zoom; + } + + // We also need to honour the width and height attributes' default values + // of 100% when we're the root of a browsing context. (GetIntrinsicSize() + // doesn't report these since there's no such thing as a percentage + // intrinsic size. Also note that explicit percentage values are mapped + // into style, so the following isn't for them.) + + auto* content = static_cast<SVGSVGElement*>(GetContent()); + + const SVGAnimatedLength& width = + content->mLengthAttributes[SVGSVGElement::ATTR_WIDTH]; + if (width.IsPercentage()) { + MOZ_ASSERT(!intrinsicSize.width, + "GetIntrinsicSize should have reported no intrinsic width"); + float val = width.GetAnimValInSpecifiedUnits() / 100.0f; + intrinsicSize.width.emplace(std::max(val, 0.0f) * + cbSize.Width(aWritingMode)); + } + + const SVGAnimatedLength& height = + content->mLengthAttributes[SVGSVGElement::ATTR_HEIGHT]; + NS_ASSERTION(aCBSize.BSize(aWritingMode) != NS_UNCONSTRAINEDSIZE, + "root should not have auto-height containing block"); + if (height.IsPercentage()) { + MOZ_ASSERT(!intrinsicSize.height, + "GetIntrinsicSize should have reported no intrinsic height"); + float val = height.GetAnimValInSpecifiedUnits() / 100.0f; + intrinsicSize.height.emplace(std::max(val, 0.0f) * + cbSize.Height(aWritingMode)); + } + MOZ_ASSERT(intrinsicSize.height && intrinsicSize.width, + "We should have just handled the only situation where" + "we lack an intrinsic height or width."); + } + + return {ComputeSizeWithIntrinsicDimensions( + aRenderingContext, aWritingMode, intrinsicSize, GetAspectRatio(), + cbSize, aMargin, aBorderPadding, aSizeOverrides, aFlags), + AspectRatioUsage::None}; +} + +void SVGOuterSVGFrame::Reflow(nsPresContext* aPresContext, + ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) { + MarkInReflow(); + DO_GLOBAL_REFLOW_COUNT("SVGOuterSVGFrame"); + DISPLAY_REFLOW(aPresContext, this, aReflowInput, aDesiredSize, aStatus); + MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!"); + NS_FRAME_TRACE( + NS_FRAME_TRACE_CALLS, + ("enter SVGOuterSVGFrame::Reflow: availSize=%d,%d", + aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight())); + + MOZ_ASSERT(HasAnyStateBits(NS_FRAME_IN_REFLOW), "frame is not in reflow"); + + aDesiredSize.Width() = + aReflowInput.ComputedWidth() + + aReflowInput.ComputedPhysicalBorderPadding().LeftRight(); + aDesiredSize.Height() = + aReflowInput.ComputedHeight() + + aReflowInput.ComputedPhysicalBorderPadding().TopBottom(); + + NS_ASSERTION(!GetPrevInFlow(), "SVG can't currently be broken across pages."); + + SVGSVGElement* svgElem = static_cast<SVGSVGElement*>(GetContent()); + + auto* anonKid = static_cast<SVGOuterSVGAnonChildFrame*>( + PrincipalChildList().FirstChild()); + + if (HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + // Initialize + svgElem->UpdateHasChildrenOnlyTransform(); + } + + // If our SVG viewport has changed, update our content and notify. + // http://www.w3.org/TR/SVG11/coords.html#ViewportSpace + + svgFloatSize newViewportSize( + nsPresContext::AppUnitsToFloatCSSPixels(aReflowInput.ComputedWidth()), + nsPresContext::AppUnitsToFloatCSSPixels(aReflowInput.ComputedHeight())); + + svgFloatSize oldViewportSize = svgElem->GetViewportSize(); + + uint32_t changeBits = 0; + if (newViewportSize != oldViewportSize) { + // When our viewport size changes, we may need to update the overflow rects + // of our child frames. This is the case if: + // + // * We have a real/synthetic viewBox (a children-only transform), since + // the viewBox transform will change as the viewport dimensions change. + // + // * We do not have a real/synthetic viewBox, but the last time we + // reflowed (or the last time UpdateOverflow() was called) we did. + // + // We only handle the former case here, in which case we mark all our child + // frames as dirty so that we reflow them below and update their overflow + // rects. + // + // In the latter case, updating of overflow rects is handled for removal of + // real viewBox (the viewBox attribute) in AttributeChanged. Synthetic + // viewBox "removal" (e.g. a document references the same SVG via both an + // <svg:image> and then as a CSS background image (a synthetic viewBox is + // used when painting the former, but not when painting the latter)) is + // handled in SVGSVGElement::FlushImageTransformInvalidation. + // + if (svgElem->HasViewBoxOrSyntheticViewBox()) { + nsIFrame* anonChild = PrincipalChildList().FirstChild(); + anonChild->MarkSubtreeDirty(); + for (nsIFrame* child : anonChild->PrincipalChildList()) { + child->MarkSubtreeDirty(); + } + } + changeBits |= COORD_CONTEXT_CHANGED; + svgElem->SetViewportSize(newViewportSize); + } + if (mIsRootContent && !mIsInIframe) { + const auto oldZoom = mFullZoom; + mFullZoom = ComputeFullZoom(); + if (oldZoom != mFullZoom) { + changeBits |= FULL_ZOOM_CHANGED; + } + } + if (changeBits && !HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + NotifyViewportOrTransformChanged(changeBits); + } + + // Now that we've marked the necessary children as dirty, call + // ReflowSVG() or ReflowSVGNonDisplayText() on them, depending + // on whether we are non-display. + mCallingReflowSVG = true; + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + ReflowSVGNonDisplayText(this); + } else { + // Update the mRects and ink overflow rects of all our descendants, + // including our anonymous wrapper kid: + anonKid->ReflowSVG(); + MOZ_ASSERT(!anonKid->GetNextSibling(), + "We should have one anonymous child frame wrapping our real " + "children"); + } + mCallingReflowSVG = false; + + // Set our anonymous kid's offset from our border box: + anonKid->SetPosition(GetContentRectRelativeToSelf().TopLeft()); + + // Including our size in our overflow rects regardless of the value of + // 'background', 'border', etc. makes sure that we usually (when we clip to + // our content area) don't have to keep changing our overflow rects as our + // descendants move about (see perf comment below). Including our size in our + // scrollable overflow rect also makes sure that we scroll if we're too big + // for our viewport. + // + // <svg> never allows scrolling to anything outside its mRect (only panning), + // so we must always keep our scrollable overflow set to our size. + // + // With regards to ink overflow, we always clip root-<svg> (see our + // BuildDisplayList method) regardless of the value of the 'overflow' + // property since that is per-spec, even for the initial 'visible' value. For + // that reason there's no point in adding descendant ink overflow to our + // own when this frame is for a root-<svg>. That said, there's also a very + // good performance reason for us wanting to avoid doing so. If we did, then + // the frame's overflow would often change as descendants that are partially + // or fully outside its rect moved (think animation on/off screen), and that + // would cause us to do a full NS_FRAME_IS_DIRTY reflow and repaint of the + // entire document tree each such move (see bug 875175). + // + // So it's only non-root outer-<svg> that has the ink overflow of its + // descendants added to its own. (Note that the default user-agent style + // sheet makes 'hidden' the default value for :not(root(svg)), so usually + // FinishAndStoreOverflow will still clip this back to the frame's rect.) + // + // WARNING!! Keep UpdateBounds below in sync with whatever we do for our + // overflow rects here! (Again, see bug 875175.) + // + aDesiredSize.SetOverflowAreasToDesiredBounds(); + + // An outer SVG will be here as a nondisplay if it fails the conditional + // processing test. In that case, we don't maintain its overflow. + if (!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + if (!mIsRootContent) { + aDesiredSize.mOverflowAreas.InkOverflow().UnionRect( + aDesiredSize.mOverflowAreas.InkOverflow(), + anonKid->InkOverflowRect() + anonKid->GetPosition()); + } + FinishAndStoreOverflow(&aDesiredSize); + } + + NS_FRAME_TRACE(NS_FRAME_TRACE_CALLS, + ("exit SVGOuterSVGFrame::Reflow: size=%d,%d", + aDesiredSize.Width(), aDesiredSize.Height())); +} + +void SVGOuterSVGFrame::DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) { + SVGDisplayContainerFrame::DidReflow(aPresContext, aReflowInput); + + // Make sure elements styled by :hover get updated if script/animation moves + // them under or out from under the pointer: + PresShell()->SynthesizeMouseMove(false); +} + +/* virtual */ +void SVGOuterSVGFrame::UnionChildOverflow(OverflowAreas& aOverflowAreas) { + // See the comments in Reflow above. + + // WARNING!! Keep this in sync with Reflow above! + + if (!mIsRootContent) { + nsIFrame* anonKid = PrincipalChildList().FirstChild(); + aOverflowAreas.InkOverflow().UnionRect( + aOverflowAreas.InkOverflow(), + anonKid->InkOverflowRect() + anonKid->GetPosition()); + } +} + +//---------------------------------------------------------------------- +// container methods + +nsresult SVGOuterSVGFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + !HasAnyStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_NONDISPLAY)) { + if (aAttribute == nsGkAtoms::viewBox || + aAttribute == nsGkAtoms::preserveAspectRatio || + aAttribute == nsGkAtoms::transform) { + // make sure our cached transform matrix gets (lazily) updated + mCanvasTM = nullptr; + + SVGUtils::NotifyChildrenOfSVGChange( + PrincipalChildList().FirstChild(), + aAttribute == nsGkAtoms::viewBox + ? TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED + : TRANSFORM_CHANGED); + + if (aAttribute != nsGkAtoms::transform) { + static_cast<SVGSVGElement*>(GetContent()) + ->ChildrenOnlyTransformChanged(); + } + } + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::viewBox) { + // Don't call ChildrenOnlyTransformChanged() here, since we call it + // under Reflow if the width/height/viewBox actually changed. + + MaybeSendIntrinsicSizeAndRatioToEmbedder(); + + if (!mIsInObjectOrEmbed) { + // We are not embedded by reference, so our 'width' and 'height' + // attributes are not overridden (and viewBox may influence our + // intrinsic aspect ratio). We need to reflow. + PresShell()->FrameNeedsReflow( + this, IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); + } + } + } + + return NS_OK; +} + +bool SVGOuterSVGFrame::IsSVGTransformed(Matrix* aOwnTransform, + Matrix* aFromParentTransform) const { + // Our anonymous child's HasChildrenOnlyTransform() implementation makes sure + // our children-only transforms are applied to our children. We only care + // about transforms that transform our own frame here. + + bool foundTransform = false; + + SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent()); + SVGAnimatedTransformList* transformList = content->GetAnimatedTransformList(); + if ((transformList && transformList->HasTransform()) || + content->GetAnimateMotionTransform()) { + if (aOwnTransform) { + *aOwnTransform = gfx::ToMatrix( + content->PrependLocalTransformsTo(gfxMatrix(), eUserSpaceToParent)); + } + foundTransform = true; + } + + return foundTransform; +} + +//---------------------------------------------------------------------- +// painting + +void SVGOuterSVGFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + return; + } + + DisplayBorderBackgroundOutline(aBuilder, aLists); + + nsRect visibleRect = aBuilder->GetVisibleRect(); + nsRect dirtyRect = aBuilder->GetDirtyRect(); + + // Per-spec, we always clip root-<svg> even when 'overflow' has its initial + // value of 'visible'. See also the "ink overflow" comments in Reflow. + DisplayListClipState::AutoSaveRestore autoSR(aBuilder); + if (mIsRootContent || StyleDisplay()->IsScrollableOverflow()) { + autoSR.ClipContainingBlockDescendantsToContentBox(aBuilder, this); + visibleRect = visibleRect.Intersect(GetContentRectRelativeToSelf()); + dirtyRect = dirtyRect.Intersect(GetContentRectRelativeToSelf()); + } + + nsDisplayListBuilder::AutoBuildingDisplayList building( + aBuilder, this, visibleRect, dirtyRect); + + nsDisplayList* contentList = aLists.Content(); + nsDisplayListSet set(contentList, contentList, contentList, contentList, + contentList, contentList); + BuildDisplayListForNonBlockChildren(aBuilder, set); +} + +//---------------------------------------------------------------------- +// ISVGSVGFrame methods: + +void SVGOuterSVGFrame::NotifyViewportOrTransformChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags && !(aFlags & ~(COORD_CONTEXT_CHANGED | TRANSFORM_CHANGED | + FULL_ZOOM_CHANGED)), + "Unexpected aFlags value"); + + SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent()); + + if (aFlags & COORD_CONTEXT_CHANGED) { + if (content->HasViewBox()) { + // Percentage lengths on children resolve against the viewBox rect so we + // don't need to notify them of the viewport change, but the viewBox + // transform will have changed, so we need to notify them of that instead. + aFlags = TRANSFORM_CHANGED; + } else if (content->ShouldSynthesizeViewBox()) { + // In the case of a synthesized viewBox, the synthetic viewBox's rect + // changes as the viewport changes. As a result we need to maintain the + // COORD_CONTEXT_CHANGED flag. + aFlags |= TRANSFORM_CHANGED; + } else if (mCanvasTM && mCanvasTM->IsSingular()) { + // A width/height of zero will result in us having a singular mCanvasTM + // even when we don't have a viewBox. So we also want to recompute our + // mCanvasTM for this width/height change even though we don't have a + // viewBox. + aFlags |= TRANSFORM_CHANGED; + } + } + + bool haveNonFulLZoomTransformChange = (aFlags & TRANSFORM_CHANGED); + + if (aFlags & FULL_ZOOM_CHANGED) { + // Convert FULL_ZOOM_CHANGED to TRANSFORM_CHANGED: + aFlags = (aFlags & ~FULL_ZOOM_CHANGED) | TRANSFORM_CHANGED; + } + + if (aFlags & TRANSFORM_CHANGED) { + // Make sure our canvas transform matrix gets (lazily) recalculated: + mCanvasTM = nullptr; + + if (haveNonFulLZoomTransformChange && + !HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + uint32_t flags = HasAnyStateBits(NS_FRAME_IN_REFLOW) + ? SVGSVGElement::eDuringReflow + : 0; + content->ChildrenOnlyTransformChanged(flags); + } + } + + SVGUtils::NotifyChildrenOfSVGChange(PrincipalChildList().FirstChild(), + aFlags); +} + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods: + +void SVGOuterSVGFrame::PaintSVG(gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION( + PrincipalChildList().FirstChild()->IsSVGOuterSVGAnonChildFrame() && + !PrincipalChildList().FirstChild()->GetNextSibling(), + "We should have a single, anonymous, child"); + auto* anonKid = static_cast<SVGOuterSVGAnonChildFrame*>( + PrincipalChildList().FirstChild()); + anonKid->PaintSVG(aContext, aTransform, aImgParams); +} + +SVGBBox SVGOuterSVGFrame::GetBBoxContribution( + const gfx::Matrix& aToBBoxUserspace, uint32_t aFlags) { + NS_ASSERTION( + PrincipalChildList().FirstChild()->IsSVGOuterSVGAnonChildFrame() && + !PrincipalChildList().FirstChild()->GetNextSibling(), + "We should have a single, anonymous, child"); + // We must defer to our child so that we don't include our + // content->PrependLocalTransformsTo() transforms. + auto* anonKid = static_cast<SVGOuterSVGAnonChildFrame*>( + PrincipalChildList().FirstChild()); + return anonKid->GetBBoxContribution(aToBBoxUserspace, aFlags); +} + +//---------------------------------------------------------------------- +// SVGContainerFrame methods: + +gfxMatrix SVGOuterSVGFrame::GetCanvasTM() { + if (!mCanvasTM) { + SVGSVGElement* content = static_cast<SVGSVGElement*>(GetContent()); + + float devPxPerCSSPx = 1.0f / nsPresContext::AppUnitsToFloatCSSPixels( + PresContext()->AppUnitsPerDevPixel()); + + gfxMatrix tm = content->PrependLocalTransformsTo( + gfxMatrix::Scaling(devPxPerCSSPx, devPxPerCSSPx)); + mCanvasTM = MakeUnique<gfxMatrix>(tm); + } + return *mCanvasTM; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +bool SVGOuterSVGFrame::IsRootOfImage() { + if (!mContent->GetParent()) { + // Our content is the document element + Document* doc = mContent->GetUncomposedDoc(); + if (doc && doc->IsBeingUsedAsImage()) { + // Our document is being used as an image + return true; + } + } + + return false; +} + +bool SVGOuterSVGFrame::VerticalScrollbarNotNeeded() const { + const SVGAnimatedLength& height = + static_cast<SVGSVGElement*>(GetContent()) + ->mLengthAttributes[SVGSVGElement::ATTR_HEIGHT]; + return height.IsPercentage() && height.GetBaseValInSpecifiedUnits() <= 100; +} + +void SVGOuterSVGFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + nsIFrame* anonKid = PrincipalChildList().FirstChild(); + MOZ_ASSERT(anonKid->IsSVGOuterSVGAnonChildFrame()); + aResult.AppendElement(OwnedAnonBox(anonKid)); +} + +void SVGOuterSVGFrame::MaybeSendIntrinsicSizeAndRatioToEmbedder() { + MaybeSendIntrinsicSizeAndRatioToEmbedder(Some(GetIntrinsicSize()), + Some(GetAspectRatio())); +} + +void SVGOuterSVGFrame::MaybeSendIntrinsicSizeAndRatioToEmbedder( + Maybe<IntrinsicSize> aIntrinsicSize, Maybe<AspectRatio> aIntrinsicRatio) { + if (!mIsInObjectOrEmbed) { + return; + } + + nsCOMPtr<nsIDocShell> docShell = PresContext()->GetDocShell(); + if (!docShell) { + return; + } + + BrowsingContext* bc = docShell->GetBrowsingContext(); + MOZ_ASSERT(bc->IsContentSubframe()); + + if (bc->GetParent()->IsInProcess()) { + if (Element* embedder = bc->GetEmbedderElement()) { + if (nsCOMPtr<nsIObjectLoadingContent> olc = do_QueryInterface(embedder)) { + static_cast<nsObjectLoadingContent*>(olc.get()) + ->SubdocumentIntrinsicSizeOrRatioChanged(aIntrinsicSize, + aIntrinsicRatio); + } + return; + } + } + + if (BrowserChild* browserChild = BrowserChild::GetFrom(docShell)) { + Unused << browserChild->SendIntrinsicSizeOrRatioChanged(aIntrinsicSize, + aIntrinsicRatio); + } +} + +void SVGOuterSVGFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + SVGDisplayContainerFrame::DidSetComputedStyle(aOldComputedStyle); + + if (!aOldComputedStyle) { + return; + } + + if (aOldComputedStyle->StylePosition()->mAspectRatio != + StylePosition()->mAspectRatio) { + // Our aspect-ratio property value changed, and an embedding <object> or + // <embed> might care about that. + MaybeSendIntrinsicSizeAndRatioToEmbedder(); + } +} + +void SVGOuterSVGFrame::Destroy(DestroyContext& aContext) { + // This handles both the case when the root <svg> element is made display:none + // (and thus loses its intrinsic size and aspect ratio), and when the frame + // is navigated elsewhere & we need to reset parent <object>/<embed>'s + // recorded intrinsic size/ratio values. + MaybeSendIntrinsicSizeAndRatioToEmbedder(Nothing(), Nothing()); + + SVGDisplayContainerFrame::Destroy(aContext); +} + +} // namespace mozilla + +//---------------------------------------------------------------------- +// Implementation of SVGOuterSVGAnonChildFrame + +nsContainerFrame* NS_NewSVGOuterSVGAnonChildFrame( + mozilla::PresShell* aPresShell, mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGOuterSVGAnonChildFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGOuterSVGAnonChildFrame) + +#ifdef DEBUG +void SVGOuterSVGAnonChildFrame::Init(nsIContent* aContent, + nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + MOZ_ASSERT(aParent->IsSVGOuterSVGFrame(), "Unexpected parent"); + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif + +void SVGOuterSVGAnonChildFrame::BuildDisplayList( + nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { + // Wrap our contents into an nsDisplaySVGWrapper. + // We wrap this frame instead of the SVGOuterSVGFrame so that the wrapper + // doesn't contain the <svg> element's CSS styles, like backgrounds or + // borders. Creating the nsDisplaySVGWrapper here also means that it'll be + // inside the nsDisplayTransform for our viewbox transform. The + // nsDisplaySVGWrapper's reference frame is this frame, because this frame + // always returns true from IsSVGTransformed. + nsDisplayList newList(aBuilder); + nsDisplayListSet set(&newList, &newList, &newList, &newList, &newList, + &newList); + BuildDisplayListForNonBlockChildren(aBuilder, set); + aLists.Content()->AppendNewToTop<nsDisplaySVGWrapper>(aBuilder, this, + &newList); +} + +static Matrix ComputeOuterSVGAnonChildFrameTransform( + const SVGOuterSVGAnonChildFrame* aFrame) { + // Our elements 'transform' attribute is applied to our SVGOuterSVGFrame + // parent, and the element's children-only transforms are applied to us, the + // anonymous child frame. Since we are the child frame, we apply the + // children-only transforms as if they are our own transform. + SVGSVGElement* content = static_cast<SVGSVGElement*>(aFrame->GetContent()); + + if (!content->HasChildrenOnlyTransform()) { + return Matrix(); + } + + // Outer-<svg> doesn't use x/y, so we can pass eChildToUserSpace here. + gfxMatrix ownMatrix = + content->PrependLocalTransformsTo(gfxMatrix(), eChildToUserSpace); + + if (ownMatrix.HasNonTranslation()) { + // viewBox, currentScale and currentTranslate should only produce a + // rectilinear transform. + MOZ_ASSERT(ownMatrix.IsRectilinear(), + "Non-rectilinear transform will break the following logic"); + + // The nsDisplayTransform code will apply this transform to our frame, + // including to our frame position. We don't want our frame position to + // be scaled though, so we need to correct for that in the transform. + // XXX Yeah, this is a bit hacky. + CSSPoint pos = CSSPixel::FromAppUnits(aFrame->GetPosition()); + CSSPoint scaledPos = CSSPoint(ownMatrix._11 * pos.x, ownMatrix._22 * pos.y); + CSSPoint deltaPos = scaledPos - pos; + ownMatrix *= gfxMatrix::Translation(-deltaPos.x, -deltaPos.y); + } + + return gfx::ToMatrix(ownMatrix); +} + +// We want this frame to be a reference frame. An easy way to achieve that is +// to always return true from this method, even for identity transforms. +// This frame being a reference frame ensures that the offset between this +// <svg> element and the parent reference frame is completely absorbed by the +// nsDisplayTransform that's created for this frame, and that this offset does +// not affect our descendants' transforms. Consequently, if the <svg> element +// moves, e.g. during scrolling, the transform matrices of our contents are +// unaffected. This simplifies invalidation. +bool SVGOuterSVGAnonChildFrame::IsSVGTransformed( + Matrix* aOwnTransform, Matrix* aFromParentTransform) const { + if (aOwnTransform) { + *aOwnTransform = ComputeOuterSVGAnonChildFrameTransform(this); + } + + return true; +} + +} // namespace mozilla diff --git a/layout/svg/SVGOuterSVGFrame.h b/layout/svg/SVGOuterSVGFrame.h new file mode 100644 index 0000000000..82c2079348 --- /dev/null +++ b/layout/svg/SVGOuterSVGFrame.h @@ -0,0 +1,223 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGOUTERSVGFRAME_H_ +#define LAYOUT_SVG_SVGOUTERSVGFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/ISVGSVGFrame.h" +#include "mozilla/SVGContainerFrame.h" + +class gfxContext; + +namespace mozilla { +class AutoSVGViewHandler; +class SVGFragmentIdentifier; +class PresShell; +} // namespace mozilla + +nsContainerFrame* NS_NewSVGOuterSVGFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); +nsContainerFrame* NS_NewSVGOuterSVGAnonChildFrame( + mozilla::PresShell* aPresShell, mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +//////////////////////////////////////////////////////////////////////// +// SVGOuterSVGFrame class + +class SVGOuterSVGFrame final : public SVGDisplayContainerFrame, + public ISVGSVGFrame { + using imgDrawingParams = image::imgDrawingParams; + + friend nsContainerFrame* ::NS_NewSVGOuterSVGFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + friend class AutoSVGViewHandler; + friend class SVGFragmentIdentifier; + + protected: + explicit SVGOuterSVGFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGOuterSVGFrame) + + // nsIFrame: + nscoord GetMinISize(gfxContext* aRenderingContext) override; + nscoord GetPrefISize(gfxContext* aRenderingContext) override; + + IntrinsicSize GetIntrinsicSize() override; + AspectRatio GetIntrinsicRatio() const override; + + SizeComputationResult ComputeSize( + gfxContext* aRenderingContext, WritingMode aWritingMode, + const LogicalSize& aCBSize, nscoord aAvailableISize, + const LogicalSize& aMargin, const LogicalSize& aBorderPadding, + const mozilla::StyleSizeOverrides& aSizeOverrides, + ComputeSizeFlags aFlags) override; + + void Reflow(nsPresContext* aPresContext, ReflowOutput& aDesiredSize, + const ReflowInput& aReflowInput, + nsReflowStatus& aStatus) override; + + void DidReflow(nsPresContext* aPresContext, + const ReflowInput* aReflowInput) override; + + void UnionChildOverflow(mozilla::OverflowAreas& aOverflowAreas) override; + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + return SVGDisplayContainerFrame::IsFrameOfType( + aFlags & + ~(eSupportsContainLayoutAndPaint | eReplaced | eReplacedSizing)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGOuterSVG"_ns, aResult); + } +#endif + + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + + void Destroy(DestroyContext&) override; + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + nsContainerFrame* GetContentInsertionFrame() override { + // Any children must be added to our single anonymous inner frame kid. + MOZ_ASSERT( + PrincipalChildList().FirstChild() && + PrincipalChildList().FirstChild()->IsSVGOuterSVGAnonChildFrame(), + "Where is our anonymous child?"); + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + bool IsSVGTransformed(Matrix* aOwnTransform, + Matrix* aFromParentTransform) const override; + + // Return our anonymous box child. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + + // ISVGSVGFrame interface: + void NotifyViewportOrTransformChanged(uint32_t aFlags) override; + + // ISVGDisplayableFrame methods: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override; + + bool HasChildrenOnlyTransform(Matrix* aTransform) const override { + // Our anonymous wrapper child must claim our children-only transforms as + // its own so that our real children (the frames it wraps) are transformed + // by them, and we must pretend we don't have any children-only transforms + // so that our anonymous child is _not_ transformed by them. + return false; + } + + /** + * Return true only if the height is unspecified (defaulting to 100%) or else + * the height is explicitly set to a percentage value no greater than 100%. + */ + bool VerticalScrollbarNotNeeded() const; + + bool IsCallingReflowSVG() const { return mCallingReflowSVG; } + + protected: + /* Returns true if our content is the document element and our document is + * being used as an image. + */ + bool IsRootOfImage(); + float ComputeFullZoom() const; + + void MaybeSendIntrinsicSizeAndRatioToEmbedder(); + void MaybeSendIntrinsicSizeAndRatioToEmbedder(Maybe<IntrinsicSize>, + Maybe<AspectRatio>); + + float mFullZoom = 1.0f; + + bool mCallingReflowSVG = false; + bool mIsRootContent = false; + bool mIsInObjectOrEmbed = false; + bool mIsInIframe = false; +}; + +//////////////////////////////////////////////////////////////////////// +// SVGOuterSVGAnonChildFrame class + +/** + * SVGOuterSVGFrames have a single direct child that is an instance of this + * class, and which is used to wrap their real child frames. Such anonymous + * wrapper frames created from this class exist because SVG frames need their + * GetPosition() offset to be their offset relative to "user space" (in app + * units) so that they can play nicely with nsDisplayTransform. This is fine + * for all SVG frames except for direct children of an SVGOuterSVGFrame, + * since an SVGOuterSVGFrame can have CSS border and padding (unlike other + * SVG frames). The direct children can't include the offsets due to any such + * border/padding in their mRects since that would break nsDisplayTransform, + * but not including these offsets would break other parts of the Mozilla code + * that assume a frame's mRect contains its border-box-to-parent-border-box + * offset, in particular nsIFrame::GetOffsetTo and the functions that depend on + * it. Wrapping an SVGOuterSVGFrame's children in an instance of this class + * with its GetPosition() set to its SVGOuterSVGFrame's border/padding offset + * keeps both nsDisplayTransform and nsIFrame::GetOffsetTo happy. + * + * The reason that this class inherit from SVGDisplayContainerFrame rather + * than simply from nsContainerFrame is so that we can avoid having special + * handling for these inner wrappers in multiple parts of the SVG code. For + * example, the implementations of IsSVGTransformed and GetCanvasTM assume + * SVGContainerFrame instances all the way up to the SVGOuterSVGFrame. + */ +class SVGOuterSVGAnonChildFrame final : public SVGDisplayContainerFrame { + friend nsContainerFrame* ::NS_NewSVGOuterSVGAnonChildFrame( + mozilla::PresShell* aPresShell, ComputedStyle* aStyle); + + explicit SVGOuterSVGAnonChildFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGDisplayContainerFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGOuterSVGAnonChildFrame) + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGOuterSVGAnonChild"_ns, aResult); + } +#endif + + bool IsSVGTransformed(Matrix* aOwnTransform, + Matrix* aFromParentTransform) const override; + + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override { + // GetCanvasTM returns the transform from an SVG frame to the frame's + // SVGOuterSVGFrame's content box, so we do not include any x/y offset + // set on us for any CSS border or padding on our SVGOuterSVGFrame. + return static_cast<SVGOuterSVGFrame*>(GetParent())->GetCanvasTM(); + } +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGOUTERSVGFRAME_H_ diff --git a/layout/svg/SVGPaintServerFrame.cpp b/layout/svg/SVGPaintServerFrame.cpp new file mode 100644 index 0000000000..6a2b337871 --- /dev/null +++ b/layout/svg/SVGPaintServerFrame.cpp @@ -0,0 +1,16 @@ +/* -*- 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 "SVGPaintServerFrame.h" + +namespace mozilla { + +NS_QUERYFRAME_HEAD(SVGPaintServerFrame) + NS_QUERYFRAME_ENTRY(SVGPaintServerFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGContainerFrame) + +} // namespace mozilla diff --git a/layout/svg/SVGPaintServerFrame.h b/layout/svg/SVGPaintServerFrame.h new file mode 100644 index 0000000000..cebed16441 --- /dev/null +++ b/layout/svg/SVGPaintServerFrame.h @@ -0,0 +1,82 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGPAINTSERVERFRAME_H_ +#define LAYOUT_SVG_SVGPAINTSERVERFRAME_H_ + +#include "gfxRect.h" +#include "mozilla/Attributes.h" +#include "mozilla/SVGContainerFrame.h" +#include "nsCOMPtr.h" +#include "nsIFrame.h" +#include "nsIFrame.h" +#include "nsQueryFrame.h" + +class gfxContext; +class gfxPattern; + +namespace mozilla { +namespace gfx { +class DrawTarget; +} // namespace gfx + +/** + * RAII class used to temporarily set and remove the + * NS_FRAME_DRAWING_AS_PAINTSERVER frame state bit while a frame is being + * drawn as a paint server. + */ +class MOZ_RAII AutoSetRestorePaintServerState { + public: + explicit AutoSetRestorePaintServerState(nsIFrame* aFrame) : mFrame(aFrame) { + mFrame->AddStateBits(NS_FRAME_DRAWING_AS_PAINTSERVER); + } + ~AutoSetRestorePaintServerState() { + mFrame->RemoveStateBits(NS_FRAME_DRAWING_AS_PAINTSERVER); + } + + private: + nsIFrame* mFrame; +}; + +class SVGPaintServerFrame : public SVGContainerFrame { + protected: + using DrawTarget = gfx::DrawTarget; + + SVGPaintServerFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + ClassID aID) + : SVGContainerFrame(aStyle, aPresContext, aID) { + AddStateBits(NS_FRAME_IS_NONDISPLAY); + } + + public: + using imgDrawingParams = image::imgDrawingParams; + + NS_DECL_ABSTRACT_FRAME(SVGPaintServerFrame) + NS_DECL_QUERYFRAME + NS_DECL_QUERYFRAME_TARGET(SVGPaintServerFrame) + + /** + * Constructs a gfxPattern of the paint server rendering. + * + * @param aContextMatrix The transform matrix that is currently applied to + * the gfxContext that is being drawn to. This is needed by SVG patterns so + * that surfaces of the correct size can be created. (SVG gradients are + * vector based, so it's not used there.) + */ + virtual already_AddRefed<gfxPattern> GetPaintServerPattern( + nsIFrame* aSource, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + float aOpacity, imgDrawingParams& aImgParams, + const gfxRect* aOverrideBounds = nullptr) = 0; + + // nsIFrame methods: + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGPAINTSERVERFRAME_H_ diff --git a/layout/svg/SVGPatternFrame.cpp b/layout/svg/SVGPatternFrame.cpp new file mode 100644 index 0000000000..ea93daef41 --- /dev/null +++ b/layout/svg/SVGPatternFrame.cpp @@ -0,0 +1,713 @@ +/* -*- 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 "SVGPatternFrame.h" + +// Keep others in (case-insensitive) order: +#include "AutoReferenceChainGuard.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxMatrix.h" +#include "gfxPattern.h" +#include "gfxPlatform.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/SVGGeometryFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGPatternElement.h" +#include "mozilla/dom/SVGUnitTypesBinding.h" +#include "mozilla/gfx/2D.h" +#include "nsGkAtoms.h" +#include "nsIFrameInlines.h" +#include "SVGAnimatedTransformList.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGUnitTypes_Binding; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Implementation + +SVGPatternFrame::SVGPatternFrame(ComputedStyle* aStyle, + nsPresContext* aPresContext) + : SVGPaintServerFrame(aStyle, aPresContext, kClassID), + mSource(nullptr), + mLoopFlag(false), + mNoHRefURI(false) {} + +NS_IMPL_FRAMEARENA_HELPERS(SVGPatternFrame) + +NS_QUERYFRAME_HEAD(SVGPatternFrame) + NS_QUERYFRAME_ENTRY(SVGPatternFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGPaintServerFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods: + +nsresult SVGPatternFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::patternUnits || + aAttribute == nsGkAtoms::patternContentUnits || + aAttribute == nsGkAtoms::patternTransform || + aAttribute == nsGkAtoms::x || aAttribute == nsGkAtoms::y || + aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::preserveAspectRatio || + aAttribute == nsGkAtoms::viewBox)) { + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + if ((aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None) && + aAttribute == nsGkAtoms::href) { + // Blow away our reference, if any + SVGObserverUtils::RemoveTemplateObserver(this); + mNoHRefURI = false; + // And update whoever references us + SVGObserverUtils::InvalidateDirectRenderingObservers(this); + } + + return SVGPaintServerFrame::AttributeChanged(aNameSpaceID, aAttribute, + aModType); +} + +#ifdef DEBUG +void SVGPatternFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::pattern), + "Content is not an SVG pattern"); + + SVGPaintServerFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +//---------------------------------------------------------------------- +// SVGContainerFrame methods: + +// If our GetCanvasTM is getting called, we +// need to return *our current* transformation +// matrix, which depends on our units parameters +// and X, Y, Width, and Height +gfxMatrix SVGPatternFrame::GetCanvasTM() { + if (mCTM) { + return *mCTM; + } + + // Do we know our rendering parent? + if (mSource) { + // Yes, use it! + return mSource->GetCanvasTM(); + } + + // We get here when geometry in the <pattern> container is updated + return gfxMatrix(); +} + +// ------------------------------------------------------------------------- +// Helper functions +// ------------------------------------------------------------------------- + +/** Calculate the maximum expansion of a matrix */ +static float MaxExpansion(const Matrix& aMatrix) { + // maximum expansion derivation from + // http://lists.cairographics.org/archives/cairo/2004-October/001980.html + // and also implemented in cairo_matrix_transformed_circle_major_axis + double a = aMatrix._11; + double b = aMatrix._12; + double c = aMatrix._21; + double d = aMatrix._22; + double f = (a * a + b * b + c * c + d * d) / 2; + double g = (a * a + b * b - c * c - d * d) / 2; + double h = a * c + b * d; + return sqrt(f + sqrt(g * g + h * h)); +} + +// The SVG specification says that the 'patternContentUnits' attribute "has no +// effect if attribute ‘viewBox’ is specified". We still need to include a bbox +// scale if the viewBox is specified and _patternUnits_ is set to or defaults to +// objectBoundingBox though, since in that case the viewBox is relative to the +// bbox +static bool IncludeBBoxScale(const SVGAnimatedViewBox& aViewBox, + uint32_t aPatternContentUnits, + uint32_t aPatternUnits) { + return (!aViewBox.IsExplicitlySet() && + aPatternContentUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) || + (aViewBox.IsExplicitlySet() && + aPatternUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX); +} + +// Given the matrix for the pattern element's own transform, this returns a +// combined matrix including the transforms applicable to its target. +static Matrix GetPatternMatrix(nsIFrame* aSource, + const StyleSVGPaint nsStyleSVG::*aFillOrStroke, + uint16_t aPatternUnits, + const gfxMatrix& patternTransform, + const gfxRect& bbox, const gfxRect& callerBBox, + const Matrix& callerCTM) { + // We really want the pattern matrix to handle translations + gfxFloat minx = bbox.X(); + gfxFloat miny = bbox.Y(); + + if (aPatternUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + minx += callerBBox.X(); + miny += callerBBox.Y(); + } + + double scale = 1.0 / MaxExpansion(callerCTM); + auto patternMatrix = patternTransform; + patternMatrix.PreScale(scale, scale); + patternMatrix.PreTranslate(minx, miny); + + // revert the vector effect transform so that the pattern appears unchanged + if (aFillOrStroke == &nsStyleSVG::mStroke) { + gfxMatrix userToOuterSVG; + if (SVGUtils::GetNonScalingStrokeTransform(aSource, &userToOuterSVG)) { + patternMatrix *= userToOuterSVG; + } + } + + return ToMatrix(patternMatrix); +} + +static nsresult GetTargetGeometry(gfxRect* aBBox, + const SVGAnimatedViewBox& aViewBox, + uint16_t aPatternContentUnits, + uint16_t aPatternUnits, nsIFrame* aTarget, + const Matrix& aContextMatrix, + const gfxRect* aOverrideBounds) { + *aBBox = + aOverrideBounds + ? *aOverrideBounds + : SVGUtils::GetBBox(aTarget, SVGUtils::eUseFrameBoundsForOuterSVG | + SVGUtils::eBBoxIncludeFillGeometry); + + // Sanity check + if (IncludeBBoxScale(aViewBox, aPatternContentUnits, aPatternUnits) && + (aBBox->Width() <= 0 || aBBox->Height() <= 0)) { + return NS_ERROR_FAILURE; + } + + // OK, now fix up the bounding box to reflect user coordinates + // We handle device unit scaling in pattern matrix + float scale = MaxExpansion(aContextMatrix); + if (scale <= 0) { + return NS_ERROR_FAILURE; + } + aBBox->Scale(scale); + return NS_OK; +} + +void SVGPatternFrame::PaintChildren(DrawTarget* aDrawTarget, + SVGPatternFrame* aPatternWithChildren, + nsIFrame* aSource, float aGraphicOpacity, + imgDrawingParams& aImgParams) { + nsIFrame* firstKid = aPatternWithChildren->mFrames.FirstChild(); + + gfxContext ctx(aDrawTarget); + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&ctx); + + if (aGraphicOpacity != 1.0f) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + aGraphicOpacity); + } + + // OK, now render -- note that we use "firstKid", which + // we got at the beginning because it takes care of the + // referenced pattern situation for us + + if (aSource->IsSVGGeometryFrame()) { + // Set the geometrical parent of the pattern we are rendering + aPatternWithChildren->mSource = static_cast<SVGGeometryFrame*>(aSource); + } + + // Delay checking NS_FRAME_DRAWING_AS_PAINTSERVER bit until here so we can + // give back a clear surface if there's a loop + if (!aPatternWithChildren->HasAnyStateBits(NS_FRAME_DRAWING_AS_PAINTSERVER)) { + AutoSetRestorePaintServerState paintServer(aPatternWithChildren); + for (nsIFrame* kid = firstKid; kid; kid = kid->GetNextSibling()) { + gfxMatrix tm = *(aPatternWithChildren->mCTM); + + // The CTM of each frame referencing us can be different + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + SVGFrame->NotifySVGChanged(ISVGDisplayableFrame::TRANSFORM_CHANGED); + tm = SVGUtils::GetTransformMatrixInUserSpace(kid) * tm; + } + + SVGUtils::PaintFrameWithEffects(kid, ctx, tm, aImgParams); + } + } + + aPatternWithChildren->mSource = nullptr; +} + +already_AddRefed<SourceSurface> SVGPatternFrame::PaintPattern( + const DrawTarget* aDrawTarget, Matrix* patternMatrix, + const Matrix& aContextMatrix, nsIFrame* aSource, + StyleSVGPaint nsStyleSVG::*aFillOrStroke, float aGraphicOpacity, + const gfxRect* aOverrideBounds, imgDrawingParams& aImgParams) { + /* + * General approach: + * Set the content geometry stuff + * Calculate our bbox (using x,y,width,height & patternUnits & + * patternTransform) + * Create the surface + * Calculate the content transformation matrix + * Get our children (we may need to get them from another Pattern) + * Call SVGPaint on all of our children + * Return + */ + + SVGPatternFrame* patternWithChildren = GetPatternWithChildren(); + if (!patternWithChildren) { + // Either no kids or a bad reference + return nullptr; + } + + const SVGAnimatedViewBox& viewBox = GetViewBox(); + + uint16_t patternContentUnits = + GetEnumValue(SVGPatternElement::PATTERNCONTENTUNITS); + uint16_t patternUnits = GetEnumValue(SVGPatternElement::PATTERNUNITS); + + /* + * Get the content geometry information. This is a little tricky -- + * our parent is probably a <defs>, but we are rendering in the context + * of some geometry source. Our content geometry information needs to + * come from our rendering parent as opposed to our content parent. We + * get that information from aSource, which is passed to us from the + * backend renderer. + * + * There are three "geometries" that we need: + * 1) The bounding box for the pattern. We use this to get the + * width and height for the surface, and as the return to + * GetBBox. + * 2) The transformation matrix for the pattern. This is not *quite* + * the same as the canvas transformation matrix that we will + * provide to our rendering children since we "fudge" it a little + * to get the renderer to handle the translations correctly for us. + * 3) The CTM that we return to our children who make up the pattern. + */ + + // Get all of the information we need from our "caller" -- i.e. + // the geometry that is being rendered with a pattern + gfxRect callerBBox; + if (NS_FAILED(GetTargetGeometry(&callerBBox, viewBox, patternContentUnits, + patternUnits, aSource, aContextMatrix, + aOverrideBounds))) { + return nullptr; + } + + // Construct the CTM that we will provide to our children when we + // render them into the tile. + gfxMatrix ctm = ConstructCTM(viewBox, patternContentUnits, patternUnits, + callerBBox, aContextMatrix, aSource); + if (ctm.IsSingular()) { + return nullptr; + } + + if (patternWithChildren->mCTM) { + *patternWithChildren->mCTM = ctm; + } else { + patternWithChildren->mCTM = MakeUnique<gfxMatrix>(ctm); + } + + // Get the bounding box of the pattern. This will be used to determine + // the size of the surface, and will also be used to define the bounding + // box for the pattern tile. + gfxRect bbox = + GetPatternRect(patternUnits, callerBBox, aContextMatrix, aSource); + if (bbox.Width() <= 0.0 || bbox.Height() <= 0.0) { + return nullptr; + } + + // Get the pattern transform + auto patternTransform = GetPatternTransform(); + + // Get the transformation matrix that we will hand to the renderer's pattern + // routine. + *patternMatrix = + GetPatternMatrix(aSource, aFillOrStroke, patternUnits, patternTransform, + bbox, callerBBox, aContextMatrix); + if (patternMatrix->IsSingular()) { + return nullptr; + } + + // Now that we have all of the necessary geometries, we can + // create our surface. + gfxSize scaledSize = bbox.Size() * MaxExpansion(ToMatrix(patternTransform)); + + bool resultOverflows; + IntSize surfaceSize = + SVGUtils::ConvertToSurfaceSize(scaledSize, &resultOverflows); + + // 0 disables rendering, < 0 is an error + if (surfaceSize.width <= 0 || surfaceSize.height <= 0) { + return nullptr; + } + + gfxFloat patternWidth = bbox.Width(); + gfxFloat patternHeight = bbox.Height(); + + if (resultOverflows || patternWidth != surfaceSize.width || + patternHeight != surfaceSize.height) { + // scale drawing to pattern surface size + patternWithChildren->mCTM->PostScale(surfaceSize.width / patternWidth, + surfaceSize.height / patternHeight); + + // and rescale pattern to compensate + patternMatrix->PreScale(patternWidth / surfaceSize.width, + patternHeight / surfaceSize.height); + } + + RefPtr<DrawTarget> dt = aDrawTarget->CreateSimilarDrawTargetWithBacking( + surfaceSize, SurfaceFormat::B8G8R8A8); + if (!dt || !dt->IsValid()) { + return nullptr; + } + dt->ClearRect(Rect(0, 0, surfaceSize.width, surfaceSize.height)); + + PaintChildren(dt, patternWithChildren, aSource, aGraphicOpacity, aImgParams); + + // caller now owns the surface + return dt->GetBackingSurface(); +} + +/* Will probably need something like this... */ +// How do we handle the insertion of a new frame? +// We really don't want to rerender this every time, +// do we? +SVGPatternFrame* SVGPatternFrame::GetPatternWithChildren() { + // Do we have any children ourselves? + if (!mFrames.IsEmpty()) { + return this; + } + + // No, see if we chain to someone who does + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return nullptr; + } + + SVGPatternFrame* next = GetReferencedPattern(); + if (!next) { + return nullptr; + } + + return next->GetPatternWithChildren(); +} + +uint16_t SVGPatternFrame::GetEnumValue(uint32_t aIndex, nsIContent* aDefault) { + SVGAnimatedEnumeration& thisEnum = + static_cast<SVGPatternElement*>(GetContent())->mEnumAttributes[aIndex]; + + if (thisEnum.IsExplicitlySet()) { + return thisEnum.GetAnimValue(); + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGPatternElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); + } + + SVGPatternFrame* next = GetReferencedPattern(); + return next ? next->GetEnumValue(aIndex, aDefault) + : static_cast<SVGPatternElement*>(aDefault) + ->mEnumAttributes[aIndex] + .GetAnimValue(); +} + +SVGAnimatedTransformList* SVGPatternFrame::GetPatternTransformList( + nsIContent* aDefault) { + SVGAnimatedTransformList* thisTransformList = + static_cast<SVGPatternElement*>(GetContent())->GetAnimatedTransformList(); + + if (thisTransformList && thisTransformList->IsExplicitlySet()) + return thisTransformList; + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGPatternElement*>(aDefault)->mPatternTransform.get(); + } + + SVGPatternFrame* next = GetReferencedPattern(); + return next ? next->GetPatternTransformList(aDefault) + : static_cast<SVGPatternElement*>(aDefault) + ->mPatternTransform.get(); +} + +gfxMatrix SVGPatternFrame::GetPatternTransform() { + SVGAnimatedTransformList* animTransformList = + GetPatternTransformList(GetContent()); + if (!animTransformList) { + return gfxMatrix(); + } + + return animTransformList->GetAnimValue().GetConsolidationMatrix(); +} + +const SVGAnimatedViewBox& SVGPatternFrame::GetViewBox(nsIContent* aDefault) { + const SVGAnimatedViewBox& thisViewBox = + static_cast<SVGPatternElement*>(GetContent())->mViewBox; + + if (thisViewBox.IsExplicitlySet()) { + return thisViewBox; + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGPatternElement*>(aDefault)->mViewBox; + } + + SVGPatternFrame* next = GetReferencedPattern(); + return next ? next->GetViewBox(aDefault) + : static_cast<SVGPatternElement*>(aDefault)->mViewBox; +} + +const SVGAnimatedPreserveAspectRatio& SVGPatternFrame::GetPreserveAspectRatio( + nsIContent* aDefault) { + const SVGAnimatedPreserveAspectRatio& thisPar = + static_cast<SVGPatternElement*>(GetContent())->mPreserveAspectRatio; + + if (thisPar.IsExplicitlySet()) { + return thisPar; + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return static_cast<SVGPatternElement*>(aDefault)->mPreserveAspectRatio; + } + + SVGPatternFrame* next = GetReferencedPattern(); + return next ? next->GetPreserveAspectRatio(aDefault) + : static_cast<SVGPatternElement*>(aDefault)->mPreserveAspectRatio; +} + +const SVGAnimatedLength* SVGPatternFrame::GetLengthValue(uint32_t aIndex, + nsIContent* aDefault) { + const SVGAnimatedLength* thisLength = + &static_cast<SVGPatternElement*>(GetContent())->mLengthAttributes[aIndex]; + + if (thisLength->IsExplicitlySet()) { + return thisLength; + } + + // Before we recurse, make sure we'll break reference loops and over long + // reference chains: + static int16_t sRefChainLengthCounter = AutoReferenceChainGuard::noChain; + AutoReferenceChainGuard refChainGuard(this, &mLoopFlag, + &sRefChainLengthCounter); + if (MOZ_UNLIKELY(!refChainGuard.Reference())) { + // Break reference chain + return &static_cast<SVGPatternElement*>(aDefault) + ->mLengthAttributes[aIndex]; + } + + SVGPatternFrame* next = GetReferencedPattern(); + return next ? next->GetLengthValue(aIndex, aDefault) + : &static_cast<SVGPatternElement*>(aDefault) + ->mLengthAttributes[aIndex]; +} + +// Private (helper) methods + +SVGPatternFrame* SVGPatternFrame::GetReferencedPattern() { + if (mNoHRefURI) { + return nullptr; + } + + auto GetHref = [this](nsAString& aHref) { + SVGPatternElement* pattern = + static_cast<SVGPatternElement*>(this->GetContent()); + if (pattern->mStringAttributes[SVGPatternElement::HREF].IsExplicitlySet()) { + pattern->mStringAttributes[SVGPatternElement::HREF].GetAnimValue(aHref, + pattern); + } else { + pattern->mStringAttributes[SVGPatternElement::XLINK_HREF].GetAnimValue( + aHref, pattern); + } + this->mNoHRefURI = aHref.IsEmpty(); + }; + + // We don't call SVGObserverUtils::RemoveTemplateObserver and set + // `mNoHRefURI = false` on failure since we want to be invalidated if the ID + // specified by our href starts resolving to a different/valid element. + + return do_QueryFrame(SVGObserverUtils::GetAndObserveTemplate(this, GetHref)); +} + +gfxRect SVGPatternFrame::GetPatternRect(uint16_t aPatternUnits, + const gfxRect& aTargetBBox, + const Matrix& aTargetCTM, + nsIFrame* aTarget) { + // We need to initialize our box + float x, y, width, height; + + // Get the pattern x,y,width, and height + const SVGAnimatedLength *tmpX, *tmpY, *tmpHeight, *tmpWidth; + tmpX = GetLengthValue(SVGPatternElement::ATTR_X); + tmpY = GetLengthValue(SVGPatternElement::ATTR_Y); + tmpHeight = GetLengthValue(SVGPatternElement::ATTR_HEIGHT); + tmpWidth = GetLengthValue(SVGPatternElement::ATTR_WIDTH); + + if (aPatternUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + x = SVGUtils::ObjectSpace(aTargetBBox, tmpX); + y = SVGUtils::ObjectSpace(aTargetBBox, tmpY); + width = SVGUtils::ObjectSpace(aTargetBBox, tmpWidth); + height = SVGUtils::ObjectSpace(aTargetBBox, tmpHeight); + } else { + if (aTarget->IsTextFrame()) { + aTarget = aTarget->GetParent(); + } + float scale = MaxExpansion(aTargetCTM); + x = SVGUtils::UserSpace(aTarget, tmpX) * scale; + y = SVGUtils::UserSpace(aTarget, tmpY) * scale; + width = SVGUtils::UserSpace(aTarget, tmpWidth) * scale; + height = SVGUtils::UserSpace(aTarget, tmpHeight) * scale; + } + + return gfxRect(x, y, width, height); +} + +gfxMatrix SVGPatternFrame::ConstructCTM(const SVGAnimatedViewBox& aViewBox, + uint16_t aPatternContentUnits, + uint16_t aPatternUnits, + const gfxRect& callerBBox, + const Matrix& callerCTM, + nsIFrame* aTarget) { + if (aTarget->IsTextFrame()) { + aTarget = aTarget->GetParent(); + } + nsIContent* targetContent = aTarget->GetContent(); + SVGViewportElement* ctx = nullptr; + gfxFloat scaleX, scaleY; + + // The objectBoundingBox conversion must be handled in the CTM: + if (IncludeBBoxScale(aViewBox, aPatternContentUnits, aPatternUnits)) { + scaleX = callerBBox.Width(); + scaleY = callerBBox.Height(); + } else { + if (targetContent->IsSVGElement()) { + ctx = static_cast<SVGElement*>(targetContent)->GetCtx(); + } + scaleX = scaleY = MaxExpansion(callerCTM); + } + + if (!aViewBox.IsExplicitlySet()) { + return gfxMatrix(scaleX, 0.0, 0.0, scaleY, 0.0, 0.0); + } + const SVGViewBox& viewBox = aViewBox.GetAnimValue(); + + if (viewBox.height <= 0.0f || viewBox.width <= 0.0f) { + return gfxMatrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); // singular + } + + float viewportWidth, viewportHeight; + if (targetContent->IsSVGElement()) { + // If we're dealing with an SVG target only retrieve the context once. + // Calling the nsIFrame* variant of GetAnimValue would look it up on + // every call. + viewportWidth = + GetLengthValue(SVGPatternElement::ATTR_WIDTH)->GetAnimValue(ctx); + viewportHeight = + GetLengthValue(SVGPatternElement::ATTR_HEIGHT)->GetAnimValue(ctx); + } else { + // No SVG target, call the nsIFrame* variant of GetAnimValue. + viewportWidth = + GetLengthValue(SVGPatternElement::ATTR_WIDTH)->GetAnimValue(aTarget); + viewportHeight = + GetLengthValue(SVGPatternElement::ATTR_HEIGHT)->GetAnimValue(aTarget); + } + + if (viewportWidth <= 0.0f || viewportHeight <= 0.0f) { + return gfxMatrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); // singular + } + + Matrix tm = SVGContentUtils::GetViewBoxTransform( + viewportWidth * scaleX, viewportHeight * scaleY, viewBox.x, viewBox.y, + viewBox.width, viewBox.height, GetPreserveAspectRatio()); + + return ThebesMatrix(tm); +} + +//---------------------------------------------------------------------- +// SVGPaintServerFrame methods: +already_AddRefed<gfxPattern> SVGPatternFrame::GetPaintServerPattern( + nsIFrame* aSource, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + float aGraphicOpacity, imgDrawingParams& aImgParams, + const gfxRect* aOverrideBounds) { + if (aGraphicOpacity == 0.0f) { + return do_AddRef(new gfxPattern(DeviceColor())); + } + + // Paint it! + Matrix pMatrix; + RefPtr<SourceSurface> surface = + PaintPattern(aDrawTarget, &pMatrix, ToMatrix(aContextMatrix), aSource, + aFillOrStroke, aGraphicOpacity, aOverrideBounds, aImgParams); + + if (!surface) { + return nullptr; + } + + RefPtr<gfxPattern> pattern = new gfxPattern(surface, pMatrix); + + if (!pattern) { + return nullptr; + } + + pattern->SetExtend(ExtendMode::REPEAT); + return pattern.forget(); +} + +} // namespace mozilla + +// ------------------------------------------------------------------------- +// Public functions +// ------------------------------------------------------------------------- + +nsIFrame* NS_NewSVGPatternFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGPatternFrame(aStyle, aPresShell->GetPresContext()); +} diff --git a/layout/svg/SVGPatternFrame.h b/layout/svg/SVGPatternFrame.h new file mode 100644 index 0000000000..bfc9d1862a --- /dev/null +++ b/layout/svg/SVGPatternFrame.h @@ -0,0 +1,140 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGPATTERNFRAME_H_ +#define LAYOUT_SVG_SVGPATTERNFRAME_H_ + +#include "mozilla/Attributes.h" +#include "gfxMatrix.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGPaintServerFrame.h" +#include "mozilla/UniquePtr.h" + +class nsIFrame; + +namespace mozilla { +class PresShell; +class SVGAnimatedLength; +class SVGAnimatedPreserveAspectRatio; +class SVGAnimatedTransformList; +class SVGAnimatedViewBox; +class SVGGeometryFrame; +} // namespace mozilla + +nsIFrame* NS_NewSVGPatternFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGPatternFrame final : public SVGPaintServerFrame { + using SourceSurface = gfx::SourceSurface; + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGPatternFrame) + NS_DECL_QUERYFRAME + + friend nsIFrame* ::NS_NewSVGPatternFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + explicit SVGPatternFrame(ComputedStyle* aStyle, nsPresContext* aPresContext); + + // SVGPaintServerFrame methods: + already_AddRefed<gfxPattern> GetPaintServerPattern( + nsIFrame* aSource, const DrawTarget* aDrawTarget, + const gfxMatrix& aContextMatrix, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + float aGraphicOpacity, imgDrawingParams& aImgParams, + const gfxRect* aOverrideBounds) override; + + public: + // SVGContainerFrame methods: + gfxMatrix GetCanvasTM() override; + + // nsIFrame interface: + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGPattern"_ns, aResult); + } +#endif // DEBUG + + protected: + /** + * Parses this frame's href and - if it references another pattern - returns + * it. It also makes this frame a rendering observer of the specified ID. + */ + SVGPatternFrame* GetReferencedPattern(); + + // Accessors to lookup pattern attributes + uint16_t GetEnumValue(uint32_t aIndex, nsIContent* aDefault); + uint16_t GetEnumValue(uint32_t aIndex) { + return GetEnumValue(aIndex, mContent); + } + SVGAnimatedTransformList* GetPatternTransformList(nsIContent* aDefault); + gfxMatrix GetPatternTransform(); + const SVGAnimatedViewBox& GetViewBox(nsIContent* aDefault); + const SVGAnimatedViewBox& GetViewBox() { return GetViewBox(mContent); } + const SVGAnimatedPreserveAspectRatio& GetPreserveAspectRatio( + nsIContent* aDefault); + const SVGAnimatedPreserveAspectRatio& GetPreserveAspectRatio() { + return GetPreserveAspectRatio(mContent); + } + const SVGAnimatedLength* GetLengthValue(uint32_t aIndex, + nsIContent* aDefault); + const SVGAnimatedLength* GetLengthValue(uint32_t aIndex) { + return GetLengthValue(aIndex, mContent); + } + + void PaintChildren(DrawTarget* aDrawTarget, + SVGPatternFrame* aPatternWithChildren, nsIFrame* aSource, + float aGraphicOpacity, imgDrawingParams& aImgParams); + + already_AddRefed<SourceSurface> PaintPattern( + const DrawTarget* aDrawTarget, Matrix* patternMatrix, + const Matrix& aContextMatrix, nsIFrame* aSource, + StyleSVGPaint nsStyleSVG::*aFillOrStroke, float aGraphicOpacity, + const gfxRect* aOverrideBounds, imgDrawingParams& aImgParams); + + /** + * A <pattern> element may reference another <pattern> element using + * xlink:href and, if it doesn't have any child content of its own, then it + * will "inherit" the children of the referenced pattern (which may itself be + * inheriting its children if it references another <pattern>). This + * function returns this SVGPatternFrame or the first pattern along the + * reference chain (if there is one) to have children. + */ + SVGPatternFrame* GetPatternWithChildren(); + + gfxRect GetPatternRect(uint16_t aPatternUnits, const gfxRect& bbox, + const Matrix& aTargetCTM, nsIFrame* aTarget); + gfxMatrix ConstructCTM(const SVGAnimatedViewBox& aViewBox, + uint16_t aPatternContentUnits, uint16_t aPatternUnits, + const gfxRect& callerBBox, const Matrix& callerCTM, + nsIFrame* aTarget); + + private: + // this is a *temporary* reference to the frame of the element currently + // referencing our pattern. This must be temporary because different + // referencing frames will all reference this one frame + SVGGeometryFrame* mSource; + UniquePtr<gfxMatrix> mCTM; + + protected: + // This flag is used to detect loops in xlink:href processing + bool mLoopFlag; + bool mNoHRefURI; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGPATTERNFRAME_H_ diff --git a/layout/svg/SVGStopFrame.cpp b/layout/svg/SVGStopFrame.cpp new file mode 100644 index 0000000000..b6a3ab69a6 --- /dev/null +++ b/layout/svg/SVGStopFrame.cpp @@ -0,0 +1,105 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "nsContainerFrame.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGGradientFrame.h" +#include "mozilla/SVGObserverUtils.h" + +// This is a very simple frame whose only purpose is to capture style change +// events and propagate them to the parent. Most of the heavy lifting is done +// within the SVGGradientFrame, which is the parent for this frame + +nsIFrame* NS_NewSVGStopFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGStopFrame : public nsIFrame { + friend nsIFrame* ::NS_NewSVGStopFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGStopFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGStopFrame) + + // nsIFrame interface: +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override {} + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~(nsIFrame::eSVG)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGStop"_ns, aResult); + } +#endif +}; + +//---------------------------------------------------------------------- +// Implementation + +NS_IMPL_FRAMEARENA_HELPERS(SVGStopFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods: + +#ifdef DEBUG +void SVGStopFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::stop), + "Content is not a stop element"); + + nsIFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGStopFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::offset) { + MOZ_ASSERT( + static_cast<SVGGradientFrame*>(do_QueryFrame(GetParent())), + "Observers observe the gradient, so that's what we must invalidate"); + SVGObserverUtils::InvalidateDirectRenderingObservers(GetParent()); + } + + return nsIFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +} // namespace mozilla + +// ------------------------------------------------------------------------- +// Public functions +// ------------------------------------------------------------------------- + +nsIFrame* NS_NewSVGStopFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGStopFrame(aStyle, aPresShell->GetPresContext()); +} diff --git a/layout/svg/SVGSwitchFrame.cpp b/layout/svg/SVGSwitchFrame.cpp new file mode 100644 index 0000000000..4238e6b017 --- /dev/null +++ b/layout/svg/SVGSwitchFrame.cpp @@ -0,0 +1,262 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "gfxRect.h" +#include "SVGGFrame.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGTextFrame.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGSwitchElement.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +nsIFrame* NS_NewSVGSwitchFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGSwitchFrame final : public SVGGFrame { + friend nsIFrame* ::NS_NewSVGSwitchFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGSwitchFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGGFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGSwitchFrame) + +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGSwitch"_ns, aResult); + } +#endif + + virtual void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + + // ISVGDisplayableFrame interface: + virtual void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + virtual SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + + private: + nsIFrame* GetActiveChildFrame(); + void ReflowAllSVGTextFramesInsideNonActiveChildren(nsIFrame* aActiveChild); + static void AlwaysReflowSVGTextFrameDoForOneKid(nsIFrame* aKid); +}; + +} // namespace mozilla + +//---------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGSwitchFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGSwitchFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGSwitchFrame) + +#ifdef DEBUG +void SVGSwitchFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::svgSwitch), + "Content is not an SVG switch"); + + SVGGFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +void SVGSwitchFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + nsIFrame* kid = GetActiveChildFrame(); + if (kid) { + BuildDisplayListForChild(aBuilder, kid, aLists); + } +} + +void SVGSwitchFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Only painting of non-display SVG should take this code path"); + + if (StyleEffects()->IsTransparent()) { + return; + } + + nsIFrame* kid = GetActiveChildFrame(); + if (kid) { + gfxMatrix tm = aTransform; + if (kid->GetContent()->IsSVGElement()) { + tm = SVGUtils::GetTransformMatrixInUserSpace(kid) * tm; + } + SVGUtils::PaintFrameWithEffects(kid, aContext, tm, aImgParams); + } +} + +nsIFrame* SVGSwitchFrame::GetFrameForPoint(const gfxPoint& aPoint) { + MOZ_ASSERT_UNREACHABLE("A clipPath cannot contain an SVGSwitch element"); + return nullptr; +} + +static bool shouldReflowSVGTextFrameInside(nsIFrame* aFrame) { + return aFrame->IsFrameOfType(nsIFrame::eSVG | nsIFrame::eSVGContainer) || + aFrame->IsSVGForeignObjectFrame() || + !aFrame->IsFrameOfType(nsIFrame::eSVG); +} + +void SVGSwitchFrame::AlwaysReflowSVGTextFrameDoForOneKid(nsIFrame* aKid) { + if (!aKid->IsSubtreeDirty()) { + return; + } + + if (aKid->IsSVGTextFrame()) { + MOZ_ASSERT(!aKid->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "A non-display SVGTextFrame directly contained in a display " + "container?"); + static_cast<SVGTextFrame*>(aKid)->ReflowSVG(); + } else if (shouldReflowSVGTextFrameInside(aKid)) { + if (!aKid->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + for (nsIFrame* kid : aKid->PrincipalChildList()) { + AlwaysReflowSVGTextFrameDoForOneKid(kid); + } + } else { + // This child is in a nondisplay context, something like: + // <switch> + // ... + // <g><mask><text></text></mask></g> + // </switch> + // We should not call ReflowSVG on it. + SVGContainerFrame::ReflowSVGNonDisplayText(aKid); + } + } +} + +void SVGSwitchFrame::ReflowAllSVGTextFramesInsideNonActiveChildren( + nsIFrame* aActiveChild) { + for (nsIFrame* kid = mFrames.FirstChild(); kid; kid = kid->GetNextSibling()) { + if (aActiveChild == kid) { + continue; + } + + AlwaysReflowSVGTextFrameDoForOneKid(kid); + } +} + +void SVGSwitchFrame::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; + } + + // If the NS_FRAME_FIRST_REFLOW bit has been removed from our parent frame, + // then our outer-<svg> has previously had its initial reflow. In that case + // we need to make sure that that bit has been removed from ourself _before_ + // recursing over our children to ensure that they know too. Otherwise, we + // need to remove it _after_ recursing over our children so that they know + // the initial reflow is currently underway. + + bool isFirstReflow = HasAnyStateBits(NS_FRAME_FIRST_REFLOW); + + bool outerSVGHasHadFirstReflow = + !GetParent()->HasAnyStateBits(NS_FRAME_FIRST_REFLOW); + + if (outerSVGHasHadFirstReflow) { + RemoveStateBits(NS_FRAME_FIRST_REFLOW); // tell our children + } + + OverflowAreas overflowRects; + + nsIFrame* child = GetActiveChildFrame(); + ReflowAllSVGTextFramesInsideNonActiveChildren(child); + + ISVGDisplayableFrame* svgChild = do_QueryFrame(child); + if (svgChild) { + MOZ_ASSERT(!child->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Check for this explicitly in the |if|, then"); + svgChild->ReflowSVG(); + + // We build up our child frame overflows here instead of using + // nsLayoutUtils::UnionChildOverflow since SVG frame's all use the same + // frame list, and we're iterating over that list now anyway. + ConsiderChildOverflow(overflowRects, child); + } else if (child && shouldReflowSVGTextFrameInside(child)) { + MOZ_ASSERT(child->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) || + !child->IsFrameOfType(nsIFrame::eSVG), + "Check for this explicitly in the |if|, then"); + ReflowSVGNonDisplayText(child); + } + + if (isFirstReflow) { + // Make sure we have our filter property (if any) before calling + // FinishAndStoreOverflow (subsequent filter changes are handled off + // nsChangeHint_UpdateEffects): + SVGObserverUtils::UpdateEffects(this); + } + + FinishAndStoreOverflow(overflowRects, mRect.Size()); + + // Remove state bits after FinishAndStoreOverflow so that it doesn't + // invalidate on first reflow: + RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | + NS_FRAME_HAS_DIRTY_CHILDREN); +} + +SVGBBox SVGSwitchFrame::GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) { + nsIFrame* kid = GetActiveChildFrame(); + ISVGDisplayableFrame* svgKid = do_QueryFrame(kid); + if (svgKid) { + nsIContent* content = kid->GetContent(); + gfxMatrix transform = ThebesMatrix(aToBBoxUserspace); + if (content->IsSVGElement()) { + transform = static_cast<SVGElement*>(content)->PrependLocalTransformsTo( + {}, eChildToUserSpace) * + SVGUtils::GetTransformMatrixInUserSpace(kid) * transform; + } + return svgKid->GetBBoxContribution(ToMatrix(transform), aFlags); + } + return SVGBBox(); +} + +nsIFrame* SVGSwitchFrame::GetActiveChildFrame() { + nsIContent* activeChild = + static_cast<dom::SVGSwitchElement*>(GetContent())->GetActiveChild(); + + if (activeChild) { + for (nsIFrame* kid = mFrames.FirstChild(); kid; + kid = kid->GetNextSibling()) { + if (activeChild == kid->GetContent()) { + return kid; + } + } + } + return nullptr; +} + +} // namespace mozilla diff --git a/layout/svg/SVGSymbolFrame.cpp b/layout/svg/SVGSymbolFrame.cpp new file mode 100644 index 0000000000..63cb31af5b --- /dev/null +++ b/layout/svg/SVGSymbolFrame.cpp @@ -0,0 +1,39 @@ +/* -*- 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 "SVGSymbolFrame.h" + +#include "mozilla/PresShell.h" + +nsIFrame* NS_NewSVGSymbolFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGSymbolFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGSymbolFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods + +NS_QUERYFRAME_HEAD(SVGSymbolFrame) + NS_QUERYFRAME_ENTRY(SVGSymbolFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGViewportFrame) + +#ifdef DEBUG +void SVGSymbolFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::symbol), + "Content is not an SVG 'symbol' element!"); + + SVGViewportFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +} // namespace mozilla diff --git a/layout/svg/SVGSymbolFrame.h b/layout/svg/SVGSymbolFrame.h new file mode 100644 index 0000000000..044e70eec7 --- /dev/null +++ b/layout/svg/SVGSymbolFrame.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGSYMBOLFRAME_H_ +#define LAYOUT_SVG_SVGSYMBOLFRAME_H_ + +#include "SVGViewportFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGSymbolFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGSymbolFrame final : public SVGViewportFrame { + friend nsIFrame* ::NS_NewSVGSymbolFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGSymbolFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGViewportFrame(aStyle, aPresContext, kClassID) {} + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGSymbolFrame) + +#ifdef DEBUG + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGSymbol"_ns, aResult); + } +#endif +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGSYMBOLFRAME_H_ diff --git a/layout/svg/SVGTextFrame.cpp b/layout/svg/SVGTextFrame.cpp new file mode 100644 index 0000000000..76bff6a1e3 --- /dev/null +++ b/layout/svg/SVGTextFrame.cpp @@ -0,0 +1,5334 @@ +/* -*- 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 "SVGTextFrame.h" + +// Keep others in (case-insensitive) order: +#include "DOMSVGPoint.h" +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxFont.h" +#include "gfxSkipChars.h" +#include "gfxTypes.h" +#include "gfxUtils.h" +#include "LookAndFeel.h" +#include "nsAlgorithm.h" +#include "nsBidiPresUtils.h" +#include "nsBlockFrame.h" +#include "nsCaret.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsQuickSort.h" +#include "SVGPaintServerFrame.h" +#include "nsTArray.h" +#include "nsTextFrame.h" +#include "SVGAnimatedNumberList.h" +#include "SVGContentUtils.h" +#include "SVGContextPaint.h" +#include "SVGLengthList.h" +#include "SVGNumberList.h" +#include "nsLayoutUtils.h" +#include "nsFrameSelection.h" +#include "nsStyleStructInlines.h" +#include "mozilla/DisplaySVGItem.h" +#include "mozilla/Likely.h" +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGOuterSVGFrame.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/DOMPointBinding.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/SVGGeometryElement.h" +#include "mozilla/dom/SVGRect.h" +#include "mozilla/dom/SVGTextContentElementBinding.h" +#include "mozilla/dom/SVGTextPathElement.h" +#include "mozilla/dom/Text.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PatternHelpers.h" +#include <algorithm> +#include <cmath> +#include <limits> + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGTextContentElement_Binding; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +// ============================================================================ +// Utility functions + +/** + * Using the specified gfxSkipCharsIterator, converts an offset and length + * in original char indexes to skipped char indexes. + * + * @param aIterator The gfxSkipCharsIterator to use for the conversion. + * @param aOriginalOffset The original offset. + * @param aOriginalLength The original length. + */ +static gfxTextRun::Range ConvertOriginalToSkipped( + gfxSkipCharsIterator& aIterator, uint32_t aOriginalOffset, + uint32_t aOriginalLength) { + uint32_t start = aIterator.ConvertOriginalToSkipped(aOriginalOffset); + aIterator.AdvanceOriginal(aOriginalLength); + return gfxTextRun::Range(start, aIterator.GetSkippedOffset()); +} + +/** + * Converts an nsPoint from app units to user space units using the specified + * nsPresContext and returns it as a gfxPoint. + */ +static gfxPoint AppUnitsToGfxUnits(const nsPoint& aPoint, + const nsPresContext* aContext) { + return gfxPoint(aContext->AppUnitsToGfxUnits(aPoint.x), + aContext->AppUnitsToGfxUnits(aPoint.y)); +} + +/** + * Converts a gfxRect that is in app units to CSS pixels using the specified + * nsPresContext and returns it as a gfxRect. + */ +static gfxRect AppUnitsToFloatCSSPixels(const gfxRect& aRect, + const nsPresContext* aContext) { + return gfxRect(nsPresContext::AppUnitsToFloatCSSPixels(aRect.x), + nsPresContext::AppUnitsToFloatCSSPixels(aRect.y), + nsPresContext::AppUnitsToFloatCSSPixels(aRect.width), + nsPresContext::AppUnitsToFloatCSSPixels(aRect.height)); +} + +/** + * Returns whether a gfxPoint lies within a gfxRect. + */ +static bool Inside(const gfxRect& aRect, const gfxPoint& aPoint) { + return aPoint.x >= aRect.x && aPoint.x < aRect.XMost() && + aPoint.y >= aRect.y && aPoint.y < aRect.YMost(); +} + +/** + * Gets the measured ascent and descent of the text in the given nsTextFrame + * in app units. + * + * @param aFrame The text frame. + * @param aAscent The ascent in app units (output). + * @param aDescent The descent in app units (output). + */ +static void GetAscentAndDescentInAppUnits(nsTextFrame* aFrame, + gfxFloat& aAscent, + gfxFloat& aDescent) { + gfxSkipCharsIterator it = aFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = aFrame->GetTextRun(nsTextFrame::eInflated); + + gfxTextRun::Range range = ConvertOriginalToSkipped( + it, aFrame->GetContentOffset(), aFrame->GetContentLength()); + + textRun->GetLineHeightMetrics(range, aAscent, aDescent); +} + +/** + * Updates an interval by intersecting it with another interval. + * The intervals are specified using a start index and a length. + */ +static void IntersectInterval(uint32_t& aStart, uint32_t& aLength, + uint32_t aStartOther, uint32_t aLengthOther) { + uint32_t aEnd = aStart + aLength; + uint32_t aEndOther = aStartOther + aLengthOther; + + if (aStartOther >= aEnd || aStart >= aEndOther) { + aLength = 0; + } else { + if (aStartOther >= aStart) aStart = aStartOther; + aLength = std::min(aEnd, aEndOther) - aStart; + } +} + +/** + * Intersects an interval as IntersectInterval does but by taking + * the offset and length of the other interval from a + * nsTextFrame::TrimmedOffsets object. + */ +static void TrimOffsets(uint32_t& aStart, uint32_t& aLength, + const nsTextFrame::TrimmedOffsets& aTrimmedOffsets) { + IntersectInterval(aStart, aLength, aTrimmedOffsets.mStart, + aTrimmedOffsets.mLength); +} + +/** + * Returns the closest ancestor-or-self node that is not an SVG <a> + * element. + */ +static nsIContent* GetFirstNonAAncestor(nsIContent* aContent) { + while (aContent && aContent->IsSVGElement(nsGkAtoms::a)) { + aContent = aContent->GetParent(); + } + return aContent; +} + +/** + * Returns whether the given node is a text content element[1], taking into + * account whether it has a valid parent. + * + * For example, in: + * + * <svg xmlns="http://www.w3.org/2000/svg"> + * <text><a/><text/></text> + * <tspan/> + * </svg> + * + * true would be returned for the outer <text> element and the <a> element, + * and false for the inner <text> element (since a <text> is not allowed + * to be a child of another <text>) and the <tspan> element (because it + * must be inside a <text> subtree). + * + * Note that we don't support the <tref> element yet and this function + * returns false for it. + * + * [1] https://svgwg.org/svg2-draft/intro.html#TermTextContentElement + */ +static bool IsTextContentElement(nsIContent* aContent) { + if (aContent->IsSVGElement(nsGkAtoms::text)) { + nsIContent* parent = GetFirstNonAAncestor(aContent->GetParent()); + return !parent || !IsTextContentElement(parent); + } + + if (aContent->IsSVGElement(nsGkAtoms::textPath)) { + nsIContent* parent = GetFirstNonAAncestor(aContent->GetParent()); + return parent && parent->IsSVGElement(nsGkAtoms::text); + } + + return aContent->IsAnyOfSVGElements(nsGkAtoms::a, nsGkAtoms::tspan); +} + +/** + * Returns whether the specified frame is an nsTextFrame that has some text + * content. + */ +static bool IsNonEmptyTextFrame(nsIFrame* aFrame) { + nsTextFrame* textFrame = do_QueryFrame(aFrame); + if (!textFrame) { + return false; + } + + return textFrame->GetContentLength() != 0; +} + +/** + * Takes an nsIFrame and if it is a text frame that has some text content, + * returns it as an nsTextFrame and its corresponding Text. + * + * @param aFrame The frame to look at. + * @param aTextFrame aFrame as an nsTextFrame (output). + * @param aTextNode The Text content of aFrame (output). + * @return true if aFrame is a non-empty text frame, false otherwise. + */ +static bool GetNonEmptyTextFrameAndNode(nsIFrame* aFrame, + nsTextFrame*& aTextFrame, + Text*& aTextNode) { + nsTextFrame* text = do_QueryFrame(aFrame); + bool isNonEmptyTextFrame = text && text->GetContentLength() != 0; + + if (isNonEmptyTextFrame) { + nsIContent* content = text->GetContent(); + NS_ASSERTION(content && content->IsText(), + "unexpected content type for nsTextFrame"); + + Text* node = content->AsText(); + MOZ_ASSERT(node->TextLength() != 0, + "frame's GetContentLength() should be 0 if the text node " + "has no content"); + + aTextFrame = text; + aTextNode = node; + } + + MOZ_ASSERT(IsNonEmptyTextFrame(aFrame) == isNonEmptyTextFrame, + "our logic should agree with IsNonEmptyTextFrame"); + return isNonEmptyTextFrame; +} + +/** + * Returns whether the specified atom is for one of the five + * glyph positioning attributes that can appear on SVG text + * elements -- x, y, dx, dy or rotate. + */ +static bool IsGlyphPositioningAttribute(nsAtom* aAttribute) { + return aAttribute == nsGkAtoms::x || aAttribute == nsGkAtoms::y || + aAttribute == nsGkAtoms::dx || aAttribute == nsGkAtoms::dy || + aAttribute == nsGkAtoms::rotate; +} + +/** + * Returns the position in app units of a given baseline (using an + * SVG dominant-baseline property value) for a given nsTextFrame. + * + * @param aFrame The text frame to inspect. + * @param aTextRun The text run of aFrame. + * @param aDominantBaseline The dominant-baseline value to use. + */ +static nscoord GetBaselinePosition(nsTextFrame* aFrame, gfxTextRun* aTextRun, + StyleDominantBaseline aDominantBaseline, + float aFontSizeScaleFactor) { + WritingMode writingMode = aFrame->GetWritingMode(); + gfxFloat ascent, descent; + aTextRun->GetLineHeightMetrics(ascent, descent); + + auto convertIfVerticalRL = [&](gfxFloat dominantBaseline) { + return writingMode.IsVerticalRL() ? ascent + descent - dominantBaseline + : dominantBaseline; + }; + + switch (aDominantBaseline) { + case StyleDominantBaseline::Hanging: + return convertIfVerticalRL(ascent * 0.2); + case StyleDominantBaseline::TextBeforeEdge: + return convertIfVerticalRL(0); + + case StyleDominantBaseline::Alphabetic: + return writingMode.IsVerticalRL() + ? ascent * 0.5 + : aFrame->GetLogicalBaseline(writingMode); + + case StyleDominantBaseline::Auto: + return convertIfVerticalRL(aFrame->GetLogicalBaseline(writingMode)); + + case StyleDominantBaseline::Middle: + return convertIfVerticalRL(aFrame->GetLogicalBaseline(writingMode) - + SVGContentUtils::GetFontXHeight(aFrame) / 2.0 * + AppUnitsPerCSSPixel() * + aFontSizeScaleFactor); + + case StyleDominantBaseline::TextAfterEdge: + case StyleDominantBaseline::Ideographic: + return writingMode.IsVerticalLR() ? 0 : ascent + descent; + + case StyleDominantBaseline::Central: + return (ascent + descent) / 2.0; + case StyleDominantBaseline::Mathematical: + return convertIfVerticalRL(ascent / 2.0); + } + + MOZ_ASSERT_UNREACHABLE("unexpected dominant-baseline value"); + return convertIfVerticalRL(aFrame->GetLogicalBaseline(writingMode)); +} + +/** + * Truncates an array to be at most the length of another array. + * + * @param aArrayToTruncate The array to truncate. + * @param aReferenceArray The array whose length will be used to truncate + * aArrayToTruncate to. + */ +template <typename T, typename U> +static void TruncateTo(nsTArray<T>& aArrayToTruncate, + const nsTArray<U>& aReferenceArray) { + uint32_t length = aReferenceArray.Length(); + if (aArrayToTruncate.Length() > length) { + aArrayToTruncate.TruncateLength(length); + } +} + +/** + * Asserts that the anonymous block child of the SVGTextFrame has been + * reflowed (or does not exist). Returns null if the child has not been + * reflowed, and the frame otherwise. + * + * We check whether the kid has been reflowed and not the frame itself + * since we sometimes need to call this function during reflow, after the + * kid has been reflowed but before we have cleared the dirty bits on the + * frame itself. + */ +static SVGTextFrame* FrameIfAnonymousChildReflowed(SVGTextFrame* aFrame) { + MOZ_ASSERT(aFrame, "aFrame must not be null"); + nsIFrame* kid = aFrame->PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + MOZ_ASSERT(false, "should have already reflowed the anonymous block child"); + return nullptr; + } + return aFrame; +} + +static double GetContextScale(const gfxMatrix& aMatrix) { + // The context scale is the ratio of the length of the transformed + // diagonal vector (1,1) to the length of the untransformed diagonal + // (which is sqrt(2)). + gfxPoint p = aMatrix.TransformPoint(gfxPoint(1, 1)) - + aMatrix.TransformPoint(gfxPoint(0, 0)); + return SVGContentUtils::ComputeNormalizedHypotenuse(p.x, p.y); +} + +// ============================================================================ +// Utility classes + +// ---------------------------------------------------------------------------- +// TextRenderedRun + +/** + * A run of text within a single nsTextFrame whose glyphs can all be painted + * with a single call to nsTextFrame::PaintText. A text rendered run can + * be created for a sequence of two or more consecutive glyphs as long as: + * + * - Only the first glyph has (or none of the glyphs have) been positioned + * with SVG text positioning attributes + * - All of the glyphs have zero rotation + * - The glyphs are not on a text path + * - The glyphs correspond to content within the one nsTextFrame + * + * A TextRenderedRunIterator produces TextRenderedRuns required for painting a + * whole SVGTextFrame. + */ +struct TextRenderedRun { + using Range = gfxTextRun::Range; + + /** + * Constructs a TextRenderedRun that is uninitialized except for mFrame + * being null. + */ + TextRenderedRun() : mFrame(nullptr) {} + + /** + * Constructs a TextRenderedRun with all of the information required to + * paint it. See the comments documenting the member variables below + * for descriptions of the arguments. + */ + TextRenderedRun(nsTextFrame* aFrame, const gfxPoint& aPosition, + float aLengthAdjustScaleFactor, double aRotate, + float aFontSizeScaleFactor, nscoord aBaseline, + uint32_t aTextFrameContentOffset, + uint32_t aTextFrameContentLength, + uint32_t aTextElementCharIndex) + : mFrame(aFrame), + mPosition(aPosition), + mLengthAdjustScaleFactor(aLengthAdjustScaleFactor), + mRotate(static_cast<float>(aRotate)), + mFontSizeScaleFactor(aFontSizeScaleFactor), + mBaseline(aBaseline), + mTextFrameContentOffset(aTextFrameContentOffset), + mTextFrameContentLength(aTextFrameContentLength), + mTextElementCharIndex(aTextElementCharIndex) {} + + /** + * Returns the text run for the text frame that this rendered run is part of. + */ + gfxTextRun* GetTextRun() const { + mFrame->EnsureTextRun(nsTextFrame::eInflated); + return mFrame->GetTextRun(nsTextFrame::eInflated); + } + + /** + * Returns whether this rendered run is RTL. + */ + bool IsRightToLeft() const { return GetTextRun()->IsRightToLeft(); } + + /** + * Returns whether this rendered run is vertical. + */ + bool IsVertical() const { return GetTextRun()->IsVertical(); } + + /** + * Returns the transform that converts from a <text> element's user space into + * the coordinate space that rendered runs can be painted directly in. + * + * The difference between this method and + * GetTransformFromRunUserSpaceToUserSpace is that when calling in to + * nsTextFrame::PaintText, it will already take into account any left clip + * edge (that is, it doesn't just apply a visual clip to the rendered text, it + * shifts the glyphs over so that they are painted with their left edge at the + * x coordinate passed in to it). Thus we need to account for this in our + * transform. + * + * + * Assume that we have: + * + * <text x="100" y="100" rotate="0 0 1 0 0 * 1">abcdef</text>. + * + * This would result in four text rendered runs: + * + * - one for "ab" + * - one for "c" + * - one for "de" + * - one for "f" + * + * Assume now that we are painting the third TextRenderedRun. It will have + * a left clip edge that is the sum of the advances of "abc", and it will + * have a right clip edge that is the advance of "f". In + * SVGTextFrame::PaintSVG(), we pass in nsPoint() (i.e., the origin) + * as the point at which to paint the text frame, and we pass in the + * clip edge values. The nsTextFrame will paint the substring of its + * text such that the top-left corner of the "d"'s glyph cell will be at + * (0, 0) in the current coordinate system. + * + * Thus, GetTransformFromUserSpaceForPainting must return a transform from + * whatever user space the <text> element is in to a coordinate space in + * device pixels (as that's what nsTextFrame works in) where the origin is at + * the same position as our user space mPositions[i].mPosition value for + * the "d" glyph, which will be (100 + userSpaceAdvance("abc"), 100). + * The translation required to do this (ignoring the scale to get from + * user space to device pixels, and ignoring the + * (100 + userSpaceAdvance("abc"), 100) translation) is: + * + * (-leftEdge, -baseline) + * + * where baseline is the distance between the baseline of the text and the top + * edge of the nsTextFrame. We translate by -leftEdge horizontally because + * the nsTextFrame will already shift the glyphs over by that amount and start + * painting glyphs at x = 0. We translate by -baseline vertically so that + * painting the top edges of the glyphs at y = 0 will result in their + * baselines being at our desired y position. + * + * + * Now for an example with RTL text. Assume our content is now + * <text x="100" y="100" rotate="0 0 1 0 0 1">WERBEH</text>. We'd have + * the following text rendered runs: + * + * - one for "EH" + * - one for "B" + * - one for "ER" + * - one for "W" + * + * Again, we are painting the third TextRenderedRun. The left clip edge + * is the advance of the "W" and the right clip edge is the sum of the + * advances of "BEH". Our translation to get the rendered "ER" glyphs + * in the right place this time is: + * + * (-frameWidth + rightEdge, -baseline) + * + * which is equivalent to: + * + * (-(leftEdge + advance("ER")), -baseline) + * + * The reason we have to shift left additionally by the width of the run + * of glyphs we are painting is that although the nsTextFrame is RTL, + * we still supply the top-left corner to paint the frame at when calling + * nsTextFrame::PaintText, even though our user space positions for each + * glyph in mPositions specifies the origin of each glyph, which for RTL + * glyphs is at the right edge of the glyph cell. + * + * + * For any other use of an nsTextFrame in the context of a particular run + * (such as hit testing, or getting its rectangle), + * GetTransformFromRunUserSpaceToUserSpace should be used. + * + * @param aContext The context to use for unit conversions. + */ + gfxMatrix GetTransformFromUserSpaceForPainting( + nsPresContext* aContext, const nscoord aVisIStartEdge, + const nscoord aVisIEndEdge) const; + + /** + * Returns the transform that converts from "run user space" to a <text> + * element's user space. Run user space is a coordinate system that has the + * same size as the <text>'s user space but rotated and translated such that + * (0,0) is the top-left of the rectangle that bounds the text. + * + * @param aContext The context to use for unit conversions. + */ + gfxMatrix GetTransformFromRunUserSpaceToUserSpace( + nsPresContext* aContext) const; + + /** + * Returns the transform that converts from "run user space" to float pixels + * relative to the nsTextFrame that this rendered run is a part of. + * + * @param aContext The context to use for unit conversions. + */ + gfxMatrix GetTransformFromRunUserSpaceToFrameUserSpace( + nsPresContext* aContext) const; + + /** + * Flag values used for the aFlags arguments of GetRunUserSpaceRect, + * GetFrameUserSpaceRect and GetUserSpaceRect. + */ + enum { + // Includes the fill geometry of the text in the returned rectangle. + eIncludeFill = 1, + // Includes the stroke geometry of the text in the returned rectangle. + eIncludeStroke = 2, + // Don't include any horizontal glyph overflow in the returned rectangle. + eNoHorizontalOverflow = 4 + }; + + /** + * Returns a rectangle that bounds the fill and/or stroke of the rendered run + * in run user space. + * + * @param aContext The context to use for unit conversions. + * @param aFlags A combination of the flags above (eIncludeFill and + * eIncludeStroke) indicating what parts of the text to include in + * the rectangle. + */ + SVGBBox GetRunUserSpaceRect(nsPresContext* aContext, uint32_t aFlags) const; + + /** + * Returns a rectangle that covers the fill and/or stroke of the rendered run + * in "frame user space". + * + * Frame user space is a coordinate space of the same scale as the <text> + * element's user space, but with its rotation set to the rotation of + * the glyphs within this rendered run and its origin set to the position + * such that placing the nsTextFrame there would result in the glyphs in + * this rendered run being at their correct positions. + * + * For example, say we have <text x="100 150" y="100">ab</text>. Assume + * the advance of both the "a" and the "b" is 12 user units, and the + * ascent of the text is 8 user units and its descent is 6 user units, + * and that we are not measuing the stroke of the text, so that we stay + * entirely within the glyph cells. + * + * There will be two text rendered runs, one for "a" and one for "b". + * + * The frame user space for the "a" run will have its origin at + * (100, 100 - 8) in the <text> element's user space and will have its + * axes aligned with the user space (since there is no rotate="" or + * text path involve) and with its scale the same as the user space. + * The rect returned by this method will be (0, 0, 12, 14), since the "a" + * glyph is right at the left of the nsTextFrame. + * + * The frame user space for the "b" run will have its origin at + * (150 - 12, 100 - 8), and scale/rotation the same as above. The rect + * returned by this method will be (12, 0, 12, 14), since we are + * advance("a") horizontally in to the text frame. + * + * @param aContext The context to use for unit conversions. + * @param aFlags A combination of the flags above (eIncludeFill and + * eIncludeStroke) indicating what parts of the text to include in + * the rectangle. + */ + SVGBBox GetFrameUserSpaceRect(nsPresContext* aContext, uint32_t aFlags) const; + + /** + * Returns a rectangle that covers the fill and/or stroke of the rendered run + * in the <text> element's user space. + * + * @param aContext The context to use for unit conversions. + * @param aFlags A combination of the flags above indicating what parts of + * the text to include in the rectangle. + * @param aAdditionalTransform An additional transform to apply to the + * frame user space rectangle before its bounds are transformed into + * user space. + */ + SVGBBox GetUserSpaceRect( + nsPresContext* aContext, uint32_t aFlags, + const gfxMatrix* aAdditionalTransform = nullptr) const; + + /** + * Gets the app unit amounts to clip from the left and right edges of + * the nsTextFrame in order to paint just this rendered run. + * + * Note that if clip edge amounts land in the middle of a glyph, the + * glyph won't be painted at all. The clip edges are thus more of + * a selection mechanism for which glyphs will be painted, rather + * than a geometric clip. + */ + void GetClipEdges(nscoord& aVisIStartEdge, nscoord& aVisIEndEdge) const; + + /** + * Returns the advance width of the whole rendered run. + */ + nscoord GetAdvanceWidth() const; + + /** + * Returns the index of the character into this rendered run whose + * glyph cell contains the given point, or -1 if there is no such + * character. This does not hit test against any overflow. + * + * @param aContext The context to use for unit conversions. + * @param aPoint The point in the user space of the <text> element. + */ + int32_t GetCharNumAtPosition(nsPresContext* aContext, + const gfxPoint& aPoint) const; + + /** + * The text frame that this rendered run lies within. + */ + nsTextFrame* mFrame; + + /** + * The point in user space that the text is positioned at. + * + * For a horizontal run: + * The x coordinate is the left edge of a LTR run of text or the right edge of + * an RTL run. The y coordinate is the baseline of the text. + * For a vertical run: + * The x coordinate is the baseline of the text. + * The y coordinate is the top edge of a LTR run, or bottom of RTL. + */ + gfxPoint mPosition; + + /** + * The horizontal scale factor to apply when painting glyphs to take + * into account textLength="". + */ + float mLengthAdjustScaleFactor; + + /** + * The rotation in radians in the user coordinate system that the text has. + */ + float mRotate; + + /** + * The scale factor that was used to transform the text run's original font + * size into a sane range for painting and measurement. + */ + double mFontSizeScaleFactor; + + /** + * The baseline in app units of this text run. The measurement is from the + * top of the text frame. (From the left edge if vertical.) + */ + nscoord mBaseline; + + /** + * The offset and length in mFrame's content Text that corresponds to + * this text rendered run. These are original char indexes. + */ + uint32_t mTextFrameContentOffset; + uint32_t mTextFrameContentLength; + + /** + * The character index in the whole SVG <text> element that this text rendered + * run begins at. + */ + uint32_t mTextElementCharIndex; +}; + +gfxMatrix TextRenderedRun::GetTransformFromUserSpaceForPainting( + nsPresContext* aContext, const nscoord aVisIStartEdge, + const nscoord aVisIEndEdge) const { + // We transform to device pixels positioned such that painting the text frame + // at (0,0) with aItem will result in the text being in the right place. + + gfxMatrix m; + if (!mFrame) { + return m; + } + + float cssPxPerDevPx = + nsPresContext::AppUnitsToFloatCSSPixels(aContext->AppUnitsPerDevPixel()); + + // Glyph position in user space. + m.PreTranslate(mPosition / cssPxPerDevPx); + + // Take into account any font size scaling and scaling due to textLength="". + m.PreScale(1.0 / mFontSizeScaleFactor, 1.0 / mFontSizeScaleFactor); + + // Rotation due to rotate="" or a <textPath>. + m.PreRotate(mRotate); + + m.PreScale(mLengthAdjustScaleFactor, 1.0); + + // Translation to get the text frame in the right place. + nsPoint t; + + if (IsVertical()) { + t = nsPoint(-mBaseline, IsRightToLeft() + ? -mFrame->GetRect().height + aVisIEndEdge + : -aVisIStartEdge); + } else { + t = nsPoint(IsRightToLeft() ? -mFrame->GetRect().width + aVisIEndEdge + : -aVisIStartEdge, + -mBaseline); + } + m.PreTranslate(AppUnitsToGfxUnits(t, aContext)); + + return m; +} + +gfxMatrix TextRenderedRun::GetTransformFromRunUserSpaceToUserSpace( + nsPresContext* aContext) const { + gfxMatrix m; + if (!mFrame) { + return m; + } + + float cssPxPerDevPx = + nsPresContext::AppUnitsToFloatCSSPixels(aContext->AppUnitsPerDevPixel()); + + nscoord start, end; + GetClipEdges(start, end); + + // Glyph position in user space. + m.PreTranslate(mPosition); + + // Rotation due to rotate="" or a <textPath>. + m.PreRotate(mRotate); + + // Scale due to textLength="". + m.PreScale(mLengthAdjustScaleFactor, 1.0); + + // Translation to get the text frame in the right place. + nsPoint t; + if (IsVertical()) { + t = nsPoint(-mBaseline, + IsRightToLeft() ? -mFrame->GetRect().height + start + end : 0); + } else { + t = nsPoint(IsRightToLeft() ? -mFrame->GetRect().width + start + end : 0, + -mBaseline); + } + m.PreTranslate(AppUnitsToGfxUnits(t, aContext) * cssPxPerDevPx / + mFontSizeScaleFactor); + + return m; +} + +gfxMatrix TextRenderedRun::GetTransformFromRunUserSpaceToFrameUserSpace( + nsPresContext* aContext) const { + gfxMatrix m; + if (!mFrame) { + return m; + } + + nscoord start, end; + GetClipEdges(start, end); + + // Translate by the horizontal distance into the text frame this + // rendered run is. + gfxFloat appPerCssPx = AppUnitsPerCSSPixel(); + gfxPoint t = IsVertical() ? gfxPoint(0, start / appPerCssPx) + : gfxPoint(start / appPerCssPx, 0); + return m.PreTranslate(t); +} + +SVGBBox TextRenderedRun::GetRunUserSpaceRect(nsPresContext* aContext, + uint32_t aFlags) const { + SVGBBox r; + if (!mFrame) { + return r; + } + + // Determine the amount of overflow above and below the frame's mRect. + // + // We need to call InkOverflowRectRelativeToSelf because this includes + // overflowing decorations, which the MeasureText call below does not. We + // assume here the decorations only overflow above and below the frame, never + // horizontally. + nsRect self = mFrame->InkOverflowRectRelativeToSelf(); + nsRect rect = mFrame->GetRect(); + bool vertical = IsVertical(); + nscoord above = vertical ? -self.x : -self.y; + nscoord below = + vertical ? self.XMost() - rect.width : self.YMost() - rect.height; + + gfxSkipCharsIterator it = mFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxSkipCharsIterator start = it; + gfxTextRun* textRun = mFrame->GetTextRun(nsTextFrame::eInflated); + + // Get the content range for this rendered run. + Range range = ConvertOriginalToSkipped(it, mTextFrameContentOffset, + mTextFrameContentLength); + if (range.Length() == 0) { + return r; + } + + // FIXME(heycam): We could create a single PropertyProvider for all + // TextRenderedRuns that correspond to the text frame, rather than recreate + // it each time here. + nsTextFrame::PropertyProvider provider(mFrame, start); + + // Measure that range. + gfxTextRun::Metrics metrics = textRun->MeasureText( + range, gfxFont::LOOSE_INK_EXTENTS, nullptr, &provider); + // Make sure it includes the font-box. + gfxRect fontBox(0, -metrics.mAscent, metrics.mAdvanceWidth, + metrics.mAscent + metrics.mDescent); + metrics.mBoundingBox.UnionRect(metrics.mBoundingBox, fontBox); + + // Determine the rectangle that covers the rendered run's fill, + // taking into account the measured vertical overflow due to + // decorations. + nscoord baseline = + NSToCoordRoundWithClamp(metrics.mBoundingBox.y + metrics.mAscent); + gfxFloat x, width; + if (aFlags & eNoHorizontalOverflow) { + x = 0.0; + width = textRun->GetAdvanceWidth(range, &provider); + if (width < 0.0) { + x = width; + width = -width; + } + } else { + x = metrics.mBoundingBox.x; + width = metrics.mBoundingBox.width; + } + nsRect fillInAppUnits( + NSToCoordRoundWithClamp(x), baseline - above, + NSToCoordRoundWithClamp(width), + NSToCoordRoundWithClamp(metrics.mBoundingBox.height) + above + below); + if (textRun->IsVertical()) { + // Swap line-relative textMetrics dimensions to physical coordinates. + std::swap(fillInAppUnits.x, fillInAppUnits.y); + std::swap(fillInAppUnits.width, fillInAppUnits.height); + } + + // Convert the app units rectangle to user units. + gfxRect fill = AppUnitsToFloatCSSPixels( + gfxRect(fillInAppUnits.x, fillInAppUnits.y, fillInAppUnits.width, + fillInAppUnits.height), + aContext); + + // Scale the rectangle up due to any mFontSizeScaleFactor. + fill.Scale(1.0 / mFontSizeScaleFactor); + + // Include the fill if requested. + if (aFlags & eIncludeFill) { + r = fill; + } + + // Include the stroke if requested. + if ((aFlags & eIncludeStroke) && !fill.IsEmpty() && + SVGUtils::GetStrokeWidth(mFrame) > 0) { + r.UnionEdges( + SVGUtils::PathExtentsToMaxStrokeExtents(fill, mFrame, gfxMatrix())); + } + + return r; +} + +SVGBBox TextRenderedRun::GetFrameUserSpaceRect(nsPresContext* aContext, + uint32_t aFlags) const { + SVGBBox r = GetRunUserSpaceRect(aContext, aFlags); + if (r.IsEmpty()) { + return r; + } + gfxMatrix m = GetTransformFromRunUserSpaceToFrameUserSpace(aContext); + return m.TransformBounds(r.ToThebesRect()); +} + +SVGBBox TextRenderedRun::GetUserSpaceRect( + nsPresContext* aContext, uint32_t aFlags, + const gfxMatrix* aAdditionalTransform) const { + SVGBBox r = GetRunUserSpaceRect(aContext, aFlags); + if (r.IsEmpty()) { + return r; + } + gfxMatrix m = GetTransformFromRunUserSpaceToUserSpace(aContext); + if (aAdditionalTransform) { + m *= *aAdditionalTransform; + } + return m.TransformBounds(r.ToThebesRect()); +} + +void TextRenderedRun::GetClipEdges(nscoord& aVisIStartEdge, + nscoord& aVisIEndEdge) const { + uint32_t contentLength = mFrame->GetContentLength(); + if (mTextFrameContentOffset == 0 && + mTextFrameContentLength == contentLength) { + // If the rendered run covers the entire content, we know we don't need + // to clip without having to measure anything. + aVisIStartEdge = 0; + aVisIEndEdge = 0; + return; + } + + gfxSkipCharsIterator it = mFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = mFrame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(mFrame, it); + + // Get the covered content offset/length for this rendered run in skipped + // characters, since that is what GetAdvanceWidth expects. + Range runRange = ConvertOriginalToSkipped(it, mTextFrameContentOffset, + mTextFrameContentLength); + + // Get the offset/length of the whole nsTextFrame. + uint32_t frameOffset = mFrame->GetContentOffset(); + uint32_t frameLength = mFrame->GetContentLength(); + + // Trim the whole-nsTextFrame offset/length to remove any leading/trailing + // white space, as the nsTextFrame when painting does not include them when + // interpreting clip edges. + nsTextFrame::TrimmedOffsets trimmedOffsets = + mFrame->GetTrimmedOffsets(mFrame->TextFragment()); + TrimOffsets(frameOffset, frameLength, trimmedOffsets); + + // Convert the trimmed whole-nsTextFrame offset/length into skipped + // characters. + Range frameRange = ConvertOriginalToSkipped(it, frameOffset, frameLength); + + // Measure the advance width in the text run between the start of + // frame's content and the start of the rendered run's content, + nscoord startEdge = textRun->GetAdvanceWidth( + Range(frameRange.start, runRange.start), &provider); + + // and between the end of the rendered run's content and the end + // of the frame's content. + nscoord endEdge = + textRun->GetAdvanceWidth(Range(runRange.end, frameRange.end), &provider); + + if (textRun->IsRightToLeft()) { + aVisIStartEdge = endEdge; + aVisIEndEdge = startEdge; + } else { + aVisIStartEdge = startEdge; + aVisIEndEdge = endEdge; + } +} + +nscoord TextRenderedRun::GetAdvanceWidth() const { + gfxSkipCharsIterator it = mFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = mFrame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(mFrame, it); + + Range range = ConvertOriginalToSkipped(it, mTextFrameContentOffset, + mTextFrameContentLength); + + return textRun->GetAdvanceWidth(range, &provider); +} + +int32_t TextRenderedRun::GetCharNumAtPosition(nsPresContext* aContext, + const gfxPoint& aPoint) const { + if (mTextFrameContentLength == 0) { + return -1; + } + + float cssPxPerDevPx = + nsPresContext::AppUnitsToFloatCSSPixels(aContext->AppUnitsPerDevPixel()); + + // Convert the point from user space into run user space, and take + // into account any mFontSizeScaleFactor. + gfxMatrix m = GetTransformFromRunUserSpaceToUserSpace(aContext); + if (!m.Invert()) { + return -1; + } + gfxPoint p = m.TransformPoint(aPoint) / cssPxPerDevPx * mFontSizeScaleFactor; + + // First check that the point lies vertically between the top and bottom + // edges of the text. + gfxFloat ascent, descent; + GetAscentAndDescentInAppUnits(mFrame, ascent, descent); + + WritingMode writingMode = mFrame->GetWritingMode(); + if (writingMode.IsVertical()) { + gfxFloat leftEdge = mFrame->GetLogicalBaseline(writingMode) - + (writingMode.IsVerticalRL() ? ascent : descent); + gfxFloat rightEdge = leftEdge + ascent + descent; + if (p.x < aContext->AppUnitsToGfxUnits(leftEdge) || + p.x > aContext->AppUnitsToGfxUnits(rightEdge)) { + return -1; + } + } else { + gfxFloat topEdge = mFrame->GetLogicalBaseline(writingMode) - ascent; + gfxFloat bottomEdge = topEdge + ascent + descent; + if (p.y < aContext->AppUnitsToGfxUnits(topEdge) || + p.y > aContext->AppUnitsToGfxUnits(bottomEdge)) { + return -1; + } + } + + gfxSkipCharsIterator it = mFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = mFrame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(mFrame, it); + + // Next check that the point lies horizontally within the left and right + // edges of the text. + Range range = ConvertOriginalToSkipped(it, mTextFrameContentOffset, + mTextFrameContentLength); + gfxFloat runAdvance = + aContext->AppUnitsToGfxUnits(textRun->GetAdvanceWidth(range, &provider)); + + gfxFloat pos = writingMode.IsVertical() ? p.y : p.x; + if (pos < 0 || pos >= runAdvance) { + return -1; + } + + // Finally, measure progressively smaller portions of the rendered run to + // find which glyph it lies within. This will need to change once we + // support letter-spacing and word-spacing. + bool rtl = textRun->IsRightToLeft(); + for (int32_t i = mTextFrameContentLength - 1; i >= 0; i--) { + range = ConvertOriginalToSkipped(it, mTextFrameContentOffset, i); + gfxFloat advance = aContext->AppUnitsToGfxUnits( + textRun->GetAdvanceWidth(range, &provider)); + if ((rtl && pos < runAdvance - advance) || (!rtl && pos >= advance)) { + return i; + } + } + return -1; +} + +// ---------------------------------------------------------------------------- +// TextNodeIterator + +enum SubtreePosition { eBeforeSubtree, eWithinSubtree, eAfterSubtree }; + +/** + * An iterator class for Text that are descendants of a given node, the + * root. Nodes are iterated in document order. An optional subtree can be + * specified, in which case the iterator will track whether the current state of + * the traversal over the tree is within that subtree or is past that subtree. + */ +class TextNodeIterator { + public: + /** + * Constructs a TextNodeIterator with the specified root node and optional + * subtree. + */ + explicit TextNodeIterator(nsIContent* aRoot, nsIContent* aSubtree = nullptr) + : mRoot(aRoot), + mSubtree(aSubtree == aRoot ? nullptr : aSubtree), + mCurrent(aRoot), + mSubtreePosition(mSubtree ? eBeforeSubtree : eWithinSubtree) { + NS_ASSERTION(aRoot, "expected non-null root"); + if (!aRoot->IsText()) { + Next(); + } + } + + /** + * Returns the current Text, or null if the iterator has finished. + */ + Text* Current() const { return mCurrent ? mCurrent->AsText() : nullptr; } + + /** + * Advances to the next Text and returns it, or null if the end of + * iteration has been reached. + */ + Text* Next(); + + /** + * Returns whether the iterator is currently within the subtree rooted + * at mSubtree. Returns true if we are not tracking a subtree (we consider + * that we're always within the subtree). + */ + bool IsWithinSubtree() const { return mSubtreePosition == eWithinSubtree; } + + /** + * Returns whether the iterator is past the subtree rooted at mSubtree. + * Returns false if we are not tracking a subtree. + */ + bool IsAfterSubtree() const { return mSubtreePosition == eAfterSubtree; } + + private: + /** + * The root under which all Text will be iterated over. + */ + nsIContent* const mRoot; + + /** + * The node rooting the subtree to track. + */ + nsIContent* const mSubtree; + + /** + * The current node during iteration. + */ + nsIContent* mCurrent; + + /** + * The current iterator position relative to mSubtree. + */ + SubtreePosition mSubtreePosition; +}; + +Text* TextNodeIterator::Next() { + // Starting from mCurrent, we do a non-recursive traversal to the next + // Text beneath mRoot, updating mSubtreePosition appropriately if we + // encounter mSubtree. + if (mCurrent) { + do { + nsIContent* next = + IsTextContentElement(mCurrent) ? mCurrent->GetFirstChild() : nullptr; + if (next) { + mCurrent = next; + if (mCurrent == mSubtree) { + mSubtreePosition = eWithinSubtree; + } + } else { + for (;;) { + if (mCurrent == mRoot) { + mCurrent = nullptr; + break; + } + if (mCurrent == mSubtree) { + mSubtreePosition = eAfterSubtree; + } + next = mCurrent->GetNextSibling(); + if (next) { + mCurrent = next; + if (mCurrent == mSubtree) { + mSubtreePosition = eWithinSubtree; + } + break; + } + if (mCurrent == mSubtree) { + mSubtreePosition = eAfterSubtree; + } + mCurrent = mCurrent->GetParent(); + } + } + } while (mCurrent && !mCurrent->IsText()); + } + + return mCurrent ? mCurrent->AsText() : nullptr; +} + +// ---------------------------------------------------------------------------- +// TextNodeCorrespondenceRecorder + +/** + * TextNodeCorrespondence is used as the value of a frame property that + * is stored on all its descendant nsTextFrames. It stores the number of DOM + * characters between it and the previous nsTextFrame that did not have an + * nsTextFrame created for them, due to either not being in a correctly + * parented text content element, or because they were display:none. + * These are called "undisplayed characters". + * + * See also TextNodeCorrespondenceRecorder below, which is what sets the + * frame property. + */ +struct TextNodeCorrespondence { + explicit TextNodeCorrespondence(uint32_t aUndisplayedCharacters) + : mUndisplayedCharacters(aUndisplayedCharacters) {} + + uint32_t mUndisplayedCharacters; +}; + +NS_DECLARE_FRAME_PROPERTY_DELETABLE(TextNodeCorrespondenceProperty, + TextNodeCorrespondence) + +/** + * Returns the number of undisplayed characters before the specified + * nsTextFrame. + */ +static uint32_t GetUndisplayedCharactersBeforeFrame(nsTextFrame* aFrame) { + void* value = aFrame->GetProperty(TextNodeCorrespondenceProperty()); + TextNodeCorrespondence* correspondence = + static_cast<TextNodeCorrespondence*>(value); + if (!correspondence) { + // FIXME bug 903785 + NS_ERROR( + "expected a TextNodeCorrespondenceProperty on nsTextFrame " + "used for SVG text"); + return 0; + } + return correspondence->mUndisplayedCharacters; +} + +/** + * Traverses the nsTextFrames for an SVGTextFrame and records a + * TextNodeCorrespondenceProperty on each for the number of undisplayed DOM + * characters between each frame. This is done by iterating simultaneously + * over the Text and nsTextFrames and noting when Text (or + * parts of them) are skipped when finding the next nsTextFrame. + */ +class TextNodeCorrespondenceRecorder { + public: + /** + * Entry point for the TextNodeCorrespondenceProperty recording. + */ + static void RecordCorrespondence(SVGTextFrame* aRoot); + + private: + explicit TextNodeCorrespondenceRecorder(SVGTextFrame* aRoot) + : mNodeIterator(aRoot->GetContent()), + mPreviousNode(nullptr), + mNodeCharIndex(0) {} + + void Record(SVGTextFrame* aRoot); + void TraverseAndRecord(nsIFrame* aFrame); + + /** + * Returns the next non-empty Text. + */ + Text* NextNode(); + + /** + * The iterator over the Text that we use as we simultaneously + * iterate over the nsTextFrames. + */ + TextNodeIterator mNodeIterator; + + /** + * The previous Text we iterated over. + */ + Text* mPreviousNode; + + /** + * The index into the current Text's character content. + */ + uint32_t mNodeCharIndex; +}; + +/* static */ +void TextNodeCorrespondenceRecorder::RecordCorrespondence(SVGTextFrame* aRoot) { + if (aRoot->HasAnyStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY)) { + // Resolve bidi so that continuation frames are created if necessary: + aRoot->MaybeResolveBidiForAnonymousBlockChild(); + TextNodeCorrespondenceRecorder recorder(aRoot); + recorder.Record(aRoot); + aRoot->RemoveStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY); + } +} + +void TextNodeCorrespondenceRecorder::Record(SVGTextFrame* aRoot) { + if (!mNodeIterator.Current()) { + // If there are no Text nodes then there is nothing to do. + return; + } + + // Traverse over all the nsTextFrames and record the number of undisplayed + // characters. + TraverseAndRecord(aRoot); + + // Find how many undisplayed characters there are after the final nsTextFrame. + uint32_t undisplayed = 0; + if (mNodeIterator.Current()) { + if (mPreviousNode && mPreviousNode->TextLength() != mNodeCharIndex) { + // The last nsTextFrame ended part way through a Text node. The + // remaining characters count as undisplayed. + NS_ASSERTION(mNodeCharIndex < mPreviousNode->TextLength(), + "incorrect tracking of undisplayed characters in " + "text nodes"); + undisplayed += mPreviousNode->TextLength() - mNodeCharIndex; + } + // All the remaining Text that we iterate must also be undisplayed. + for (Text* textNode = mNodeIterator.Current(); textNode; + textNode = NextNode()) { + undisplayed += textNode->TextLength(); + } + } + + // Record the trailing number of undisplayed characters on the + // SVGTextFrame. + aRoot->mTrailingUndisplayedCharacters = undisplayed; +} + +Text* TextNodeCorrespondenceRecorder::NextNode() { + mPreviousNode = mNodeIterator.Current(); + Text* next; + do { + next = mNodeIterator.Next(); + } while (next && next->TextLength() == 0); + return next; +} + +void TextNodeCorrespondenceRecorder::TraverseAndRecord(nsIFrame* aFrame) { + // Recursively iterate over the frame tree, for frames that correspond + // to text content elements. + if (IsTextContentElement(aFrame->GetContent())) { + for (nsIFrame* f : aFrame->PrincipalChildList()) { + TraverseAndRecord(f); + } + return; + } + + nsTextFrame* frame; // The current text frame. + Text* node; // The text node for the current text frame. + if (!GetNonEmptyTextFrameAndNode(aFrame, frame, node)) { + // If this isn't an nsTextFrame, or is empty, nothing to do. + return; + } + + NS_ASSERTION(frame->GetContentOffset() >= 0, + "don't know how to handle negative content indexes"); + + uint32_t undisplayed = 0; + if (!mPreviousNode) { + // Must be the very first text frame. + NS_ASSERTION(mNodeCharIndex == 0, + "incorrect tracking of undisplayed " + "characters in text nodes"); + if (!mNodeIterator.Current()) { + MOZ_ASSERT_UNREACHABLE( + "incorrect tracking of correspondence between " + "text frames and text nodes"); + } else { + // Each whole Text we find before we get to the text node for the + // first text frame must be undisplayed. + while (mNodeIterator.Current() != node) { + undisplayed += mNodeIterator.Current()->TextLength(); + NextNode(); + } + // If the first text frame starts at a non-zero content offset, then those + // earlier characters are also undisplayed. + undisplayed += frame->GetContentOffset(); + NextNode(); + } + } else if (mPreviousNode == node) { + // Same text node as last time. + if (static_cast<uint32_t>(frame->GetContentOffset()) != mNodeCharIndex) { + // We have some characters in the middle of the text node + // that are undisplayed. + NS_ASSERTION( + mNodeCharIndex < static_cast<uint32_t>(frame->GetContentOffset()), + "incorrect tracking of undisplayed characters in " + "text nodes"); + undisplayed = frame->GetContentOffset() - mNodeCharIndex; + } + } else { + // Different text node from last time. + if (mPreviousNode->TextLength() != mNodeCharIndex) { + NS_ASSERTION(mNodeCharIndex < mPreviousNode->TextLength(), + "incorrect tracking of undisplayed characters in " + "text nodes"); + // Any trailing characters at the end of the previous Text are + // undisplayed. + undisplayed = mPreviousNode->TextLength() - mNodeCharIndex; + } + // Each whole Text we find before we get to the text node for + // the current text frame must be undisplayed. + while (mNodeIterator.Current() && mNodeIterator.Current() != node) { + undisplayed += mNodeIterator.Current()->TextLength(); + NextNode(); + } + // If the current text frame starts at a non-zero content offset, then those + // earlier characters are also undisplayed. + undisplayed += frame->GetContentOffset(); + NextNode(); + } + + // Set the frame property. + frame->SetProperty(TextNodeCorrespondenceProperty(), + new TextNodeCorrespondence(undisplayed)); + + // Remember how far into the current Text we are. + mNodeCharIndex = frame->GetContentEnd(); +} + +// ---------------------------------------------------------------------------- +// TextFrameIterator + +/** + * An iterator class for nsTextFrames that are descendants of an + * SVGTextFrame. The iterator can optionally track whether the + * current nsTextFrame is for a descendant of, or past, a given subtree + * content node or frame. (This functionality is used for example by the SVG + * DOM text methods to get only the nsTextFrames for a particular <tspan>.) + * + * TextFrameIterator also tracks and exposes other information about the + * current nsTextFrame: + * + * * how many undisplayed characters came just before it + * * its position (in app units) relative to the SVGTextFrame's anonymous + * block frame + * * what nsInlineFrame corresponding to a <textPath> element it is a + * descendant of + * * what computed dominant-baseline value applies to it + * + * Note that any text frames that are empty -- whose ContentLength() is 0 -- + * will be skipped over. + */ +class MOZ_STACK_CLASS TextFrameIterator { + public: + /** + * Constructs a TextFrameIterator for the specified SVGTextFrame + * with an optional frame subtree to restrict iterated text frames to. + */ + explicit TextFrameIterator(SVGTextFrame* aRoot, + const nsIFrame* aSubtree = nullptr) + : mRootFrame(aRoot), + mSubtree(aSubtree), + mCurrentFrame(aRoot), + mCurrentPosition(), + mSubtreePosition(mSubtree ? eBeforeSubtree : eWithinSubtree) { + Init(); + } + + /** + * Constructs a TextFrameIterator for the specified SVGTextFrame + * with an optional frame content subtree to restrict iterated text frames to. + */ + TextFrameIterator(SVGTextFrame* aRoot, nsIContent* aSubtree) + : mRootFrame(aRoot), + mSubtree(aRoot && aSubtree && aSubtree != aRoot->GetContent() + ? aSubtree->GetPrimaryFrame() + : nullptr), + mCurrentFrame(aRoot), + mCurrentPosition(), + mSubtreePosition(mSubtree ? eBeforeSubtree : eWithinSubtree) { + Init(); + } + + /** + * Returns the root SVGTextFrame this TextFrameIterator is iterating over. + */ + SVGTextFrame* Root() const { return mRootFrame; } + + /** + * Returns the current nsTextFrame. + */ + nsTextFrame* Current() const { return do_QueryFrame(mCurrentFrame); } + + /** + * Returns the number of undisplayed characters in the DOM just before the + * current frame. + */ + uint32_t UndisplayedCharacters() const; + + /** + * Returns the current frame's position, in app units, relative to the + * root SVGTextFrame's anonymous block frame. + */ + nsPoint Position() const { return mCurrentPosition; } + + /** + * Advances to the next nsTextFrame and returns it. + */ + nsTextFrame* Next(); + + /** + * Returns whether the iterator is within the subtree. + */ + bool IsWithinSubtree() const { return mSubtreePosition == eWithinSubtree; } + + /** + * Returns whether the iterator is past the subtree. + */ + bool IsAfterSubtree() const { return mSubtreePosition == eAfterSubtree; } + + /** + * Returns the frame corresponding to the <textPath> element, if we + * are inside one. + */ + nsIFrame* TextPathFrame() const { + return mTextPathFrames.IsEmpty() + ? nullptr + : mTextPathFrames.ElementAt(mTextPathFrames.Length() - 1); + } + + /** + * Returns the current frame's computed dominant-baseline value. + */ + StyleDominantBaseline DominantBaseline() const { + return mBaselines.ElementAt(mBaselines.Length() - 1); + } + + /** + * Finishes the iterator. + */ + void Close() { mCurrentFrame = nullptr; } + + private: + /** + * Initializes the iterator and advances to the first item. + */ + void Init() { + if (!mRootFrame) { + return; + } + + mBaselines.AppendElement(mRootFrame->StyleSVG()->mDominantBaseline); + Next(); + } + + /** + * Pushes the specified frame's computed dominant-baseline value. + * If the value of the property is "auto", then the parent frame's + * computed value is used. + */ + void PushBaseline(nsIFrame* aNextFrame); + + /** + * Pops the current dominant-baseline off the stack. + */ + void PopBaseline(); + + /** + * The root frame we are iterating through. + */ + SVGTextFrame* const mRootFrame; + + /** + * The frame for the subtree we are also interested in tracking. + */ + const nsIFrame* const mSubtree; + + /** + * The current value of the iterator. + */ + nsIFrame* mCurrentFrame; + + /** + * The position, in app units, of the current frame relative to mRootFrame. + */ + nsPoint mCurrentPosition; + + /** + * Stack of frames corresponding to <textPath> elements that are in scope + * for the current frame. + */ + AutoTArray<nsIFrame*, 1> mTextPathFrames; + + /** + * Stack of dominant-baseline values to record as we traverse through the + * frame tree. + */ + AutoTArray<StyleDominantBaseline, 8> mBaselines; + + /** + * The iterator's current position relative to mSubtree. + */ + SubtreePosition mSubtreePosition; +}; + +uint32_t TextFrameIterator::UndisplayedCharacters() const { + MOZ_ASSERT( + !mRootFrame->HasAnyStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY), + "Text correspondence must be up to date"); + + if (!mCurrentFrame) { + return mRootFrame->mTrailingUndisplayedCharacters; + } + + nsTextFrame* frame = do_QueryFrame(mCurrentFrame); + return GetUndisplayedCharactersBeforeFrame(frame); +} + +nsTextFrame* TextFrameIterator::Next() { + // Starting from mCurrentFrame, we do a non-recursive traversal to the next + // nsTextFrame beneath mRoot, updating mSubtreePosition appropriately if we + // encounter mSubtree. + if (mCurrentFrame) { + do { + nsIFrame* next = IsTextContentElement(mCurrentFrame->GetContent()) + ? mCurrentFrame->PrincipalChildList().FirstChild() + : nullptr; + if (next) { + // Descend into this frame, and accumulate its position. + mCurrentPosition += next->GetPosition(); + if (next->GetContent()->IsSVGElement(nsGkAtoms::textPath)) { + // Record this <textPath> frame. + mTextPathFrames.AppendElement(next); + } + // Record the frame's baseline. + PushBaseline(next); + mCurrentFrame = next; + if (mCurrentFrame == mSubtree) { + // If the current frame is mSubtree, we have now moved into it. + mSubtreePosition = eWithinSubtree; + } + } else { + for (;;) { + // We want to move past the current frame. + if (mCurrentFrame == mRootFrame) { + // If we've reached the root frame, we're finished. + mCurrentFrame = nullptr; + break; + } + // Remove the current frame's position. + mCurrentPosition -= mCurrentFrame->GetPosition(); + if (mCurrentFrame->GetContent()->IsSVGElement(nsGkAtoms::textPath)) { + // Pop off the <textPath> frame if this is a <textPath>. + mTextPathFrames.RemoveLastElement(); + } + // Pop off the current baseline. + PopBaseline(); + if (mCurrentFrame == mSubtree) { + // If this was mSubtree, we have now moved past it. + mSubtreePosition = eAfterSubtree; + } + next = mCurrentFrame->GetNextSibling(); + if (next) { + // Moving to the next sibling. + mCurrentPosition += next->GetPosition(); + if (next->GetContent()->IsSVGElement(nsGkAtoms::textPath)) { + // Record this <textPath> frame. + mTextPathFrames.AppendElement(next); + } + // Record the frame's baseline. + PushBaseline(next); + mCurrentFrame = next; + if (mCurrentFrame == mSubtree) { + // If the current frame is mSubtree, we have now moved into it. + mSubtreePosition = eWithinSubtree; + } + break; + } + if (mCurrentFrame == mSubtree) { + // If there is no next sibling frame, and the current frame is + // mSubtree, we have now moved past it. + mSubtreePosition = eAfterSubtree; + } + // Ascend out of this frame. + mCurrentFrame = mCurrentFrame->GetParent(); + } + } + } while (mCurrentFrame && !IsNonEmptyTextFrame(mCurrentFrame)); + } + + return Current(); +} + +void TextFrameIterator::PushBaseline(nsIFrame* aNextFrame) { + StyleDominantBaseline baseline = aNextFrame->StyleSVG()->mDominantBaseline; + mBaselines.AppendElement(baseline); +} + +void TextFrameIterator::PopBaseline() { + NS_ASSERTION(!mBaselines.IsEmpty(), "popped too many baselines"); + mBaselines.RemoveLastElement(); +} + +// ----------------------------------------------------------------------------- +// TextRenderedRunIterator + +/** + * Iterator for TextRenderedRun objects for the SVGTextFrame. + */ +class TextRenderedRunIterator { + public: + /** + * Values for the aFilter argument of the constructor, to indicate which + * frames we should be limited to iterating TextRenderedRun objects for. + */ + enum RenderedRunFilter { + // Iterate TextRenderedRuns for all nsTextFrames. + eAllFrames, + // Iterate only TextRenderedRuns for nsTextFrames that are + // visibility:visible. + eVisibleFrames + }; + + /** + * Constructs a TextRenderedRunIterator with an optional frame subtree to + * restrict iterated rendered runs to. + * + * @param aSVGTextFrame The SVGTextFrame whose rendered runs to iterate + * through. + * @param aFilter Indicates whether to iterate rendered runs for non-visible + * nsTextFrames. + * @param aSubtree An optional frame subtree to restrict iterated rendered + * runs to. + */ + explicit TextRenderedRunIterator(SVGTextFrame* aSVGTextFrame, + RenderedRunFilter aFilter = eAllFrames, + const nsIFrame* aSubtree = nullptr) + : mFrameIterator(FrameIfAnonymousChildReflowed(aSVGTextFrame), aSubtree), + mFilter(aFilter), + mTextElementCharIndex(0), + mFrameStartTextElementCharIndex(0), + mFontSizeScaleFactor(aSVGTextFrame->mFontSizeScaleFactor), + mCurrent(First()) {} + + /** + * Constructs a TextRenderedRunIterator with a content subtree to restrict + * iterated rendered runs to. + * + * @param aSVGTextFrame The SVGTextFrame whose rendered runs to iterate + * through. + * @param aFilter Indicates whether to iterate rendered runs for non-visible + * nsTextFrames. + * @param aSubtree A content subtree to restrict iterated rendered runs to. + */ + TextRenderedRunIterator(SVGTextFrame* aSVGTextFrame, + RenderedRunFilter aFilter, nsIContent* aSubtree) + : mFrameIterator(FrameIfAnonymousChildReflowed(aSVGTextFrame), aSubtree), + mFilter(aFilter), + mTextElementCharIndex(0), + mFrameStartTextElementCharIndex(0), + mFontSizeScaleFactor(aSVGTextFrame->mFontSizeScaleFactor), + mCurrent(First()) {} + + /** + * Returns the current TextRenderedRun. + */ + TextRenderedRun Current() const { return mCurrent; } + + /** + * Advances to the next TextRenderedRun and returns it. + */ + TextRenderedRun Next(); + + private: + /** + * Returns the root SVGTextFrame this iterator is for. + */ + SVGTextFrame* Root() const { return mFrameIterator.Root(); } + + /** + * Advances to the first TextRenderedRun and returns it. + */ + TextRenderedRun First(); + + /** + * The frame iterator to use. + */ + TextFrameIterator mFrameIterator; + + /** + * The filter indicating which TextRenderedRuns to return. + */ + RenderedRunFilter mFilter; + + /** + * The character index across the entire <text> element we are currently + * up to. + */ + uint32_t mTextElementCharIndex; + + /** + * The character index across the entire <text> for the start of the current + * frame. + */ + uint32_t mFrameStartTextElementCharIndex; + + /** + * The font-size scale factor we used when constructing the nsTextFrames. + */ + double mFontSizeScaleFactor; + + /** + * The current TextRenderedRun. + */ + TextRenderedRun mCurrent; +}; + +TextRenderedRun TextRenderedRunIterator::Next() { + if (!mFrameIterator.Current()) { + // If there are no more frames, then there are no more rendered runs to + // return. + mCurrent = TextRenderedRun(); + return mCurrent; + } + + // The values we will use to initialize the TextRenderedRun with. + nsTextFrame* frame; + gfxPoint pt; + double rotate; + nscoord baseline; + uint32_t offset, length; + uint32_t charIndex; + + // We loop, because we want to skip over rendered runs that either aren't + // within our subtree of interest, because they don't match the filter, + // or because they are hidden due to having fallen off the end of a + // <textPath>. + for (;;) { + if (mFrameIterator.IsAfterSubtree()) { + mCurrent = TextRenderedRun(); + return mCurrent; + } + + frame = mFrameIterator.Current(); + + charIndex = mTextElementCharIndex; + + // Find the end of the rendered run, by looking through the + // SVGTextFrame's positions array until we find one that is recorded + // as a run boundary. + uint32_t runStart, + runEnd; // XXX Replace runStart with mTextElementCharIndex. + runStart = mTextElementCharIndex; + runEnd = runStart + 1; + while (runEnd < Root()->mPositions.Length() && + !Root()->mPositions[runEnd].mRunBoundary) { + runEnd++; + } + + // Convert the global run start/end indexes into an offset/length into the + // current frame's Text. + offset = + frame->GetContentOffset() + runStart - mFrameStartTextElementCharIndex; + length = runEnd - runStart; + + // If the end of the frame's content comes before the run boundary we found + // in SVGTextFrame's position array, we need to shorten the rendered run. + uint32_t contentEnd = frame->GetContentEnd(); + if (offset + length > contentEnd) { + length = contentEnd - offset; + } + + NS_ASSERTION(offset >= uint32_t(frame->GetContentOffset()), + "invalid offset"); + NS_ASSERTION(offset + length <= contentEnd, "invalid offset or length"); + + // Get the frame's baseline position. + frame->EnsureTextRun(nsTextFrame::eInflated); + baseline = GetBaselinePosition( + frame, frame->GetTextRun(nsTextFrame::eInflated), + mFrameIterator.DominantBaseline(), mFontSizeScaleFactor); + + // Trim the offset/length to remove any leading/trailing white space. + uint32_t untrimmedOffset = offset; + uint32_t untrimmedLength = length; + nsTextFrame::TrimmedOffsets trimmedOffsets = + frame->GetTrimmedOffsets(frame->TextFragment()); + TrimOffsets(offset, length, trimmedOffsets); + charIndex += offset - untrimmedOffset; + + // Get the position and rotation of the character that begins this + // rendered run. + pt = Root()->mPositions[charIndex].mPosition; + rotate = Root()->mPositions[charIndex].mAngle; + + // Determine if we should skip this rendered run. + bool skip = !mFrameIterator.IsWithinSubtree() || + Root()->mPositions[mTextElementCharIndex].mHidden; + if (mFilter == eVisibleFrames) { + skip = skip || !frame->StyleVisibility()->IsVisible(); + } + + // Update our global character index to move past the characters + // corresponding to this rendered run. + mTextElementCharIndex += untrimmedLength; + + // If we have moved past the end of the current frame's content, we need to + // advance to the next frame. + if (offset + untrimmedLength >= contentEnd) { + mFrameIterator.Next(); + mTextElementCharIndex += mFrameIterator.UndisplayedCharacters(); + mFrameStartTextElementCharIndex = mTextElementCharIndex; + } + + if (!mFrameIterator.Current()) { + if (skip) { + // That was the last frame, and we skipped this rendered run. So we + // have no rendered run to return. + mCurrent = TextRenderedRun(); + return mCurrent; + } + break; + } + + if (length && !skip) { + // Only return a rendered run if it didn't get collapsed away entirely + // (due to it being all white space) and if we don't want to skip it. + break; + } + } + + mCurrent = TextRenderedRun(frame, pt, Root()->mLengthAdjustScaleFactor, + rotate, mFontSizeScaleFactor, baseline, offset, + length, charIndex); + return mCurrent; +} + +TextRenderedRun TextRenderedRunIterator::First() { + if (!mFrameIterator.Current()) { + return TextRenderedRun(); + } + + if (Root()->mPositions.IsEmpty()) { + mFrameIterator.Close(); + return TextRenderedRun(); + } + + // Get the character index for the start of this rendered run, by skipping + // any undisplayed characters. + mTextElementCharIndex = mFrameIterator.UndisplayedCharacters(); + mFrameStartTextElementCharIndex = mTextElementCharIndex; + + return Next(); +} + +// ----------------------------------------------------------------------------- +// CharIterator + +/** + * Iterator for characters within an SVGTextFrame. + */ +class MOZ_STACK_CLASS CharIterator { + using Range = gfxTextRun::Range; + + public: + /** + * Values for the aFilter argument of the constructor, to indicate which + * characters we should be iterating over. + */ + enum CharacterFilter { + // Iterate over all original characters from the DOM that are within valid + // text content elements. + eOriginal, + // Iterate only over characters that are not skipped characters. + eUnskipped, + // Iterate only over characters that are addressable by the positioning + // attributes x="", y="", etc. This includes all characters after + // collapsing white space as required by the value of 'white-space'. + eAddressable, + }; + + /** + * Constructs a CharIterator. + * + * @param aSVGTextFrame The SVGTextFrame whose characters to iterate + * through. + * @param aFilter Indicates which characters to iterate over. + * @param aSubtree A content subtree to track whether the current character + * is within. + */ + CharIterator(SVGTextFrame* aSVGTextFrame, CharacterFilter aFilter, + nsIContent* aSubtree, bool aPostReflow = true); + + /** + * Returns whether the iterator is finished. + */ + bool AtEnd() const { return !mFrameIterator.Current(); } + + /** + * Advances to the next matching character. Returns true if there was a + * character to advance to, and false otherwise. + */ + bool Next(); + + /** + * Advances ahead aCount matching characters. Returns true if there were + * enough characters to advance past, and false otherwise. + */ + bool Next(uint32_t aCount); + + /** + * Advances ahead up to aCount matching characters. + */ + void NextWithinSubtree(uint32_t aCount); + + /** + * Advances to the character with the specified index. The index is in the + * space of original characters (i.e., all DOM characters under the <text> + * that are within valid text content elements). + */ + bool AdvanceToCharacter(uint32_t aTextElementCharIndex); + + /** + * Advances to the first matching character after the current nsTextFrame. + */ + bool AdvancePastCurrentFrame(); + + /** + * Advances to the first matching character after the frames within + * the current <textPath>. + */ + bool AdvancePastCurrentTextPathFrame(); + + /** + * Advances to the first matching character of the subtree. Returns true + * if we successfully advance to the subtree, or if we are already within + * the subtree. Returns false if we are past the subtree. + */ + bool AdvanceToSubtree(); + + /** + * Returns the nsTextFrame for the current character. + */ + nsTextFrame* TextFrame() const { return mFrameIterator.Current(); } + + /** + * Returns whether the iterator is within the subtree. + */ + bool IsWithinSubtree() const { return mFrameIterator.IsWithinSubtree(); } + + /** + * Returns whether the iterator is past the subtree. + */ + bool IsAfterSubtree() const { return mFrameIterator.IsAfterSubtree(); } + + /** + * Returns whether the current character is a skipped character. + */ + bool IsOriginalCharSkipped() const { + return mSkipCharsIterator.IsOriginalCharSkipped(); + } + + /** + * Returns whether the current character is the start of a cluster and + * ligature group. + */ + bool IsClusterAndLigatureGroupStart() const { + return mTextRun->IsLigatureGroupStart( + mSkipCharsIterator.GetSkippedOffset()) && + mTextRun->IsClusterStart(mSkipCharsIterator.GetSkippedOffset()); + } + + /** + * Returns the glyph run for the current character. + */ + const gfxTextRun::GlyphRun& GlyphRun() const { + return *mTextRun->FindFirstGlyphRunContaining( + mSkipCharsIterator.GetSkippedOffset()); + } + + /** + * Returns whether the current character is trimmed away when painting, + * due to it being leading/trailing white space. + */ + bool IsOriginalCharTrimmed() const; + + /** + * Returns whether the current character is unaddressable from the SVG glyph + * positioning attributes. + */ + bool IsOriginalCharUnaddressable() const { + return IsOriginalCharSkipped() || IsOriginalCharTrimmed(); + } + + /** + * Returns the text run for the current character. + */ + gfxTextRun* TextRun() const { return mTextRun; } + + /** + * Returns the current character index. + */ + uint32_t TextElementCharIndex() const { return mTextElementCharIndex; } + + /** + * Returns the character index for the start of the cluster/ligature group it + * is part of. + */ + uint32_t GlyphStartTextElementCharIndex() const { + return mGlyphStartTextElementCharIndex; + } + + /** + * Gets the advance, in user units, of the current character. If the + * character is a part of ligature, then the advance returned will be + * a fraction of the ligature glyph's advance. + * + * @param aContext The context to use for unit conversions. + */ + gfxFloat GetAdvance(nsPresContext* aContext) const; + + /** + * Returns the frame corresponding to the <textPath> that the current + * character is within. + */ + nsIFrame* TextPathFrame() const { return mFrameIterator.TextPathFrame(); } + +#ifdef DEBUG + /** + * Returns the subtree we were constructed with. + */ + nsIContent* GetSubtree() const { return mSubtree; } + + /** + * Returns the CharacterFilter mode in use. + */ + CharacterFilter Filter() const { return mFilter; } +#endif + + private: + /** + * Advances to the next character without checking it against the filter. + * Returns true if there was a next character to advance to, or false + * otherwise. + */ + bool NextCharacter(); + + /** + * Returns whether the current character matches the filter. + */ + bool MatchesFilter() const; + + /** + * If this is the start of a glyph, record it. + */ + void UpdateGlyphStartTextElementCharIndex() { + if (!IsOriginalCharSkipped() && IsClusterAndLigatureGroupStart()) { + mGlyphStartTextElementCharIndex = mTextElementCharIndex; + } + } + + /** + * The filter to use. + */ + CharacterFilter mFilter; + + /** + * The iterator for text frames. + */ + TextFrameIterator mFrameIterator; + +#ifdef DEBUG + /** + * The subtree we were constructed with. + */ + nsIContent* const mSubtree; +#endif + + /** + * A gfxSkipCharsIterator for the text frame the current character is + * a part of. + */ + gfxSkipCharsIterator mSkipCharsIterator; + + // Cache for information computed by IsOriginalCharTrimmed. + mutable nsTextFrame* mFrameForTrimCheck; + mutable uint32_t mTrimmedOffset; + mutable uint32_t mTrimmedLength; + + /** + * The text run the current character is a part of. + */ + gfxTextRun* mTextRun; + + /** + * The current character's index. + */ + uint32_t mTextElementCharIndex; + + /** + * The index of the character that starts the cluster/ligature group the + * current character is a part of. + */ + uint32_t mGlyphStartTextElementCharIndex; + + /** + * The scale factor to apply to glyph advances returned by + * GetAdvance etc. to take into account textLength="". + */ + float mLengthAdjustScaleFactor; + + /** + * Whether the instance of this class is being used after reflow has occurred + * or not. + */ + bool mPostReflow; +}; + +CharIterator::CharIterator(SVGTextFrame* aSVGTextFrame, + CharIterator::CharacterFilter aFilter, + nsIContent* aSubtree, bool aPostReflow) + : mFilter(aFilter), + mFrameIterator(aSVGTextFrame, aSubtree), +#ifdef DEBUG + mSubtree(aSubtree), +#endif + mFrameForTrimCheck(nullptr), + mTrimmedOffset(0), + mTrimmedLength(0), + mTextRun(nullptr), + mTextElementCharIndex(0), + mGlyphStartTextElementCharIndex(0), + mLengthAdjustScaleFactor(aSVGTextFrame->mLengthAdjustScaleFactor), + mPostReflow(aPostReflow) { + if (!AtEnd()) { + mSkipCharsIterator = TextFrame()->EnsureTextRun(nsTextFrame::eInflated); + mTextRun = TextFrame()->GetTextRun(nsTextFrame::eInflated); + mTextElementCharIndex = mFrameIterator.UndisplayedCharacters(); + UpdateGlyphStartTextElementCharIndex(); + if (!MatchesFilter()) { + Next(); + } + } +} + +bool CharIterator::Next() { + while (NextCharacter()) { + if (MatchesFilter()) { + return true; + } + } + return false; +} + +bool CharIterator::Next(uint32_t aCount) { + if (aCount == 0 && AtEnd()) { + return false; + } + while (aCount) { + if (!Next()) { + return false; + } + aCount--; + } + return true; +} + +void CharIterator::NextWithinSubtree(uint32_t aCount) { + while (IsWithinSubtree() && aCount) { + --aCount; + if (!Next()) { + return; + } + } +} + +bool CharIterator::AdvanceToCharacter(uint32_t aTextElementCharIndex) { + while (mTextElementCharIndex < aTextElementCharIndex) { + if (!Next()) { + return false; + } + } + return true; +} + +bool CharIterator::AdvancePastCurrentFrame() { + // XXX Can do this better than one character at a time if it matters. + nsTextFrame* currentFrame = TextFrame(); + do { + if (!Next()) { + return false; + } + } while (TextFrame() == currentFrame); + return true; +} + +bool CharIterator::AdvancePastCurrentTextPathFrame() { + nsIFrame* currentTextPathFrame = TextPathFrame(); + NS_ASSERTION(currentTextPathFrame, + "expected AdvancePastCurrentTextPathFrame to be called only " + "within a text path frame"); + do { + if (!AdvancePastCurrentFrame()) { + return false; + } + } while (TextPathFrame() == currentTextPathFrame); + return true; +} + +bool CharIterator::AdvanceToSubtree() { + while (!IsWithinSubtree()) { + if (IsAfterSubtree()) { + return false; + } + if (!AdvancePastCurrentFrame()) { + return false; + } + } + return true; +} + +bool CharIterator::IsOriginalCharTrimmed() const { + if (mFrameForTrimCheck != TextFrame()) { + // Since we do a lot of trim checking, we cache the trimmed offsets and + // lengths while we are in the same frame. + mFrameForTrimCheck = TextFrame(); + uint32_t offset = mFrameForTrimCheck->GetContentOffset(); + uint32_t length = mFrameForTrimCheck->GetContentLength(); + nsTextFrame::TrimmedOffsets trim = mFrameForTrimCheck->GetTrimmedOffsets( + mFrameForTrimCheck->TextFragment(), + (mPostReflow ? nsTextFrame::TrimmedOffsetFlags::Default + : nsTextFrame::TrimmedOffsetFlags::NotPostReflow)); + TrimOffsets(offset, length, trim); + mTrimmedOffset = offset; + mTrimmedLength = length; + } + + // A character is trimmed if it is outside the mTrimmedOffset/mTrimmedLength + // range and it is not a significant newline character. + uint32_t index = mSkipCharsIterator.GetOriginalOffset(); + return !( + (index >= mTrimmedOffset && index < mTrimmedOffset + mTrimmedLength) || + (index >= mTrimmedOffset + mTrimmedLength && + mFrameForTrimCheck->StyleText()->NewlineIsSignificant( + mFrameForTrimCheck) && + mFrameForTrimCheck->TextFragment()->CharAt(index) == '\n')); +} + +gfxFloat CharIterator::GetAdvance(nsPresContext* aContext) const { + float cssPxPerDevPx = + nsPresContext::AppUnitsToFloatCSSPixels(aContext->AppUnitsPerDevPixel()); + + gfxSkipCharsIterator start = + TextFrame()->EnsureTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(TextFrame(), start); + + uint32_t offset = mSkipCharsIterator.GetSkippedOffset(); + gfxFloat advance = + mTextRun->GetAdvanceWidth(Range(offset, offset + 1), &provider); + return aContext->AppUnitsToGfxUnits(advance) * mLengthAdjustScaleFactor * + cssPxPerDevPx; +} + +bool CharIterator::NextCharacter() { + if (AtEnd()) { + return false; + } + + mTextElementCharIndex++; + + // Advance within the current text run. + mSkipCharsIterator.AdvanceOriginal(1); + if (mSkipCharsIterator.GetOriginalOffset() < TextFrame()->GetContentEnd()) { + // We're still within the part of the text run for the current text frame. + UpdateGlyphStartTextElementCharIndex(); + return true; + } + + // Advance to the next frame. + mFrameIterator.Next(); + + // Skip any undisplayed characters. + uint32_t undisplayed = mFrameIterator.UndisplayedCharacters(); + mTextElementCharIndex += undisplayed; + if (!TextFrame()) { + // We're at the end. + mSkipCharsIterator = gfxSkipCharsIterator(); + return false; + } + + mSkipCharsIterator = TextFrame()->EnsureTextRun(nsTextFrame::eInflated); + mTextRun = TextFrame()->GetTextRun(nsTextFrame::eInflated); + UpdateGlyphStartTextElementCharIndex(); + return true; +} + +bool CharIterator::MatchesFilter() const { + switch (mFilter) { + case eOriginal: + return true; + case eUnskipped: + return !IsOriginalCharSkipped(); + case eAddressable: + return !IsOriginalCharSkipped() && !IsOriginalCharUnaddressable(); + } + MOZ_ASSERT_UNREACHABLE("Invalid mFilter value"); + return true; +} + +// ----------------------------------------------------------------------------- +// SVGTextDrawPathCallbacks + +/** + * Text frame draw callback class that paints the text and text decoration parts + * of an nsTextFrame using SVG painting properties, and selection backgrounds + * and decorations as they would normally. + * + * An instance of this class is passed to nsTextFrame::PaintText if painting + * cannot be done directly (e.g. if we are using an SVG pattern fill, stroking + * the text, etc.). + */ +class SVGTextDrawPathCallbacks final : public nsTextFrame::DrawPathCallbacks { + using imgDrawingParams = image::imgDrawingParams; + + public: + /** + * Constructs an SVGTextDrawPathCallbacks. + * + * @param aSVGTextFrame The ancestor text frame. + * @param aContext The context to use for painting. + * @param aFrame The nsTextFrame to paint. + * @param aCanvasTM The transformation matrix to set when painting; this + * should be the FOR_OUTERSVG_TM canvas TM of the text, so that + * paint servers are painted correctly. + * @param aImgParams Whether we need to synchronously decode images. + * @param aShouldPaintSVGGlyphs Whether SVG glyphs should be painted. + */ + SVGTextDrawPathCallbacks(SVGTextFrame* aSVGTextFrame, gfxContext& aContext, + nsTextFrame* aFrame, const gfxMatrix& aCanvasTM, + imgDrawingParams& aImgParams, + bool aShouldPaintSVGGlyphs) + : DrawPathCallbacks(aShouldPaintSVGGlyphs), + mSVGTextFrame(aSVGTextFrame), + mContext(aContext), + mFrame(aFrame), + mCanvasTM(aCanvasTM), + mImgParams(aImgParams), + mColor(0) {} + + void NotifySelectionBackgroundNeedsFill(const Rect& aBackgroundRect, + nscolor aColor, + DrawTarget& aDrawTarget) override; + void PaintDecorationLine(Rect aPath, nscolor aColor) override; + void PaintSelectionDecorationLine(Rect aPath, nscolor aColor) override; + void NotifyBeforeText(nscolor aColor) override; + void NotifyGlyphPathEmitted() override; + void NotifyAfterText() override; + + private: + void SetupContext(); + + bool IsClipPathChild() const { + return mSVGTextFrame->HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD); + } + + /** + * Paints a piece of text geometry. This is called when glyphs + * or text decorations have been emitted to the gfxContext. + */ + void HandleTextGeometry(); + + /** + * Sets the gfxContext paint to the appropriate color or pattern + * for filling text geometry. + */ + void MakeFillPattern(GeneralPattern* aOutPattern); + + /** + * Fills and strokes a piece of text geometry, using group opacity + * if the selection style requires it. + */ + void FillAndStrokeGeometry(); + + /** + * Fills a piece of text geometry. + */ + void FillGeometry(); + + /** + * Strokes a piece of text geometry. + */ + void StrokeGeometry(); + + SVGTextFrame* const mSVGTextFrame; + gfxContext& mContext; + nsTextFrame* const mFrame; + const gfxMatrix& mCanvasTM; + imgDrawingParams& mImgParams; + + /** + * The color that we were last told from one of the path callback functions. + * This color can be the special NS_SAME_AS_FOREGROUND_COLOR, + * NS_40PERCENT_FOREGROUND_COLOR and NS_TRANSPARENT colors when we are + * painting selections or IME decorations. + */ + nscolor mColor; +}; + +void SVGTextDrawPathCallbacks::NotifySelectionBackgroundNeedsFill( + const Rect& aBackgroundRect, nscolor aColor, DrawTarget& aDrawTarget) { + if (IsClipPathChild()) { + // Don't paint selection backgrounds when in a clip path. + return; + } + + mColor = aColor; // currently needed by MakeFillPattern + + GeneralPattern fillPattern; + MakeFillPattern(&fillPattern); + if (fillPattern.GetPattern()) { + DrawOptions drawOptions(aColor == NS_40PERCENT_FOREGROUND_COLOR ? 0.4 + : 1.0); + aDrawTarget.FillRect(aBackgroundRect, fillPattern, drawOptions); + } +} + +void SVGTextDrawPathCallbacks::NotifyBeforeText(nscolor aColor) { + mColor = aColor; + SetupContext(); + mContext.NewPath(); +} + +void SVGTextDrawPathCallbacks::NotifyGlyphPathEmitted() { + HandleTextGeometry(); + mContext.NewPath(); +} + +void SVGTextDrawPathCallbacks::NotifyAfterText() { mContext.Restore(); } + +void SVGTextDrawPathCallbacks::PaintDecorationLine(Rect aPath, nscolor aColor) { + mColor = aColor; + AntialiasMode aaMode = + SVGUtils::ToAntialiasMode(mFrame->StyleText()->mTextRendering); + + mContext.Save(); + mContext.NewPath(); + mContext.SetAntialiasMode(aaMode); + mContext.Rectangle(ThebesRect(aPath)); + HandleTextGeometry(); + mContext.NewPath(); + mContext.Restore(); +} + +void SVGTextDrawPathCallbacks::PaintSelectionDecorationLine(Rect aPath, + nscolor aColor) { + if (IsClipPathChild()) { + // Don't paint selection decorations when in a clip path. + return; + } + + mColor = aColor; + + mContext.Save(); + mContext.NewPath(); + mContext.Rectangle(ThebesRect(aPath)); + FillAndStrokeGeometry(); + mContext.Restore(); +} + +void SVGTextDrawPathCallbacks::SetupContext() { + mContext.Save(); + + // XXX This is copied from nsSVGGlyphFrame::Render, but cairo doesn't actually + // seem to do anything with the antialias mode. So we can perhaps remove it, + // or make SetAntialiasMode set cairo text antialiasing too. + switch (mFrame->StyleText()->mTextRendering) { + case StyleTextRendering::Optimizespeed: + mContext.SetAntialiasMode(AntialiasMode::NONE); + break; + default: + mContext.SetAntialiasMode(AntialiasMode::SUBPIXEL); + break; + } +} + +void SVGTextDrawPathCallbacks::HandleTextGeometry() { + if (IsClipPathChild()) { + RefPtr<Path> path = mContext.GetPath(); + ColorPattern white( + DeviceColor(1.f, 1.f, 1.f, 1.f)); // for masking, so no ToDeviceColor + mContext.GetDrawTarget()->Fill(path, white); + } else { + // Normal painting. + gfxContextMatrixAutoSaveRestore saveMatrix(&mContext); + mContext.SetMatrixDouble(mCanvasTM); + + FillAndStrokeGeometry(); + } +} + +void SVGTextDrawPathCallbacks::MakeFillPattern(GeneralPattern* aOutPattern) { + if (mColor == NS_SAME_AS_FOREGROUND_COLOR || + mColor == NS_40PERCENT_FOREGROUND_COLOR) { + SVGUtils::MakeFillPatternFor(mFrame, &mContext, aOutPattern, mImgParams); + return; + } + + if (mColor == NS_TRANSPARENT) { + return; + } + + aOutPattern->InitColorPattern(ToDeviceColor(mColor)); +} + +void SVGTextDrawPathCallbacks::FillAndStrokeGeometry() { + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&mContext); + if (mColor == NS_40PERCENT_FOREGROUND_COLOR) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, 0.4f); + } + + uint32_t paintOrder = mFrame->StyleSVG()->mPaintOrder; + if (!paintOrder) { + FillGeometry(); + StrokeGeometry(); + } else { + while (paintOrder) { + auto component = StylePaintOrder(paintOrder & kPaintOrderMask); + switch (component) { + case StylePaintOrder::Fill: + FillGeometry(); + break; + case StylePaintOrder::Stroke: + StrokeGeometry(); + break; + default: + MOZ_FALLTHROUGH_ASSERT("Unknown paint-order value"); + case StylePaintOrder::Markers: + case StylePaintOrder::Normal: + break; + } + paintOrder >>= kPaintOrderShift; + } + } +} + +void SVGTextDrawPathCallbacks::FillGeometry() { + GeneralPattern fillPattern; + MakeFillPattern(&fillPattern); + if (fillPattern.GetPattern()) { + RefPtr<Path> path = mContext.GetPath(); + FillRule fillRule = SVGUtils::ToFillRule(mFrame->StyleSVG()->mFillRule); + if (fillRule != path->GetFillRule()) { + RefPtr<PathBuilder> builder = path->CopyToBuilder(fillRule); + path = builder->Finish(); + } + mContext.GetDrawTarget()->Fill(path, fillPattern); + } +} + +void SVGTextDrawPathCallbacks::StrokeGeometry() { + // We don't paint the stroke when we are filling with a selection color. + if (mColor == NS_SAME_AS_FOREGROUND_COLOR || + mColor == NS_40PERCENT_FOREGROUND_COLOR) { + if (SVGUtils::HasStroke(mFrame, /*aContextPaint*/ nullptr)) { + GeneralPattern strokePattern; + SVGUtils::MakeStrokePatternFor(mFrame, &mContext, &strokePattern, + mImgParams, /*aContextPaint*/ nullptr); + if (strokePattern.GetPattern()) { + if (!mFrame->GetParent()->GetContent()->IsSVGElement()) { + // The cast that follows would be unsafe + MOZ_ASSERT(false, "Our nsTextFrame's parent's content should be SVG"); + return; + } + SVGElement* svgOwner = + static_cast<SVGElement*>(mFrame->GetParent()->GetContent()); + + // Apply any stroke-specific transform + gfxMatrix outerSVGToUser; + if (SVGUtils::GetNonScalingStrokeTransform(mFrame, &outerSVGToUser) && + outerSVGToUser.Invert()) { + mContext.Multiply(outerSVGToUser); + } + + RefPtr<Path> path = mContext.GetPath(); + SVGContentUtils::AutoStrokeOptions strokeOptions; + SVGContentUtils::GetStrokeOptions(&strokeOptions, svgOwner, + mFrame->Style(), + /*aContextPaint*/ nullptr); + DrawOptions drawOptions; + drawOptions.mAntialiasMode = + SVGUtils::ToAntialiasMode(mFrame->StyleText()->mTextRendering); + mContext.GetDrawTarget()->Stroke(path, strokePattern, strokeOptions); + } + } + } +} + +// ============================================================================ +// SVGTextFrame + +// ---------------------------------------------------------------------------- +// Display list item + +class DisplaySVGText final : public DisplaySVGItem { + public: + DisplaySVGText(nsDisplayListBuilder* aBuilder, SVGTextFrame* aFrame) + : DisplaySVGItem(aBuilder, aFrame) { + MOZ_COUNT_CTOR(DisplaySVGText); + } + + MOZ_COUNTED_DTOR_OVERRIDE(DisplaySVGText) + + NS_DISPLAY_DECL_NAME("DisplaySVGText", TYPE_SVG_TEXT) + + nsDisplayItemGeometry* AllocateGeometry( + nsDisplayListBuilder* aBuilder) override { + return new nsDisplayItemGenericGeometry(this, aBuilder); + } + + nsRect GetComponentAlphaBounds( + nsDisplayListBuilder* aBuilder) const override { + bool snap; + return GetBounds(aBuilder, &snap); + } +}; + +// --------------------------------------------------------------------- +// nsQueryFrame methods + +NS_QUERYFRAME_HEAD(SVGTextFrame) + NS_QUERYFRAME_ENTRY(SVGTextFrame) +NS_QUERYFRAME_TAIL_INHERITING(SVGDisplayContainerFrame) + +} // namespace mozilla + +// --------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGTextFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGTextFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGTextFrame) + +// --------------------------------------------------------------------- +// nsIFrame methods + +void SVGTextFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::text), + "Content is not an SVG text"); + + SVGDisplayContainerFrame::Init(aContent, aParent, aPrevInFlow); + AddStateBits(aParent->GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD); + + mMutationObserver = new MutationObserver(this); + + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // We're inserting a new <text> element into a non-display context. + // Ensure that we get reflowed. + ScheduleReflowSVGNonDisplayText( + IntrinsicDirty::FrameAncestorsAndDescendants); + } +} + +void SVGTextFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) { + if (IsSubtreeDirty()) { + // We can sometimes be asked to paint before reflow happens and we + // have updated mPositions, etc. In this case, we just avoid + // painting. + return; + } + if (!IsVisibleForPainting() && aBuilder->IsForPainting()) { + return; + } + DisplayOutline(aBuilder, aLists); + aLists.Content()->AppendNewToTop<DisplaySVGText>(aBuilder, this); +} + +nsresult SVGTextFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + if (aNameSpaceID != kNameSpaceID_None) { + return NS_OK; + } + + if (aAttribute == nsGkAtoms::transform) { + // 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 (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW) && mCanvasTM && + mCanvasTM->IsSingular()) { + // We won't have calculated the glyph positions correctly. + NotifyGlyphMetricsChange(false); + } + mCanvasTM = nullptr; + } else if (IsGlyphPositioningAttribute(aAttribute) || + aAttribute == nsGkAtoms::textLength || + aAttribute == nsGkAtoms::lengthAdjust) { + NotifyGlyphMetricsChange(false); + } + + return NS_OK; +} + +void SVGTextFrame::ReflowSVGNonDisplayText() { + MOZ_ASSERT(SVGUtils::AnyOuterSVGIsCallingReflowSVG(this), + "only call ReflowSVGNonDisplayText when an outer SVG frame is " + "under ReflowSVG"); + MOZ_ASSERT(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "only call ReflowSVGNonDisplayText if the frame is " + "NS_FRAME_IS_NONDISPLAY"); + + // We had a style change, so we mark this frame as dirty so that the next + // time it is painted, we reflow the anonymous block frame. + this->MarkSubtreeDirty(); + + // Finally, we need to actually reflow the anonymous block frame and update + // mPositions, in case we are being reflowed immediately after a DOM + // mutation that needs frame reconstruction. + MaybeReflowAnonymousBlockChild(); + UpdateGlyphPositioning(); +} + +void SVGTextFrame::ScheduleReflowSVGNonDisplayText(IntrinsicDirty aReason) { + MOZ_ASSERT(!SVGUtils::OuterSVGIsCallingReflowSVG(this), + "do not call ScheduleReflowSVGNonDisplayText when the outer SVG " + "frame is under ReflowSVG"); + MOZ_ASSERT(!HasAnyStateBits(NS_STATE_SVG_TEXT_IN_REFLOW), + "do not call ScheduleReflowSVGNonDisplayText while reflowing the " + "anonymous block child"); + + // We need to find an ancestor frame that we can call FrameNeedsReflow + // on that will cause the document to be marked as needing relayout, + // and for that ancestor (or some further ancestor) to be marked as + // a root to reflow. We choose the closest ancestor frame that is not + // NS_FRAME_IS_NONDISPLAY and which is either an outer SVG frame or a + // non-SVG frame. (We don't consider displayed SVG frame ancestors other + // than SVGOuterSVGFrame, since calling FrameNeedsReflow on those other + // SVG frames would do a bunch of unnecessary work on the SVG frames up to + // the SVGOuterSVGFrame.) + + nsIFrame* f = this; + while (f) { + if (!f->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + if (f->IsSubtreeDirty()) { + // This is a displayed frame, so if it is already dirty, we will be + // reflowed soon anyway. No need to call FrameNeedsReflow again, then. + return; + } + if (!f->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + break; + } + f->AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); + } + f = f->GetParent(); + } + + MOZ_ASSERT(f, "should have found an ancestor frame to reflow"); + + PresShell()->FrameNeedsReflow(f, aReason, NS_FRAME_IS_DIRTY); +} + +NS_IMPL_ISUPPORTS(SVGTextFrame::MutationObserver, nsIMutationObserver) + +void SVGTextFrame::MutationObserver::ContentAppended( + nsIContent* aFirstNewContent) { + mFrame->NotifyGlyphMetricsChange(true); +} + +void SVGTextFrame::MutationObserver::ContentInserted(nsIContent* aChild) { + mFrame->NotifyGlyphMetricsChange(true); +} + +void SVGTextFrame::MutationObserver::ContentRemoved( + nsIContent* aChild, nsIContent* aPreviousSibling) { + mFrame->NotifyGlyphMetricsChange(true); +} + +void SVGTextFrame::MutationObserver::CharacterDataChanged( + nsIContent* aContent, const CharacterDataChangeInfo&) { + mFrame->NotifyGlyphMetricsChange(true); +} + +void SVGTextFrame::MutationObserver::AttributeChanged( + Element* aElement, int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType, const nsAttrValue* aOldValue) { + if (!aElement->IsSVGElement()) { + return; + } + + // Attribute changes on this element will be handled by + // SVGTextFrame::AttributeChanged. + if (aElement == mFrame->GetContent()) { + return; + } + + mFrame->HandleAttributeChangeInDescendant(aElement, aNameSpaceID, aAttribute); +} + +void SVGTextFrame::HandleAttributeChangeInDescendant(Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute) { + if (aElement->IsSVGElement(nsGkAtoms::textPath)) { + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::startOffset || + aAttribute == nsGkAtoms::path || aAttribute == nsGkAtoms::side_)) { + NotifyGlyphMetricsChange(false); + } else if ((aNameSpaceID == kNameSpaceID_XLink || + aNameSpaceID == kNameSpaceID_None) && + aAttribute == nsGkAtoms::href) { + // Blow away our reference, if any + nsIFrame* childElementFrame = aElement->GetPrimaryFrame(); + if (childElementFrame) { + SVGObserverUtils::RemoveTextPathObserver(childElementFrame); + NotifyGlyphMetricsChange(false); + } + } + } else { + if (aNameSpaceID == kNameSpaceID_None && + IsGlyphPositioningAttribute(aAttribute)) { + NotifyGlyphMetricsChange(false); + } + } +} + +void SVGTextFrame::FindCloserFrameForSelection( + const nsPoint& aPoint, FrameWithDistance* aCurrentBestFrame) { + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + return; + } + + UpdateGlyphPositioning(); + + nsPresContext* presContext = PresContext(); + + // Find the frame that has the closest rendered run rect to aPoint. + TextRenderedRunIterator it(this); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + uint32_t flags = TextRenderedRun::eIncludeFill | + TextRenderedRun::eIncludeStroke | + TextRenderedRun::eNoHorizontalOverflow; + SVGBBox userRect = run.GetUserSpaceRect(presContext, flags); + float devPxPerCSSPx = presContext->CSSPixelsToDevPixels(1.f); + userRect.Scale(devPxPerCSSPx); + + if (!userRect.IsEmpty()) { + gfxMatrix m; + nsRect rect = + SVGUtils::ToCanvasBounds(userRect.ToThebesRect(), m, presContext); + + if (nsLayoutUtils::PointIsCloserToRect(aPoint, rect, + aCurrentBestFrame->mXDistance, + aCurrentBestFrame->mYDistance)) { + aCurrentBestFrame->mFrame = run.mFrame; + } + } + } +} + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +void SVGTextFrame::NotifySVGChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), + "Invalidation logic may need adjusting"); + + bool needNewBounds = false; + bool needGlyphMetricsUpdate = false; + bool needNewCanvasTM = false; + + if ((aFlags & COORD_CONTEXT_CHANGED) && + HasAnyStateBits(NS_STATE_SVG_POSITIONING_MAY_USE_PERCENTAGES)) { + needGlyphMetricsUpdate = true; + } + + if (aFlags & TRANSFORM_CHANGED) { + needNewCanvasTM = true; + if (mCanvasTM && mCanvasTM->IsSingular()) { + // We won't have calculated the glyph positions correctly. + needNewBounds = true; + needGlyphMetricsUpdate = true; + } + if (StyleSVGReset()->HasNonScalingStroke()) { + // Stroke currently contributes to our mRect, and our stroke depends on + // the transform to our outer-<svg> if |vector-effect:non-scaling-stroke|. + needNewBounds = true; + } + } + + // If the scale at which we computed our mFontSizeScaleFactor has changed by + // at least a factor of two, reflow the text. This avoids reflowing text + // at every tick of a transform animation, but ensures our glyph metrics + // do not get too far out of sync with the final font size on the screen. + if (needNewCanvasTM && mLastContextScale != 0.0f) { + mCanvasTM = nullptr; + // If we are a non-display frame, then we don't want to call + // GetCanvasTM(), since the context scale does not use it. + gfxMatrix newTM = + HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) ? gfxMatrix() : GetCanvasTM(); + // Compare the old and new context scales. + float scale = GetContextScale(newTM); + float change = scale / mLastContextScale; + if (change >= 2.0f || change <= 0.5f) { + needNewBounds = true; + needGlyphMetricsUpdate = true; + } + } + + if (needNewBounds) { + // Ancestor changes can't affect how we render from the perspective of + // any rendering observers that we may have, so we don't need to + // invalidate them. We also don't need to invalidate ourself, since our + // changed ancestor will have invalidated its entire area, which includes + // our area. + ScheduleReflowSVG(); + } + + if (needGlyphMetricsUpdate) { + // If we are positioned using percentage values we need to update our + // position whenever our viewport's dimensions change. But only do this if + // we have been reflowed once, otherwise the glyph positioning will be + // wrong. (We need to wait until bidi reordering has been done.) + if (!HasAnyStateBits(NS_FRAME_FIRST_REFLOW)) { + NotifyGlyphMetricsChange(false); + } + } +} + +/** + * Gets the offset into a DOM node that the specified caret is positioned at. + */ +static int32_t GetCaretOffset(nsCaret* aCaret) { + RefPtr<Selection> selection = aCaret->GetSelection(); + if (!selection) { + return -1; + } + + return selection->AnchorOffset(); +} + +/** + * Returns whether the caret should be painted for a given TextRenderedRun + * by checking whether the caret is in the range covered by the rendered run. + * + * @param aThisRun The TextRenderedRun to be painted. + * @param aCaret The caret. + */ +static bool ShouldPaintCaret(const TextRenderedRun& aThisRun, nsCaret* aCaret) { + int32_t caretOffset = GetCaretOffset(aCaret); + + if (caretOffset < 0) { + return false; + } + + return uint32_t(caretOffset) >= aThisRun.mTextFrameContentOffset && + uint32_t(caretOffset) < aThisRun.mTextFrameContentOffset + + aThisRun.mTextFrameContentLength; +} + +void SVGTextFrame::PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + DrawTarget& aDrawTarget = *aContext.GetDrawTarget(); + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + nsPresContext* presContext = PresContext(); + + gfxMatrix initialMatrix = aContext.CurrentMatrixDouble(); + + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // If we are in a canvas DrawWindow call that used the + // DRAWWINDOW_DO_NOT_FLUSH flag, then we may still have out + // of date frames. Just don't paint anything if they are + // dirty. + if (presContext->PresShell()->InDrawWindowNotFlushing() && + IsSubtreeDirty()) { + return; + } + // Text frames inside <clipPath>, <mask>, etc. will never have had + // ReflowSVG called on them, so call UpdateGlyphPositioning to do this now. + UpdateGlyphPositioning(); + } else if (IsSubtreeDirty()) { + // If we are asked to paint before reflow has recomputed mPositions etc. + // directly via PaintSVG, rather than via a display list, then we need + // to bail out here too. + return; + } + + const float epsilon = 0.0001; + if (abs(mLengthAdjustScaleFactor) < epsilon) { + // A zero scale factor can be caused by having forced the text length to + // zero. In this situation there is nothing to show. + return; + } + + if (aTransform.IsSingular()) { + NS_WARNING("Can't render text element!"); + return; + } + + gfxMatrix matrixForPaintServers = aTransform * initialMatrix; + + // SVG frames' PaintSVG methods paint in CSS px, but normally frames paint in + // dev pixels. Here we multiply a CSS-px-to-dev-pixel factor onto aTransform + // so our non-SVG nsTextFrame children paint correctly. + auto auPerDevPx = presContext->AppUnitsPerDevPixel(); + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels(auPerDevPx); + gfxMatrix canvasTMForChildren = aTransform; + canvasTMForChildren.PreScale(cssPxPerDevPx, cssPxPerDevPx); + initialMatrix.PreScale(1 / cssPxPerDevPx, 1 / cssPxPerDevPx); + + gfxContextMatrixAutoSaveRestore matSR(&aContext); + aContext.NewPath(); + aContext.Multiply(canvasTMForChildren); + gfxMatrix currentMatrix = aContext.CurrentMatrixDouble(); + + RefPtr<nsCaret> caret = presContext->PresShell()->GetCaret(); + nsRect caretRect; + nsIFrame* caretFrame = caret->GetPaintGeometry(&caretRect); + + gfxContextAutoSaveRestore ctxSR; + TextRenderedRunIterator it(this, TextRenderedRunIterator::eVisibleFrames); + TextRenderedRun run = it.Current(); + + SVGContextPaint* outerContextPaint = + SVGContextPaint::GetContextPaint(GetContent()); + + while (run.mFrame) { + nsTextFrame* frame = run.mFrame; + + RefPtr<SVGContextPaintImpl> contextPaint = new SVGContextPaintImpl(); + DrawMode drawMode = contextPaint->Init(&aDrawTarget, initialMatrix, frame, + outerContextPaint, aImgParams); + if (drawMode & DrawMode::GLYPH_STROKE) { + ctxSR.EnsureSaved(&aContext); + // This may change the gfxContext's transform (for non-scaling stroke), + // in which case this needs to happen before we call SetMatrix() below. + SVGUtils::SetupStrokeGeometry(frame->GetParent(), &aContext, + outerContextPaint); + } + + nscoord startEdge, endEdge; + run.GetClipEdges(startEdge, endEdge); + + // Set up the transform for painting the text frame for the substring + // indicated by the run. + gfxMatrix runTransform = run.GetTransformFromUserSpaceForPainting( + presContext, startEdge, endEdge) * + currentMatrix; + aContext.SetMatrixDouble(runTransform); + + if (drawMode != DrawMode(0)) { + bool paintSVGGlyphs; + nsTextFrame::PaintTextParams params(&aContext); + params.framePt = Point(); + params.dirtyRect = + LayoutDevicePixel::FromAppUnits(frame->InkOverflowRect(), auPerDevPx); + params.contextPaint = contextPaint; + bool isSelected; + if (HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD)) { + params.state = nsTextFrame::PaintTextParams::GenerateTextMask; + isSelected = false; + } else { + isSelected = frame->IsSelected(); + } + gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aContext); + float opacity = 1.0f; + nsIFrame* ancestor = frame->GetParent(); + while (ancestor != this) { + opacity *= ancestor->StyleEffects()->mOpacity; + ancestor = ancestor->GetParent(); + } + if (opacity < 1.0f) { + autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + opacity); + } + + if (ShouldRenderAsPath(frame, paintSVGGlyphs)) { + SVGTextDrawPathCallbacks callbacks(this, aContext, frame, + matrixForPaintServers, aImgParams, + paintSVGGlyphs); + params.callbacks = &callbacks; + frame->PaintText(params, startEdge, endEdge, nsPoint(), isSelected); + } else { + frame->PaintText(params, startEdge, endEdge, nsPoint(), isSelected); + } + } + + if (frame == caretFrame && ShouldPaintCaret(run, caret)) { + // XXX Should we be looking at the fill/stroke colours to paint the + // caret with, rather than using the color property? + caret->PaintCaret(aDrawTarget, frame, nsPoint()); + aContext.NewPath(); + } + + run = it.Next(); + } +} + +nsIFrame* SVGTextFrame::GetFrameForPoint(const gfxPoint& aPoint) { + NS_ASSERTION(PrincipalChildList().FirstChild(), "must have a child frame"); + + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // Text frames inside <clipPath> will never have had ReflowSVG called on + // them, so call UpdateGlyphPositioning to do this now. (Text frames + // inside <mask> and other non-display containers will never need to + // be hit tested.) + UpdateGlyphPositioning(); + } else { + NS_ASSERTION(!IsSubtreeDirty(), "reflow should have happened"); + } + + // Hit-testing any clip-path will typically be a lot quicker than the + // hit-testing of our text frames in the loop below, so we do the former up + // front to avoid unnecessarily wasting cycles on the latter. + if (!SVGUtils::HitTestClip(this, aPoint)) { + return nullptr; + } + + nsPresContext* presContext = PresContext(); + + // Ideally we'd iterate backwards so that we can just return the first frame + // that is under aPoint. In practice this will rarely matter though since it + // is rare for text in/under an SVG <text> element to overlap (i.e. the first + // text frame that is hit will likely be the only text frame that is hit). + + TextRenderedRunIterator it(this); + nsIFrame* hit = nullptr; + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + uint16_t hitTestFlags = SVGUtils::GetGeometryHitTestFlags(run.mFrame); + if (!hitTestFlags) { + continue; + } + + gfxMatrix m = run.GetTransformFromRunUserSpaceToUserSpace(presContext); + if (!m.Invert()) { + return nullptr; + } + + gfxPoint pointInRunUserSpace = m.TransformPoint(aPoint); + gfxRect frameRect = run.GetRunUserSpaceRect( + presContext, TextRenderedRun::eIncludeFill | + TextRenderedRun::eIncludeStroke) + .ToThebesRect(); + + if (Inside(frameRect, pointInRunUserSpace)) { + hit = run.mFrame; + } + } + return hit; +} + +void SVGTextFrame::ReflowSVG() { + MOZ_ASSERT(SVGUtils::AnyOuterSVGIsCallingReflowSVG(this), + "This call is probaby a wasteful mistake"); + + MOZ_ASSERT(!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "ReflowSVG mechanism not designed for this"); + + if (!SVGUtils::NeedsReflowSVG(this)) { + MOZ_ASSERT(!HasAnyStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY | + NS_STATE_SVG_POSITIONING_DIRTY), + "How did this happen?"); + return; + } + + MaybeReflowAnonymousBlockChild(); + UpdateGlyphPositioning(); + + nsPresContext* presContext = PresContext(); + + SVGBBox r; + TextRenderedRunIterator it(this, TextRenderedRunIterator::eAllFrames); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + uint32_t runFlags = 0; + if (!run.mFrame->StyleSVG()->mFill.kind.IsNone()) { + runFlags |= TextRenderedRun::eIncludeFill; + } + if (SVGUtils::HasStroke(run.mFrame)) { + runFlags |= TextRenderedRun::eIncludeStroke; + } + // 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'. The text-shadow is not part of the hit-test area. + uint16_t hitTestFlags = SVGUtils::GetGeometryHitTestFlags(run.mFrame); + if (hitTestFlags & SVG_HIT_TEST_FILL) { + runFlags |= TextRenderedRun::eIncludeFill; + } + if (hitTestFlags & SVG_HIT_TEST_STROKE) { + runFlags |= TextRenderedRun::eIncludeStroke; + } + + if (runFlags) { + r.UnionEdges(run.GetUserSpaceRect(presContext, runFlags)); + } + } + + if (r.IsEmpty()) { + mRect.SetEmpty(); + } else { + mRect = nsLayoutUtils::RoundGfxRectToAppRect(r.ToThebesRect(), + AppUnitsPerCSSPixel()); + + // Due to rounding issues when we have a transform applied, we sometimes + // don't include an additional row of pixels. For now, just inflate our + // covered region. + mRect.Inflate(ceil(presContext->AppUnitsPerDevPixel() / mLastContextScale)); + } + + 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); + } + + // Now unset the various reflow bits. Do this before calling + // FinishAndStoreOverflow since FinishAndStoreOverflow can require glyph + // positions (to resolve transform-origin). + RemoveStateBits(NS_FRAME_FIRST_REFLOW | NS_FRAME_IS_DIRTY | + NS_FRAME_HAS_DIRTY_CHILDREN); + + nsRect overflow = nsRect(nsPoint(0, 0), mRect.Size()); + OverflowAreas overflowAreas(overflow, overflow); + FinishAndStoreOverflow(overflowAreas, mRect.Size()); +} + +/** + * Converts SVGUtils::eBBox* flags into TextRenderedRun flags appropriate + * for the specified rendered run. + */ +static uint32_t TextRenderedRunFlagsForBBoxContribution( + const TextRenderedRun& aRun, uint32_t aBBoxFlags) { + uint32_t flags = 0; + if ((aBBoxFlags & SVGUtils::eBBoxIncludeFillGeometry) || + ((aBBoxFlags & SVGUtils::eBBoxIncludeFill) && + !aRun.mFrame->StyleSVG()->mFill.kind.IsNone())) { + flags |= TextRenderedRun::eIncludeFill; + } + if ((aBBoxFlags & SVGUtils::eBBoxIncludeStrokeGeometry) || + ((aBBoxFlags & SVGUtils::eBBoxIncludeStroke) && + SVGUtils::HasStroke(aRun.mFrame))) { + flags |= TextRenderedRun::eIncludeStroke; + } + return flags; +} + +SVGBBox SVGTextFrame::GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) { + NS_ASSERTION(PrincipalChildList().FirstChild(), "must have a child frame"); + SVGBBox bbox; + + if (aFlags & SVGUtils::eForGetClientRects) { + Rect rect = NSRectToRect(mRect, AppUnitsPerCSSPixel()); + if (!rect.IsEmpty()) { + bbox = aToBBoxUserspace.TransformBounds(rect); + } + return bbox; + } + + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid && kid->IsSubtreeDirty()) { + // Return an empty bbox if our kid's subtree is dirty. This may be called + // in that situation, e.g. when we're building a display list after an + // interrupted reflow. This can also be called during reflow before we've + // been reflowed, e.g. if an earlier sibling is calling + // FinishAndStoreOverflow and needs our parent's perspective matrix, which + // depends on the SVG bbox contribution of this frame. In the latter + // situation, when all siblings have been reflowed, the parent will compute + // its perspective and rerun FinishAndStoreOverflow for all its children. + return bbox; + } + + UpdateGlyphPositioning(); + + nsPresContext* presContext = PresContext(); + + TextRenderedRunIterator it(this); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + uint32_t flags = TextRenderedRunFlagsForBBoxContribution(run, aFlags); + gfxMatrix m = ThebesMatrix(aToBBoxUserspace); + SVGBBox bboxForRun = run.GetUserSpaceRect(presContext, flags, &m); + bbox.UnionEdges(bboxForRun); + } + + return bbox; +} + +//---------------------------------------------------------------------- +// SVGTextFrame SVG DOM methods + +/** + * Returns whether the specified node has any non-empty Text + * beneath it. + */ +static bool HasTextContent(nsIContent* aContent) { + NS_ASSERTION(aContent, "expected non-null aContent"); + + TextNodeIterator it(aContent); + for (Text* text = it.Current(); text; text = it.Next()) { + if (text->TextLength() != 0) { + return true; + } + } + return false; +} + +/** + * Returns the number of DOM characters beneath the specified node. + */ +static uint32_t GetTextContentLength(nsIContent* aContent) { + NS_ASSERTION(aContent, "expected non-null aContent"); + + uint32_t length = 0; + TextNodeIterator it(aContent); + for (Text* text = it.Current(); text; text = it.Next()) { + length += text->TextLength(); + } + return length; +} + +int32_t SVGTextFrame::ConvertTextElementCharIndexToAddressableIndex( + int32_t aIndex, nsIContent* aContent) { + CharIterator it(this, CharIterator::eOriginal, aContent); + if (!it.AdvanceToSubtree()) { + return -1; + } + int32_t result = 0; + int32_t textElementCharIndex; + while (!it.AtEnd() && it.IsWithinSubtree()) { + bool addressable = !it.IsOriginalCharUnaddressable(); + textElementCharIndex = it.TextElementCharIndex(); + it.Next(); + uint32_t delta = it.TextElementCharIndex() - textElementCharIndex; + aIndex -= delta; + if (addressable) { + if (aIndex < 0) { + return result; + } + result += delta; + } + } + return -1; +} + +/** + * Implements the SVG DOM GetNumberOfChars method for the specified + * text content element. + */ +uint32_t SVGTextFrame::GetNumberOfChars(nsIContent* aContent) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + return 0; + } + + UpdateGlyphPositioning(); + + uint32_t n = 0; + CharIterator it(this, CharIterator::eAddressable, aContent); + if (it.AdvanceToSubtree()) { + while (!it.AtEnd() && it.IsWithinSubtree()) { + n++; + it.Next(); + } + } + return n; +} + +/** + * Implements the SVG DOM GetComputedTextLength method for the specified + * text child element. + */ +float SVGTextFrame::GetComputedTextLength(nsIContent* aContent) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + // + // If we ever decide that we need to return accurate values here, + // we could do similar work to GetSubStringLength. + return 0; + } + + UpdateGlyphPositioning(); + + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + PresContext()->AppUnitsPerDevPixel()); + + nscoord length = 0; + TextRenderedRunIterator it(this, TextRenderedRunIterator::eAllFrames, + aContent); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + length += run.GetAdvanceWidth(); + } + + return PresContext()->AppUnitsToGfxUnits(length) * cssPxPerDevPx * + mLengthAdjustScaleFactor / mFontSizeScaleFactor; +} + +/** + * Implements the SVG DOM SelectSubString method for the specified + * text content element. + */ +void SVGTextFrame::SelectSubString(nsIContent* aContent, uint32_t charnum, + uint32_t nchars, ErrorResult& aRv) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + // XXXbz Should this just return without throwing like the no-frame case? + aRv.ThrowInvalidStateError("No layout information available for SVG text"); + return; + } + + UpdateGlyphPositioning(); + + // Convert charnum/nchars from addressable characters relative to + // aContent to global character indices. + CharIterator chit(this, CharIterator::eAddressable, aContent); + if (!chit.AdvanceToSubtree() || !chit.Next(charnum) || + chit.IsAfterSubtree()) { + aRv.ThrowIndexSizeError("Character index out of range"); + return; + } + charnum = chit.TextElementCharIndex(); + const RefPtr<nsIContent> content = chit.TextFrame()->GetContent(); + chit.NextWithinSubtree(nchars); + nchars = chit.TextElementCharIndex() - charnum; + + RefPtr<nsFrameSelection> frameSelection = GetFrameSelection(); + + frameSelection->HandleClick(content, charnum, charnum + nchars, + nsFrameSelection::FocusMode::kCollapseToNewPoint, + CARET_ASSOCIATE_BEFORE); +} + +/** + * For some content we cannot (or currently cannot) compute the length + * without reflowing. In those cases we need to fall back to using + * GetSubStringLengthSlowFallback. + * + * We fall back for textPath since we need glyph positioning in order to + * tell if any characters should be ignored due to having fallen off the + * end of the textPath. + * + * We fall back for bidi because GetTrimmedOffsets does not produce the + * correct results for bidi continuations when passed aPostReflow = false. + * XXX It may be possible to determine which continuations to trim from (and + * which sides), but currently we don't do that. It would require us to + * identify the visual (rather than logical) start and end of the line, to + * avoid trimming at line-internal frame boundaries. Maybe nsBidiPresUtils + * methods like GetFrameToRightOf and GetFrameToLeftOf would help? + * + */ +bool SVGTextFrame::RequiresSlowFallbackForSubStringLength() { + TextFrameIterator frameIter(this); + for (nsTextFrame* frame = frameIter.Current(); frame; + frame = frameIter.Next()) { + if (frameIter.TextPathFrame() || frame->GetNextContinuation()) { + return true; + } + } + return false; +} + +/** + * Implements the SVG DOM GetSubStringLength method for the specified + * text content element. + */ +float SVGTextFrame::GetSubStringLengthFastPath(nsIContent* aContent, + uint32_t charnum, + uint32_t nchars, + ErrorResult& aRv) { + MOZ_ASSERT(!RequiresSlowFallbackForSubStringLength()); + + // We only need our text correspondence to be up to date (no need to call + // UpdateGlyphPositioning). + TextNodeCorrespondenceRecorder::RecordCorrespondence(this); + + // Convert charnum/nchars from addressable characters relative to + // aContent to global character indices. + CharIterator chit(this, CharIterator::eAddressable, aContent, + /* aPostReflow */ false); + if (!chit.AdvanceToSubtree() || !chit.Next(charnum) || + chit.IsAfterSubtree()) { + aRv.ThrowIndexSizeError("Character index out of range"); + return 0; + } + + // We do this after the ThrowIndexSizeError() bit so JS calls correctly throw + // when necessary. + if (nchars == 0) { + return 0.0f; + } + + charnum = chit.TextElementCharIndex(); + chit.NextWithinSubtree(nchars); + nchars = chit.TextElementCharIndex() - charnum; + + // Sum of the substring advances. + nscoord textLength = 0; + + TextFrameIterator frit(this); // aSubtree = nullptr + + // Index of the first non-skipped char in the frame, and of a subsequent char + // that we're interested in. Both are relative to the index of the first + // non-skipped char in the ancestor <text> element. + uint32_t frameStartTextElementCharIndex = 0; + uint32_t textElementCharIndex; + + for (nsTextFrame* frame = frit.Current(); frame; frame = frit.Next()) { + frameStartTextElementCharIndex += frit.UndisplayedCharacters(); + textElementCharIndex = frameStartTextElementCharIndex; + + // Offset into frame's Text: + const uint32_t untrimmedOffset = frame->GetContentOffset(); + const uint32_t untrimmedLength = frame->GetContentEnd() - untrimmedOffset; + + // Trim the offset/length to remove any leading/trailing white space. + uint32_t trimmedOffset = untrimmedOffset; + uint32_t trimmedLength = untrimmedLength; + nsTextFrame::TrimmedOffsets trimmedOffsets = frame->GetTrimmedOffsets( + frame->TextFragment(), nsTextFrame::TrimmedOffsetFlags::NotPostReflow); + TrimOffsets(trimmedOffset, trimmedLength, trimmedOffsets); + + textElementCharIndex += trimmedOffset - untrimmedOffset; + + if (textElementCharIndex >= charnum + nchars) { + break; // we're past the end of the substring + } + + uint32_t offset = textElementCharIndex; + + // Intersect the substring we are interested in with the range covered by + // the nsTextFrame. + IntersectInterval(offset, trimmedLength, charnum, nchars); + + if (trimmedLength != 0) { + // Convert offset into an index into the frame. + offset += trimmedOffset - textElementCharIndex; + + gfxSkipCharsIterator it = frame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = frame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(frame, it); + + Range range = ConvertOriginalToSkipped(it, offset, trimmedLength); + + // Accumulate the advance. + textLength += textRun->GetAdvanceWidth(range, &provider); + } + + // Advance, ready for next call: + frameStartTextElementCharIndex += untrimmedLength; + } + + nsPresContext* presContext = PresContext(); + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + + return presContext->AppUnitsToGfxUnits(textLength) * cssPxPerDevPx / + mFontSizeScaleFactor; +} + +float SVGTextFrame::GetSubStringLengthSlowFallback(nsIContent* aContent, + uint32_t charnum, + uint32_t nchars, + ErrorResult& aRv) { + UpdateGlyphPositioning(); + + // Convert charnum/nchars from addressable characters relative to + // aContent to global character indices. + CharIterator chit(this, CharIterator::eAddressable, aContent); + if (!chit.AdvanceToSubtree() || !chit.Next(charnum) || + chit.IsAfterSubtree()) { + aRv.ThrowIndexSizeError("Character index out of range"); + return 0; + } + + if (nchars == 0) { + return 0.0f; + } + + charnum = chit.TextElementCharIndex(); + chit.NextWithinSubtree(nchars); + nchars = chit.TextElementCharIndex() - charnum; + + // Find each rendered run that intersects with the range defined + // by charnum/nchars. + nscoord textLength = 0; + TextRenderedRunIterator runIter(this, TextRenderedRunIterator::eAllFrames); + TextRenderedRun run = runIter.Current(); + while (run.mFrame) { + // If this rendered run is past the substring we are interested in, we + // are done. + uint32_t offset = run.mTextElementCharIndex; + if (offset >= charnum + nchars) { + break; + } + + // Intersect the substring we are interested in with the range covered by + // the rendered run. + uint32_t length = run.mTextFrameContentLength; + IntersectInterval(offset, length, charnum, nchars); + + if (length != 0) { + // Convert offset into an index into the frame. + offset += run.mTextFrameContentOffset - run.mTextElementCharIndex; + + gfxSkipCharsIterator it = + run.mFrame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = run.mFrame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(run.mFrame, it); + + Range range = ConvertOriginalToSkipped(it, offset, length); + + // Accumulate the advance. + textLength += textRun->GetAdvanceWidth(range, &provider); + } + + run = runIter.Next(); + } + + nsPresContext* presContext = PresContext(); + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + + return presContext->AppUnitsToGfxUnits(textLength) * cssPxPerDevPx / + mFontSizeScaleFactor; +} + +/** + * Implements the SVG DOM GetCharNumAtPosition method for the specified + * text content element. + */ +int32_t SVGTextFrame::GetCharNumAtPosition(nsIContent* aContent, + const DOMPointInit& aPoint) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + return -1; + } + + UpdateGlyphPositioning(); + + nsPresContext* context = PresContext(); + + gfxPoint p(aPoint.mX, aPoint.mY); + + int32_t result = -1; + + TextRenderedRunIterator it(this, TextRenderedRunIterator::eAllFrames, + aContent); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + // Hit test this rendered run. Later runs will override earlier ones. + int32_t index = run.GetCharNumAtPosition(context, p); + if (index != -1) { + result = index + run.mTextElementCharIndex; + } + } + + if (result == -1) { + return result; + } + + return ConvertTextElementCharIndexToAddressableIndex(result, aContent); +} + +/** + * Implements the SVG DOM GetStartPositionOfChar method for the specified + * text content element. + */ +already_AddRefed<DOMSVGPoint> SVGTextFrame::GetStartPositionOfChar( + nsIContent* aContent, uint32_t aCharNum, ErrorResult& aRv) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + aRv.ThrowInvalidStateError("No layout information available for SVG text"); + return nullptr; + } + + UpdateGlyphPositioning(); + + CharIterator it(this, CharIterator::eAddressable, aContent); + if (!it.AdvanceToSubtree() || !it.Next(aCharNum)) { + aRv.ThrowIndexSizeError("Character index out of range"); + return nullptr; + } + + // We need to return the start position of the whole glyph. + uint32_t startIndex = it.GlyphStartTextElementCharIndex(); + + RefPtr<DOMSVGPoint> point = + new DOMSVGPoint(ToPoint(mPositions[startIndex].mPosition)); + return point.forget(); +} + +/** + * Returns the advance of the entire glyph whose starting character is at + * aTextElementCharIndex. + * + * aIterator, if provided, must be a CharIterator that already points to + * aTextElementCharIndex that is restricted to aContent and is using + * filter mode eAddressable. + */ +static gfxFloat GetGlyphAdvance(SVGTextFrame* aFrame, nsIContent* aContent, + uint32_t aTextElementCharIndex, + CharIterator* aIterator) { + MOZ_ASSERT(!aIterator || (aIterator->Filter() == CharIterator::eAddressable && + aIterator->GetSubtree() == aContent && + aIterator->GlyphStartTextElementCharIndex() == + aTextElementCharIndex), + "Invalid aIterator"); + + Maybe<CharIterator> newIterator; + CharIterator* it = aIterator; + if (!it) { + newIterator.emplace(aFrame, CharIterator::eAddressable, aContent); + if (!newIterator->AdvanceToSubtree()) { + MOZ_ASSERT_UNREACHABLE("Invalid aContent"); + return 0.0; + } + it = newIterator.ptr(); + } + + while (it->GlyphStartTextElementCharIndex() != aTextElementCharIndex) { + if (!it->Next()) { + MOZ_ASSERT_UNREACHABLE("Invalid aTextElementCharIndex"); + return 0.0; + } + } + + if (it->AtEnd()) { + MOZ_ASSERT_UNREACHABLE("Invalid aTextElementCharIndex"); + return 0.0; + } + + nsPresContext* presContext = aFrame->PresContext(); + gfxFloat advance = 0.0; + + for (;;) { + advance += it->GetAdvance(presContext); + if (!it->Next() || + it->GlyphStartTextElementCharIndex() != aTextElementCharIndex) { + break; + } + } + + return advance; +} + +/** + * Implements the SVG DOM GetEndPositionOfChar method for the specified + * text content element. + */ +already_AddRefed<DOMSVGPoint> SVGTextFrame::GetEndPositionOfChar( + nsIContent* aContent, uint32_t aCharNum, ErrorResult& aRv) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + aRv.ThrowInvalidStateError("No layout information available for SVG text"); + return nullptr; + } + + UpdateGlyphPositioning(); + + CharIterator it(this, CharIterator::eAddressable, aContent); + if (!it.AdvanceToSubtree() || !it.Next(aCharNum)) { + aRv.ThrowIndexSizeError("Character index out of range"); + return nullptr; + } + + // We need to return the end position of the whole glyph. + uint32_t startIndex = it.GlyphStartTextElementCharIndex(); + + // Get the advance of the glyph. + gfxFloat advance = + GetGlyphAdvance(this, aContent, startIndex, + it.IsClusterAndLigatureGroupStart() ? &it : nullptr); + if (it.TextRun()->IsRightToLeft()) { + advance = -advance; + } + + // The end position is the start position plus the advance in the direction + // of the glyph's rotation. + Matrix m = Matrix::Rotation(mPositions[startIndex].mAngle) * + Matrix::Translation(ToPoint(mPositions[startIndex].mPosition)); + Point p = m.TransformPoint(Point(advance / mFontSizeScaleFactor, 0)); + + RefPtr<DOMSVGPoint> point = new DOMSVGPoint(p); + return point.forget(); +} + +/** + * Implements the SVG DOM GetExtentOfChar method for the specified + * text content element. + */ +already_AddRefed<SVGRect> SVGTextFrame::GetExtentOfChar(nsIContent* aContent, + uint32_t aCharNum, + ErrorResult& aRv) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + aRv.ThrowInvalidStateError("No layout information available for SVG text"); + return nullptr; + } + + UpdateGlyphPositioning(); + + // Search for the character whose addressable index is aCharNum. + CharIterator it(this, CharIterator::eAddressable, aContent); + if (!it.AdvanceToSubtree() || !it.Next(aCharNum)) { + aRv.ThrowIndexSizeError("Character index out of range"); + return nullptr; + } + + nsPresContext* presContext = PresContext(); + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + + nsTextFrame* textFrame = it.TextFrame(); + uint32_t startIndex = it.GlyphStartTextElementCharIndex(); + bool isRTL = it.TextRun()->IsRightToLeft(); + bool isVertical = it.TextRun()->IsVertical(); + + // Get the glyph advance. + gfxFloat advance = + GetGlyphAdvance(this, aContent, startIndex, + it.IsClusterAndLigatureGroupStart() ? &it : nullptr); + gfxFloat x = isRTL ? -advance : 0.0; + + // The ascent and descent gives the height of the glyph. + gfxFloat ascent, descent; + GetAscentAndDescentInAppUnits(textFrame, ascent, descent); + + // The horizontal extent is the origin of the glyph plus the advance + // in the direction of the glyph's rotation. + gfxMatrix m; + m.PreTranslate(mPositions[startIndex].mPosition); + m.PreRotate(mPositions[startIndex].mAngle); + m.PreScale(1 / mFontSizeScaleFactor, 1 / mFontSizeScaleFactor); + + gfxRect glyphRect; + if (isVertical) { + glyphRect = gfxRect( + -presContext->AppUnitsToGfxUnits(descent) * cssPxPerDevPx, x, + presContext->AppUnitsToGfxUnits(ascent + descent) * cssPxPerDevPx, + advance); + } else { + glyphRect = gfxRect( + x, -presContext->AppUnitsToGfxUnits(ascent) * cssPxPerDevPx, advance, + presContext->AppUnitsToGfxUnits(ascent + descent) * cssPxPerDevPx); + } + + // Transform the glyph's rect into user space. + gfxRect r = m.TransformBounds(glyphRect); + + RefPtr<SVGRect> rect = new SVGRect(aContent, ToRect(r)); + return rect.forget(); +} + +/** + * Implements the SVG DOM GetRotationOfChar method for the specified + * text content element. + */ +float SVGTextFrame::GetRotationOfChar(nsIContent* aContent, uint32_t aCharNum, + ErrorResult& aRv) { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid->IsSubtreeDirty()) { + // We're never reflowed if we're under a non-SVG element that is + // never reflowed (such as the HTML 'caption' element). + aRv.ThrowInvalidStateError("No layout information available for SVG text"); + return 0; + } + + UpdateGlyphPositioning(); + + CharIterator it(this, CharIterator::eAddressable, aContent); + if (!it.AdvanceToSubtree() || !it.Next(aCharNum)) { + aRv.ThrowIndexSizeError("Character index out of range"); + return 0; + } + + // we need to account for the glyph's underlying orientation + const gfxTextRun::GlyphRun& glyphRun = it.GlyphRun(); + int32_t glyphOrientation = + 90 * (glyphRun.IsSidewaysRight() - glyphRun.IsSidewaysLeft()); + + return mPositions[it.TextElementCharIndex()].mAngle * 180.0 / M_PI + + glyphOrientation; +} + +//---------------------------------------------------------------------- +// SVGTextFrame text layout methods + +/** + * Given the character position array before values have been filled in + * to any unspecified positions, and an array of dx/dy values, returns whether + * a character at a given index should start a new rendered run. + * + * @param aPositions The array of character positions before unspecified + * positions have been filled in and dx/dy values have been added to them. + * @param aDeltas The array of dx/dy values. + * @param aIndex The character index in question. + */ +static bool ShouldStartRunAtIndex(const nsTArray<CharPosition>& aPositions, + const nsTArray<gfxPoint>& aDeltas, + uint32_t aIndex) { + if (aIndex == 0) { + return true; + } + + if (aIndex < aPositions.Length()) { + // If an explicit x or y value was given, start a new run. + if (aPositions[aIndex].IsXSpecified() || + aPositions[aIndex].IsYSpecified()) { + return true; + } + + // If a non-zero rotation was given, or the previous character had a non- + // zero rotation, start a new run. + if ((aPositions[aIndex].IsAngleSpecified() && + aPositions[aIndex].mAngle != 0.0f) || + (aPositions[aIndex - 1].IsAngleSpecified() && + (aPositions[aIndex - 1].mAngle != 0.0f))) { + return true; + } + } + + if (aIndex < aDeltas.Length()) { + // If a non-zero dx or dy value was given, start a new run. + if (aDeltas[aIndex].x != 0.0 || aDeltas[aIndex].y != 0.0) { + return true; + } + } + + return false; +} + +bool SVGTextFrame::ResolvePositionsForNode(nsIContent* aContent, + uint32_t& aIndex, bool aInTextPath, + bool& aForceStartOfChunk, + nsTArray<gfxPoint>& aDeltas) { + if (aContent->IsText()) { + // We found a text node. + uint32_t length = aContent->AsText()->TextLength(); + if (length) { + uint32_t end = aIndex + length; + if (MOZ_UNLIKELY(end > mPositions.Length())) { + MOZ_ASSERT_UNREACHABLE( + "length of mPositions does not match characters " + "found by iterating content"); + return false; + } + if (aForceStartOfChunk) { + // Note this character as starting a new anchored chunk. + mPositions[aIndex].mStartOfChunk = true; + aForceStartOfChunk = false; + } + while (aIndex < end) { + // Record whether each of these characters should start a new rendered + // run. That is always the case for characters on a text path. + // + // Run boundaries due to rotate="" values are handled in + // DoGlyphPositioning. + if (aInTextPath || ShouldStartRunAtIndex(mPositions, aDeltas, aIndex)) { + mPositions[aIndex].mRunBoundary = true; + } + aIndex++; + } + } + return true; + } + + // Skip past elements that aren't text content elements. + if (!IsTextContentElement(aContent)) { + return true; + } + + if (aContent->IsSVGElement(nsGkAtoms::textPath)) { + // Any ‘y’ attributes on horizontal <textPath> elements are ignored. + // Similarly, for vertical <texPath>s x attributes are ignored. + // <textPath> elements behave as if they have x="0" y="0" on them, but only + // if there is not a value for the non-ignored coordinate that got inherited + // from a parent. We skip this if there is no text content, so that empty + // <textPath>s don't interrupt the layout of text in the parent element. + if (HasTextContent(aContent)) { + if (MOZ_UNLIKELY(aIndex >= mPositions.Length())) { + MOZ_ASSERT_UNREACHABLE( + "length of mPositions does not match characters " + "found by iterating content"); + return false; + } + bool vertical = GetWritingMode().IsVertical(); + if (vertical || !mPositions[aIndex].IsXSpecified()) { + mPositions[aIndex].mPosition.x = 0.0; + } + if (!vertical || !mPositions[aIndex].IsYSpecified()) { + mPositions[aIndex].mPosition.y = 0.0; + } + mPositions[aIndex].mStartOfChunk = true; + } + } else if (!aContent->IsSVGElement(nsGkAtoms::a)) { + MOZ_ASSERT(aContent->IsSVGElement()); + + // We have a text content element that can have x/y/dx/dy/rotate attributes. + SVGElement* element = static_cast<SVGElement*>(aContent); + + // Get x, y, dx, dy. + SVGUserUnitList x, y, dx, dy; + element->GetAnimatedLengthListValues(&x, &y, &dx, &dy, nullptr); + + // Get rotate. + const SVGNumberList* rotate = nullptr; + SVGAnimatedNumberList* animatedRotate = + element->GetAnimatedNumberList(nsGkAtoms::rotate); + if (animatedRotate) { + rotate = &animatedRotate->GetAnimValue(); + } + + bool percentages = false; + uint32_t count = GetTextContentLength(aContent); + + if (MOZ_UNLIKELY(aIndex + count > mPositions.Length())) { + MOZ_ASSERT_UNREACHABLE( + "length of mPositions does not match characters " + "found by iterating content"); + return false; + } + + // New text anchoring chunks start at each character assigned a position + // with x="" or y="", or if we forced one with aForceStartOfChunk due to + // being just after a <textPath>. + uint32_t newChunkCount = std::max(x.Length(), y.Length()); + if (!newChunkCount && aForceStartOfChunk) { + newChunkCount = 1; + } + for (uint32_t i = 0, j = 0; i < newChunkCount && j < count; j++) { + if (!mPositions[aIndex + j].mUnaddressable) { + mPositions[aIndex + j].mStartOfChunk = true; + i++; + } + } + + // Copy dx="" and dy="" values into aDeltas. + if (!dx.IsEmpty() || !dy.IsEmpty()) { + // Any unspecified deltas when we grow the array just get left as 0s. + aDeltas.EnsureLengthAtLeast(aIndex + count); + for (uint32_t i = 0, j = 0; i < dx.Length() && j < count; j++) { + if (!mPositions[aIndex + j].mUnaddressable) { + aDeltas[aIndex + j].x = dx[i]; + percentages = percentages || dx.HasPercentageValueAt(i); + i++; + } + } + for (uint32_t i = 0, j = 0; i < dy.Length() && j < count; j++) { + if (!mPositions[aIndex + j].mUnaddressable) { + aDeltas[aIndex + j].y = dy[i]; + percentages = percentages || dy.HasPercentageValueAt(i); + i++; + } + } + } + + // Copy x="" and y="" values. + for (uint32_t i = 0, j = 0; i < x.Length() && j < count; j++) { + if (!mPositions[aIndex + j].mUnaddressable) { + mPositions[aIndex + j].mPosition.x = x[i]; + percentages = percentages || x.HasPercentageValueAt(i); + i++; + } + } + for (uint32_t i = 0, j = 0; i < y.Length() && j < count; j++) { + if (!mPositions[aIndex + j].mUnaddressable) { + mPositions[aIndex + j].mPosition.y = y[i]; + percentages = percentages || y.HasPercentageValueAt(i); + i++; + } + } + + // Copy rotate="" values. + if (rotate && !rotate->IsEmpty()) { + uint32_t i = 0, j = 0; + while (i < rotate->Length() && j < count) { + if (!mPositions[aIndex + j].mUnaddressable) { + mPositions[aIndex + j].mAngle = M_PI * (*rotate)[i] / 180.0; + i++; + } + j++; + } + // Propagate final rotate="" value to the end of this element. + while (j < count) { + mPositions[aIndex + j].mAngle = mPositions[aIndex + j - 1].mAngle; + j++; + } + } + + if (percentages) { + AddStateBits(NS_STATE_SVG_POSITIONING_MAY_USE_PERCENTAGES); + } + } + + // Recurse to children. + bool inTextPath = aInTextPath || aContent->IsSVGElement(nsGkAtoms::textPath); + for (nsIContent* child = aContent->GetFirstChild(); child; + child = child->GetNextSibling()) { + bool ok = ResolvePositionsForNode(child, aIndex, inTextPath, + aForceStartOfChunk, aDeltas); + if (!ok) { + return false; + } + } + + if (aContent->IsSVGElement(nsGkAtoms::textPath)) { + // Force a new anchored chunk just after a <textPath>. + aForceStartOfChunk = true; + } + + return true; +} + +bool SVGTextFrame::ResolvePositions(nsTArray<gfxPoint>& aDeltas, + bool aRunPerGlyph) { + NS_ASSERTION(mPositions.IsEmpty(), "expected mPositions to be empty"); + RemoveStateBits(NS_STATE_SVG_POSITIONING_MAY_USE_PERCENTAGES); + + CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr); + if (it.AtEnd()) { + return false; + } + + // We assume the first character position is (0,0) unless we later see + // otherwise, and note it as unaddressable if it is. + bool firstCharUnaddressable = it.IsOriginalCharUnaddressable(); + mPositions.AppendElement(CharPosition::Unspecified(firstCharUnaddressable)); + + // Fill in unspecified positions for all remaining characters, noting + // them as unaddressable if they are. + uint32_t index = 0; + while (it.Next()) { + while (++index < it.TextElementCharIndex()) { + mPositions.AppendElement(CharPosition::Unspecified(false)); + } + mPositions.AppendElement( + CharPosition::Unspecified(it.IsOriginalCharUnaddressable())); + } + while (++index < it.TextElementCharIndex()) { + mPositions.AppendElement(CharPosition::Unspecified(false)); + } + + // Recurse over the content and fill in character positions as we go. + bool forceStartOfChunk = false; + index = 0; + bool ok = ResolvePositionsForNode(mContent, index, aRunPerGlyph, + forceStartOfChunk, aDeltas); + return ok && index > 0; +} + +void SVGTextFrame::DetermineCharPositions(nsTArray<nsPoint>& aPositions) { + NS_ASSERTION(aPositions.IsEmpty(), "expected aPositions to be empty"); + + nsPoint position; + + TextFrameIterator frit(this); + for (nsTextFrame* frame = frit.Current(); frame; frame = frit.Next()) { + gfxSkipCharsIterator it = frame->EnsureTextRun(nsTextFrame::eInflated); + gfxTextRun* textRun = frame->GetTextRun(nsTextFrame::eInflated); + nsTextFrame::PropertyProvider provider(frame, it); + + // Reset the position to the new frame's position. + position = frit.Position(); + if (textRun->IsVertical()) { + if (textRun->IsRightToLeft()) { + position.y += frame->GetRect().height; + } + position.x += GetBaselinePosition(frame, textRun, frit.DominantBaseline(), + mFontSizeScaleFactor); + } else { + if (textRun->IsRightToLeft()) { + position.x += frame->GetRect().width; + } + position.y += GetBaselinePosition(frame, textRun, frit.DominantBaseline(), + mFontSizeScaleFactor); + } + + // Any characters not in a frame, e.g. when display:none. + for (uint32_t i = 0; i < frit.UndisplayedCharacters(); i++) { + aPositions.AppendElement(position); + } + + // Any white space characters trimmed at the start of the line of text. + nsTextFrame::TrimmedOffsets trimmedOffsets = + frame->GetTrimmedOffsets(frame->TextFragment()); + while (it.GetOriginalOffset() < trimmedOffsets.mStart) { + aPositions.AppendElement(position); + it.AdvanceOriginal(1); + } + + // Visible characters in the text frame. + while (it.GetOriginalOffset() < frame->GetContentEnd()) { + aPositions.AppendElement(position); + if (!it.IsOriginalCharSkipped()) { + // Accumulate partial ligature advance into position. (We must get + // partial advances rather than get the advance of the whole ligature + // group / cluster at once, since the group may span text frames, and + // the PropertyProvider only has spacing information for the current + // text frame.) + uint32_t offset = it.GetSkippedOffset(); + nscoord advance = + textRun->GetAdvanceWidth(Range(offset, offset + 1), &provider); + (textRun->IsVertical() ? position.y : position.x) += + textRun->IsRightToLeft() ? -advance : advance; + } + it.AdvanceOriginal(1); + } + } + + // Finally any characters at the end that are not in a frame. + for (uint32_t i = 0; i < frit.UndisplayedCharacters(); i++) { + aPositions.AppendElement(position); + } +} + +/** + * Physical text-anchor values. + */ +enum TextAnchorSide { eAnchorLeft, eAnchorMiddle, eAnchorRight }; + +/** + * Converts a logical text-anchor value to its physical value, based on whether + * it is for an RTL frame. + */ +static TextAnchorSide ConvertLogicalTextAnchorToPhysical( + StyleTextAnchor aTextAnchor, bool aIsRightToLeft) { + NS_ASSERTION(uint8_t(aTextAnchor) <= 3, "unexpected value for aTextAnchor"); + if (!aIsRightToLeft) { + return TextAnchorSide(uint8_t(aTextAnchor)); + } + return TextAnchorSide(2 - uint8_t(aTextAnchor)); +} + +/** + * Shifts the recorded character positions for an anchored chunk. + * + * @param aCharPositions The recorded character positions. + * @param aChunkStart The character index the starts the anchored chunk. This + * character's initial position is the anchor point. + * @param aChunkEnd The character index just after the end of the anchored + * chunk. + * @param aVisIStartEdge The left/top-most edge of any of the glyphs within the + * anchored chunk. + * @param aVisIEndEdge The right/bottom-most edge of any of the glyphs within + * the anchored chunk. + * @param aAnchorSide The direction to anchor. + */ +static void ShiftAnchoredChunk(nsTArray<CharPosition>& aCharPositions, + uint32_t aChunkStart, uint32_t aChunkEnd, + gfxFloat aVisIStartEdge, gfxFloat aVisIEndEdge, + TextAnchorSide aAnchorSide, bool aVertical) { + NS_ASSERTION(aVisIStartEdge <= aVisIEndEdge, + "unexpected anchored chunk edges"); + NS_ASSERTION(aChunkStart < aChunkEnd, + "unexpected values for aChunkStart and aChunkEnd"); + + gfxFloat shift = aVertical ? aCharPositions[aChunkStart].mPosition.y + : aCharPositions[aChunkStart].mPosition.x; + switch (aAnchorSide) { + case eAnchorLeft: + shift -= aVisIStartEdge; + break; + case eAnchorMiddle: + shift -= (aVisIStartEdge + aVisIEndEdge) / 2; + break; + case eAnchorRight: + shift -= aVisIEndEdge; + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected value for aAnchorSide"); + } + + if (shift != 0.0) { + if (aVertical) { + for (uint32_t i = aChunkStart; i < aChunkEnd; i++) { + aCharPositions[i].mPosition.y += shift; + } + } else { + for (uint32_t i = aChunkStart; i < aChunkEnd; i++) { + aCharPositions[i].mPosition.x += shift; + } + } + } +} + +void SVGTextFrame::AdjustChunksForLineBreaks() { + nsBlockFrame* block = do_QueryFrame(PrincipalChildList().FirstChild()); + NS_ASSERTION(block, "expected block frame"); + + nsBlockFrame::LineIterator line = block->LinesBegin(); + + CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr); + while (!it.AtEnd() && line != block->LinesEnd()) { + if (it.TextFrame() == line->mFirstChild) { + mPositions[it.TextElementCharIndex()].mStartOfChunk = true; + line++; + } + it.AdvancePastCurrentFrame(); + } +} + +void SVGTextFrame::AdjustPositionsForClusters() { + nsPresContext* presContext = PresContext(); + + // Find all of the characters that are in the middle of a cluster or + // ligature group, and adjust their positions and rotations to match + // the first character of the cluster/group. + // + // Also move the boundaries of text rendered runs and anchored chunks to + // not lie in the middle of cluster/group. + + // The partial advance of the current cluster or ligature group that we + // have accumulated. + gfxFloat partialAdvance = 0.0; + + CharIterator it(this, CharIterator::eUnskipped, /* aSubtree */ nullptr); + while (!it.AtEnd()) { + if (it.IsClusterAndLigatureGroupStart()) { + // If we're at the start of a new cluster or ligature group, reset our + // accumulated partial advance. + partialAdvance = 0.0; + } else { + // Otherwise, we're in the middle of a cluster or ligature group, and + // we need to use the currently accumulated partial advance to adjust + // the character's position and rotation. + + // Find the start of the cluster/ligature group. + uint32_t charIndex = it.TextElementCharIndex(); + uint32_t startIndex = it.GlyphStartTextElementCharIndex(); + MOZ_ASSERT(charIndex != startIndex, + "If the current character is in the middle of a cluster or " + "ligature group, then charIndex must be different from " + "startIndex"); + + mPositions[charIndex].mClusterOrLigatureGroupMiddle = true; + + // Don't allow different rotations on ligature parts. + bool rotationAdjusted = false; + double angle = mPositions[startIndex].mAngle; + if (mPositions[charIndex].mAngle != angle) { + mPositions[charIndex].mAngle = angle; + rotationAdjusted = true; + } + + // Update the character position. + gfxFloat advance = partialAdvance / mFontSizeScaleFactor; + gfxPoint direction = gfxPoint(cos(angle), sin(angle)) * + (it.TextRun()->IsRightToLeft() ? -1.0 : 1.0); + if (it.TextRun()->IsVertical()) { + std::swap(direction.x, direction.y); + } + mPositions[charIndex].mPosition = + mPositions[startIndex].mPosition + direction * advance; + + // Ensure any runs that would end in the middle of a ligature now end just + // after the ligature. + if (mPositions[charIndex].mRunBoundary) { + mPositions[charIndex].mRunBoundary = false; + if (charIndex + 1 < mPositions.Length()) { + mPositions[charIndex + 1].mRunBoundary = true; + } + } else if (rotationAdjusted) { + if (charIndex + 1 < mPositions.Length()) { + mPositions[charIndex + 1].mRunBoundary = true; + } + } + + // Ensure any anchored chunks that would begin in the middle of a ligature + // now begin just after the ligature. + if (mPositions[charIndex].mStartOfChunk) { + mPositions[charIndex].mStartOfChunk = false; + if (charIndex + 1 < mPositions.Length()) { + mPositions[charIndex + 1].mStartOfChunk = true; + } + } + } + + // Accumulate the current character's partial advance. + partialAdvance += it.GetAdvance(presContext); + + it.Next(); + } +} + +already_AddRefed<Path> SVGTextFrame::GetTextPath(nsIFrame* aTextPathFrame) { + nsIContent* content = aTextPathFrame->GetContent(); + SVGTextPathElement* tp = static_cast<SVGTextPathElement*>(content); + if (tp->mPath.IsRendered()) { + // This is just an attribute so there's no transform that can apply + // so we can just return the path directly. + return tp->mPath.GetAnimValue().BuildPathForMeasuring(); + } + + SVGGeometryElement* geomElement = + SVGObserverUtils::GetAndObserveTextPathsPath(aTextPathFrame); + if (!geomElement) { + return nullptr; + } + + RefPtr<Path> path = geomElement->GetOrBuildPathForMeasuring(); + if (!path) { + return nullptr; + } + + gfxMatrix matrix = geomElement->PrependLocalTransformsTo(gfxMatrix()); + if (!matrix.IsIdentity()) { + // Apply the geometry element's transform + RefPtr<PathBuilder> builder = + path->TransformedCopyToBuilder(ToMatrix(matrix)); + path = builder->Finish(); + } + + return path.forget(); +} + +gfxFloat SVGTextFrame::GetOffsetScale(nsIFrame* aTextPathFrame) { + nsIContent* content = aTextPathFrame->GetContent(); + SVGTextPathElement* tp = static_cast<SVGTextPathElement*>(content); + if (tp->mPath.IsRendered()) { + // A path attribute has no pathLength or transform + // so we return a unit scale. + return 1.0; + } + + SVGGeometryElement* geomElement = + SVGObserverUtils::GetAndObserveTextPathsPath(aTextPathFrame); + if (!geomElement) { + return 1.0; + } + return geomElement->GetPathLengthScale(SVGGeometryElement::eForTextPath); +} + +gfxFloat SVGTextFrame::GetStartOffset(nsIFrame* aTextPathFrame) { + SVGTextPathElement* tp = + static_cast<SVGTextPathElement*>(aTextPathFrame->GetContent()); + SVGAnimatedLength* length = + &tp->mLengthAttributes[SVGTextPathElement::STARTOFFSET]; + + if (length->IsPercentage()) { + if (!std::isfinite(GetOffsetScale(aTextPathFrame))) { + // Either pathLength="0" for this path or the path has 0 length. + return 0.0; + } + RefPtr<Path> data = GetTextPath(aTextPathFrame); + return data ? length->GetAnimValInSpecifiedUnits() * data->ComputeLength() / + 100.0 + : 0.0; + } + float lengthValue = length->GetAnimValue(tp); + // If offsetScale is infinity we want to return 0 not NaN + return lengthValue == 0 ? 0.0 : lengthValue * GetOffsetScale(aTextPathFrame); +} + +void SVGTextFrame::DoTextPathLayout() { + nsPresContext* context = PresContext(); + + CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr); + while (!it.AtEnd()) { + nsIFrame* textPathFrame = it.TextPathFrame(); + if (!textPathFrame) { + // Skip past this frame if we're not in a text path. + it.AdvancePastCurrentFrame(); + continue; + } + + // Get the path itself. + RefPtr<Path> path = GetTextPath(textPathFrame); + if (!path) { + uint32_t start = it.TextElementCharIndex(); + it.AdvancePastCurrentTextPathFrame(); + uint32_t end = it.TextElementCharIndex(); + for (uint32_t i = start; i < end; i++) { + mPositions[i].mHidden = true; + } + continue; + } + + SVGTextPathElement* textPath = + static_cast<SVGTextPathElement*>(textPathFrame->GetContent()); + uint16_t side = + textPath->EnumAttributes()[SVGTextPathElement::SIDE].GetAnimValue(); + + gfxFloat offset = GetStartOffset(textPathFrame); + Float pathLength = path->ComputeLength(); + + // If the first character within the text path is in the middle of a + // cluster or ligature group, just skip it and don't apply text path + // positioning. + while (!it.AtEnd()) { + if (it.IsOriginalCharSkipped()) { + it.Next(); + continue; + } + if (it.IsClusterAndLigatureGroupStart()) { + break; + } + it.Next(); + } + + bool skippedEndOfTextPath = false; + + // Loop for each character in the text path. + while (!it.AtEnd() && it.TextPathFrame() && + it.TextPathFrame()->GetContent() == textPath) { + // The index of the cluster or ligature group's first character. + uint32_t i = it.TextElementCharIndex(); + + // The index of the next character of the cluster or ligature. + // We track this as we loop over the characters below so that we + // can detect undisplayed characters and append entries into + // partialAdvances for them. + uint32_t j = i + 1; + + MOZ_ASSERT(!mPositions[i].mClusterOrLigatureGroupMiddle); + + gfxFloat sign = it.TextRun()->IsRightToLeft() ? -1.0 : 1.0; + bool vertical = it.TextRun()->IsVertical(); + + // Compute cumulative advances for each character of the cluster or + // ligature group. + AutoTArray<gfxFloat, 4> partialAdvances; + gfxFloat partialAdvance = it.GetAdvance(context); + partialAdvances.AppendElement(partialAdvance); + while (it.Next()) { + // Append entries for any undisplayed characters the CharIterator + // skipped over. + MOZ_ASSERT(j <= it.TextElementCharIndex()); + while (j < it.TextElementCharIndex()) { + partialAdvances.AppendElement(partialAdvance); + ++j; + } + // This loop may end up outside of the current text path, but + // that's OK; we'll consider any complete cluster or ligature + // group that begins inside the text path as being affected + // by it. + if (it.IsOriginalCharSkipped()) { + if (!it.TextPathFrame()) { + skippedEndOfTextPath = true; + break; + } + // Leave partialAdvance unchanged. + } else if (it.IsClusterAndLigatureGroupStart()) { + break; + } else { + partialAdvance += it.GetAdvance(context); + } + partialAdvances.AppendElement(partialAdvance); + } + + if (!skippedEndOfTextPath) { + // Any final undisplayed characters the CharIterator skipped over. + MOZ_ASSERT(j <= it.TextElementCharIndex()); + while (j < it.TextElementCharIndex()) { + partialAdvances.AppendElement(partialAdvance); + ++j; + } + } + + gfxFloat halfAdvance = + partialAdvances.LastElement() / mFontSizeScaleFactor / 2.0; + gfxFloat midx = + (vertical ? mPositions[i].mPosition.y : mPositions[i].mPosition.x) + + sign * halfAdvance + offset; + + // Hide the character if it falls off the end of the path. + mPositions[i].mHidden = midx < 0 || midx > pathLength; + + // Position the character on the path at the right angle. + Point tangent; // Unit vector tangent to the point we find. + Point pt; + if (side == TEXTPATH_SIDETYPE_RIGHT) { + pt = path->ComputePointAtLength(Float(pathLength - midx), &tangent); + tangent = -tangent; + } else { + pt = path->ComputePointAtLength(Float(midx), &tangent); + } + Float rotation = vertical ? atan2f(-tangent.x, tangent.y) + : atan2f(tangent.y, tangent.x); + Point normal(-tangent.y, tangent.x); // Unit vector normal to the point. + Point offsetFromPath = normal * (vertical ? -mPositions[i].mPosition.x + : mPositions[i].mPosition.y); + pt += offsetFromPath; + Point direction = tangent * sign; + mPositions[i].mPosition = + ThebesPoint(pt) - ThebesPoint(direction) * halfAdvance; + mPositions[i].mAngle += rotation; + + // Position any characters for a partial ligature. + for (uint32_t k = i + 1; k < j; k++) { + gfxPoint partialAdvance = ThebesPoint(direction) * + partialAdvances[k - i] / mFontSizeScaleFactor; + mPositions[k].mPosition = mPositions[i].mPosition + partialAdvance; + mPositions[k].mAngle = mPositions[i].mAngle; + mPositions[k].mHidden = mPositions[i].mHidden; + } + } + } +} + +void SVGTextFrame::DoAnchoring() { + nsPresContext* presContext = PresContext(); + + CharIterator it(this, CharIterator::eOriginal, /* aSubtree */ nullptr); + + // Don't need to worry about skipped or trimmed characters. + while (!it.AtEnd() && + (it.IsOriginalCharSkipped() || it.IsOriginalCharTrimmed())) { + it.Next(); + } + + bool vertical = GetWritingMode().IsVertical(); + uint32_t start = it.TextElementCharIndex(); + while (start < mPositions.Length()) { + it.AdvanceToCharacter(start); + nsTextFrame* chunkFrame = it.TextFrame(); + + // Measure characters in this chunk to find the left-most and right-most + // edges of all glyphs within the chunk. + uint32_t index = it.TextElementCharIndex(); + uint32_t end = start; + gfxFloat left = std::numeric_limits<gfxFloat>::infinity(); + gfxFloat right = -std::numeric_limits<gfxFloat>::infinity(); + do { + if (!it.IsOriginalCharSkipped() && !it.IsOriginalCharTrimmed()) { + gfxFloat advance = it.GetAdvance(presContext) / mFontSizeScaleFactor; + gfxFloat pos = it.TextRun()->IsVertical() + ? mPositions[index].mPosition.y + : mPositions[index].mPosition.x; + if (it.TextRun()->IsRightToLeft()) { + left = std::min(left, pos - advance); + right = std::max(right, pos); + } else { + left = std::min(left, pos); + right = std::max(right, pos + advance); + } + } + it.Next(); + index = end = it.TextElementCharIndex(); + } while (!it.AtEnd() && !mPositions[end].mStartOfChunk); + + if (left != std::numeric_limits<gfxFloat>::infinity()) { + bool isRTL = + chunkFrame->StyleVisibility()->mDirection == StyleDirection::Rtl; + TextAnchorSide anchor = ConvertLogicalTextAnchorToPhysical( + chunkFrame->StyleSVG()->mTextAnchor, isRTL); + + ShiftAnchoredChunk(mPositions, start, end, left, right, anchor, vertical); + } + + start = it.TextElementCharIndex(); + } +} + +void SVGTextFrame::DoGlyphPositioning() { + mPositions.Clear(); + RemoveStateBits(NS_STATE_SVG_POSITIONING_DIRTY); + + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (kid && kid->IsSubtreeDirty()) { + MOZ_ASSERT(false, "should have already reflowed the kid"); + return; + } + + // Since we can be called directly via GetBBoxContribution, our correspondence + // may not be up to date. + TextNodeCorrespondenceRecorder::RecordCorrespondence(this); + + // Determine the positions of each character in app units. + AutoTArray<nsPoint, 64> charPositions; + DetermineCharPositions(charPositions); + + if (charPositions.IsEmpty()) { + // No characters, so nothing to do. + return; + } + + // If the textLength="" attribute was specified, then we need ResolvePositions + // to record that a new run starts with each glyph. + SVGTextContentElement* element = + static_cast<SVGTextContentElement*>(GetContent()); + SVGAnimatedLength* textLengthAttr = + element->GetAnimatedLength(nsGkAtoms::textLength); + uint16_t lengthAdjust = + element->EnumAttributes()[SVGTextContentElement::LENGTHADJUST] + .GetAnimValue(); + bool adjustingTextLength = textLengthAttr->IsExplicitlySet(); + float expectedTextLength = textLengthAttr->GetAnimValue(element); + + if (adjustingTextLength && + (expectedTextLength < 0.0f || lengthAdjust == LENGTHADJUST_UNKNOWN)) { + // If textLength="" is less than zero or lengthAdjust is unknown, ignore it. + adjustingTextLength = false; + } + + // Get the x, y, dx, dy, rotate values for the subtree. + AutoTArray<gfxPoint, 16> deltas; + if (!ResolvePositions(deltas, adjustingTextLength)) { + // If ResolvePositions returned false, it means either there were some + // characters in the DOM but none of them are displayed, or there was + // an error in processing mPositions. Clear out mPositions so that we don't + // attempt to do any painting later. + mPositions.Clear(); + return; + } + + // XXX We might be able to do less work when there is at most a single + // x/y/dx/dy position. + + // Truncate the positioning arrays to the actual number of characters present. + TruncateTo(deltas, charPositions); + TruncateTo(mPositions, charPositions); + + // Fill in an unspecified character position at index 0. + if (!mPositions[0].IsXSpecified()) { + mPositions[0].mPosition.x = 0.0; + } + if (!mPositions[0].IsYSpecified()) { + mPositions[0].mPosition.y = 0.0; + } + if (!mPositions[0].IsAngleSpecified()) { + mPositions[0].mAngle = 0.0; + } + + nsPresContext* presContext = PresContext(); + bool vertical = GetWritingMode().IsVertical(); + + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + double factor = cssPxPerDevPx / mFontSizeScaleFactor; + + // Determine how much to compress or expand glyph positions due to + // textLength="" and lengthAdjust="". + double adjustment = 0.0; + mLengthAdjustScaleFactor = 1.0f; + if (adjustingTextLength) { + nscoord frameLength = + vertical ? PrincipalChildList().FirstChild()->GetRect().height + : PrincipalChildList().FirstChild()->GetRect().width; + float actualTextLength = static_cast<float>( + presContext->AppUnitsToGfxUnits(frameLength) * factor); + + switch (lengthAdjust) { + case LENGTHADJUST_SPACINGANDGLYPHS: + // Scale the glyphs and their positions. + if (actualTextLength > 0) { + mLengthAdjustScaleFactor = expectedTextLength / actualTextLength; + } + break; + + default: + MOZ_ASSERT(lengthAdjust == LENGTHADJUST_SPACING); + // Just add space between each glyph. + int32_t adjustableSpaces = 0; + for (uint32_t i = 1; i < mPositions.Length(); i++) { + if (!mPositions[i].mUnaddressable) { + adjustableSpaces++; + } + } + if (adjustableSpaces) { + adjustment = + (expectedTextLength - actualTextLength) / adjustableSpaces; + } + break; + } + } + + // Fill in any unspecified character positions based on the positions recorded + // in charPositions, and also add in the dx/dy values. + if (!deltas.IsEmpty()) { + mPositions[0].mPosition += deltas[0]; + } + + gfxFloat xLengthAdjustFactor = vertical ? 1.0 : mLengthAdjustScaleFactor; + gfxFloat yLengthAdjustFactor = vertical ? mLengthAdjustScaleFactor : 1.0; + for (uint32_t i = 1; i < mPositions.Length(); i++) { + // Fill in unspecified x position. + if (!mPositions[i].IsXSpecified()) { + nscoord d = charPositions[i].x - charPositions[i - 1].x; + mPositions[i].mPosition.x = + mPositions[i - 1].mPosition.x + + presContext->AppUnitsToGfxUnits(d) * factor * xLengthAdjustFactor; + if (!vertical && !mPositions[i].mUnaddressable) { + mPositions[i].mPosition.x += adjustment; + } + } + // Fill in unspecified y position. + if (!mPositions[i].IsYSpecified()) { + nscoord d = charPositions[i].y - charPositions[i - 1].y; + mPositions[i].mPosition.y = + mPositions[i - 1].mPosition.y + + presContext->AppUnitsToGfxUnits(d) * factor * yLengthAdjustFactor; + if (vertical && !mPositions[i].mUnaddressable) { + mPositions[i].mPosition.y += adjustment; + } + } + // Add in dx/dy. + if (i < deltas.Length()) { + mPositions[i].mPosition += deltas[i]; + } + // Fill in unspecified rotation values. + if (!mPositions[i].IsAngleSpecified()) { + mPositions[i].mAngle = 0.0f; + } + } + + MOZ_ASSERT(mPositions.Length() == charPositions.Length()); + + AdjustChunksForLineBreaks(); + AdjustPositionsForClusters(); + DoAnchoring(); + DoTextPathLayout(); +} + +bool SVGTextFrame::ShouldRenderAsPath(nsTextFrame* aFrame, + bool& aShouldPaintSVGGlyphs) { + // Rendering to a clip path. + if (HasAnyStateBits(NS_STATE_SVG_CLIPPATH_CHILD)) { + aShouldPaintSVGGlyphs = false; + return true; + } + + aShouldPaintSVGGlyphs = true; + + const nsStyleSVG* style = aFrame->StyleSVG(); + + // Fill is a non-solid paint, has a non-default fill-rule or has + // non-1 opacity. + if (!(style->mFill.kind.IsNone() || + (style->mFill.kind.IsColor() && style->mFillOpacity.IsOpacity() && + style->mFillOpacity.AsOpacity() == 1))) { + return true; + } + + // Text has a stroke. + if (style->HasStroke()) { + if (style->mStrokeWidth.IsContextValue()) { + return true; + } + if (SVGContentUtils::CoordToFloat( + static_cast<SVGElement*>(GetContent()), + style->mStrokeWidth.AsLengthPercentage()) > 0) { + return true; + } + } + + return false; +} + +void SVGTextFrame::ScheduleReflowSVG() { + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + ScheduleReflowSVGNonDisplayText( + IntrinsicDirty::FrameAncestorsAndDescendants); + } else { + SVGUtils::ScheduleReflowSVG(this); + } +} + +void SVGTextFrame::NotifyGlyphMetricsChange(bool aUpdateTextCorrespondence) { + if (aUpdateTextCorrespondence) { + AddStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY); + } + AddStateBits(NS_STATE_SVG_POSITIONING_DIRTY); + nsLayoutUtils::PostRestyleEvent(mContent->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + ScheduleReflowSVG(); +} + +void SVGTextFrame::UpdateGlyphPositioning() { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + if (HasAnyStateBits(NS_STATE_SVG_POSITIONING_DIRTY)) { + DoGlyphPositioning(); + } +} + +void SVGTextFrame::MaybeResolveBidiForAnonymousBlockChild() { + nsIFrame* kid = PrincipalChildList().FirstChild(); + + if (kid && kid->HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) && + PresContext()->BidiEnabled()) { + MOZ_ASSERT(static_cast<nsBlockFrame*>(do_QueryFrame(kid)), + "Expect anonymous child to be an nsBlockFrame"); + nsBidiPresUtils::Resolve(static_cast<nsBlockFrame*>(kid)); + } +} + +void SVGTextFrame::MaybeReflowAnonymousBlockChild() { + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + NS_ASSERTION(!kid->HasAnyStateBits(NS_FRAME_IN_REFLOW), + "should not be in reflow when about to reflow again"); + + if (IsSubtreeDirty()) { + if (HasAnyStateBits(NS_FRAME_IS_DIRTY)) { + // If we require a full reflow, ensure our kid is marked fully dirty. + // (Note that our anonymous nsBlockFrame is not an ISVGDisplayableFrame, + // so even when we are called via our ReflowSVG this will not be done for + // us by SVGDisplayContainerFrame::ReflowSVG.) + kid->MarkSubtreeDirty(); + } + + // The RecordCorrespondence and DoReflow calls can result in new text frames + // being created (due to bidi resolution or reflow). We set this bit to + // guard against unnecessarily calling back in to + // ScheduleReflowSVGNonDisplayText from nsIFrame::DidSetComputedStyle on + // those new text frames. + AddStateBits(NS_STATE_SVG_TEXT_IN_REFLOW); + + TextNodeCorrespondenceRecorder::RecordCorrespondence(this); + + MOZ_ASSERT(SVGUtils::AnyOuterSVGIsCallingReflowSVG(this), + "should be under ReflowSVG"); + nsPresContext::InterruptPreventer noInterrupts(PresContext()); + DoReflow(); + + RemoveStateBits(NS_STATE_SVG_TEXT_IN_REFLOW); + } +} + +void SVGTextFrame::DoReflow() { + MOZ_ASSERT(HasAnyStateBits(NS_STATE_SVG_TEXT_IN_REFLOW)); + + // Since we are going to reflow the anonymous block frame, we will + // need to update mPositions. + // We also mark our text correspondence as dirty since we can end up needing + // reflow in ways that do not set NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY. + // (We'd then fail the "expected a TextNodeCorrespondenceProperty" assertion + // when UpdateGlyphPositioning() is called after we return.) + AddStateBits(NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY | + NS_STATE_SVG_POSITIONING_DIRTY); + + if (HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + // Normally, these dirty flags would be cleared in ReflowSVG(), but that + // doesn't get called for non-display frames. We don't want to reflow our + // descendants every time SVGTextFrame::PaintSVG makes sure that we have + // valid positions by calling UpdateGlyphPositioning(), so we need to clear + // these dirty bits. Note that this also breaks an invalidation loop where + // our descendants invalidate as they reflow, which invalidates rendering + // observers, which reschedules the frame that is currently painting by + // referencing us to paint again. See bug 839958 comment 7. Hopefully we + // will break that loop more convincingly at some point. + RemoveStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN); + } + + nsPresContext* presContext = PresContext(); + nsIFrame* kid = PrincipalChildList().FirstChild(); + if (!kid) { + return; + } + + UniquePtr<gfxContext> renderingContext = + presContext->PresShell()->CreateReferenceRenderingContext(); + + if (UpdateFontSizeScaleFactor()) { + // If the font size scale factor changed, we need the block to report + // an updated preferred width. + kid->MarkIntrinsicISizesDirty(); + } + + nscoord inlineSize = kid->GetPrefISize(renderingContext.get()); + WritingMode wm = kid->GetWritingMode(); + ReflowInput reflowInput(presContext, kid, renderingContext.get(), + LogicalSize(wm, inlineSize, NS_UNCONSTRAINEDSIZE)); + ReflowOutput desiredSize(reflowInput); + nsReflowStatus status; + + NS_ASSERTION( + reflowInput.ComputedPhysicalBorderPadding() == nsMargin(0, 0, 0, 0) && + reflowInput.ComputedPhysicalMargin() == nsMargin(0, 0, 0, 0), + "style system should ensure that :-moz-svg-text " + "does not get styled"); + + kid->Reflow(presContext, desiredSize, reflowInput, status); + kid->DidReflow(presContext, &reflowInput); + kid->SetSize(wm, desiredSize.Size(wm)); +} + +// Usable font size range in devpixels / user-units +#define CLAMP_MIN_SIZE 8.0 +#define CLAMP_MAX_SIZE 200.0 +#define PRECISE_SIZE 200.0 + +bool SVGTextFrame::UpdateFontSizeScaleFactor() { + double oldFontSizeScaleFactor = mFontSizeScaleFactor; + + nsPresContext* presContext = PresContext(); + + bool geometricPrecision = false; + CSSCoord min = std::numeric_limits<float>::max(); + CSSCoord max = std::numeric_limits<float>::min(); + bool anyText = false; + + // Find the minimum and maximum font sizes used over all the + // nsTextFrames. + TextFrameIterator it(this); + nsTextFrame* f = it.Current(); + while (f) { + if (!geometricPrecision) { + // Unfortunately we can't treat text-rendering:geometricPrecision + // separately for each text frame. + geometricPrecision = f->StyleText()->mTextRendering == + StyleTextRendering::Geometricprecision; + } + const auto& fontSize = f->StyleFont()->mFont.size; + if (!fontSize.IsZero()) { + min = std::min(min, fontSize.ToCSSPixels()); + max = std::max(max, fontSize.ToCSSPixels()); + anyText = true; + } + f = it.Next(); + } + + if (!anyText) { + // No text, so no need for scaling. + mFontSizeScaleFactor = 1.0; + return mFontSizeScaleFactor != oldFontSizeScaleFactor; + } + + if (geometricPrecision) { + // We want to ensure minSize is scaled to PRECISE_SIZE. + mFontSizeScaleFactor = PRECISE_SIZE / min; + return mFontSizeScaleFactor != oldFontSizeScaleFactor; + } + + // When we are non-display, we could be painted in different coordinate + // spaces, and we don't want to have to reflow for each of these. We + // just assume that the context scale is 1.0 for them all, so we don't + // get stuck with a font size scale factor based on whichever referencing + // frame happens to reflow first. + double contextScale = 1.0; + if (!HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + gfxMatrix m(GetCanvasTM()); + if (!m.IsSingular()) { + contextScale = GetContextScale(m); + if (!std::isfinite(contextScale)) { + contextScale = 1.0f; + } + } + } + mLastContextScale = contextScale; + + // But we want to ignore any scaling required due to HiDPI displays, since + // regular CSS text frames will still create text runs using the font size + // in CSS pixels, and we want SVG text to have the same rendering as HTML + // text for regular font sizes. + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + contextScale *= cssPxPerDevPx; + + double minTextRunSize = min * contextScale; + double maxTextRunSize = max * contextScale; + + if (minTextRunSize >= CLAMP_MIN_SIZE && maxTextRunSize <= CLAMP_MAX_SIZE) { + // We are already in the ideal font size range for all text frames, + // so we only have to take into account the contextScale. + mFontSizeScaleFactor = contextScale; + } else if (max / min > CLAMP_MAX_SIZE / CLAMP_MIN_SIZE) { + // We can't scale the font sizes so that all of the text frames lie + // within our ideal font size range. + // Heuristically, if the maxTextRunSize is within the CLAMP_MAX_SIZE + // as a reasonable value, it's likely to be the user's intent to + // get a valid font for the maxTextRunSize one, we should honor it. + // The same for minTextRunSize. + if (maxTextRunSize <= CLAMP_MAX_SIZE) { + mFontSizeScaleFactor = CLAMP_MAX_SIZE / max; + } else if (minTextRunSize >= CLAMP_MIN_SIZE) { + mFontSizeScaleFactor = CLAMP_MIN_SIZE / min; + } else { + // So maxTextRunSize is too big, minTextRunSize is too small, + // we can't really do anything for this case, just leave it as is. + mFontSizeScaleFactor = contextScale; + } + } else if (minTextRunSize < CLAMP_MIN_SIZE) { + mFontSizeScaleFactor = CLAMP_MIN_SIZE / min; + } else { + mFontSizeScaleFactor = CLAMP_MAX_SIZE / max; + } + + return mFontSizeScaleFactor != oldFontSizeScaleFactor; +} + +double SVGTextFrame::GetFontSizeScaleFactor() const { + return mFontSizeScaleFactor; +} + +/** + * Take aPoint, which is in the <text> element's user space, and convert + * it to the appropriate frame user space of aChildFrame according to + * which rendered run the point hits. + */ +Point SVGTextFrame::TransformFramePointToTextChild( + const Point& aPoint, const nsIFrame* aChildFrame) { + NS_ASSERTION(aChildFrame && nsLayoutUtils::GetClosestFrameOfType( + aChildFrame->GetParent(), + LayoutFrameType::SVGText) == this, + "aChildFrame must be a descendant of this frame"); + + UpdateGlyphPositioning(); + + nsPresContext* presContext = PresContext(); + + // Add in the mRect offset to aPoint, as that will have been taken into + // account when transforming the point from the ancestor frame down + // to this one. + float cssPxPerDevPx = nsPresContext::AppUnitsToFloatCSSPixels( + presContext->AppUnitsPerDevPixel()); + float factor = AppUnitsPerCSSPixel(); + Point framePosition(NSAppUnitsToFloatPixels(mRect.x, factor), + NSAppUnitsToFloatPixels(mRect.y, factor)); + Point pointInUserSpace = aPoint * cssPxPerDevPx + framePosition; + + // Find the closest rendered run for the text frames beneath aChildFrame. + TextRenderedRunIterator it(this, TextRenderedRunIterator::eAllFrames, + aChildFrame); + TextRenderedRun hit; + gfxPoint pointInRun; + nscoord dx = nscoord_MAX; + nscoord dy = nscoord_MAX; + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + uint32_t flags = TextRenderedRun::eIncludeFill | + TextRenderedRun::eIncludeStroke | + TextRenderedRun::eNoHorizontalOverflow; + gfxRect runRect = + run.GetRunUserSpaceRect(presContext, flags).ToThebesRect(); + + gfxMatrix m = run.GetTransformFromRunUserSpaceToUserSpace(presContext); + if (!m.Invert()) { + return aPoint; + } + gfxPoint pointInRunUserSpace = + m.TransformPoint(ThebesPoint(pointInUserSpace)); + + if (Inside(runRect, pointInRunUserSpace)) { + // The point was inside the rendered run's rect, so we choose it. + dx = 0; + dy = 0; + pointInRun = pointInRunUserSpace; + hit = run; + } else if (nsLayoutUtils::PointIsCloserToRect(pointInRunUserSpace, runRect, + dx, dy)) { + // The point was closer to this rendered run's rect than any others + // we've seen so far. + pointInRun.x = + clamped(pointInRunUserSpace.x.value, runRect.X(), runRect.XMost()); + pointInRun.y = + clamped(pointInRunUserSpace.y.value, runRect.Y(), runRect.YMost()); + hit = run; + } + } + + if (!hit.mFrame) { + // We didn't find any rendered runs for the frame. + return aPoint; + } + + // Return the point in user units relative to the nsTextFrame, + // but taking into account mFontSizeScaleFactor. + gfxMatrix m = hit.GetTransformFromRunUserSpaceToFrameUserSpace(presContext); + m.PreScale(mFontSizeScaleFactor, mFontSizeScaleFactor); + return ToPoint(m.TransformPoint(pointInRun) / cssPxPerDevPx); +} + +/** + * For each rendered run beneath aChildFrame, translate aRect from + * aChildFrame to the run's text frame, transform it then into + * the run's frame user space, intersect it with the run's + * frame user space rect, then transform it up to user space. + * The result is the union of all of these. + */ +gfxRect SVGTextFrame::TransformFrameRectFromTextChild( + const nsRect& aRect, const nsIFrame* aChildFrame) { + NS_ASSERTION(aChildFrame && nsLayoutUtils::GetClosestFrameOfType( + aChildFrame->GetParent(), + LayoutFrameType::SVGText) == this, + "aChildFrame must be a descendant of this frame"); + + UpdateGlyphPositioning(); + + nsPresContext* presContext = PresContext(); + + gfxRect result; + TextRenderedRunIterator it(this, TextRenderedRunIterator::eAllFrames, + aChildFrame); + for (TextRenderedRun run = it.Current(); run.mFrame; run = it.Next()) { + // First, translate aRect from aChildFrame to this run's frame. + nsRect rectInTextFrame = aRect + aChildFrame->GetOffsetTo(run.mFrame); + + // Scale it into frame user space. + gfxRect rectInFrameUserSpace = AppUnitsToFloatCSSPixels( + gfxRect(rectInTextFrame.x, rectInTextFrame.y, rectInTextFrame.width, + rectInTextFrame.height), + presContext); + + // Intersect it with the run. + uint32_t flags = + TextRenderedRun::eIncludeFill | TextRenderedRun::eIncludeStroke; + + if (rectInFrameUserSpace.IntersectRect( + rectInFrameUserSpace, + run.GetFrameUserSpaceRect(presContext, flags).ToThebesRect())) { + // Transform it up to user space of the <text> + gfxMatrix m = run.GetTransformFromRunUserSpaceToUserSpace(presContext); + gfxRect rectInUserSpace = m.TransformRect(rectInFrameUserSpace); + + // Union it into the result. + result.UnionRect(result, rectInUserSpace); + } + } + + // Subtract the mRect offset from the result, as our user space for + // this frame is relative to the top-left of mRect. + float factor = AppUnitsPerCSSPixel(); + gfxPoint framePosition(NSAppUnitsToFloatPixels(mRect.x, factor), + NSAppUnitsToFloatPixels(mRect.y, factor)); + + return result - framePosition; +} + +Rect SVGTextFrame::TransformFrameRectFromTextChild( + const Rect& aRect, const nsIFrame* aChildFrame) { + nscoord appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); + nsRect r = LayoutDevicePixel::ToAppUnits( + LayoutDeviceRect::FromUnknownRect(aRect), appUnitsPerDevPixel); + gfxRect resultCssUnits = TransformFrameRectFromTextChild(r, aChildFrame); + float devPixelPerCSSPixel = + float(AppUnitsPerCSSPixel()) / appUnitsPerDevPixel; + resultCssUnits.Scale(devPixelPerCSSPixel); + return ToRect(resultCssUnits); +} + +Point SVGTextFrame::TransformFramePointFromTextChild( + const Point& aPoint, const nsIFrame* aChildFrame) { + return TransformFrameRectFromTextChild(Rect(aPoint, Size(1, 1)), aChildFrame) + .TopLeft(); +} + +void SVGTextFrame::AppendDirectlyOwnedAnonBoxes( + nsTArray<OwnedAnonBox>& aResult) { + MOZ_ASSERT(PrincipalChildList().FirstChild(), "Must have our anon box"); + aResult.AppendElement(OwnedAnonBox(PrincipalChildList().FirstChild())); +} + +} // namespace mozilla diff --git a/layout/svg/SVGTextFrame.h b/layout/svg/SVGTextFrame.h new file mode 100644 index 0000000000..6789e90c0f --- /dev/null +++ b/layout/svg/SVGTextFrame.h @@ -0,0 +1,589 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGTEXTFRAME_H_ +#define LAYOUT_SVG_SVGTEXTFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/PresShellForwards.h" +#include "mozilla/RefPtr.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/gfx/2D.h" +#include "gfxMatrix.h" +#include "gfxRect.h" +#include "gfxTextRun.h" +#include "nsIContent.h" // for GetContent +#include "nsStubMutationObserver.h" +#include "nsTextFrame.h" + +class gfxContext; + +namespace mozilla { + +class CharIterator; +class DisplaySVGText; +class SVGTextFrame; +class TextFrameIterator; +class TextNodeCorrespondenceRecorder; +struct TextRenderedRun; +class TextRenderedRunIterator; + +namespace dom { +struct DOMPointInit; +class DOMSVGPoint; +class SVGRect; +class SVGGeometryElement; +} // namespace dom +} // namespace mozilla + +nsIFrame* NS_NewSVGTextFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +/** + * Information about the positioning for a single character in an SVG <text> + * element. + * + * During SVG text layout, we use infinity values to represent positions and + * rotations that are not explicitly specified with x/y/rotate attributes. + */ +struct CharPosition { + CharPosition() + : mAngle(0), + mHidden(false), + mUnaddressable(false), + mClusterOrLigatureGroupMiddle(false), + mRunBoundary(false), + mStartOfChunk(false) {} + + CharPosition(gfxPoint aPosition, double aAngle) + : mPosition(aPosition), + mAngle(aAngle), + mHidden(false), + mUnaddressable(false), + mClusterOrLigatureGroupMiddle(false), + mRunBoundary(false), + mStartOfChunk(false) {} + + static CharPosition Unspecified(bool aUnaddressable) { + CharPosition cp(UnspecifiedPoint(), UnspecifiedAngle()); + cp.mUnaddressable = aUnaddressable; + return cp; + } + + bool IsAngleSpecified() const { return mAngle != UnspecifiedAngle(); } + + bool IsXSpecified() const { return mPosition.x != UnspecifiedCoord(); } + + bool IsYSpecified() const { return mPosition.y != UnspecifiedCoord(); } + + gfxPoint mPosition; + double mAngle; + + // not displayed due to falling off the end of a <textPath> + bool mHidden; + + // skipped in positioning attributes due to being collapsed-away white space + bool mUnaddressable; + + // a preceding character is what positioning attributes address + bool mClusterOrLigatureGroupMiddle; + + // rendering is split here since an explicit position or rotation was given + bool mRunBoundary; + + // an anchored chunk begins here + bool mStartOfChunk; + + private: + static gfxFloat UnspecifiedCoord() { + return std::numeric_limits<gfxFloat>::infinity(); + } + + static double UnspecifiedAngle() { + return std::numeric_limits<double>::infinity(); + } + + static gfxPoint UnspecifiedPoint() { + return gfxPoint(UnspecifiedCoord(), UnspecifiedCoord()); + } +}; + +/** + * A runnable to mark glyph positions as needing to be recomputed + * and to invalid the bounds of the SVGTextFrame frame. + */ +class GlyphMetricsUpdater : public Runnable { + public: + NS_DECL_NSIRUNNABLE + explicit GlyphMetricsUpdater(SVGTextFrame* aFrame) + : Runnable("GlyphMetricsUpdater"), mFrame(aFrame) {} + static void Run(SVGTextFrame* aFrame); + void Revoke() { mFrame = nullptr; } + + private: + SVGTextFrame* mFrame; +}; + +/** + * Frame class for SVG <text> elements. + * + * An SVGTextFrame manages SVG text layout, painting and interaction for + * all descendent text content elements. The frame tree will look like this: + * + * SVGTextFrame -- for <text> + * <anonymous block frame> + * ns{Block,Inline,Text}Frames -- for text nodes, <tspan>s, <a>s, etc. + * + * SVG text layout is done by: + * + * 1. Reflowing the anonymous block frame. + * 2. Inspecting the (app unit) positions of the glyph for each character in + * the nsTextFrames underneath the anonymous block frame. + * 3. Determining the (user unit) positions for each character in the <text> + * using the x/y/dx/dy/rotate attributes on all the text content elements, + * and using the step 2 results to fill in any gaps. + * 4. Applying any other SVG specific text layout (anchoring and text paths) + * to the positions computed in step 3. + * + * Rendering of the text is done by splitting up each nsTextFrame into ranges + * that can be contiguously painted. (For example <text x="10 20">abcd</text> + * would have two contiguous ranges: one for the "a" and one for the "bcd".) + * Each range is called a "text rendered run", represented by a TextRenderedRun + * object. The TextRenderedRunIterator class performs that splitting and + * returns a TextRenderedRun for each bit of text to be painted separately. + * + * Each rendered run is painted by calling nsTextFrame::PaintText. If the text + * formatting is simple enough (solid fill, no stroking, etc.), PaintText will + * itself do the painting. Otherwise, a DrawPathCallback is passed to + * PaintText so that we can fill the text geometry with SVG paint servers. + */ +class SVGTextFrame final : public SVGDisplayContainerFrame { + friend nsIFrame* ::NS_NewSVGTextFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + friend class CharIterator; + friend class DisplaySVGText; + friend class GlyphMetricsUpdater; + friend class MutationObserver; + friend class TextFrameIterator; + friend class TextNodeCorrespondenceRecorder; + friend struct TextRenderedRun; + friend class TextRenderedRunIterator; + + using Range = gfxTextRun::Range; + using DrawTarget = gfx::DrawTarget; + using Path = gfx::Path; + using Point = gfx::Point; + using Rect = gfx::Rect; + + protected: + explicit SVGTextFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGDisplayContainerFrame(aStyle, aPresContext, kClassID), + mTrailingUndisplayedCharacters(0), + mFontSizeScaleFactor(1.0f), + mLastContextScale(1.0f), + mLengthAdjustScaleFactor(1.0f) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_SVG_TEXT | + NS_STATE_SVG_TEXT_CORRESPONDENCE_DIRTY | + NS_STATE_SVG_POSITIONING_DIRTY); + } + + ~SVGTextFrame() = default; + + public: + NS_DECL_QUERYFRAME + NS_DECL_FRAMEARENA_HELPERS(SVGTextFrame) + + // nsIFrame: + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + nsresult AttributeChanged(int32_t aNamespaceID, nsAtom* aAttribute, + int32_t aModType) override; + + nsContainerFrame* GetContentInsertionFrame() override { + return PrincipalChildList().FirstChild()->GetContentInsertionFrame(); + } + + void BuildDisplayList(nsDisplayListBuilder* aBuilder, + const nsDisplayListSet& aLists) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGText"_ns, aResult); + } +#endif + + /** + * Finds the nsTextFrame for the closest rendered run to the specified point. + */ + void FindCloserFrameForSelection( + const nsPoint& aPoint, FrameWithDistance* aCurrentBestFrame) override; + + // ISVGDisplayableFrame interface: + void NotifySVGChanged(uint32_t aFlags) override; + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + void ReflowSVG() override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + + // SVG DOM text methods: + uint32_t GetNumberOfChars(nsIContent* aContent); + float GetComputedTextLength(nsIContent* aContent); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void SelectSubString(nsIContent* aContent, + uint32_t charnum, + uint32_t nchars, + ErrorResult& aRv); + bool RequiresSlowFallbackForSubStringLength(); + float GetSubStringLengthFastPath(nsIContent* aContent, uint32_t charnum, + uint32_t nchars, ErrorResult& aRv); + /** + * This fallback version of GetSubStringLength takes + * into account glyph positioning and requires us to have flushed layout + * before calling it. As per the SVG 2 spec, typically glyph + * positioning does not affect the results of getSubStringLength, but one + * exception is text in a textPath where we need to ignore characters that + * fall off the end of the textPath path. + */ + float GetSubStringLengthSlowFallback(nsIContent* aContent, uint32_t charnum, + uint32_t nchars, ErrorResult& aRv); + + int32_t GetCharNumAtPosition(nsIContent* aContent, + const dom::DOMPointInit& aPoint); + + already_AddRefed<dom::DOMSVGPoint> GetStartPositionOfChar( + nsIContent* aContent, uint32_t aCharNum, ErrorResult& aRv); + already_AddRefed<dom::DOMSVGPoint> GetEndPositionOfChar(nsIContent* aContent, + uint32_t aCharNum, + ErrorResult& aRv); + already_AddRefed<dom::SVGRect> GetExtentOfChar(nsIContent* aContent, + uint32_t aCharNum, + ErrorResult& aRv); + float GetRotationOfChar(nsIContent* aContent, uint32_t aCharNum, + ErrorResult& aRv); + + // SVGTextFrame methods: + + /** + * Handles a base or animated attribute value change to a descendant + * text content element. + */ + void HandleAttributeChangeInDescendant(dom::Element* aElement, + int32_t aNameSpaceID, + nsAtom* aAttribute); + + /** + * Calls ScheduleReflowSVGNonDisplayText if this is a non-display frame, + * and SVGUtils::ScheduleReflowSVG otherwise. + */ + void ScheduleReflowSVG(); + + /** + * Reflows the anonymous block frame of this non-display SVGTextFrame. + * + * When we are under SVGDisplayContainerFrame::ReflowSVG, we need to + * reflow any SVGTextFrame frames in the subtree in case they are + * being observed (by being for example in a <mask>) and the change + * that caused the reflow would not already have caused a reflow. + * + * Note that displayed SVGTextFrames are reflowed as needed, when PaintSVG + * is called or some SVG DOM method is called on the element. + */ + void ReflowSVGNonDisplayText(); + + /** + * This is a function that behaves similarly to SVGUtils::ScheduleReflowSVG, + * but which will skip over any ancestor non-display container frames on the + * way to the SVGOuterSVGFrame. It exists for the situation where a + * non-display <text> element has changed and needs to ensure ReflowSVG will + * be called on its closest display container frame, so that + * SVGDisplayContainerFrame::ReflowSVG will call ReflowSVGNonDisplayText on + * it. + * + * We have to do this in two cases: in response to a style change on a + * non-display <text>, where aReason will be + * IntrinsicDirty::FrameAncestorsAndDescendants (the common case), and also in + * response to glyphs changes on non-display <text> (i.e., animated + * SVG-in-OpenType glyphs), in which case aReason will be None, since layout + * doesn't need to be recomputed. + */ + void ScheduleReflowSVGNonDisplayText(IntrinsicDirty aReason); + + /** + * Updates the mFontSizeScaleFactor value by looking at the range of + * font-sizes used within the <text>. + * + * @return Whether mFontSizeScaleFactor changed. + */ + bool UpdateFontSizeScaleFactor(); + + double GetFontSizeScaleFactor() const; + + /** + * Takes a point from the <text> element's user space and + * converts it to the appropriate frame user space of aChildFrame, + * according to which rendered run the point hits. + */ + Point TransformFramePointToTextChild(const Point& aPoint, + const nsIFrame* aChildFrame); + + /** + * Takes an app unit rectangle in the coordinate space of a given descendant + * frame of this frame, and returns a rectangle in the <text> element's user + * space that covers all parts of rendered runs that intersect with the + * rectangle. + */ + gfxRect TransformFrameRectFromTextChild(const nsRect& aRect, + const nsIFrame* aChildFrame); + + /** As above, but taking and returning a device px rect. */ + Rect TransformFrameRectFromTextChild(const Rect& aRect, + const nsIFrame* aChildFrame); + + /** As above, but with a single point */ + Point TransformFramePointFromTextChild(const Point& aPoint, + const nsIFrame* aChildFrame); + + // Return our ::-moz-svg-text anonymous box. + void AppendDirectlyOwnedAnonBoxes(nsTArray<OwnedAnonBox>& aResult) override; + + private: + /** + * Mutation observer used to watch for text positioning attribute changes + * on descendent text content elements (like <tspan>s). + */ + class MutationObserver final : public nsStubMutationObserver { + public: + explicit MutationObserver(SVGTextFrame* aFrame) : mFrame(aFrame) { + MOZ_ASSERT(mFrame, "MutationObserver needs a non-null frame"); + mFrame->GetContent()->AddMutationObserver(this); + } + + // nsISupports + NS_DECL_ISUPPORTS + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + private: + ~MutationObserver() { mFrame->GetContent()->RemoveMutationObserver(this); } + + SVGTextFrame* const mFrame; + }; + + /** + * Resolves Bidi for the anonymous block child if it needs it. + */ + void MaybeResolveBidiForAnonymousBlockChild(); + + /** + * Reflows the anonymous block child if it is dirty or has dirty + * children, or if the SVGTextFrame itself is dirty. + */ + void MaybeReflowAnonymousBlockChild(); + + /** + * Performs the actual work of reflowing the anonymous block child. + */ + void DoReflow(); + + /** + * Schedules mPositions to be recomputed and the covered region to be + * updated. + */ + void NotifyGlyphMetricsChange(bool aUpdateTextCorrespondence); + + /** + * Recomputes mPositions by calling DoGlyphPositioning if this information + * is out of date. + */ + void UpdateGlyphPositioning(); + + /** + * Populates mPositions with positioning information for each character + * within the <text>. + */ + void DoGlyphPositioning(); + + /** + * Converts the specified index into mPositions to an addressable + * character index (as can be used with the SVG DOM text methods) + * relative to the specified text child content element. + * + * @param aIndex The global character index. + * @param aContent The descendant text child content element that + * the returned addressable index will be relative to; null + * means the same as the <text> element. + * @return The addressable index, or -1 if the index cannot be + * represented as an addressable index relative to aContent. + */ + int32_t ConvertTextElementCharIndexToAddressableIndex(int32_t aIndex, + nsIContent* aContent); + + /** + * Recursive helper for ResolvePositions below. + * + * @param aContent The current node. + * @param aIndex (in/out) The current character index. + * @param aInTextPath Whether we are currently under a <textPath> element. + * @param aForceStartOfChunk (in/out) Whether the next character we find + * should start a new anchored chunk. + * @param aDeltas (in/out) Receives the resolved dx/dy values for each + * character. + * @return false if we discover that mPositions did not have enough + * elements; true otherwise. + */ + bool ResolvePositionsForNode(nsIContent* aContent, uint32_t& aIndex, + bool aInTextPath, bool& aForceStartOfChunk, + nsTArray<gfxPoint>& aDeltas); + + /** + * Initializes mPositions with character position information based on + * x/y/rotate attributes, leaving unspecified values in the array if a + * position was not given for that character. Also fills aDeltas with values + * based on dx/dy attributes. + * + * @param aDeltas (in/out) Receives the resolved dx/dy values for each + * character. + * @param aRunPerGlyph Whether mPositions should record that a new run begins + * at each glyph. + * @return false if we did not record any positions (due to having no + * displayed characters) or if we discover that mPositions did not have + * enough elements; true otherwise. + */ + bool ResolvePositions(nsTArray<gfxPoint>& aDeltas, bool aRunPerGlyph); + + /** + * Determines the position, in app units, of each character in the <text> as + * laid out by reflow, and appends them to aPositions. Any characters that + * are undisplayed or trimmed away just get the last position. + */ + void DetermineCharPositions(nsTArray<nsPoint>& aPositions); + + /** + * Sets mStartOfChunk to true for each character in mPositions that starts a + * line of text. + */ + void AdjustChunksForLineBreaks(); + + /** + * Adjusts recorded character positions in mPositions to account for glyph + * boundaries. Four things are done: + * + * 1. mClusterOrLigatureGroupMiddle is set to true for all such characters. + * + * 2. Any run and anchored chunk boundaries that begin in the middle of a + * cluster/ligature group get moved to the start of the next + * cluster/ligature group. + * + * 3. The position of any character in the middle of a cluster/ligature + * group is updated to take into account partial ligatures and any + * rotation the glyph as a whole has. (The values that come out of + * DetermineCharPositions which then get written into mPositions in + * ResolvePositions store the same position value for each part of the + * ligature.) + * + * 4. The rotation of any character in the middle of a cluster/ligature + * group is set to the rotation of the first character. + */ + void AdjustPositionsForClusters(); + + /** + * Updates the character positions stored in mPositions to account for + * text anchoring. + */ + void DoAnchoring(); + + /** + * Updates character positions in mPositions for those characters inside a + * <textPath>. + */ + void DoTextPathLayout(); + + /** + * Returns whether we need to render the text using + * nsTextFrame::DrawPathCallbacks rather than directly painting + * the text frames. + * + * @param aShouldPaintSVGGlyphs (out) Whether SVG glyphs in the text + * should be painted. + */ + bool ShouldRenderAsPath(nsTextFrame* aFrame, bool& aShouldPaintSVGGlyphs); + + // Methods to get information for a <textPath> frame. + already_AddRefed<Path> GetTextPath(nsIFrame* aTextPathFrame); + gfxFloat GetOffsetScale(nsIFrame* aTextPathFrame); + gfxFloat GetStartOffset(nsIFrame* aTextPathFrame); + + /** + * The MutationObserver we have registered for the <text> element subtree. + */ + RefPtr<MutationObserver> mMutationObserver; + + /** + * The number of characters in the DOM after the final nsTextFrame. For + * example, with + * + * <text>abcd<tspan display="none">ef</tspan></text> + * + * mTrailingUndisplayedCharacters would be 2. + */ + uint32_t mTrailingUndisplayedCharacters; + + /** + * Computed position information for each DOM character within the <text>. + */ + nsTArray<CharPosition> mPositions; + + /** + * mFontSizeScaleFactor is used to cause the nsTextFrames to create text + * runs with a font size different from the actual font-size property value. + * This is used so that, for example with: + * + * <svg> + * <g transform="scale(2)"> + * <text font-size="10">abc</text> + * </g> + * </svg> + * + * a font size of 20 would be used. It's preferable to use a font size that + * is identical or close to the size that the text will appear on the screen, + * because at very small or large font sizes, text metrics will be computed + * differently due to the limited precision that text runs have. + * + * mFontSizeScaleFactor is the amount the actual font-size property value + * should be multiplied by to cause the text run font size to (a) be within a + * "reasonable" range, and (b) be close to the actual size to be painted on + * screen. (The "reasonable" range as determined by some #defines in + * SVGTextFrame.cpp is 8..200.) + */ + float mFontSizeScaleFactor; + + /** + * The scale of the context that we last used to compute mFontSizeScaleFactor. + * We record this so that we can tell when our scale transform has changed + * enough to warrant reflowing the text. + */ + float mLastContextScale; + + /** + * The amount that we need to scale each rendered run to account for + * lengthAdjust="spacingAndGlyphs". + */ + float mLengthAdjustScaleFactor; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGTEXTFRAME_H_ diff --git a/layout/svg/SVGUseFrame.cpp b/layout/svg/SVGUseFrame.cpp new file mode 100644 index 0000000000..c655f7b24f --- /dev/null +++ b/layout/svg/SVGUseFrame.cpp @@ -0,0 +1,145 @@ +/* -*- 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/. */ + +#include "SVGUseFrame.h" + +#include "mozilla/PresShell.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/MutationEvent.h" +#include "mozilla/dom/SVGUseElement.h" +#include "nsLayoutUtils.h" + +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Implementation + +nsIFrame* NS_NewSVGUseFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGUseFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGUseFrame) + +//---------------------------------------------------------------------- +// nsIFrame methods: + +void SVGUseFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::use), + "Content is not an SVG use!"); + + mHasValidDimensions = + static_cast<SVGUseElement*>(aContent)->HasValidDimensions(); + + SVGGFrame::Init(aContent, aParent, aPrevInFlow); +} + +nsresult SVGUseFrame::AttributeChanged(int32_t aNamespaceID, nsAtom* aAttribute, + int32_t aModType) { + // Currently our SMIL implementation does not modify the DOM attributes. Once + // we implement the SVG 2 SMIL behaviour this can be removed + // SVGUseElement::AfterSetAttr's implementation will be sufficient. + if (aModType == MutationEvent_Binding::SMIL) { + auto* content = SVGUseElement::FromNode(GetContent()); + content->ProcessAttributeChange(aNamespaceID, aAttribute); + } + + return SVGGFrame::AttributeChanged(aNamespaceID, aAttribute, aModType); +} + +void SVGUseFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { + SVGGFrame::DidSetComputedStyle(aOldComputedStyle); + + if (!aOldComputedStyle) { + return; + } + const auto* newSVGReset = StyleSVGReset(); + const auto* oldSVGReset = aOldComputedStyle->StyleSVGReset(); + + if (newSVGReset->mX != oldSVGReset->mX || + newSVGReset->mY != oldSVGReset->mY) { + // make sure our cached transform matrix gets (lazily) updated + mCanvasTM = nullptr; + SVGUtils::ScheduleReflowSVG(this); + SVGUtils::NotifyChildrenOfSVGChange(this, TRANSFORM_CHANGED); + } +} + +void SVGUseFrame::DimensionAttributeChanged(bool aHadValidDimensions, + bool aAttributeIsUsed) { + bool invalidate = aAttributeIsUsed; + if (mHasValidDimensions != aHadValidDimensions) { + mHasValidDimensions = !mHasValidDimensions; + invalidate = true; + } + + if (invalidate) { + nsLayoutUtils::PostRestyleEvent(GetContent()->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(this); + } +} + +void SVGUseFrame::HrefChanged() { + nsLayoutUtils::PostRestyleEvent(GetContent()->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(this); +} + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +void SVGUseFrame::ReflowSVG() { + // We only handle x/y offset here, since any width/height that is in force is + // handled by the SVGOuterSVGFrame for the anonymous <svg> that will be + // created for that purpose. + auto* content = SVGUseElement::FromNode(GetContent()); + float x = SVGContentUtils::CoordToFloat(content, StyleSVGReset()->mX, + SVGContentUtils::X); + float y = SVGContentUtils::CoordToFloat(content, StyleSVGReset()->mY, + SVGContentUtils::Y); + mRect.MoveTo(nsLayoutUtils::RoundGfxRectToAppRect(gfxRect(x, y, 0, 0), + AppUnitsPerCSSPixel()) + .TopLeft()); + + // If we have a filter, we need to invalidate ourselves because filter + // output can change even if none of our descendants need repainting. + if (StyleEffects()->HasFilters()) { + InvalidateFrame(); + } + + SVGGFrame::ReflowSVG(); +} + +void SVGUseFrame::NotifySVGChanged(uint32_t aFlags) { + if (aFlags & COORD_CONTEXT_CHANGED && !(aFlags & TRANSFORM_CHANGED)) { + // Coordinate context changes affect mCanvasTM if we have a + // percentage 'x' or 'y' + if (StyleSVGReset()->mX.HasPercent() || StyleSVGReset()->mY.HasPercent()) { + aFlags |= TRANSFORM_CHANGED; + // Ancestor changes can't affect how we render from the perspective of + // any rendering observers that we may have, so we don't need to + // invalidate them. We also don't need to invalidate ourself, since our + // changed ancestor will have invalidated its entire area, which includes + // our area. + // For perf reasons we call this before calling NotifySVGChanged() below. + SVGUtils::ScheduleReflowSVG(this); + } + } + + // We don't remove the TRANSFORM_CHANGED flag here if we have a viewBox or + // non-percentage width/height, since if they're set then they are cloned to + // an anonymous child <svg>, and its SVGInnerSVGFrame will do that. + + SVGGFrame::NotifySVGChanged(aFlags); +} + +} // namespace mozilla diff --git a/layout/svg/SVGUseFrame.h b/layout/svg/SVGUseFrame.h new file mode 100644 index 0000000000..b72d29247a --- /dev/null +++ b/layout/svg/SVGUseFrame.h @@ -0,0 +1,64 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGUSEFRAME_H_ +#define LAYOUT_SVG_SVGUSEFRAME_H_ + +// Keep in (case-insensitive) order: +#include "SVGGFrame.h" + +namespace mozilla { +class PresShell; +} // namespace mozilla + +nsIFrame* NS_NewSVGUseFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +class SVGUseFrame final : public SVGGFrame { + friend nsIFrame* ::NS_NewSVGUseFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGUseFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : SVGGFrame(aStyle, aPresContext, kClassID), mHasValidDimensions(true) {} + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGUseFrame) + + // nsIFrame interface: + void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; + + // Called when the href attributes changed. + void HrefChanged(); + + // Called when the width or height attributes changed. + void DimensionAttributeChanged(bool aHadValidDimensions, + bool aAttributeIsUsed); + + nsresult AttributeChanged(int32_t aNamespaceID, nsAtom* aAttribute, + int32_t aModType) override; + void DidSetComputedStyle(ComputedStyle* aOldComputedStyle) override; + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGUse"_ns, aResult); + } +#endif + + // ISVGDisplayableFrame interface: + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + + private: + bool mHasValidDimensions; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGUSEFRAME_H_ diff --git a/layout/svg/SVGUtils.cpp b/layout/svg/SVGUtils.cpp new file mode 100644 index 0000000000..206f56cedd --- /dev/null +++ b/layout/svg/SVGUtils.cpp @@ -0,0 +1,1558 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Main header first: +// This is also necessary to ensure our definition of M_SQRT1_2 is picked up +#include "SVGUtils.h" +#include <algorithm> + +// Keep others in (case-insensitive) order: +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "gfxMatrix.h" +#include "gfxPlatform.h" +#include "gfxRect.h" +#include "gfxUtils.h" +#include "nsCSSFrameConstructor.h" +#include "nsDisplayList.h" +#include "nsFrameList.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsIFrame.h" +#include "nsIFrameInlines.h" +#include "nsLayoutUtils.h" +#include "nsPresContext.h" +#include "nsStyleStruct.h" +#include "nsStyleTransformMatrix.h" +#include "SVGAnimatedLength.h" +#include "SVGPaintServerFrame.h" +#include "nsTextFrame.h" +#include "mozilla/CSSClipPathInstance.h" +#include "mozilla/FilterInstance.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_svg.h" +#include "mozilla/SVGClipPathFrame.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/SVGContextPaint.h" +#include "mozilla/SVGForeignObjectFrame.h" +#include "mozilla/SVGIntegrationUtils.h" +#include "mozilla/SVGGeometryFrame.h" +#include "mozilla/SVGMaskFrame.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/SVGOuterSVGFrame.h" +#include "mozilla/SVGTextFrame.h" +#include "mozilla/Unused.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/PatternHelpers.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/SVGClipPathElement.h" +#include "mozilla/dom/SVGGeometryElement.h" +#include "mozilla/dom/SVGPathElement.h" +#include "mozilla/dom/SVGUnitTypesBinding.h" +#include "mozilla/dom/SVGViewportElement.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGUnitTypes_Binding; +using namespace mozilla::gfx; +using namespace mozilla::image; + +bool NS_SVGNewGetBBoxEnabled() { + return mozilla::StaticPrefs::svg_new_getBBox_enabled(); +} + +namespace mozilla { + +// we only take the address of this: +static gfx::UserDataKey sSVGAutoRenderStateKey; + +SVGAutoRenderState::SVGAutoRenderState(DrawTarget* aDrawTarget) + : mDrawTarget(aDrawTarget), + mOriginalRenderState(nullptr), + mPaintingToWindow(false) { + mOriginalRenderState = aDrawTarget->RemoveUserData(&sSVGAutoRenderStateKey); + // We always remove ourselves from aContext before it dies, so + // passing nullptr as the destroy function is okay. + aDrawTarget->AddUserData(&sSVGAutoRenderStateKey, this, nullptr); +} + +SVGAutoRenderState::~SVGAutoRenderState() { + mDrawTarget->RemoveUserData(&sSVGAutoRenderStateKey); + if (mOriginalRenderState) { + mDrawTarget->AddUserData(&sSVGAutoRenderStateKey, mOriginalRenderState, + nullptr); + } +} + +void SVGAutoRenderState::SetPaintingToWindow(bool aPaintingToWindow) { + mPaintingToWindow = aPaintingToWindow; +} + +/* static */ +bool SVGAutoRenderState::IsPaintingToWindow(DrawTarget* aDrawTarget) { + void* state = aDrawTarget->GetUserData(&sSVGAutoRenderStateKey); + if (state) { + return static_cast<SVGAutoRenderState*>(state)->mPaintingToWindow; + } + return false; +} + +// Unlike containers, leaf frames do not include GetPosition() in +// GetCanvasTM(). +static bool FrameDoesNotIncludePositionInTM(const nsIFrame* aFrame) { + return aFrame->IsSVGGeometryFrame() || aFrame->IsSVGImageFrame() || + aFrame->IsInSVGTextSubtree(); +} + +nsRect SVGUtils::GetPostFilterInkOverflowRect(nsIFrame* aFrame, + const nsRect& aPreFilterRect) { + MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT), + "Called on invalid frame type"); + + // Note: we do not return here for eHasNoRefs since we must still handle any + // CSS filter functions. + // TODO: We currently pass nullptr instead of an nsTArray* here, but we + // actually should get the filter frames and then pass them into + // GetPostFilterBounds below! See bug 1494263. + // TODO: we should really return an empty rect for eHasRefsSomeInvalid since + // in that case we disable painting of the element. + if (!aFrame->StyleEffects()->HasFilters() || + SVGObserverUtils::GetAndObserveFilters(aFrame, nullptr) == + SVGObserverUtils::eHasRefsSomeInvalid) { + return aPreFilterRect; + } + + return FilterInstance::GetPostFilterBounds(aFrame, nullptr, &aPreFilterRect) + .valueOr(aPreFilterRect); +} + +bool SVGUtils::OuterSVGIsCallingReflowSVG(nsIFrame* aFrame) { + return GetOuterSVGFrame(aFrame)->IsCallingReflowSVG(); +} + +bool SVGUtils::AnyOuterSVGIsCallingReflowSVG(nsIFrame* aFrame) { + SVGOuterSVGFrame* outer = GetOuterSVGFrame(aFrame); + do { + if (outer->IsCallingReflowSVG()) { + return true; + } + outer = GetOuterSVGFrame(outer->GetParent()); + } while (outer); + return false; +} + +void SVGUtils::ScheduleReflowSVG(nsIFrame* aFrame) { + MOZ_ASSERT(aFrame->IsFrameOfType(nsIFrame::eSVG), "Passed bad frame!"); + + // If this is triggered, the callers should be fixed to call us before + // ReflowSVG is called. If we try to mark dirty bits on frames while we're + // in the process of removing them, things will get messed up. + MOZ_ASSERT(!OuterSVGIsCallingReflowSVG(aFrame), + "Do not call under ISVGDisplayableFrame::ReflowSVG!"); + + // We don't call SVGObserverUtils::InvalidateRenderingObservers here because + // we should only be called under InvalidateAndScheduleReflowSVG (which + // calls InvalidateBounds) or SVGDisplayContainerFrame::InsertFrames + // (at which point the frame has no observers). + + if (aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + return; + } + + if (aFrame->HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_FIRST_REFLOW)) { + // Nothing to do if we're already dirty, or if the outer-<svg> + // hasn't yet had its initial reflow. + return; + } + + SVGOuterSVGFrame* outerSVGFrame = nullptr; + + // We must not add dirty bits to the SVGOuterSVGFrame or else + // PresShell::FrameNeedsReflow won't work when we pass it in below. + if (aFrame->IsSVGOuterSVGFrame()) { + outerSVGFrame = static_cast<SVGOuterSVGFrame*>(aFrame); + } else { + aFrame->MarkSubtreeDirty(); + + nsIFrame* f = aFrame->GetParent(); + while (f && !f->IsSVGOuterSVGFrame()) { + if (f->HasAnyStateBits(NS_FRAME_IS_DIRTY | NS_FRAME_HAS_DIRTY_CHILDREN)) { + return; + } + f->AddStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); + f = f->GetParent(); + MOZ_ASSERT(f->IsFrameOfType(nsIFrame::eSVG), + "IsSVGOuterSVGFrame check above not valid!"); + } + + outerSVGFrame = static_cast<SVGOuterSVGFrame*>(f); + + MOZ_ASSERT(outerSVGFrame && outerSVGFrame->IsSVGOuterSVGFrame(), + "Did not find SVGOuterSVGFrame!"); + } + + if (outerSVGFrame->HasAnyStateBits(NS_FRAME_IN_REFLOW)) { + // We're currently under an SVGOuterSVGFrame::Reflow call so there is no + // need to call PresShell::FrameNeedsReflow, since we have an + // SVGOuterSVGFrame::DidReflow call pending. + return; + } + + nsFrameState dirtyBit = + (outerSVGFrame == aFrame ? NS_FRAME_IS_DIRTY + : NS_FRAME_HAS_DIRTY_CHILDREN); + + aFrame->PresShell()->FrameNeedsReflow(outerSVGFrame, IntrinsicDirty::None, + dirtyBit); +} + +bool SVGUtils::NeedsReflowSVG(const nsIFrame* aFrame) { + MOZ_ASSERT(aFrame->IsFrameOfType(nsIFrame::eSVG), + "SVG uses bits differently!"); + + // The flags we test here may change, hence why we have this separate + // function. + return aFrame->IsSubtreeDirty(); +} + +Size SVGUtils::GetContextSize(const nsIFrame* aFrame) { + Size size; + + MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast"); + const SVGElement* element = static_cast<SVGElement*>(aFrame->GetContent()); + + SVGViewportElement* ctx = element->GetCtx(); + if (ctx) { + size.width = ctx->GetLength(SVGContentUtils::X); + size.height = ctx->GetLength(SVGContentUtils::Y); + } + return size; +} + +float SVGUtils::ObjectSpace(const gfxRect& aRect, + const SVGAnimatedLength* aLength) { + float axis; + + switch (aLength->GetCtxType()) { + case SVGContentUtils::X: + axis = aRect.Width(); + break; + case SVGContentUtils::Y: + axis = aRect.Height(); + break; + case SVGContentUtils::XY: + axis = float(SVGContentUtils::ComputeNormalizedHypotenuse( + aRect.Width(), aRect.Height())); + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected ctx type"); + axis = 0.0f; + break; + } + if (aLength->IsPercentage()) { + // Multiply first to avoid precision errors: + return axis * aLength->GetAnimValInSpecifiedUnits() / 100; + } + return aLength->GetAnimValue(static_cast<SVGViewportElement*>(nullptr)) * + axis; +} + +float SVGUtils::UserSpace(SVGElement* aSVGElement, + const SVGAnimatedLength* aLength) { + return aLength->GetAnimValue(aSVGElement); +} + +float SVGUtils::UserSpace(nsIFrame* aNonSVGContext, + const SVGAnimatedLength* aLength) { + MOZ_ASSERT(!aNonSVGContext->IsTextFrame(), "Not expecting text content"); + return aLength->GetAnimValue(aNonSVGContext); +} + +float SVGUtils::UserSpace(const UserSpaceMetrics& aMetrics, + const SVGAnimatedLength* aLength) { + return aLength->GetAnimValue(aMetrics); +} + +SVGOuterSVGFrame* SVGUtils::GetOuterSVGFrame(nsIFrame* aFrame) { + return static_cast<SVGOuterSVGFrame*>(nsLayoutUtils::GetClosestFrameOfType( + aFrame, LayoutFrameType::SVGOuterSVG)); +} + +nsIFrame* SVGUtils::GetOuterSVGFrameAndCoveredRegion(nsIFrame* aFrame, + nsRect* aRect) { + ISVGDisplayableFrame* svg = do_QueryFrame(aFrame); + if (!svg) { + return nullptr; + } + SVGOuterSVGFrame* outer = GetOuterSVGFrame(aFrame); + if (outer == svg) { + return nullptr; + } + + if (aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + *aRect = nsRect(0, 0, 0, 0); + } else { + uint32_t flags = SVGUtils::eForGetClientRects | SVGUtils::eBBoxIncludeFill | + SVGUtils::eBBoxIncludeStroke | + SVGUtils::eBBoxIncludeMarkers | + SVGUtils::eUseUserSpaceOfUseElement; + + auto ctm = nsLayoutUtils::GetTransformToAncestor(RelativeTo{aFrame}, + RelativeTo{outer}); + + float initPositionX = NSAppUnitsToFloatPixels(aFrame->GetPosition().x, + AppUnitsPerCSSPixel()), + initPositionY = NSAppUnitsToFloatPixels(aFrame->GetPosition().y, + AppUnitsPerCSSPixel()); + + Matrix mm; + ctm.ProjectTo2D(); + ctm.CanDraw2D(&mm); + gfxMatrix m = ThebesMatrix(mm); + + float appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); + float devPixelPerCSSPixel = + float(AppUnitsPerCSSPixel()) / appUnitsPerDevPixel; + + // The matrix that GetBBox accepts should operate on "user space", + // i.e. with CSS pixel unit. + m = m.PreScale(devPixelPerCSSPixel, devPixelPerCSSPixel); + + // Both SVGUtils::GetBBox and nsLayoutUtils::GetTransformToAncestor + // will count this displacement, we should remove it here to avoid + // double-counting. + m = m.PreTranslate(-initPositionX, -initPositionY); + + gfxRect bbox = SVGUtils::GetBBox(aFrame, flags, &m); + *aRect = nsLayoutUtils::RoundGfxRectToAppRect(bbox, appUnitsPerDevPixel); + } + + return outer; +} + +gfxMatrix SVGUtils::GetCanvasTM(nsIFrame* aFrame) { + // XXX yuck, we really need a common interface for GetCanvasTM + + if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + return GetCSSPxToDevPxMatrix(aFrame); + } + + if (aFrame->IsSVGForeignObjectFrame()) { + return static_cast<SVGForeignObjectFrame*>(aFrame)->GetCanvasTM(); + } + + if (SVGContainerFrame* containerFrame = do_QueryFrame(aFrame)) { + return containerFrame->GetCanvasTM(); + } + + MOZ_ASSERT(aFrame->GetParent()->IsFrameOfType(nsIFrame::eSVGContainer)); + + auto* parent = static_cast<SVGContainerFrame*>(aFrame->GetParent()); + auto* content = static_cast<SVGElement*>(aFrame->GetContent()); + + return content->PrependLocalTransformsTo(parent->GetCanvasTM()); +} + +bool SVGUtils::IsSVGTransformed(const nsIFrame* aFrame, + gfx::Matrix* aOwnTransform, + gfx::Matrix* aFromParentTransform) { + MOZ_ASSERT(aFrame->HasAllStateBits(NS_FRAME_SVG_LAYOUT | + NS_FRAME_MAY_BE_TRANSFORMED), + "Expecting an SVG frame that can be transformed"); + bool foundTransform = false; + + // Check if our parent has children-only transforms: + if (SVGContainerFrame* parent = do_QueryFrame(aFrame->GetParent())) { + foundTransform = parent->HasChildrenOnlyTransform(aFromParentTransform); + } + + if (auto* content = SVGElement::FromNode(aFrame->GetContent())) { + auto* transformList = content->GetAnimatedTransformList(); + if ((transformList && transformList->HasTransform()) || + content->GetAnimateMotionTransform()) { + if (aOwnTransform) { + *aOwnTransform = gfx::ToMatrix( + content->PrependLocalTransformsTo(gfxMatrix(), eUserSpaceToParent)); + } + foundTransform = true; + } + } + return foundTransform; +} + +void SVGUtils::NotifyChildrenOfSVGChange(nsIFrame* aFrame, uint32_t aFlags) { + for (nsIFrame* kid : aFrame->PrincipalChildList()) { + ISVGDisplayableFrame* SVGFrame = do_QueryFrame(kid); + if (SVGFrame) { + SVGFrame->NotifySVGChanged(aFlags); + } else { + NS_ASSERTION( + kid->IsFrameOfType(nsIFrame::eSVG) || kid->IsInSVGTextSubtree(), + "SVG frame expected"); + // recurse into the children of container frames e.g. <clipPath>, <mask> + // in case they have child frames with transformation matrices + if (kid->IsFrameOfType(nsIFrame::eSVG)) { + NotifyChildrenOfSVGChange(kid, aFlags); + } + } + } +} + +// ************************************************************ + +float SVGUtils::ComputeOpacity(const nsIFrame* aFrame, bool aHandleOpacity) { + const auto* styleEffects = aFrame->StyleEffects(); + + if (!styleEffects->IsOpaque() && + (SVGUtils::CanOptimizeOpacity(aFrame) || !aHandleOpacity)) { + return 1.0f; + } + + return styleEffects->mOpacity; +} + +void SVGUtils::DetermineMaskUsage(const nsIFrame* aFrame, bool aHandleOpacity, + MaskUsage& aUsage) { + using ClipPathType = StyleClipPath::Tag; + + aUsage.opacity = ComputeOpacity(aFrame, aHandleOpacity); + + nsIFrame* firstFrame = + nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame); + + const nsStyleSVGReset* svgReset = firstFrame->StyleSVGReset(); + + nsTArray<SVGMaskFrame*> maskFrames; + // XXX check return value? + SVGObserverUtils::GetAndObserveMasks(firstFrame, &maskFrames); + aUsage.shouldGenerateMaskLayer = (maskFrames.Length() > 0); + + SVGClipPathFrame* clipPathFrame; + // XXX check return value? + SVGObserverUtils::GetAndObserveClipPath(firstFrame, &clipPathFrame); + MOZ_ASSERT(!clipPathFrame || svgReset->mClipPath.IsUrl()); + + switch (svgReset->mClipPath.tag) { + case ClipPathType::Url: + if (clipPathFrame) { + if (clipPathFrame->IsTrivial()) { + aUsage.shouldApplyClipPath = true; + } else { + aUsage.shouldGenerateClipMaskLayer = true; + } + } + break; + case ClipPathType::Shape: + case ClipPathType::Box: + case ClipPathType::Path: + aUsage.shouldApplyBasicShapeOrPath = true; + break; + case ClipPathType::None: + MOZ_ASSERT(!aUsage.shouldGenerateClipMaskLayer && + !aUsage.shouldApplyClipPath && + !aUsage.shouldApplyBasicShapeOrPath); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unsupported clip-path type."); + break; + } +} + +class MixModeBlender { + public: + using Factory = gfx::Factory; + + MixModeBlender(nsIFrame* aFrame, gfxContext* aContext) + : mFrame(aFrame), mSourceCtx(aContext) { + MOZ_ASSERT(mFrame && mSourceCtx); + } + + bool ShouldCreateDrawTargetForBlend() const { + return mFrame->StyleEffects()->HasMixBlendMode(); + } + + gfxContext* CreateBlendTarget(const gfxMatrix& aTransform) { + MOZ_ASSERT(ShouldCreateDrawTargetForBlend()); + + // Create a temporary context to draw to so we can blend it back with + // another operator. + IntRect drawRect = ComputeClipExtsInDeviceSpace(aTransform); + if (drawRect.IsEmpty()) { + return nullptr; + } + + RefPtr<DrawTarget> targetDT = + mSourceCtx->GetDrawTarget()->CreateSimilarDrawTarget( + drawRect.Size(), SurfaceFormat::B8G8R8A8); + if (!targetDT || !targetDT->IsValid()) { + return nullptr; + } + + MOZ_ASSERT(!mTargetCtx, + "CreateBlendTarget is designed to be used once only."); + + mTargetCtx = gfxContext::CreateOrNull(targetDT); + MOZ_ASSERT(mTargetCtx); // already checked the draw target above + mTargetCtx->SetMatrix(mSourceCtx->CurrentMatrix() * + Matrix::Translation(-drawRect.TopLeft())); + + mTargetOffset = drawRect.TopLeft(); + + return mTargetCtx.get(); + } + + void BlendToTarget() { + MOZ_ASSERT(ShouldCreateDrawTargetForBlend()); + MOZ_ASSERT(mTargetCtx, + "BlendToTarget should be used after CreateBlendTarget."); + + RefPtr<SourceSurface> targetSurf = mTargetCtx->GetDrawTarget()->Snapshot(); + + gfxContextAutoSaveRestore save(mSourceCtx); + mSourceCtx->SetMatrix(Matrix()); // This will be restored right after. + RefPtr<gfxPattern> pattern = new gfxPattern( + targetSurf, Matrix::Translation(mTargetOffset.x, mTargetOffset.y)); + mSourceCtx->SetPattern(pattern); + mSourceCtx->Paint(); + } + + private: + MixModeBlender() = delete; + + IntRect ComputeClipExtsInDeviceSpace(const gfxMatrix& aTransform) { + // These are used if we require a temporary surface for a custom blend + // mode. Clip the source context first, so that we can generate a smaller + // temporary surface. (Since we will clip this context in + // SetupContextMatrix, a pair of save/restore is needed.) + gfxContextAutoSaveRestore saver; + + if (!mFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + saver.SetContext(mSourceCtx); + // aFrame has a valid ink overflow rect, so clip to it before calling + // PushGroup() to minimize the size of the surfaces we'll composite: + gfxContextMatrixAutoSaveRestore matrixAutoSaveRestore(mSourceCtx); + mSourceCtx->Multiply(aTransform); + nsRect overflowRect = mFrame->InkOverflowRectRelativeToSelf(); + if (FrameDoesNotIncludePositionInTM(mFrame)) { + overflowRect = overflowRect + mFrame->GetPosition(); + } + mSourceCtx->Clip(NSRectToSnappedRect( + overflowRect, mFrame->PresContext()->AppUnitsPerDevPixel(), + *mSourceCtx->GetDrawTarget())); + } + + // Get the clip extents in device space. + gfxRect clippedFrameSurfaceRect = + mSourceCtx->GetClipExtents(gfxContext::eDeviceSpace); + clippedFrameSurfaceRect.RoundOut(); + + IntRect result; + ToRect(clippedFrameSurfaceRect).ToIntRect(&result); + + return Factory::CheckSurfaceSize(result.Size()) ? result : IntRect(); + } + + nsIFrame* mFrame; + gfxContext* mSourceCtx; + UniquePtr<gfxContext> mTargetCtx; + IntPoint mTargetOffset; +}; + +void SVGUtils::PaintFrameWithEffects(nsIFrame* aFrame, gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION(aFrame->HasAnyStateBits(NS_FRAME_IS_NONDISPLAY) || + aFrame->PresContext()->Document()->IsSVGGlyphsDocument(), + "Only painting of non-display SVG should take this code path"); + + ISVGDisplayableFrame* svgFrame = do_QueryFrame(aFrame); + if (!svgFrame) { + return; + } + + MaskUsage maskUsage; + DetermineMaskUsage(aFrame, true, maskUsage); + if (maskUsage.opacity == 0.0f) { + return; + } + + if (auto* svg = SVGElement::FromNode(aFrame->GetContent())) { + if (!svg->HasValidDimensions()) { + return; + } + } + + /* SVG defines the following rendering model: + * + * 1. Render fill + * 2. Render stroke + * 3. Render markers + * 4. Apply filter + * 5. Apply clipping, masking, group opacity + * + * We follow this, but perform a couple of optimizations: + * + * + Use cairo's clipPath when representable natively (single object + * clip region). + * + * + Merge opacity and masking if both used together. + */ + + /* Properties are added lazily and may have been removed by a restyle, + so make sure all applicable ones are set again. */ + SVGClipPathFrame* clipPathFrame; + nsTArray<SVGMaskFrame*> maskFrames; + // TODO: We currently pass nullptr instead of an nsTArray* here, but we + // actually should get the filter frames and then pass them into + // PaintFilteredFrame below! See bug 1494263. + const bool hasInvalidFilter = + SVGObserverUtils::GetAndObserveFilters(aFrame, nullptr) == + SVGObserverUtils::eHasRefsSomeInvalid; + if (SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame) == + SVGObserverUtils::eHasRefsSomeInvalid || + SVGObserverUtils::GetAndObserveMasks(aFrame, &maskFrames) == + SVGObserverUtils::eHasRefsSomeInvalid) { + // Some resource is invalid. We shouldn't paint anything. + return; + } + + SVGMaskFrame* maskFrame = maskFrames.IsEmpty() ? nullptr : maskFrames[0]; + + MixModeBlender blender(aFrame, &aContext); + gfxContext* target = blender.ShouldCreateDrawTargetForBlend() + ? blender.CreateBlendTarget(aTransform) + : &aContext; + + if (!target) { + return; + } + + /* Check if we need to do additional operations on this child's + * rendering, which necessitates rendering into another surface. */ + bool shouldGenerateMask = + (maskUsage.opacity != 1.0f || maskUsage.shouldGenerateClipMaskLayer || + maskUsage.shouldGenerateMaskLayer); + bool shouldPushMask = false; + + if (shouldGenerateMask) { + RefPtr<SourceSurface> maskSurface; + + // maskFrame can be nullptr even if maskUsage.shouldGenerateMaskLayer is + // true. That happens when a user gives an unresolvable mask-id, such as + // mask:url() + // mask:url(#id-which-does-not-exist) + // Since we only uses SVGUtils with SVG elements, not like mask on an + // HTML element, we should treat an unresolvable mask as no-mask here. + if (maskUsage.shouldGenerateMaskLayer && maskFrame) { + StyleMaskMode maskMode = + aFrame->StyleSVGReset()->mMask.mLayers[0].mMaskMode; + SVGMaskFrame::MaskParams params(aContext.GetDrawTarget(), aFrame, + aTransform, maskUsage.opacity, maskMode, + aImgParams); + + maskSurface = maskFrame->GetMaskForMaskedFrame(params); + + if (!maskSurface) { + // Either entire surface is clipped out, or gfx buffer allocation + // failure in SVGMaskFrame::GetMaskForMaskedFrame. + return; + } + shouldPushMask = true; + } + + if (maskUsage.shouldGenerateClipMaskLayer) { + RefPtr<SourceSurface> clipMaskSurface = + clipPathFrame->GetClipMask(aContext, aFrame, aTransform, maskSurface); + if (clipMaskSurface) { + maskSurface = clipMaskSurface; + } else { + // Either entire surface is clipped out, or gfx buffer allocation + // failure in SVGClipPathFrame::GetClipMask. + return; + } + shouldPushMask = true; + } + + if (!maskUsage.shouldGenerateClipMaskLayer && + !maskUsage.shouldGenerateMaskLayer) { + shouldPushMask = true; + } + + // SVG mask multiply opacity into maskSurface already, so we do not bother + // to apply opacity again. + if (shouldPushMask) { + // We want the mask to be untransformed so use the inverse of the + // current transform as the maskTransform to compensate. + Matrix maskTransform = aContext.CurrentMatrix(); + maskTransform.Invert(); + target->PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, + maskFrame ? 1.0 : maskUsage.opacity, + maskSurface, maskTransform); + } + } + + /* If this frame has only a trivial clipPath, set up cairo's clipping now so + * we can just do normal painting and get it clipped appropriately. + */ + if (maskUsage.shouldApplyClipPath || maskUsage.shouldApplyBasicShapeOrPath) { + if (maskUsage.shouldApplyClipPath) { + clipPathFrame->ApplyClipPath(aContext, aFrame, aTransform); + } else { + CSSClipPathInstance::ApplyBasicShapeOrPathClip(aContext, aFrame, + aTransform); + } + } + + /* Paint the child */ + + // Invalid filters should render the unfiltered contents per spec. + if (aFrame->StyleEffects()->HasFilters() && !hasInvalidFilter) { + gfxContextMatrixAutoSaveRestore autoSR(target); + + // 'target' is currently scaled such that its user space units are CSS + // pixels (SVG user space units). But PaintFilteredFrame expects it to be + // scaled in such a way that its user space units are device pixels. So we + // have to adjust the scale. + gfxMatrix reverseScaleMatrix = SVGUtils::GetCSSPxToDevPxMatrix(aFrame); + DebugOnly<bool> invertible = reverseScaleMatrix.Invert(); + target->SetMatrixDouble(reverseScaleMatrix * aTransform * + target->CurrentMatrixDouble()); + + auto callback = [&](gfxContext& aContext, imgDrawingParams& aImgParams, + const gfxMatrix* aFilterTransform, + const nsIntRect* aDirtyRect) { + svgFrame->PaintSVG(aContext, + aFilterTransform + ? SVGUtils::GetCSSPxToDevPxMatrix(aFrame) + : aTransform, + aImgParams); + }; + FilterInstance::PaintFilteredFrame( + aFrame, aFrame->StyleEffects()->mFilters.AsSpan(), target, callback, + nullptr, aImgParams); + } else { + svgFrame->PaintSVG(*target, aTransform, aImgParams); + } + + if (maskUsage.shouldApplyClipPath || maskUsage.shouldApplyBasicShapeOrPath) { + aContext.PopClip(); + } + + if (shouldPushMask) { + target->PopGroupAndBlend(); + } + + if (blender.ShouldCreateDrawTargetForBlend()) { + MOZ_ASSERT(target != &aContext); + blender.BlendToTarget(); + } +} + +bool SVGUtils::HitTestClip(nsIFrame* aFrame, const gfxPoint& aPoint) { + const nsStyleSVGReset* svgReset = aFrame->StyleSVGReset(); + if (!svgReset->HasClipPath()) { + return true; + } + if (svgReset->mClipPath.IsUrl()) { + // If the clip-path property references non-existent or invalid clipPath + // element(s) we ignore it. + SVGClipPathFrame* clipPathFrame; + SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame); + return !clipPathFrame || + clipPathFrame->PointIsInsideClipPath(aFrame, aPoint); + } + return CSSClipPathInstance::HitTestBasicShapeOrPathClip(aFrame, aPoint); +} + +IntSize SVGUtils::ConvertToSurfaceSize(const gfxSize& aSize, + bool* aResultOverflows) { + IntSize surfaceSize(ClampToInt(ceil(aSize.width)), + ClampToInt(ceil(aSize.height))); + + *aResultOverflows = surfaceSize.width != ceil(aSize.width) || + surfaceSize.height != ceil(aSize.height); + + if (!Factory::AllowedSurfaceSize(surfaceSize)) { + surfaceSize.width = + std::min(NS_SVG_OFFSCREEN_MAX_DIMENSION, surfaceSize.width); + surfaceSize.height = + std::min(NS_SVG_OFFSCREEN_MAX_DIMENSION, surfaceSize.height); + *aResultOverflows = true; + } + + return surfaceSize; +} + +bool SVGUtils::HitTestRect(const gfx::Matrix& aMatrix, float aRX, float aRY, + float aRWidth, float aRHeight, float aX, float aY) { + gfx::Rect rect(aRX, aRY, aRWidth, aRHeight); + if (rect.IsEmpty() || aMatrix.IsSingular()) { + return false; + } + gfx::Matrix toRectSpace = aMatrix; + toRectSpace.Invert(); + gfx::Point p = toRectSpace.TransformPoint(gfx::Point(aX, aY)); + return rect.x <= p.x && p.x <= rect.XMost() && rect.y <= p.y && + p.y <= rect.YMost(); +} + +gfxRect SVGUtils::GetClipRectForFrame(const nsIFrame* aFrame, float aX, + float aY, float aWidth, float aHeight) { + const nsStyleDisplay* disp = aFrame->StyleDisplay(); + const nsStyleEffects* effects = aFrame->StyleEffects(); + + bool clipApplies = disp->mOverflowX == StyleOverflow::Hidden || + disp->mOverflowY == StyleOverflow::Hidden; + + if (!clipApplies || effects->mClip.IsAuto()) { + return gfxRect(aX, aY, aWidth, aHeight); + } + + const auto& rect = effects->mClip.AsRect(); + nsRect coordClipRect = rect.ToLayoutRect(); + nsIntRect clipPxRect = coordClipRect.ToOutsidePixels( + aFrame->PresContext()->AppUnitsPerDevPixel()); + gfxRect clipRect = + gfxRect(clipPxRect.x, clipPxRect.y, clipPxRect.width, clipPxRect.height); + if (rect.right.IsAuto()) { + clipRect.width = aWidth - clipRect.X(); + } + if (rect.bottom.IsAuto()) { + clipRect.height = aHeight - clipRect.Y(); + } + if (disp->mOverflowX != StyleOverflow::Hidden) { + clipRect.x = aX; + clipRect.width = aWidth; + } + if (disp->mOverflowY != StyleOverflow::Hidden) { + clipRect.y = aY; + clipRect.height = aHeight; + } + return clipRect; +} + +gfxRect SVGUtils::GetBBox(nsIFrame* aFrame, uint32_t aFlags, + const gfxMatrix* aToBoundsSpace) { + if (aFrame->IsTextFrame()) { + aFrame = aFrame->GetParent(); + } + + if (aFrame->IsInSVGTextSubtree()) { + // It is possible to apply a gradient, pattern, clipping path, mask or + // filter to text. When one of these facilities is applied to text + // the bounding box is the entire text element in all cases. + aFrame = + nsLayoutUtils::GetClosestFrameOfType(aFrame, LayoutFrameType::SVGText); + } + + ISVGDisplayableFrame* svg = do_QueryFrame(aFrame); + const bool hasSVGLayout = aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT); + if (hasSVGLayout && !svg) { + // An SVG frame, but not one that can be displayed directly (for + // example, nsGradientFrame). These can't contribute to the bbox. + return gfxRect(); + } + + const bool isOuterSVG = svg && !hasSVGLayout; + MOZ_ASSERT(!isOuterSVG || aFrame->IsSVGOuterSVGFrame()); + if (!svg || (isOuterSVG && (aFlags & eUseFrameBoundsForOuterSVG))) { + // An HTML element or an SVG outer frame. + MOZ_ASSERT(!hasSVGLayout); + bool onlyCurrentFrame = aFlags & eIncludeOnlyCurrentFrameForNonSVGElement; + return SVGIntegrationUtils::GetSVGBBoxForNonSVGFrame( + aFrame, + /* aUnionContinuations = */ !onlyCurrentFrame); + } + + MOZ_ASSERT(svg); + + if (auto* element = SVGElement::FromNodeOrNull(aFrame->GetContent())) { + if (!element->HasValidDimensions()) { + return gfxRect(); + } + } + + // Clean out flags which have no effects on returning bbox from now, so that + // we can cache and reuse ObjectBoundingBoxProperty() in the code below. + aFlags &= + ~(eIncludeOnlyCurrentFrameForNonSVGElement | eUseFrameBoundsForOuterSVG); + if (!aFrame->IsSVGUseFrame()) { + aFlags &= ~eUseUserSpaceOfUseElement; + } + + if (aFlags == eBBoxIncludeFillGeometry && + // We only cache bbox in element's own user space + !aToBoundsSpace) { + gfxRect* prop = aFrame->GetProperty(ObjectBoundingBoxProperty()); + if (prop) { + return *prop; + } + } + + gfxMatrix matrix; + if (aToBoundsSpace) { + matrix = *aToBoundsSpace; + } + + if (aFrame->IsSVGForeignObjectFrame() || + aFlags & SVGUtils::eUseUserSpaceOfUseElement) { + // The spec says getBBox "Returns the tight bounding box in *current user + // space*". So we should really be doing this for all elements, but that + // needs investigation to check that we won't break too much content. + // NOTE: When changing this to apply to other frame types, make sure to + // also update SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset. + MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast"); + SVGElement* element = static_cast<SVGElement*>(aFrame->GetContent()); + matrix = element->PrependLocalTransformsTo(matrix, eChildToUserSpace); + } + gfxRect bbox = + svg->GetBBoxContribution(ToMatrix(matrix), aFlags).ToThebesRect(); + // Account for 'clipped'. + if (aFlags & SVGUtils::eBBoxIncludeClipped) { + gfxRect clipRect; + float x, y, width, height; + gfxRect fillBBox = + svg->GetBBoxContribution({}, SVGUtils::eBBoxIncludeFill).ToThebesRect(); + x = fillBBox.x; + y = fillBBox.y; + width = fillBBox.width; + height = fillBBox.height; + bool hasClip = aFrame->StyleDisplay()->IsScrollableOverflow(); + if (hasClip) { + clipRect = SVGUtils::GetClipRectForFrame(aFrame, x, y, width, height); + if (aFrame->IsSVGForeignObjectFrame() || aFrame->IsSVGUseFrame()) { + clipRect = matrix.TransformBounds(clipRect); + } + } + SVGClipPathFrame* clipPathFrame; + if (SVGObserverUtils::GetAndObserveClipPath(aFrame, &clipPathFrame) == + SVGObserverUtils::eHasRefsSomeInvalid) { + bbox = gfxRect(0, 0, 0, 0); + } else { + if (clipPathFrame) { + SVGClipPathElement* clipContent = + static_cast<SVGClipPathElement*>(clipPathFrame->GetContent()); + if (clipContent->IsUnitsObjectBoundingBox()) { + matrix.PreTranslate(gfxPoint(x, y)); + matrix.PreScale(width, height); + } else if (aFrame->IsSVGForeignObjectFrame()) { + matrix = gfxMatrix(); + } + matrix *= SVGUtils::GetTransformMatrixInUserSpace(clipPathFrame); + + bbox = clipPathFrame->GetBBoxForClipPathFrame(bbox, matrix, aFlags) + .ToThebesRect(); + } + + if (hasClip) { + bbox = bbox.Intersect(clipRect); + } + + if (bbox.IsEmpty()) { + bbox = gfxRect(0, 0, 0, 0); + } + } + } + + if (aFlags == eBBoxIncludeFillGeometry && + // We only cache bbox in element's own user space + !aToBoundsSpace) { + // Obtaining the bbox for objectBoundingBox calculations is common so we + // cache the result for future calls, since calculation can be expensive: + aFrame->SetProperty(ObjectBoundingBoxProperty(), new gfxRect(bbox)); + } + + return bbox; +} + +gfxPoint SVGUtils::FrameSpaceInCSSPxToUserSpaceOffset(const nsIFrame* aFrame) { + if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + // The user space for non-SVG frames is defined as the bounding box of the + // frame's border-box rects over all continuations. + return gfxPoint(); + } + + // Leaf frames apply their own offset inside their user space. + if (FrameDoesNotIncludePositionInTM(aFrame)) { + return nsLayoutUtils::RectToGfxRect(aFrame->GetRect(), + AppUnitsPerCSSPixel()) + .TopLeft(); + } + + // For foreignObject frames, SVGUtils::GetBBox applies their local + // transform, so we need to do the same here. + if (aFrame->IsSVGForeignObjectFrame()) { + gfxMatrix transform = + static_cast<SVGElement*>(aFrame->GetContent()) + ->PrependLocalTransformsTo(gfxMatrix(), eChildToUserSpace); + NS_ASSERTION(!transform.HasNonTranslation(), + "we're relying on this being an offset-only transform"); + return transform.GetTranslation(); + } + + return gfxPoint(); +} + +static gfxRect GetBoundingBoxRelativeRect(const SVGAnimatedLength* aXYWH, + const gfxRect& aBBox) { + return gfxRect(aBBox.x + SVGUtils::ObjectSpace(aBBox, &aXYWH[0]), + aBBox.y + SVGUtils::ObjectSpace(aBBox, &aXYWH[1]), + SVGUtils::ObjectSpace(aBBox, &aXYWH[2]), + SVGUtils::ObjectSpace(aBBox, &aXYWH[3])); +} + +gfxRect SVGUtils::GetRelativeRect(uint16_t aUnits, + const SVGAnimatedLength* aXYWH, + const gfxRect& aBBox, + const UserSpaceMetrics& aMetrics) { + if (aUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + return GetBoundingBoxRelativeRect(aXYWH, aBBox); + } + return gfxRect(UserSpace(aMetrics, &aXYWH[0]), UserSpace(aMetrics, &aXYWH[1]), + UserSpace(aMetrics, &aXYWH[2]), + UserSpace(aMetrics, &aXYWH[3])); +} + +gfxRect SVGUtils::GetRelativeRect(uint16_t aUnits, + const SVGAnimatedLength* aXYWH, + const gfxRect& aBBox, nsIFrame* aFrame) { + if (aUnits == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + return GetBoundingBoxRelativeRect(aXYWH, aBBox); + } + if (SVGElement* svgElement = SVGElement::FromNode(aFrame->GetContent())) { + return GetRelativeRect(aUnits, aXYWH, aBBox, SVGElementMetrics(svgElement)); + } + return GetRelativeRect(aUnits, aXYWH, aBBox, + NonSVGFrameUserSpaceMetrics(aFrame)); +} + +bool SVGUtils::CanOptimizeOpacity(const nsIFrame* aFrame) { + if (!aFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { + return false; + } + auto* content = aFrame->GetContent(); + if (!content->IsSVGGeometryElement() && + !content->IsSVGElement(nsGkAtoms::image)) { + return false; + } + if (aFrame->StyleEffects()->HasFilters()) { + return false; + } + // XXX The SVG WG is intending to allow fill, stroke and markers on <image> + if (content->IsSVGElement(nsGkAtoms::image)) { + return true; + } + const nsStyleSVG* style = aFrame->StyleSVG(); + if (style->HasMarker() && + static_cast<SVGGeometryElement*>(content)->IsMarkable()) { + return false; + } + + if (nsLayoutUtils::HasAnimationOfPropertySet( + aFrame, nsCSSPropertyIDSet::OpacityProperties())) { + return false; + } + + return !style->HasFill() || !HasStroke(aFrame); +} + +gfxMatrix SVGUtils::AdjustMatrixForUnits(const gfxMatrix& aMatrix, + const SVGAnimatedEnumeration* aUnits, + nsIFrame* aFrame, uint32_t aFlags) { + if (aFrame && aUnits->GetAnimValue() == SVG_UNIT_TYPE_OBJECTBOUNDINGBOX) { + gfxRect bbox = GetBBox(aFrame, aFlags); + gfxMatrix tm = aMatrix; + tm.PreTranslate(gfxPoint(bbox.X(), bbox.Y())); + tm.PreScale(bbox.Width(), bbox.Height()); + return tm; + } + return aMatrix; +} + +bool SVGUtils::GetNonScalingStrokeTransform(const nsIFrame* aFrame, + gfxMatrix* aUserToOuterSVG) { + if (aFrame->GetContent()->IsText()) { + aFrame = aFrame->GetParent(); + } + + if (!aFrame->StyleSVGReset()->HasNonScalingStroke()) { + return false; + } + + MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "should be an SVG element"); + + *aUserToOuterSVG = ThebesMatrix(SVGContentUtils::GetCTM( + static_cast<SVGElement*>(aFrame->GetContent()), true)); + + return aUserToOuterSVG->HasNonTranslation(); +} + +// The logic here comes from _cairo_stroke_style_max_distance_from_path +static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents, + const nsIFrame* aFrame, + double aStyleExpansionFactor, + const gfxMatrix& aMatrix) { + double style_expansion = + aStyleExpansionFactor * SVGUtils::GetStrokeWidth(aFrame); + + gfxMatrix matrix = aMatrix; + + gfxMatrix outerSVGToUser; + if (SVGUtils::GetNonScalingStrokeTransform(aFrame, &outerSVGToUser)) { + outerSVGToUser.Invert(); + matrix.PreMultiply(outerSVGToUser); + } + + double dx = style_expansion * (fabs(matrix._11) + fabs(matrix._21)); + double dy = style_expansion * (fabs(matrix._22) + fabs(matrix._12)); + + gfxRect strokeExtents = aPathExtents; + strokeExtents.Inflate(dx, dy); + return strokeExtents; +} + +/*static*/ +gfxRect SVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents, + const nsTextFrame* aFrame, + const gfxMatrix& aMatrix) { + NS_ASSERTION(aFrame->IsInSVGTextSubtree(), + "expected an nsTextFrame for SVG text"); + return mozilla::PathExtentsToMaxStrokeExtents(aPathExtents, aFrame, 0.5, + aMatrix); +} + +/*static*/ +gfxRect SVGUtils::PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents, + const SVGGeometryFrame* aFrame, + const gfxMatrix& aMatrix) { + bool strokeMayHaveCorners = + !SVGContentUtils::ShapeTypeHasNoCorners(aFrame->GetContent()); + + // For a shape without corners the stroke can only extend half the stroke + // width from the path in the x/y-axis directions. For shapes with corners + // the stroke can extend by sqrt(1/2) (think 45 degree rotated rect, or line + // with stroke-linecaps="square"). + double styleExpansionFactor = strokeMayHaveCorners ? M_SQRT1_2 : 0.5; + + // The stroke can extend even further for paths that can be affected by + // stroke-miterlimit. + // We only need to do this if the limit is greater than 1, but it's probably + // not worth optimizing for that. + bool affectedByMiterlimit = aFrame->GetContent()->IsAnyOfSVGElements( + nsGkAtoms::path, nsGkAtoms::polyline, nsGkAtoms::polygon); + + if (affectedByMiterlimit) { + const nsStyleSVG* style = aFrame->StyleSVG(); + if (style->mStrokeLinejoin == StyleStrokeLinejoin::Miter && + styleExpansionFactor < style->mStrokeMiterlimit / 2.0) { + styleExpansionFactor = style->mStrokeMiterlimit / 2.0; + } + } + + return mozilla::PathExtentsToMaxStrokeExtents(aPathExtents, aFrame, + styleExpansionFactor, aMatrix); +} + +// ---------------------------------------------------------------------- + +/* static */ +nscolor SVGUtils::GetFallbackOrPaintColor( + const ComputedStyle& aStyle, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + nscolor aDefaultContextFallbackColor) { + const auto& paint = aStyle.StyleSVG()->*aFillOrStroke; + nscolor color; + switch (paint.kind.tag) { + case StyleSVGPaintKind::Tag::PaintServer: + color = paint.fallback.IsColor() + ? paint.fallback.AsColor().CalcColor(aStyle) + : NS_RGBA(0, 0, 0, 0); + break; + case StyleSVGPaintKind::Tag::ContextStroke: + case StyleSVGPaintKind::Tag::ContextFill: + color = paint.fallback.IsColor() + ? paint.fallback.AsColor().CalcColor(aStyle) + : aDefaultContextFallbackColor; + break; + default: + color = paint.kind.AsColor().CalcColor(aStyle); + break; + } + if (const auto* styleIfVisited = aStyle.GetStyleIfVisited()) { + const auto& paintIfVisited = styleIfVisited->StyleSVG()->*aFillOrStroke; + // To prevent Web content from detecting if a user has visited a URL + // (via URL loading triggered by paint servers or performance + // differences between paint servers or between a paint server and a + // color), we do not allow whether links are visited to change which + // paint server is used or switch between paint servers and simple + // colors. A :visited style may only override a simple color with + // another simple color. + if (paintIfVisited.kind.IsColor() && paint.kind.IsColor()) { + nscolor colors[2] = { + color, paintIfVisited.kind.AsColor().CalcColor(*styleIfVisited)}; + return ComputedStyle::CombineVisitedColors(colors, + aStyle.RelevantLinkVisited()); + } + } + return color; +} + +/* static */ +void SVGUtils::MakeFillPatternFor(nsIFrame* aFrame, gfxContext* aContext, + GeneralPattern* aOutPattern, + imgDrawingParams& aImgParams, + SVGContextPaint* aContextPaint) { + const nsStyleSVG* style = aFrame->StyleSVG(); + if (style->mFill.kind.IsNone()) { + return; + } + + const auto* styleEffects = aFrame->StyleEffects(); + + float fillOpacity = GetOpacity(style->mFillOpacity, aContextPaint); + if (!styleEffects->IsOpaque() && SVGUtils::CanOptimizeOpacity(aFrame)) { + // Combine the group opacity into the fill opacity (we will have skipped + // creating an offscreen surface to apply the group opacity). + fillOpacity *= styleEffects->mOpacity; + } + + const DrawTarget* dt = aContext->GetDrawTarget(); + + SVGPaintServerFrame* ps = + SVGObserverUtils::GetAndObservePaintServer(aFrame, &nsStyleSVG::mFill); + + if (ps) { + RefPtr<gfxPattern> pattern = + ps->GetPaintServerPattern(aFrame, dt, aContext->CurrentMatrixDouble(), + &nsStyleSVG::mFill, fillOpacity, aImgParams); + if (pattern) { + pattern->CacheColorStops(dt); + aOutPattern->Init(*pattern->GetPattern(dt)); + return; + } + } + + if (aContextPaint) { + RefPtr<gfxPattern> pattern; + switch (style->mFill.kind.tag) { + case StyleSVGPaintKind::Tag::ContextFill: + pattern = aContextPaint->GetFillPattern( + dt, fillOpacity, aContext->CurrentMatrixDouble(), aImgParams); + break; + case StyleSVGPaintKind::Tag::ContextStroke: + pattern = aContextPaint->GetStrokePattern( + dt, fillOpacity, aContext->CurrentMatrixDouble(), aImgParams); + break; + default:; + } + if (pattern) { + aOutPattern->Init(*pattern->GetPattern(dt)); + return; + } + } + + if (style->mFill.fallback.IsNone()) { + return; + } + + // On failure, use the fallback colour in case we have an + // objectBoundingBox where the width or height of the object is zero. + // See http://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox + sRGBColor color(sRGBColor::FromABGR(GetFallbackOrPaintColor( + *aFrame->Style(), &nsStyleSVG::mFill, NS_RGB(0, 0, 0)))); + color.a *= fillOpacity; + aOutPattern->InitColorPattern(ToDeviceColor(color)); +} + +/* static */ +void SVGUtils::MakeStrokePatternFor(nsIFrame* aFrame, gfxContext* aContext, + GeneralPattern* aOutPattern, + imgDrawingParams& aImgParams, + SVGContextPaint* aContextPaint) { + const nsStyleSVG* style = aFrame->StyleSVG(); + if (style->mStroke.kind.IsNone()) { + return; + } + + const auto* styleEffects = aFrame->StyleEffects(); + + float strokeOpacity = GetOpacity(style->mStrokeOpacity, aContextPaint); + if (!styleEffects->IsOpaque() && SVGUtils::CanOptimizeOpacity(aFrame)) { + // Combine the group opacity into the stroke opacity (we will have skipped + // creating an offscreen surface to apply the group opacity). + strokeOpacity *= styleEffects->mOpacity; + } + + const DrawTarget* dt = aContext->GetDrawTarget(); + + SVGPaintServerFrame* ps = + SVGObserverUtils::GetAndObservePaintServer(aFrame, &nsStyleSVG::mStroke); + + if (ps) { + RefPtr<gfxPattern> pattern = ps->GetPaintServerPattern( + aFrame, dt, aContext->CurrentMatrixDouble(), &nsStyleSVG::mStroke, + strokeOpacity, aImgParams); + if (pattern) { + pattern->CacheColorStops(dt); + aOutPattern->Init(*pattern->GetPattern(dt)); + return; + } + } + + if (aContextPaint) { + RefPtr<gfxPattern> pattern; + switch (style->mStroke.kind.tag) { + case StyleSVGPaintKind::Tag::ContextFill: + pattern = aContextPaint->GetFillPattern( + dt, strokeOpacity, aContext->CurrentMatrixDouble(), aImgParams); + break; + case StyleSVGPaintKind::Tag::ContextStroke: + pattern = aContextPaint->GetStrokePattern( + dt, strokeOpacity, aContext->CurrentMatrixDouble(), aImgParams); + break; + default:; + } + if (pattern) { + aOutPattern->Init(*pattern->GetPattern(dt)); + return; + } + } + + if (style->mStroke.fallback.IsNone()) { + return; + } + + // On failure, use the fallback colour in case we have an + // objectBoundingBox where the width or height of the object is zero. + // See http://www.w3.org/TR/SVG11/coords.html#ObjectBoundingBox + sRGBColor color(sRGBColor::FromABGR(GetFallbackOrPaintColor( + *aFrame->Style(), &nsStyleSVG::mStroke, NS_RGBA(0, 0, 0, 0)))); + color.a *= strokeOpacity; + aOutPattern->InitColorPattern(ToDeviceColor(color)); +} + +/* static */ +float SVGUtils::GetOpacity(const StyleSVGOpacity& aOpacity, + const SVGContextPaint* aContextPaint) { + float opacity = 1.0f; + switch (aOpacity.tag) { + case StyleSVGOpacity::Tag::Opacity: + return aOpacity.AsOpacity(); + case StyleSVGOpacity::Tag::ContextFillOpacity: + if (aContextPaint) { + opacity = aContextPaint->GetFillOpacity(); + } + break; + case StyleSVGOpacity::Tag::ContextStrokeOpacity: + if (aContextPaint) { + opacity = aContextPaint->GetStrokeOpacity(); + } + break; + } + return opacity; +} + +bool SVGUtils::HasStroke(const nsIFrame* aFrame, + const SVGContextPaint* aContextPaint) { + const nsStyleSVG* style = aFrame->StyleSVG(); + return style->HasStroke() && GetStrokeWidth(aFrame, aContextPaint) > 0; +} + +float SVGUtils::GetStrokeWidth(const nsIFrame* aFrame, + const SVGContextPaint* aContextPaint) { + nsIContent* content = aFrame->GetContent(); + if (content->IsText()) { + content = content->GetParent(); + } + + auto* ctx = SVGElement::FromNode(content); + return SVGContentUtils::GetStrokeWidth(ctx, aFrame->Style(), aContextPaint); +} + +void SVGUtils::SetupStrokeGeometry(nsIFrame* aFrame, gfxContext* aContext, + SVGContextPaint* aContextPaint) { + MOZ_ASSERT(aFrame->GetContent()->IsSVGElement(), "bad cast"); + SVGContentUtils::AutoStrokeOptions strokeOptions; + SVGContentUtils::GetStrokeOptions(&strokeOptions, + SVGElement::FromNode(aFrame->GetContent()), + aFrame->Style(), aContextPaint); + + if (strokeOptions.mLineWidth <= 0) { + return; + } + + // SVGContentUtils::GetStrokeOptions gets the stroke options in CSS px; + // convert to device pixels for gfxContext. + float devPxPerCSSPx = aFrame->PresContext()->CSSToDevPixelScale().scale; + + aContext->SetLineWidth(strokeOptions.mLineWidth * devPxPerCSSPx); + aContext->SetLineCap(strokeOptions.mLineCap); + aContext->SetMiterLimit(strokeOptions.mMiterLimit); + aContext->SetLineJoin(strokeOptions.mLineJoin); + aContext->SetDash(strokeOptions.mDashPattern, strokeOptions.mDashLength, + strokeOptions.mDashOffset, devPxPerCSSPx); +} + +uint16_t SVGUtils::GetGeometryHitTestFlags(const nsIFrame* aFrame) { + uint16_t flags = 0; + + switch (aFrame->Style()->PointerEvents()) { + case StylePointerEvents::None: + break; + case StylePointerEvents::Auto: + case StylePointerEvents::Visiblepainted: + if (aFrame->StyleVisibility()->IsVisible()) { + if (!aFrame->StyleSVG()->mFill.kind.IsNone()) { + flags = SVG_HIT_TEST_FILL; + } + if (!aFrame->StyleSVG()->mStroke.kind.IsNone()) { + flags |= SVG_HIT_TEST_STROKE; + } + } + break; + case StylePointerEvents::Visiblefill: + if (aFrame->StyleVisibility()->IsVisible()) { + flags = SVG_HIT_TEST_FILL; + } + break; + case StylePointerEvents::Visiblestroke: + if (aFrame->StyleVisibility()->IsVisible()) { + flags = SVG_HIT_TEST_STROKE; + } + break; + case StylePointerEvents::Visible: + if (aFrame->StyleVisibility()->IsVisible()) { + flags = SVG_HIT_TEST_FILL | SVG_HIT_TEST_STROKE; + } + break; + case StylePointerEvents::Painted: + if (!aFrame->StyleSVG()->mFill.kind.IsNone()) { + flags = SVG_HIT_TEST_FILL; + } + if (!aFrame->StyleSVG()->mStroke.kind.IsNone()) { + flags |= SVG_HIT_TEST_STROKE; + } + break; + case StylePointerEvents::Fill: + flags = SVG_HIT_TEST_FILL; + break; + case StylePointerEvents::Stroke: + flags = SVG_HIT_TEST_STROKE; + break; + case StylePointerEvents::All: + flags = SVG_HIT_TEST_FILL | SVG_HIT_TEST_STROKE; + break; + default: + NS_ERROR("not reached"); + break; + } + + return flags; +} + +void SVGUtils::PaintSVGGlyph(Element* aElement, gfxContext* aContext) { + nsIFrame* frame = aElement->GetPrimaryFrame(); + ISVGDisplayableFrame* svgFrame = do_QueryFrame(frame); + if (!svgFrame) { + return; + } + gfxMatrix m; + if (frame->GetContent()->IsSVGElement()) { + // PaintSVG() expects the passed transform to be the transform to its own + // SVG user space, so we need to account for any 'transform' attribute: + m = SVGUtils::GetTransformMatrixInUserSpace(frame); + } + + // SVG-in-OpenType is not allowed to paint external resources, so we can + // just pass a dummy params into PatintSVG. + imgDrawingParams dummy; + svgFrame->PaintSVG(*aContext, m, dummy); +} + +bool SVGUtils::GetSVGGlyphExtents(const Element* aElement, + const gfxMatrix& aSVGToAppSpace, + gfxRect* aResult) { + nsIFrame* frame = aElement->GetPrimaryFrame(); + ISVGDisplayableFrame* svgFrame = do_QueryFrame(frame); + if (!svgFrame) { + return false; + } + + gfxMatrix transform(aSVGToAppSpace); + if (auto* svg = SVGElement::FromNode(frame->GetContent())) { + transform = svg->PrependLocalTransformsTo(aSVGToAppSpace); + } + + *aResult = + svgFrame + ->GetBBoxContribution(gfx::ToMatrix(transform), + SVGUtils::eBBoxIncludeFill | + SVGUtils::eBBoxIncludeFillGeometry | + SVGUtils::eBBoxIncludeStroke | + SVGUtils::eBBoxIncludeStrokeGeometry | + SVGUtils::eBBoxIncludeMarkers) + .ToThebesRect(); + return true; +} + +nsRect SVGUtils::ToCanvasBounds(const gfxRect& aUserspaceRect, + const gfxMatrix& aToCanvas, + const nsPresContext* presContext) { + return nsLayoutUtils::RoundGfxRectToAppRect( + aToCanvas.TransformBounds(aUserspaceRect), + presContext->AppUnitsPerDevPixel()); +} + +gfxMatrix SVGUtils::GetCSSPxToDevPxMatrix(const nsIFrame* aNonSVGFrame) { + float devPxPerCSSPx = aNonSVGFrame->PresContext()->CSSToDevPixelScale().scale; + + return gfxMatrix(devPxPerCSSPx, 0.0, 0.0, devPxPerCSSPx, 0.0, 0.0); +} + +gfxMatrix SVGUtils::GetTransformMatrixInUserSpace(const nsIFrame* aFrame) { + // We check element instead of aFrame directly because SVG element + // may have non-SVG frame, <tspan> for example. + MOZ_ASSERT(aFrame->GetContent() && aFrame->GetContent()->IsSVGElement(), + "Only use this wrapper for SVG elements"); + + if (!aFrame->IsTransformed()) { + return {}; + } + + nsStyleTransformMatrix::TransformReferenceBox refBox(aFrame); + nsDisplayTransform::FrameTransformProperties properties{ + aFrame, refBox, AppUnitsPerCSSPixel()}; + + // SVG elements can have x/y offset, their default transform origin + // is the origin of user space, not the top left point of the frame. + Point3D svgTransformOrigin{ + properties.mToTransformOrigin.x - CSSPixel::FromAppUnits(refBox.X()), + properties.mToTransformOrigin.y - CSSPixel::FromAppUnits(refBox.Y()), + properties.mToTransformOrigin.z}; + + Matrix svgTransform; + Matrix4x4 trans; + (void)aFrame->IsSVGTransformed(&svgTransform); + + if (properties.HasTransform()) { + trans = nsStyleTransformMatrix::ReadTransforms( + properties.mTranslate, properties.mRotate, properties.mScale, + properties.mMotion, properties.mTransform, refBox, + AppUnitsPerCSSPixel()); + } else { + trans = Matrix4x4::From2D(svgTransform); + } + + trans.ChangeBasis(svgTransformOrigin); + + Matrix mm; + trans.ProjectTo2D(); + (void)trans.CanDraw2D(&mm); + + return ThebesMatrix(mm); +} + +} // namespace mozilla diff --git a/layout/svg/SVGUtils.h b/layout/svg/SVGUtils.h new file mode 100644 index 0000000000..72a900c3e3 --- /dev/null +++ b/layout/svg/SVGUtils.h @@ -0,0 +1,585 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGUTILS_H_ +#define LAYOUT_SVG_SVGUTILS_H_ + +// include math.h to pick up definition of M_ maths defines e.g. M_PI +#include <math.h> + +#include "DrawMode.h" +#include "ImgDrawResult.h" +#include "gfx2DGlue.h" +#include "gfxMatrix.h" +#include "gfxPoint.h" +#include "gfxRect.h" +#include "mozilla/gfx/Rect.h" +#include "nsAlgorithm.h" +#include "nsChangeHint.h" +#include "nsColor.h" +#include "nsCOMPtr.h" +#include "nsID.h" +#include "nsIFrame.h" +#include "nsISupports.h" +#include "nsMathUtils.h" +#include "nsStyleStruct.h" +#include <algorithm> + +class gfxContext; +class nsFrameList; +class nsIContent; + +class nsPresContext; +class nsTextFrame; + +struct nsStyleSVG; +struct nsRect; + +namespace mozilla { +class SVGAnimatedEnumeration; +class SVGAnimatedLength; +class SVGContextPaint; +struct SVGContextPaintImpl; +class SVGDisplayContainerFrame; +class SVGGeometryFrame; +class SVGOuterSVGFrame; +namespace dom { +class Element; +class SVGElement; +class UserSpaceMetrics; +} // namespace dom +namespace gfx { +class DrawTarget; +class GeneralPattern; +} // namespace gfx +} // namespace mozilla + +// maximum dimension of an offscreen surface - choose so that +// the surface size doesn't overflow a 32-bit signed int using +// 4 bytes per pixel; in line with Factory::CheckSurfaceSize +// In fact Macs can't even manage that +#define NS_SVG_OFFSCREEN_MAX_DIMENSION 4096 + +#define SVG_HIT_TEST_FILL 0x01 +#define SVG_HIT_TEST_STROKE 0x02 + +bool NS_SVGNewGetBBoxEnabled(); + +namespace mozilla { + +/** + * Sometimes we need to distinguish between an empty box and a box + * that contains an element that has no size e.g. a point at the origin. + */ +class SVGBBox final { + using Rect = gfx::Rect; + + public: + SVGBBox() : mIsEmpty(true) {} + + MOZ_IMPLICIT SVGBBox(const Rect& aRect) : mBBox(aRect), mIsEmpty(false) {} + + MOZ_IMPLICIT SVGBBox(const gfxRect& aRect) + : mBBox(ToRect(aRect)), mIsEmpty(false) {} + + operator const Rect&() { return mBBox; } + + gfxRect ToThebesRect() const { return ThebesRect(mBBox); } + + bool IsEmpty() const { return mIsEmpty; } + + bool IsFinite() const { return mBBox.IsFinite(); } + + void Scale(float aScale) { mBBox.Scale(aScale); } + + void UnionEdges(const SVGBBox& aSVGBBox) { + if (aSVGBBox.mIsEmpty) { + return; + } + mBBox = mIsEmpty ? aSVGBBox.mBBox : mBBox.UnionEdges(aSVGBBox.mBBox); + mIsEmpty = false; + } + + void Intersect(const SVGBBox& aSVGBBox) { + if (!mIsEmpty && !aSVGBBox.mIsEmpty) { + mBBox = mBBox.Intersect(aSVGBBox.mBBox); + if (mBBox.IsEmpty()) { + mIsEmpty = true; + mBBox = Rect(0, 0, 0, 0); + } + } else { + mIsEmpty = true; + mBBox = Rect(0, 0, 0, 0); + } + } + + private: + Rect mBBox; + bool mIsEmpty; +}; + +// GRRR WINDOWS HATE HATE HATE +#undef CLIP_MASK + +class MOZ_RAII SVGAutoRenderState final { + using DrawTarget = gfx::DrawTarget; + + public: + explicit SVGAutoRenderState(DrawTarget* aDrawTarget); + ~SVGAutoRenderState(); + + void SetPaintingToWindow(bool aPaintingToWindow); + + static bool IsPaintingToWindow(DrawTarget* aDrawTarget); + + private: + DrawTarget* mDrawTarget; + void* mOriginalRenderState; + bool mPaintingToWindow; +}; + +/** + * General functions used by all of SVG layout and possibly content code. + * If a method is used by content and depends only on other content methods + * it should go in SVGContentUtils instead. + */ +class SVGUtils final { + public: + using Element = dom::Element; + using SVGElement = dom::SVGElement; + using AntialiasMode = gfx::AntialiasMode; + using DrawTarget = gfx::DrawTarget; + using FillRule = gfx::FillRule; + using GeneralPattern = gfx::GeneralPattern; + using Size = gfx::Size; + using imgDrawingParams = image::imgDrawingParams; + + NS_DECLARE_FRAME_PROPERTY_DELETABLE(ObjectBoundingBoxProperty, gfxRect) + + /** + * Returns the frame's post-filter ink overflow rect when passed the + * frame's pre-filter ink overflow rect. If the frame is not currently + * being filtered, this function simply returns aUnfilteredRect. + */ + static nsRect GetPostFilterInkOverflowRect(nsIFrame* aFrame, + const nsRect& aPreFilterRect); + + /** + * Schedules an update of the frame's bounds (which will in turn invalidate + * the new area that the frame should paint to). + * + * This does nothing when passed an NS_FRAME_IS_NONDISPLAY frame. + * In future we may want to allow ReflowSVG to be called on such frames, + * but that would be better implemented as a ForceReflowSVG function to + * be called synchronously while painting them without marking or paying + * attention to dirty bits like this function. + * + * This is very similar to PresShell::FrameNeedsReflow. The main reason that + * we have this function instead of using FrameNeedsReflow is because we need + * to be able to call it under SVGOuterSVGFrame::NotifyViewportChange when + * that function is called by SVGOuterSVGFrame::Reflow. FrameNeedsReflow + * is not suitable for calling during reflow though, and it asserts as much. + * The reason that we want to be callable under NotifyViewportChange is + * because we want to synchronously notify and dirty the SVGOuterSVGFrame's + * children so that when SVGOuterSVGFrame::DidReflow is called its children + * will be updated for the new size as appropriate. Otherwise we'd have to + * post an event to the event loop to mark dirty flags and request an update. + * + * Another reason that we don't currently want to call + * PresShell::FrameNeedsReflow is because passing eRestyle to it to get it to + * mark descendants dirty would cause it to descend through + * SVGForeignObjectFrame frames to mark their children dirty, but we want to + * handle SVGForeignObjectFrame specially. It would also do unnecessary work + * descending into NS_FRAME_IS_NONDISPLAY frames. + */ + static void ScheduleReflowSVG(nsIFrame* aFrame); + + /** + * Returns true if the frame or any of its children need ReflowSVG + * to be called on them. + */ + static bool NeedsReflowSVG(const nsIFrame* aFrame); + + /** + * Percentage lengths in SVG are resolved against the width/height of the + * nearest viewport (or its viewBox, if set). This helper returns the size + * of this "context" for the given frame so that percentage values can be + * resolved. + */ + static Size GetContextSize(const nsIFrame* aFrame); + + /* Computes the input length in terms of object space coordinates. + Input: rect - bounding box + length - length to be converted + */ + static float ObjectSpace(const gfxRect& aRect, + const SVGAnimatedLength* aLength); + + /* Computes the input length in terms of user space coordinates. + Input: content - object to be used for determining user space + Input: length - length to be converted + */ + static float UserSpace(SVGElement* aSVGElement, + const SVGAnimatedLength* aLength); + static float UserSpace(nsIFrame* aNonSVGContext, + const SVGAnimatedLength* aLength); + static float UserSpace(const dom::UserSpaceMetrics& aMetrics, + const SVGAnimatedLength* aLength); + + /* Find the outermost SVG frame of the passed frame */ + static SVGOuterSVGFrame* GetOuterSVGFrame(nsIFrame* aFrame); + + /** + * Get the covered region for a frame. Return null if it's not an SVG frame. + * @param aRect gets a rectangle in app units + * @return the outer SVG frame which aRect is relative to + */ + static nsIFrame* GetOuterSVGFrameAndCoveredRegion(nsIFrame* aFrame, + nsRect* aRect); + + /* Paint SVG frame with SVG effects + */ + static void PaintFrameWithEffects(nsIFrame* aFrame, gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams); + + /* Hit testing - check if point hits the clipPath of indicated + * frame. Returns true if no clipPath set. */ + static bool HitTestClip(nsIFrame* aFrame, const gfxPoint& aPoint); + + /* + * Returns the CanvasTM of the indicated frame, whether it's a + * child SVG frame, container SVG frame, or a regular frame. + * For regular frames, we just return an identity matrix. + */ + static gfxMatrix GetCanvasTM(nsIFrame* aFrame); + + /* + * Returns whether the frame is transformed and what those transforms are. + */ + static bool IsSVGTransformed(const nsIFrame* aFrame, + gfx::Matrix* aOwnTransform, + gfx::Matrix* aFromParentTransform); + + /** + * Notify the descendants of aFrame of a change to one of their ancestors + * that might affect them. + */ + static void NotifyChildrenOfSVGChange(nsIFrame* aFrame, uint32_t aFlags); + + /* + * Convert a surface size to an integer for use by thebes + * possibly making it smaller in the process so the surface does not + * use excessive memory. + * + * @param aSize the desired surface size + * @param aResultOverflows true if the desired surface size is too big + * @return the surface size to use + */ + static gfx::IntSize ConvertToSurfaceSize(const gfxSize& aSize, + bool* aResultOverflows); + + /* + * Hit test a given rectangle/matrix. + */ + static bool HitTestRect(const gfx::Matrix& aMatrix, float aRX, float aRY, + float aRWidth, float aRHeight, float aX, float aY); + + /** + * Get the clip rect for the given frame, taking into account the CSS 'clip' + * property. See: + * http://www.w3.org/TR/SVG11/masking.html#OverflowAndClipProperties + * The arguments for aX, aY, aWidth and aHeight should be the dimensions of + * the viewport established by aFrame. + */ + static gfxRect GetClipRectForFrame(const nsIFrame* aFrame, float aX, float aY, + float aWidth, float aHeight); + + /* Using group opacity instead of fill or stroke opacity on a + * geometry object seems to be a common authoring mistake. If we're + * not applying filters and not both stroking and filling, we can + * generate the same result without going through the overhead of a + * push/pop group. */ + static bool CanOptimizeOpacity(const nsIFrame* aFrame); + + /** + * Take the CTM to userspace for an element, and adjust it to a CTM to its + * object bounding box space if aUnits is SVG_UNIT_TYPE_OBJECTBOUNDINGBOX. + * (I.e. so that [0,0] is at the top left of its bbox, and [1,1] is at the + * bottom right of its bbox). + * + * If the bbox is empty, this will return a singular matrix. + * + * @param aFlags One or more of the BBoxFlags values defined below. + */ + static gfxMatrix AdjustMatrixForUnits(const gfxMatrix& aMatrix, + const SVGAnimatedEnumeration* aUnits, + nsIFrame* aFrame, uint32_t aFlags); + + enum BBoxFlags { + eBBoxIncludeFill = 1 << 0, + // Include the geometry of the fill even when the fill does not + // actually render (e.g. when fill="none" or fill-opacity="0") + eBBoxIncludeFillGeometry = 1 << 1, + eBBoxIncludeStroke = 1 << 2, + // Include the geometry of the stroke even when the stroke does not + // actually render (e.g. when stroke="none" or stroke-opacity="0") + eBBoxIncludeStrokeGeometry = 1 << 3, + eBBoxIncludeMarkers = 1 << 4, + eBBoxIncludeClipped = 1 << 5, + // Normally a getBBox call on outer-<svg> should only return the + // bounds of the elements children. This flag will cause the + // element's bounds to be returned instead. + eUseFrameBoundsForOuterSVG = 1 << 6, + // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect + eForGetClientRects = 1 << 7, + // If the given frame is an HTML element, only include the region of the + // given frame, instead of all continuations of it, while computing bbox if + // this flag is set. + eIncludeOnlyCurrentFrameForNonSVGElement = 1 << 8, + // This flag is only has an effect when the target is a <use> element. + // getBBox returns the bounds of the elements children in user space if + // this flag is set; Otherwise, getBBox returns the union bounds in + // the coordinate system formed by the <use> element. + eUseUserSpaceOfUseElement = 1 << 9, + // For a frame with a clip-path, if this flag is set then the result + // will not be clipped to the bbox of the content inside the clip-path. + eDoNotClipToBBoxOfContentInsideClipPath = 1 << 10, + }; + /** + * This function in primarily for implementing the SVG DOM function getBBox() + * and the SVG attribute value 'objectBoundingBox'. However, it has been + * extended with various extra parameters in order to become more of a + * general purpose getter of all sorts of bounds that we might need to obtain + * for SVG elements, or even for other elements that have SVG effects applied + * to them. + * + * @param aFrame The frame of the element for which the bounds are to be + * obtained. + * @param aFlags One or more of the BBoxFlags values defined above. + * @param aToBoundsSpace If not specified the returned rect is in aFrame's + * element's "user space". A matrix can optionally be pass to specify a + * transform from aFrame's user space to the bounds space of interest + * (typically this will be the ancestor SVGOuterSVGFrame, but it could be + * to any other coordinate space). + */ + static gfxRect GetBBox(nsIFrame* aFrame, + // If the default arg changes, update the handling for + // ObjectBoundingBoxProperty() in the implementation. + uint32_t aFlags = eBBoxIncludeFillGeometry, + const gfxMatrix* aToBoundsSpace = nullptr); + + /* + * "User space" is the space that the frame's BBox (as calculated by + * SVGUtils::GetBBox) is in. "Frame space" is the space that has its origin + * at the top left of the union of the frame's border-box rects over all + * continuations. + * This function returns the offset one needs to add to something in frame + * space in order to get its coordinates in user space. + */ + static gfxPoint FrameSpaceInCSSPxToUserSpaceOffset(const nsIFrame* aFrame); + + /** + * Convert a userSpaceOnUse/objectBoundingBoxUnits rectangle that's specified + * using four SVGAnimatedLength values into a user unit rectangle in user + * space. + * + * @param aXYWH pointer to 4 consecutive SVGAnimatedLength objects containing + * the x, y, width and height values in that order + * @param aBBox the bounding box of the object the rect is relative to; + * may be null if aUnits is not SVG_UNIT_TYPE_OBJECTBOUNDINGBOX + * @param aFrame the object in which to interpret user-space units; + * may be null if aUnits is SVG_UNIT_TYPE_OBJECTBOUNDINGBOX + */ + static gfxRect GetRelativeRect(uint16_t aUnits, + const SVGAnimatedLength* aXYWH, + const gfxRect& aBBox, nsIFrame* aFrame); + + static gfxRect GetRelativeRect(uint16_t aUnits, + const SVGAnimatedLength* aXYWH, + const gfxRect& aBBox, + const dom::UserSpaceMetrics& aMetrics); + + static bool OuterSVGIsCallingReflowSVG(nsIFrame* aFrame); + static bool AnyOuterSVGIsCallingReflowSVG(nsIFrame* aFrame); + + /** + * See https://svgwg.org/svg2-draft/painting.html#NonScalingStroke + * + * If the computed value of the 'vector-effect' property on aFrame is + * 'non-scaling-stroke', then this function will set aUserToOuterSVG to the + * transform from aFrame's SVG user space to the initial coordinate system + * established by the viewport of aFrame's outer-<svg>'s (the coordinate + * system in which the stroke is fixed). If aUserToOuterSVG is set to a + * non-identity matrix this function returns true, else it returns false. + */ + static bool GetNonScalingStrokeTransform(const nsIFrame* aFrame, + gfxMatrix* aUserToOuterSVG); + + /** + * Compute the maximum possible device space stroke extents of a path given + * the path's device space path extents, its stroke style and its ctm. + * + * This is a workaround for the lack of suitable cairo API for getting the + * tight device space stroke extents of a path. This basically gives us the + * tightest extents that we can guarantee fully enclose the inked stroke + * without doing the calculations for the actual tight extents. We exploit + * the fact that cairo does have an API for getting the tight device space + * fill/path extents. + * + * This should die once bug 478152 is fixed. + */ + static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents, + const nsTextFrame* aFrame, + const gfxMatrix& aMatrix); + static gfxRect PathExtentsToMaxStrokeExtents(const gfxRect& aPathExtents, + const SVGGeometryFrame* aFrame, + const gfxMatrix& aMatrix); + + /** + * Convert a floating-point value to a 32-bit integer value, clamping to + * the range of valid integers. + */ + static int32_t ClampToInt(double aVal) { + return NS_lround( + std::max(double(INT32_MIN), std::min(double(INT32_MAX), aVal))); + } + + /** + * Convert a floating-point value to a 64-bit integer value, clamping to + * the lowest and highest integers that can be safely compared to a double. + */ + static int64_t ClampToInt64(double aVal) { + return static_cast<int64_t>( + std::clamp<double>(aVal, INT64_MIN, std::nexttoward(INT64_MAX, 0))); + } + + static nscolor GetFallbackOrPaintColor( + const ComputedStyle&, StyleSVGPaint nsStyleSVG::*aFillOrStroke, + nscolor aDefaultContextFallbackColor); + + static void MakeFillPatternFor(nsIFrame* aFrame, gfxContext* aContext, + GeneralPattern* aOutPattern, + imgDrawingParams& aImgParams, + SVGContextPaint* aContextPaint = nullptr); + + static void MakeStrokePatternFor(nsIFrame* aFrame, gfxContext* aContext, + GeneralPattern* aOutPattern, + imgDrawingParams& aImgParams, + SVGContextPaint* aContextPaint = nullptr); + + static float GetOpacity(const StyleSVGOpacity&, const SVGContextPaint*); + + /* + * @return false if there is no stroke + */ + static bool HasStroke(const nsIFrame* aFrame, + const SVGContextPaint* aContextPaint = nullptr); + + static float GetStrokeWidth(const nsIFrame* aFrame, + const SVGContextPaint* aContextPaint = nullptr); + + /* + * Set up a context for a stroked path (including any dashing that applies). + */ + static void SetupStrokeGeometry(nsIFrame* aFrame, gfxContext* aContext, + SVGContextPaint* aContextPaint = nullptr); + + /** + * This function returns a set of bit flags indicating which parts of the + * element (fill, stroke, bounds) should intercept pointer events. It takes + * into account the type of element and the value of the 'pointer-events' + * property on the element. + */ + static uint16_t GetGeometryHitTestFlags(const nsIFrame* aFrame); + + static FillRule ToFillRule(StyleFillRule aFillRule) { + return aFillRule == StyleFillRule::Evenodd ? FillRule::FILL_EVEN_ODD + : FillRule::FILL_WINDING; + } + + static AntialiasMode ToAntialiasMode(StyleTextRendering aTextRendering) { + return aTextRendering == StyleTextRendering::Optimizespeed + ? AntialiasMode::NONE + : AntialiasMode::SUBPIXEL; + } + + /** + * Render a SVG glyph. + * @param aElement the SVG glyph element to render + * @param aContext the thebes aContext to draw to + * @return true if rendering succeeded + */ + static void PaintSVGGlyph(Element* aElement, gfxContext* aContext); + + /** + * Get the extents of a SVG glyph. + * @param aElement the SVG glyph element + * @param aSVGToAppSpace the matrix mapping the SVG glyph space to the + * target context space + * @param aResult the result (valid when true is returned) + * @return true if calculating the extents succeeded + */ + static bool GetSVGGlyphExtents(const Element* aElement, + const gfxMatrix& aSVGToAppSpace, + gfxRect* aResult); + + /** + * Returns the app unit canvas bounds of a userspace rect. + * + * @param aToCanvas Transform from userspace to canvas device space. + */ + static nsRect ToCanvasBounds(const gfxRect& aUserspaceRect, + const gfxMatrix& aToCanvas, + const nsPresContext* presContext); + + struct MaskUsage { + bool shouldGenerateMaskLayer; + bool shouldGenerateClipMaskLayer; + bool shouldApplyClipPath; + bool shouldApplyBasicShapeOrPath; + float opacity; + + MaskUsage() + : shouldGenerateMaskLayer(false), + shouldGenerateClipMaskLayer(false), + shouldApplyClipPath(false), + shouldApplyBasicShapeOrPath(false), + opacity(0.0) {} + + bool shouldDoSomething() { + return shouldGenerateMaskLayer || shouldGenerateClipMaskLayer || + shouldApplyClipPath || shouldApplyBasicShapeOrPath || + opacity != 1.0; + } + }; + + static void DetermineMaskUsage(const nsIFrame* aFrame, bool aHandleOpacity, + MaskUsage& aUsage); + + static float ComputeOpacity(const nsIFrame* aFrame, bool aHandleOpacity); + + /** + * SVG frames expect to paint in SVG user units, which are equal to CSS px + * units. This method provides a transform matrix to multiply onto a + * gfxContext's current transform to convert the context's current units from + * its usual dev pixels to SVG user units/CSS px to keep the SVG code happy. + */ + static gfxMatrix GetCSSPxToDevPxMatrix(const nsIFrame* aNonSVGFrame); + + /** + * It is a replacement of + * SVGElement::PrependLocalTransformsTo(eUserSpaceToParent). + * If no CSS transform is involved, they should behave exactly the same; + * if there are CSS transforms, this one will take them into account + * while SVGElement::PrependLocalTransformsTo won't. + */ + static gfxMatrix GetTransformMatrixInUserSpace(const nsIFrame* aFrame); +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGUTILS_H_ diff --git a/layout/svg/SVGViewFrame.cpp b/layout/svg/SVGViewFrame.cpp new file mode 100644 index 0000000000..e7105ceb6c --- /dev/null +++ b/layout/svg/SVGViewFrame.cpp @@ -0,0 +1,119 @@ +/* -*- 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/. */ + +// Keep in (case-insensitive) order: +#include "mozilla/PresShell.h" +#include "mozilla/SVGOuterSVGFrame.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGSVGElement.h" +#include "mozilla/dom/SVGViewElement.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" + +using namespace mozilla::dom; + +nsIFrame* NS_NewSVGViewFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle); + +namespace mozilla { + +/** + * While views are not directly rendered in SVG they can be linked to + * and thereby override attributes of an <svg> element via a fragment + * identifier. The SVGViewFrame class passes on any attribute changes + * the view receives to the overridden <svg> element (if there is one). + **/ +class SVGViewFrame final : public nsIFrame { + friend nsIFrame* ::NS_NewSVGViewFrame(mozilla::PresShell* aPresShell, + ComputedStyle* aStyle); + + protected: + explicit SVGViewFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) + : nsIFrame(aStyle, aPresContext, kClassID) { + AddStateBits(NS_FRAME_SVG_LAYOUT | NS_FRAME_IS_NONDISPLAY); + } + + public: + NS_DECL_FRAMEARENA_HELPERS(SVGViewFrame) + +#ifdef DEBUG + virtual void Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) override; +#endif + + bool IsFrameOfType(uint32_t aFlags) const override { + if (aFlags & eSupportsContainLayoutAndPaint) { + return false; + } + + return nsIFrame::IsFrameOfType(aFlags & ~(nsIFrame::eSVG)); + } + +#ifdef DEBUG_FRAME_DUMP + nsresult GetFrameName(nsAString& aResult) const override { + return MakeFrameName(u"SVGView"_ns, aResult); + } +#endif + + virtual nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + bool ComputeCustomOverflow(OverflowAreas& aOverflowAreas) override { + // We don't maintain a ink overflow rect + return false; + } +}; + +} // namespace mozilla + +nsIFrame* NS_NewSVGViewFrame(mozilla::PresShell* aPresShell, + mozilla::ComputedStyle* aStyle) { + return new (aPresShell) + mozilla::SVGViewFrame(aStyle, aPresShell->GetPresContext()); +} + +namespace mozilla { + +NS_IMPL_FRAMEARENA_HELPERS(SVGViewFrame) + +#ifdef DEBUG +void SVGViewFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, + nsIFrame* aPrevInFlow) { + NS_ASSERTION(aContent->IsSVGElement(nsGkAtoms::view), + "Content is not an SVG view"); + + nsIFrame::Init(aContent, aParent, aPrevInFlow); +} +#endif /* DEBUG */ + +nsresult SVGViewFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType) { + // Ignore zoomAndPan as it does not cause the <svg> element to re-render + + if (aNameSpaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::preserveAspectRatio || + aAttribute == nsGkAtoms::viewBox)) { + SVGOuterSVGFrame* outerSVGFrame = SVGUtils::GetOuterSVGFrame(this); + NS_ASSERTION(outerSVGFrame->GetContent()->IsSVGElement(nsGkAtoms::svg), + "Expecting an <svg> element"); + + SVGSVGElement* svgElement = + static_cast<SVGSVGElement*>(outerSVGFrame->GetContent()); + + nsAutoString viewID; + mContent->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::id, viewID); + + if (svgElement->IsOverriddenBy(viewID)) { + // We're the view that's providing overrides, so pretend that the frame + // we're overriding was updated. + outerSVGFrame->AttributeChanged(aNameSpaceID, aAttribute, aModType); + } + } + + return nsIFrame::AttributeChanged(aNameSpaceID, aAttribute, aModType); +} + +} // namespace mozilla diff --git a/layout/svg/SVGViewportFrame.cpp b/layout/svg/SVGViewportFrame.cpp new file mode 100644 index 0000000000..b21d9df67b --- /dev/null +++ b/layout/svg/SVGViewportFrame.cpp @@ -0,0 +1,254 @@ +/* -*- 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 "SVGViewportFrame.h" + +// Keep others in (case-insensitive) order: +#include "gfx2DGlue.h" +#include "gfxContext.h" +#include "mozilla/ISVGDisplayableFrame.h" +#include "mozilla/SVGContainerFrame.h" +#include "mozilla/SVGUtils.h" +#include "mozilla/dom/SVGViewportElement.h" +#include "nsLayoutUtils.h" + +using namespace mozilla::dom; +using namespace mozilla::gfx; +using namespace mozilla::image; + +namespace mozilla { + +//---------------------------------------------------------------------- +// ISVGDisplayableFrame methods + +void SVGViewportFrame::PaintSVG(gfxContext& aContext, + const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) { + NS_ASSERTION(HasAnyStateBits(NS_FRAME_IS_NONDISPLAY), + "Only painting of non-display SVG should take this code path"); + + gfxClipAutoSaveRestore autoSaveClip(&aContext); + + if (StyleDisplay()->IsScrollableOverflow()) { + float x, y, width, height; + static_cast<SVGViewportElement*>(GetContent()) + ->GetAnimatedLengthValues(&x, &y, &width, &height, nullptr); + + if (width <= 0 || height <= 0) { + return; + } + + gfxRect clipRect = SVGUtils::GetClipRectForFrame(this, x, y, width, height); + autoSaveClip.TransformedClip(aTransform, clipRect); + } + + SVGDisplayContainerFrame::PaintSVG(aContext, aTransform, aImgParams); +} + +void SVGViewportFrame::ReflowSVG() { + // mRect must be set before FinishAndStoreOverflow is called in order + // for our overflow areas to be clipped correctly. + float x, y, width, height; + static_cast<SVGViewportElement*>(GetContent()) + ->GetAnimatedLengthValues(&x, &y, &width, &height, nullptr); + mRect = nsLayoutUtils::RoundGfxRectToAppRect(gfxRect(x, y, width, height), + AppUnitsPerCSSPixel()); + + // If we have a filter, we need to invalidate ourselves because filter + // output can change even if none of our descendants need repainting. + if (StyleEffects()->HasFilters()) { + InvalidateFrame(); + } + + SVGDisplayContainerFrame::ReflowSVG(); +} + +void SVGViewportFrame::NotifySVGChanged(uint32_t aFlags) { + MOZ_ASSERT(aFlags & (TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED), + "Invalidation logic may need adjusting"); + + if (aFlags & COORD_CONTEXT_CHANGED) { + SVGViewportElement* svg = static_cast<SVGViewportElement*>(GetContent()); + + bool xOrYIsPercentage = + svg->mLengthAttributes[SVGViewportElement::ATTR_X].IsPercentage() || + svg->mLengthAttributes[SVGViewportElement::ATTR_Y].IsPercentage(); + bool widthOrHeightIsPercentage = + svg->mLengthAttributes[SVGViewportElement::ATTR_WIDTH].IsPercentage() || + svg->mLengthAttributes[SVGViewportElement::ATTR_HEIGHT].IsPercentage(); + + if (xOrYIsPercentage || widthOrHeightIsPercentage) { + // Ancestor changes can't affect how we render from the perspective of + // any rendering observers that we may have, so we don't need to + // invalidate them. We also don't need to invalidate ourself, since our + // changed ancestor will have invalidated its entire area, which includes + // our area. + // For perf reasons we call this before calling NotifySVGChanged() below. + SVGUtils::ScheduleReflowSVG(this); + } + + // Coordinate context changes affect mCanvasTM if we have a + // percentage 'x' or 'y', or if we have a percentage 'width' or 'height' AND + // a 'viewBox'. + + if (!(aFlags & TRANSFORM_CHANGED) && + (xOrYIsPercentage || + (widthOrHeightIsPercentage && svg->HasViewBox()))) { + aFlags |= TRANSFORM_CHANGED; + } + + if (svg->HasViewBox() || !widthOrHeightIsPercentage) { + // Remove COORD_CONTEXT_CHANGED, since we establish the coordinate + // context for our descendants and this notification won't change its + // dimensions: + aFlags &= ~COORD_CONTEXT_CHANGED; + + if (!aFlags) { + return; // No notification flags left + } + } + } + + SVGDisplayContainerFrame::NotifySVGChanged(aFlags); +} + +SVGBBox SVGViewportFrame::GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) { + // XXXjwatt It seems like authors would want the result to be clipped by the + // viewport we establish if IsScrollableOverflow() is true. We should + // consider doing that. See bug 1350755. + + SVGBBox bbox; + + if (aFlags & SVGUtils::eForGetClientRects) { + // XXXjwatt For consistency with the old code this code includes the + // viewport we establish in the result, but only includes the bounds of our + // descendants if they are not clipped to that viewport. However, this is + // both inconsistent with Chrome and with the specs. See bug 1350755. + // Ideally getClientRects/getBoundingClientRect should be consistent with + // getBBox. + float x, y, w, h; + static_cast<SVGViewportElement*>(GetContent()) + ->GetAnimatedLengthValues(&x, &y, &w, &h, nullptr); + if (w < 0.0f) w = 0.0f; + if (h < 0.0f) h = 0.0f; + Rect viewport(x, y, w, h); + bbox = aToBBoxUserspace.TransformBounds(viewport); + if (StyleDisplay()->IsScrollableOverflow()) { + return bbox; + } + // Else we're not clipping to our viewport so we fall through and include + // the bounds of our children. + } + + SVGBBox descendantsBbox = + SVGDisplayContainerFrame::GetBBoxContribution(aToBBoxUserspace, aFlags); + + bbox.UnionEdges(descendantsBbox); + + return bbox; +} + +nsresult SVGViewportFrame::AttributeChanged(int32_t aNameSpaceID, + nsAtom* aAttribute, + int32_t aModType) { + if (aNameSpaceID == kNameSpaceID_None && + !HasAnyStateBits(NS_FRAME_IS_NONDISPLAY)) { + SVGViewportElement* content = + static_cast<SVGViewportElement*>(GetContent()); + + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height) { + nsLayoutUtils::PostRestyleEvent( + mContent->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(this); + + if (content->HasViewBoxOrSyntheticViewBox()) { + // make sure our cached transform matrix gets (lazily) updated + mCanvasTM = nullptr; + content->ChildrenOnlyTransformChanged(); + SVGUtils::NotifyChildrenOfSVGChange(this, TRANSFORM_CHANGED); + } else { + uint32_t flags = COORD_CONTEXT_CHANGED; + if (mCanvasTM && mCanvasTM->IsSingular()) { + mCanvasTM = nullptr; + flags |= TRANSFORM_CHANGED; + } + SVGUtils::NotifyChildrenOfSVGChange(this, flags); + } + + } else if (aAttribute == nsGkAtoms::transform || + aAttribute == nsGkAtoms::preserveAspectRatio || + aAttribute == nsGkAtoms::viewBox || aAttribute == nsGkAtoms::x || + aAttribute == nsGkAtoms::y) { + // make sure our cached transform matrix gets (lazily) updated + mCanvasTM = nullptr; + + SVGUtils::NotifyChildrenOfSVGChange( + this, aAttribute == nsGkAtoms::viewBox + ? TRANSFORM_CHANGED | COORD_CONTEXT_CHANGED + : TRANSFORM_CHANGED); + + // 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 (aAttribute == nsGkAtoms::x || aAttribute == nsGkAtoms::y) { + nsLayoutUtils::PostRestyleEvent( + mContent->AsElement(), RestyleHint{0}, + nsChangeHint_InvalidateRenderingObservers); + SVGUtils::ScheduleReflowSVG(this); + } else if (aAttribute == nsGkAtoms::viewBox || + (aAttribute == nsGkAtoms::preserveAspectRatio && + content->HasViewBoxOrSyntheticViewBox())) { + content->ChildrenOnlyTransformChanged(); + // SchedulePaint sets a global state flag so we only need to call it + // once (on ourself is fine), not once on each child (despite bug + // 828240). + SchedulePaint(); + } + } + } + + return NS_OK; +} + +nsIFrame* SVGViewportFrame::GetFrameForPoint(const gfxPoint& aPoint) { + MOZ_ASSERT_UNREACHABLE("A clipPath cannot contain svg or symbol elements"); + return nullptr; +} + +//---------------------------------------------------------------------- +// ISVGSVGFrame methods: + +void SVGViewportFrame::NotifyViewportOrTransformChanged(uint32_t aFlags) { + // The dimensions of inner-<svg> frames are purely defined by their "width" + // and "height" attributes, and transform changes can only occur as a result + // of changes to their "width", "height", "viewBox" or "preserveAspectRatio" + // attributes. Changes to all of these attributes are handled in + // AttributeChanged(), so we should never be called. + NS_ERROR("Not called for SVGViewportFrame"); +} + +//---------------------------------------------------------------------- +// SVGContainerFrame methods: + +bool SVGViewportFrame::HasChildrenOnlyTransform(gfx::Matrix* aTransform) const { + SVGViewportElement* content = static_cast<SVGViewportElement*>(GetContent()); + + if (content->HasViewBoxOrSyntheticViewBox()) { + // XXX Maybe return false if the transform is the identity transform? + if (aTransform) { + *aTransform = content->GetViewBoxTransform(); + } + return true; + } + return false; +} + +} // namespace mozilla diff --git a/layout/svg/SVGViewportFrame.h b/layout/svg/SVGViewportFrame.h new file mode 100644 index 0000000000..56a953bd01 --- /dev/null +++ b/layout/svg/SVGViewportFrame.h @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +#ifndef LAYOUT_SVG_SVGVIEWPORTFRAME_H_ +#define LAYOUT_SVG_SVGVIEWPORTFRAME_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/ISVGSVGFrame.h" +#include "mozilla/SVGContainerFrame.h" + +class gfxContext; + +namespace mozilla { + +/** + * Superclass for inner SVG frames and symbol frames. + */ +class SVGViewportFrame : public SVGDisplayContainerFrame, public ISVGSVGFrame { + protected: + SVGViewportFrame(ComputedStyle* aStyle, nsPresContext* aPresContext, + nsIFrame::ClassID aID) + : SVGDisplayContainerFrame(aStyle, aPresContext, aID) {} + + public: + NS_DECL_ABSTRACT_FRAME(SVGViewportFrame) + + nsresult AttributeChanged(int32_t aNameSpaceID, nsAtom* aAttribute, + int32_t aModType) override; + + // ISVGDisplayableFrame interface: + void PaintSVG(gfxContext& aContext, const gfxMatrix& aTransform, + imgDrawingParams& aImgParams) override; + void ReflowSVG() override; + void NotifySVGChanged(uint32_t aFlags) override; + SVGBBox GetBBoxContribution(const Matrix& aToBBoxUserspace, + uint32_t aFlags) override; + nsIFrame* GetFrameForPoint(const gfxPoint& aPoint) override; + + // SVGContainerFrame methods: + bool HasChildrenOnlyTransform(Matrix* aTransform) const override; + + // ISVGSVGFrame interface: + void NotifyViewportOrTransformChanged(uint32_t aFlags) override; +}; + +} // namespace mozilla + +#endif // LAYOUT_SVG_SVGVIEWPORTFRAME_H_ diff --git a/layout/svg/crashtests/1016145.svg b/layout/svg/crashtests/1016145.svg new file mode 100644 index 0000000000..5c362a17e1 --- /dev/null +++ b/layout/svg/crashtests/1016145.svg @@ -0,0 +1,5 @@ +<!-- svg mime type but html root --> +<html xmlns="http://www.w3.org/1999/xhtml"> + <body style="display: table-column-group;" /> +</html> + diff --git a/layout/svg/crashtests/1028512.svg b/layout/svg/crashtests/1028512.svg new file mode 100644 index 0000000000..0c9458b478 --- /dev/null +++ b/layout/svg/crashtests/1028512.svg @@ -0,0 +1,15 @@ +<!-- {lower,upper}-{roman,alpha} in svg --> +<svg xmlns="http://www.w3.org/2000/svg"> +<script> +<![CDATA[ + +function boom() +{ + document.documentElement.style.transition = "2s"; + document.documentElement.style.listStyleType = "lower-roman"; +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/layout/svg/crashtests/1072758.html b/layout/svg/crashtests/1072758.html new file mode 100644 index 0000000000..1d4c85fb87 --- /dev/null +++ b/layout/svg/crashtests/1072758.html @@ -0,0 +1,35 @@ +<style> +#x9 { + display:none; +} +</style> + +<body onload="go()"> +<svg> +<path id="a"></path> + +<mask id="m"> + <text id="y"> + <tspan id="x1"></tspan> + <textPath id="x2"></textPath> + <a id="x3">Hello</a> + <tspan><tspan id="x4"></tspan></tspan> + <tspan id="x5"></tspan> + </text> +</mask> + +<rect width="600" height="400" mask="url(#m)"/> +</svg> +</body> + +<script> + +function go() { + x1.style.display = "none"; + x2.style.display = "none"; + x3.style.display = "none"; + x4.style.display = "none"; + x5.id = "x9"; +}; + +</script> diff --git a/layout/svg/crashtests/1140080-1.svg b/layout/svg/crashtests/1140080-1.svg new file mode 100644 index 0000000000..d424562468 --- /dev/null +++ b/layout/svg/crashtests/1140080-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<script> + +window.addEventListener("load", function() { + var stop = document.getElementsByTagName("stop")[0]; + stop.setAttribute("offset", "0"); +}, false); + +</script> +<stop/> +</svg> diff --git a/layout/svg/crashtests/1149542-1.svg b/layout/svg/crashtests/1149542-1.svg new file mode 100644 index 0000000000..7353f12775 --- /dev/null +++ b/layout/svg/crashtests/1149542-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <style> + text { white-space: pre } + text::first-letter { color: red; } + tspan { display: none } + </style> + <text textLength="64"> +<tspan>a</tspan>b</text> +</svg> diff --git a/layout/svg/crashtests/1156581-1.svg b/layout/svg/crashtests/1156581-1.svg new file mode 100644 index 0000000000..97e5fb1ca9 --- /dev/null +++ b/layout/svg/crashtests/1156581-1.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="filter: url(#a); clip: rect(0px, 4rem, 2px, 2px);"> + <script> + function boom() + { + document.getElementById("a").style.overflow = "hidden"; + document.documentElement.style.fontSize = "10px"; + } + window.addEventListener("load", boom, false); + </script> + + <set id="a"/> +</svg> diff --git a/layout/svg/crashtests/1182496-1.html b/layout/svg/crashtests/1182496-1.html new file mode 100644 index 0000000000..1d95905a2d --- /dev/null +++ b/layout/svg/crashtests/1182496-1.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <script> + function tweak(){ + document.body.innerHTML="fuzz" + } + </script> +</head> +<body onload="tweak()"> + <svg xmlns="http://www.w3.org/2000/svg"> + <text> + <foreignObject requiredFeatures="foo"> + <svg style="position: absolute;"/> + </foreignObject> + </text> + </svg> +</body> +</html> + + diff --git a/layout/svg/crashtests/1209525-1.svg b/layout/svg/crashtests/1209525-1.svg new file mode 100644 index 0000000000..21134df33b --- /dev/null +++ b/layout/svg/crashtests/1209525-1.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg" + width="0" height="0"> + + <rect width="10" height="10" stroke="black" + vector-effect="non-scaling-stroke" /> + +</svg> diff --git a/layout/svg/crashtests/1223281-1.svg b/layout/svg/crashtests/1223281-1.svg new file mode 100644 index 0000000000..b548a9a600 --- /dev/null +++ b/layout/svg/crashtests/1223281-1.svg @@ -0,0 +1,24 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<script> +<![CDATA[ + +function forceFrameConstruction() +{ + document.documentElement.getBoundingClientRect(); +} + +function boom() +{ + document.documentElement.style.overflow = "scroll"; + forceFrameConstruction() + document.documentElement.style.visibility = "visible"; +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<rect style="perspective: 10em;" /> +</svg> + diff --git a/layout/svg/crashtests/1234726-1.svg b/layout/svg/crashtests/1234726-1.svg new file mode 100644 index 0000000000..ec807e2aba --- /dev/null +++ b/layout/svg/crashtests/1234726-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<script> + +window.addEventListener("load", function() { + document.documentElement.style.fontSize = "70%"; + document.documentElement.setAttribute("transform", "scale(2)"); + var filt = document.createElementNS("http://www.w3.org/2000/svg", "filter"); + filt.style.borderWidth = "2rem"; + document.documentElement.appendChild(filt); +}, false); + +</script> +</svg> diff --git a/layout/svg/crashtests/1322537-1.html b/layout/svg/crashtests/1322537-1.html new file mode 100644 index 0000000000..04207db4ad --- /dev/null +++ b/layout/svg/crashtests/1322537-1.html @@ -0,0 +1,2 @@ +<svg> +<animateMotion path='M8,0l69,97m45,-17592186044414A71,23 46,0,0 16382,98Q50,10 48,72T21,0Z'/>
\ No newline at end of file diff --git a/layout/svg/crashtests/1322537-2.html b/layout/svg/crashtests/1322537-2.html new file mode 100644 index 0000000000..d0495d79af --- /dev/null +++ b/layout/svg/crashtests/1322537-2.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<head> + <script> + function go() { + var path = document.getElementById("myPath"); + var len = path.getTotalLength(); + console.log(len); + } + </script> +</head> +<body onload="go()"> + <svg> + <path id="myPath" d="M45,-17592186044414A71,23 46,0,0 16382,98"/> + </svg> +</body> diff --git a/layout/svg/crashtests/1322852.html b/layout/svg/crashtests/1322852.html new file mode 100644 index 0000000000..728ea5c7a9 --- /dev/null +++ b/layout/svg/crashtests/1322852.html @@ -0,0 +1,2 @@ +<svg> +<ellipse stroke='-moz-mac-menushadow'/>
\ No newline at end of file diff --git a/layout/svg/crashtests/1348564.svg b/layout/svg/crashtests/1348564.svg new file mode 100644 index 0000000000..0cb28948e6 --- /dev/null +++ b/layout/svg/crashtests/1348564.svg @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> +<g style="outline-style:auto;"> +<radialGradient style="transform-box:fill-box;transform:rotatex(15deg);"></radialGradient> +<rect x="200"></rect> +</g> +</svg> diff --git a/layout/svg/crashtests/1402109.html b/layout/svg/crashtests/1402109.html new file mode 100644 index 0000000000..81ef78a058 --- /dev/null +++ b/layout/svg/crashtests/1402109.html @@ -0,0 +1,11 @@ +<script> +function jsfuzzer() { +try { svgvar00012.getStartPositionOfChar(0); } catch(e) { } +} +</script> +<body onload=jsfuzzer()> +<table> +<caption>,HM>v((Ndeoi=&</caption> +<caption> +<svg> +<text id="svgvar00012"> diff --git a/layout/svg/crashtests/1402124.html b/layout/svg/crashtests/1402124.html new file mode 100644 index 0000000000..793b46b9a1 --- /dev/null +++ b/layout/svg/crashtests/1402124.html @@ -0,0 +1,10 @@ +<script> +function jsfuzzer() { +try { a.getExtentOfChar(0); } catch(e) { } +} +</script> +<body onload=jsfuzzer()> +<svg> +<switch> +<path/> +<text id="a"> diff --git a/layout/svg/crashtests/1402486.html b/layout/svg/crashtests/1402486.html new file mode 100644 index 0000000000..af0c72e351 --- /dev/null +++ b/layout/svg/crashtests/1402486.html @@ -0,0 +1,12 @@ +<script> +function jsfuzzer() { +try { a.getEndPositionOfChar(0); } catch(e) { } +} +</script> +<body onload=jsfuzzer()> +<svg> +<switch> +<text id="a"> +</text> +<feTurbulence +<!-- a -->
\ No newline at end of file diff --git a/layout/svg/crashtests/1403656-1.html b/layout/svg/crashtests/1403656-1.html new file mode 100644 index 0000000000..996562d150 --- /dev/null +++ b/layout/svg/crashtests/1403656-1.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html class="filtered"> +<style> + .filtered { + filter: url(#notsvg); + } + #notsvg { + float: left; + overflow: auto; + } +</style> +<script> +function go() { + const div = document.createElement('div') + div.setAttribute('id', 'notsvg') + document.documentElement.appendChild(div) +} +</script> +<body onload="go();" class="filtered"> diff --git a/layout/svg/crashtests/1403656-2.html b/layout/svg/crashtests/1403656-2.html new file mode 100644 index 0000000000..e4287e933f --- /dev/null +++ b/layout/svg/crashtests/1403656-2.html @@ -0,0 +1,21 @@ +<!-- MUST BE QUIRKS MODE --> +<style> +span { top: 0%; } +</style> +<script> +function jsfuzzer() { + try { text.setAttribute("font-size", "10px"); } catch(e) { } +} +</script> +<body onload=jsfuzzer()> + <div style="overflow: scroll"> + <span></span> + </div> + <svg> + <tref id="marker"> + <text id="text"/> + </tref> + <polyline marker-end="url(#marker)"> + </polyline> + </svg> +</body> diff --git a/layout/svg/crashtests/1403656-3.html b/layout/svg/crashtests/1403656-3.html new file mode 100644 index 0000000000..a5a7632665 --- /dev/null +++ b/layout/svg/crashtests/1403656-3.html @@ -0,0 +1,8 @@ +<style> +* { columns: 0px } +</style> +A +<svg id="a"> +<line marker-end="url(#a)"> +</svg> +<keygen> diff --git a/layout/svg/crashtests/1403656-4.html b/layout/svg/crashtests/1403656-4.html new file mode 100644 index 0000000000..1da8979aa7 --- /dev/null +++ b/layout/svg/crashtests/1403656-4.html @@ -0,0 +1,9 @@ +<style>
+#a {}
+* {
+ text-align: end;
+ writing-mode: sideways-lr;
+ -webkit-filter: url(#a);
+}
+</style>
+<canvas>AAAAAAAAAAAAAAAA</canvas><ol id="a">@
diff --git a/layout/svg/crashtests/1403656-5.html b/layout/svg/crashtests/1403656-5.html new file mode 100644 index 0000000000..3d64bc5b50 --- /dev/null +++ b/layout/svg/crashtests/1403656-5.html @@ -0,0 +1,11 @@ +<style> +* { + -webkit-filter: url(#a); + padding-right: 1vmin; + overflow-y: scroll; +} +</style> +<details id="a"></details> +<image style="bottom: 1em;display:flex"></image> +<canvas height="1"> +<font> diff --git a/layout/svg/crashtests/1404086.html b/layout/svg/crashtests/1404086.html new file mode 100644 index 0000000000..df2ad6a3b3 --- /dev/null +++ b/layout/svg/crashtests/1404086.html @@ -0,0 +1,2 @@ +<svg> +<text textLength="0" lengthAdjust="spacingAndGlyphs">t diff --git a/layout/svg/crashtests/1421807-1.html b/layout/svg/crashtests/1421807-1.html new file mode 100644 index 0000000000..fe5a3e8d66 --- /dev/null +++ b/layout/svg/crashtests/1421807-1.html @@ -0,0 +1,5 @@ +<body onload=b.appendChild(a)> +<div id="a" style="display: contents"> +</div> +<svg> +<text id="b"> diff --git a/layout/svg/crashtests/1421807-2.html b/layout/svg/crashtests/1421807-2.html new file mode 100644 index 0000000000..30b100642c --- /dev/null +++ b/layout/svg/crashtests/1421807-2.html @@ -0,0 +1,15 @@ +<style> +.c1 { display: contents; } +</style> +<script> +function go() { + a.attachShadow({mode: "open"}).innerHTML = `<slot> </slot> `; + b.appendChild(a); +} +</script> +<body onload=go()> +<div id="a" class="c1"> + <span></span> +</div> +<svg> +<text id="b"> diff --git a/layout/svg/crashtests/1422226.html b/layout/svg/crashtests/1422226.html new file mode 100644 index 0000000000..5826e60b69 --- /dev/null +++ b/layout/svg/crashtests/1422226.html @@ -0,0 +1,39 @@ +<style id="htmlvar00001"> +* { + filter: saturate(1) hue-rotate(0deg); + marker-mid: url() +} +</style> +<script> +function jsfuzzer() { +try { var var00078 = window.find("foo",true,true); } catch(e) { } +try { htmlvar00004.setAttribute("onselect", "eventhandler2()"); } catch(e) { } +try { htmlvar00004.selectionStart = 1; } catch(e) { } +try { svgvar00030.addEventListener("DOMSubtreeModified", eventhandler5); } catch(e) { } +} +function eventhandler1() { +try { var var00098 = document.createElement("select"); } catch(e) { } +try { htmlvar00001.appendChild(var00098); } catch(e) { } +} +function eventhandler2() { +try { htmlvar00015.href = "3" } catch(e) { } +try { svgvar00008.before(svgvar00013); } catch(e) { } +try { var var00014 = window.getSelection(); } catch(e) { } +try { var00014.setBaseAndExtent(htmlvar00011,0,htmlvar00010,0); } catch(e) { } +try { var00014.collapseToStart(); } catch(e) { } +} +function eventhandler5() { +try { svgvar00001.addEventListener("DOMNodeRemoved", eventhandler1); } catch(e) { } +} +</script> +<body onload=jsfuzzer()> +<input id="htmlvar00004"> +<svg id="svgvar00001"> +<marker> +<meshgradient id="svgvar00008"> +<foreignObject id="svgvar00013"> +<discard id="svgvar00030"/> +<a id="htmlvar00010" contenteditable="true"></a> +<a id="htmlvar00011"> +<base id="htmlvar00015"> + diff --git a/layout/svg/crashtests/1424031.html b/layout/svg/crashtests/1424031.html new file mode 100644 index 0000000000..447e01cb15 --- /dev/null +++ b/layout/svg/crashtests/1424031.html @@ -0,0 +1,16 @@ +<script> +function eh1() { + a.setAttribute("points", "1"); +} +function eh2() { + a.addEventListener("DOMSubtreeModified", eh1); + a.points.getItem(1).y = 0; +} +function go() { + window.requestIdleCallback(eh2, {timeout: 62}); +} +</script> +<style onload="go()"></style> +<svg> +<polygon id="a" points="0 0 58 1 -1 1"> + diff --git a/layout/svg/crashtests/1425434-1.html b/layout/svg/crashtests/1425434-1.html new file mode 100644 index 0000000000..cec773ca25 --- /dev/null +++ b/layout/svg/crashtests/1425434-1.html @@ -0,0 +1,59 @@ +<html>
+<head>
+<style>
+
+#htmlvar00002,.class4,strong:last-of-type {
+ border-image-repeat: round stretch;
+ counter-increment:c
+}
+.class6,#htmlvar00001,#htmlvar00008 {
+ height:63%;
+ grid-column:span 0 middle / middle
+}
+#htmlvar00002 {
+ overflow-y:hidden;
+ grid-column-end:0 inherit
+}
+
+</style>
+<script>
+
+function eventhandler5() {
+
+ var var00066 = document.createElement("audio");
+ try { var00066.controls = true; } catch(e) { }
+
+ try { document.all['htmlvar00002'].appendChild(htmlvar00001); } catch(e) { }
+ try { document.all['htmlvar00002'].appendChild(var00066); } catch(e) { }
+}
+
+</script>
+</head>
+<body onload=eventhandler5()>
+
+ <shadow id="htmlvar00001" axis="PDcIvMde3=%- S^@dz" ondblclick="eventhandler1()" item="8z6h" checked="checked" type="text/x-javascript">
+ aaa
+ </shadow>
+
+ <ul id="htmlvar00002" type="video/mp4; codecs='avc1.4D400C'" type="turbulence" type="javascript_1.0"
+ role="complementary" dir="ltr" required="required" formnovalidate="formnovalidate" scheme="NIST" expanded="true" slot="slot2">
+ bbb
+ </ul>
+
+ <svg id="svgvar00001" marker-end="url(#svgvar00005)" transform="translate(0, 0) scale(0.113899652038) rotate(1) translate(0, 1)"
+ marker-mid="url(#svgvar00009)" xml:id="test-title" viewBox="0 1 78 1" click="none" glyph-orientation-vertical="-1" dominant-baseline="middle" ry="61%" k="1">
+
+ <path id="svgvar00009" d="M 0 0 L 1 0" stroke-dashoffset="inherit" onclick="eventhandler5()" stroke-linejoin="inherit"
+ fill-rule="evenodd" onmouseover="eventhandler3()" onfocusin="eventhandler4()" pointsAtY="1" target="_self" primitiveUnits="userSpaceOnUse" horiz-adv-x="0" />
+
+ <line id="svgvar00010" x1="0px" y1="55%" x2="76%" y2="62%" transform="translate(0 100) scale(0 -1)"
+ transform="translate(00.32347924876,0)" visibility="hidden" stroke-width="inherit" pointer-events="painted"
+ xlink:type="simple" onrepeat="eventhandler4()" clip-path="url(#svgvar00001)" vector-effect="non-scaling-stroke" azimuth="-1">
+ ccc
+ </line>
+
+ </svg>
+
+<!--endhtml-->
+</body>
+</html>
diff --git a/layout/svg/crashtests/1426575.html b/layout/svg/crashtests/1426575.html new file mode 100644 index 0000000000..6ea99585e9 --- /dev/null +++ b/layout/svg/crashtests/1426575.html @@ -0,0 +1,16 @@ +<script> +function go() { + var var00046 = svgvar00022.createSVGLength(); + svgvar00015.y.baseVal.appendItem(var00046); + svgvar00015.addEventListener("DOMSubtreeModified", eh); + var00046.newValueSpecifiedUnits(7,0); +} +function eh() { + svgvar00015.setAttribute("y", "2 1 93"); +} +</script> +<body onload=go()> +<svg> +<tspan id="svgvar00015" y="32 45 1 1"/> +<svg id="svgvar00022"> + diff --git a/layout/svg/crashtests/1443092-helper.svg b/layout/svg/crashtests/1443092-helper.svg new file mode 100644 index 0000000000..134cbefb58 --- /dev/null +++ b/layout/svg/crashtests/1443092-helper.svg @@ -0,0 +1,6 @@ +<svg class='class2' xmlns='http://www.w3.org/2000/svg'> +<clipPath clipPathUnits='userSpaceOnUse' id='id9' lighting-color='pink' class='class0 class1' > +</clipPath> +<a id='id0' clip-path='url(#id9)' > +</a> +</svg> diff --git a/layout/svg/crashtests/1443092.html b/layout/svg/crashtests/1443092.html new file mode 100644 index 0000000000..7be4561e61 --- /dev/null +++ b/layout/svg/crashtests/1443092.html @@ -0,0 +1,34 @@ +<script> +function start() { + o7=document.createElement('div'); + o7.innerHTML='<style>@keyframes key7{ from{ transform: rotate(-536870911deg)}}\n*{ animation-name: key7; animation-duration: 0.001s'; + o17=document.createElement('div'); + o17.innerHTML='<svg><style>@font-face{}\n*{ outline-style: dotted</style><style>@font-face{ font-family: font3; src: url('; + o18=o17.firstChild.getElementsByTagName('*'); + o20=o18[0]; + o23=o18[1]; + o114=document.createElement('iframe'); + o114.src='1443092-helper.svg'; + o114.addEventListener('load', fun0,false); + document.body.appendChild(o114); +} +function fun0() { + o117=o114.contentDocument; + document.replaceChild(o117.documentElement,document.documentElement); + o124=document.createElement('iframe'); + document.documentElement.appendChild(o124); + o145=document.createElement('div'); + o145.innerHTML='<svg><set attributeName="text-decoration">'; + document.documentElement.appendChild(o20); + document.documentElement.appendChild(o23); + document.documentElement.appendChild(o7); + o124.src='x'; + document.documentElement.appendChild(o145); + o264=document.createElement('style'); + o265=document.createTextNode('*{ float: left'); + o264.appendChild(o265); + o23.appendChild(o264); + setTimeout("location.reload()",40); +} +</script> +<body onload="start()"></body> diff --git a/layout/svg/crashtests/1454201-1.html b/layout/svg/crashtests/1454201-1.html new file mode 100644 index 0000000000..b92a7aa595 --- /dev/null +++ b/layout/svg/crashtests/1454201-1.html @@ -0,0 +1,52 @@ +<html> +<head> +<script> +function loader() { +var tbody = document.createElement("tbody"); +try { document.all[90%document.all.length].appendChild(tbody); } catch(e) { } +} +</script> +</head> +<body onload=loader()> +<span> +<a></a> +</span> +<a></a> +<svg marker-end="url(#poly)"> +<a></a> +<a></a> +<symbol > +<a></a> +</symbol> +<polyline id="poly"> +<a></a> +<a></a> +<a></a> +<a></a> +</polyline> +<line > +<a></a> +</line> +<a></a> +<span> +<a></a> +</span> +<a></a> +<a></a> +<a></a> +</svg> +<dialog style="-moz-appearance: checkbox; overflow-x: auto; max-width: 0em;" open="true"></dialog> +<a></a> +<a></a> +<span> +<a></a> +<a></a> +<a></a> +<a></a> +</span> +<a></a> +<a></a> +<a></a> +<a></a> +</body> +</html> diff --git a/layout/svg/crashtests/1467552-1.html b/layout/svg/crashtests/1467552-1.html new file mode 100644 index 0000000000..df85665c72 --- /dev/null +++ b/layout/svg/crashtests/1467552-1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<html> +<style> +fieldset { + mask: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciLz4=), + padding-box; + margin-right: 75px; +} +span { + vertical-align: 672in; +} +</style> +<fieldset><span><video></video></span></fieldset> +</html> diff --git a/layout/svg/crashtests/1474982.html b/layout/svg/crashtests/1474982.html new file mode 100644 index 0000000000..c33e2be474 --- /dev/null +++ b/layout/svg/crashtests/1474982.html @@ -0,0 +1,2 @@ +<svg> +<text lengthAdjust="" textLength="0">A</text> diff --git a/layout/svg/crashtests/1480224.html b/layout/svg/crashtests/1480224.html new file mode 100644 index 0000000000..0bb0dfa7b5 --- /dev/null +++ b/layout/svg/crashtests/1480224.html @@ -0,0 +1,6 @@ +<svg> +<symbol id="a"> +<foreignObject> +<input type="file"> +</symbol> +<use xlink:href="#a"> diff --git a/layout/svg/crashtests/1480275.html b/layout/svg/crashtests/1480275.html new file mode 100644 index 0000000000..b21ebdd062 --- /dev/null +++ b/layout/svg/crashtests/1480275.html @@ -0,0 +1,15 @@ +<script> +function go() { + b.appendChild(a); + d.outerHTML = f.outerHTML; +} +</script> +<table> +<tr id="a"> +<th> +<svg id="c" onload="go()"> +<use xlink:href="#c"/> +</tr> +<code id="d"></code> +<video id="f"> +<details id="b"> diff --git a/layout/svg/crashtests/1502936.html b/layout/svg/crashtests/1502936.html new file mode 100644 index 0000000000..87892ab461 --- /dev/null +++ b/layout/svg/crashtests/1502936.html @@ -0,0 +1,11 @@ +<script> +function go() { + a.setAttribute("requiredExtensions", "x"); + b.getBoundingClientRect(); + a.setAttribute("y", "-1px"); +} +</script> +<body onload=go()> +<svg> +<use id="a"> +<feGaussianBlur id="b"> diff --git a/layout/svg/crashtests/1504072.html b/layout/svg/crashtests/1504072.html new file mode 100644 index 0000000000..a44e2f0251 --- /dev/null +++ b/layout/svg/crashtests/1504072.html @@ -0,0 +1,4 @@ +<style> +svg { perspective: 0px } +</style> +<svg overflow="scroll" systemLanguage=""> diff --git a/layout/svg/crashtests/1504918.svg b/layout/svg/crashtests/1504918.svg new file mode 100644 index 0000000000..63cd3a08e0 --- /dev/null +++ b/layout/svg/crashtests/1504918.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg">
+ <style>textPath { display: contents; }</style>
+ <text>x<textPath><textPath><tspan>y</tspan></textPath></textPath></text>
+</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/1535517-1.svg b/layout/svg/crashtests/1535517-1.svg new file mode 100644 index 0000000000..6f3f17eabf --- /dev/null +++ b/layout/svg/crashtests/1535517-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<marker><text><tspan id="x"/></text></marker> + +<script> +window.addEventListener("load", function() { + document.getElementById("x").appendChild(document.createTextNode("\u062Ax")); +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/1536892.html b/layout/svg/crashtests/1536892.html new file mode 100644 index 0000000000..8bc237138e --- /dev/null +++ b/layout/svg/crashtests/1536892.html @@ -0,0 +1,13 @@ +<style>
+* { -webkit-filter: blur(5px) }
+</style>
+<script>
+function go() {
+ a.setAttribute("text-decoration", "overline")
+}
+</script>
+<body onload=go()>
+<svg id="a">
+<marker>
+<foreignObject>
+<li style="-webkit-box-shadow:8px 0 1px">
diff --git a/layout/svg/crashtests/1539318-1.svg b/layout/svg/crashtests/1539318-1.svg new file mode 100644 index 0000000000..d832f448a3 --- /dev/null +++ b/layout/svg/crashtests/1539318-1.svg @@ -0,0 +1,10 @@ +<script> +window.onload = function() { + a.getComputedTextLength() +} +</script> +<body> +<svg> +<switch> +<hatch> +<text id="a">A</text> diff --git a/layout/svg/crashtests/1548985-1.html b/layout/svg/crashtests/1548985-1.html new file mode 100644 index 0000000000..7156eaf36b --- /dev/null +++ b/layout/svg/crashtests/1548985-1.html @@ -0,0 +1,16 @@ +<!-- a -->
+<style>
+:root { contain: size }
+</style>
+<script>
+window.requestIdleCallback(window.close)
+function go() {
+ a.appendChild(document.head)
+ let b = d.getRootNode({composed: true})
+ b.replaceChild(c, b.childNodes[1])
+}
+</script>
+<body onload=go()>
+<svg id="c">
+<feTile id="d" />
+<foreignObject id="a">
diff --git a/layout/svg/crashtests/1548985-2.svg b/layout/svg/crashtests/1548985-2.svg new file mode 100644 index 0000000000..700facb411 --- /dev/null +++ b/layout/svg/crashtests/1548985-2.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" + style="contain:size"> + <rect width="100px" height="100px" fill="lime"/> +</svg> diff --git a/layout/svg/crashtests/1555851.html b/layout/svg/crashtests/1555851.html new file mode 100644 index 0000000000..2b6f5a596a --- /dev/null +++ b/layout/svg/crashtests/1555851.html @@ -0,0 +1,8 @@ +<body>
+<script>
+ var o0 = document.createElementNS("http://www.w3.org/2000/svg", "svg")
+ document.body.appendChild(o0)
+ var o3 = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion")
+ o0.appendChild(o3)
+ o3.setAttribute("path", "M9,2l2e37,3A40,2 85,102-1")
+</script>
diff --git a/layout/svg/crashtests/1563779.html b/layout/svg/crashtests/1563779.html new file mode 100644 index 0000000000..4c51d0884d --- /dev/null +++ b/layout/svg/crashtests/1563779.html @@ -0,0 +1,19 @@ +<script id="a"> +let count = 0; +function go() { + if (count++ == 3) + return; + try { a.appendChild(c) } catch(e) { } + try { window.getSelection().getRangeAt(0).insertNode(b) } catch(e) { } + try { d.selectSubString(0,-1) } catch(e) { } + document.documentElement.style.display = "none" + document.documentElement.getBoundingClientRect() + document.documentElement.style.display = "" +} +</script> +<pre id="b" style="display:contents">a</pre> +<span id="c"> +<style onload="go()"></style> +</span> +<svg> +<text id="d"><textPath xml:space="preserve"> diff --git a/layout/svg/crashtests/1600855.html b/layout/svg/crashtests/1600855.html new file mode 100644 index 0000000000..31c4809ecb --- /dev/null +++ b/layout/svg/crashtests/1600855.html @@ -0,0 +1,8 @@ +<script> +window.onload = () => { + a.append(String.fromCodePoint(71341)) +} +</script> +<font style="word-spacing: 31pc"> +<svg> +<text id="a">Text</text> diff --git a/layout/svg/crashtests/1601824.html b/layout/svg/crashtests/1601824.html new file mode 100644 index 0000000000..04d0de825d --- /dev/null +++ b/layout/svg/crashtests/1601824.html @@ -0,0 +1,7 @@ +<svg> +<polygon id="a" points="0 1"> +</polygon> +<text> +<textPath xlink:href="#a"> +<marker /> +<a systemLanguage=""> diff --git a/layout/svg/crashtests/1605223-1.html b/layout/svg/crashtests/1605223-1.html new file mode 100644 index 0000000000..6b476a7fd0 --- /dev/null +++ b/layout/svg/crashtests/1605223-1.html @@ -0,0 +1,4 @@ +<svg filter="url(#a)"> +<filter id="a"> +<feComponentTransfer> +<feFuncG type="" /> diff --git a/layout/svg/crashtests/1609663.html b/layout/svg/crashtests/1609663.html new file mode 100644 index 0000000000..c5a5d938e0 --- /dev/null +++ b/layout/svg/crashtests/1609663.html @@ -0,0 +1,34 @@ +<html>
+<head>
+</head>
+<body>
+<div id="main">
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="mySvg" width="400" height="800">
+ <defs>
+ <pattern id="pattern1" x="30" y="10" width="30" height="30" patternUnits="userSpaceOnUse" >
+ <text id="idText1" x="15" y="15" font-size="20">1</text>
+ </pattern>
+ </defs>
+ <rect id="idRect1" fill="url(#pattern1)" width="300" height="200"/>
+ </svg>
+</div>
+<script>
+document.body.offsetHeight;
+
+var pattern = document.getElementById('pattern1');
+var text = document.getElementById('idText1');
+pattern.removeChild(text);
+
+var svgNS = "http://www.w3.org/2000/svg";
+var newText = document.createElementNS(svgNS,"text");
+newText.setAttributeNS(null,"id",'idText1');
+newText.setAttributeNS(null,"x",15);
+newText.setAttributeNS(null,"y",15);
+newText.setAttributeNS(null,"font-size","20");
+
+var textNode = document.createTextNode('x');
+newText.appendChild(textNode);
+pattern.appendChild(newText);
+</script>
+</body>
+</html>
diff --git a/layout/svg/crashtests/1671950.html b/layout/svg/crashtests/1671950.html new file mode 100644 index 0000000000..173ace4ea9 --- /dev/null +++ b/layout/svg/crashtests/1671950.html @@ -0,0 +1,27 @@ +<html> +<head> + <style> + * { + break-before: always ! important; + } + </style> + <script> + window.addEventListener('load', () => { + const style = document.createElement('style') + document.head.appendChild(style) + const svg_1 = document.getElementById('id_3') + const svg_2 = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const switch_0 = document.createElementNS('http://www.w3.org/2000/svg', 'switch') + const c_0 = document.createElementNS('http://www.w3.org/2000/svg', 'c') + switch_0.appendChild(c_0) + svg_1.appendChild(switch_0) + svg_2.appendChild(svg_1) + document.documentElement.appendChild(svg_2) + style.sheet.insertRule('@-moz-document url-prefix(){*,a{all:inherit', 0) + SpecialPowers.wrap(window).printPreview()?.close() + }) + </script> + <svg id='id_3'></svg> +</head> +</html> + diff --git a/layout/svg/crashtests/1678947.html b/layout/svg/crashtests/1678947.html new file mode 100644 index 0000000000..96a3a37862 --- /dev/null +++ b/layout/svg/crashtests/1678947.html @@ -0,0 +1,21 @@ +<html> +<head> +<style> +ol { float: right; } +</style> +<script> +function start() { + document.elementFromPoint(0,1); + document.dir = "rtl"; +} +</script> +</head> +<body> +<svg onload="start()" requiredExtensions="x"> + <g id="a"/></g> +<text> +<textPath xlink:href="#a"> +</svg> +<ol></ol> +</body> +</html> diff --git a/layout/svg/crashtests/1693032.html b/layout/svg/crashtests/1693032.html new file mode 100644 index 0000000000..49a2bf3c17 --- /dev/null +++ b/layout/svg/crashtests/1693032.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> + <script> + document.addEventListener('DOMContentLoaded', () => { + const svg_1 = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const svg_2 = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const switch_1 = document.createElementNS('http://www.w3.org/2000/svg', 'switch') + const metadata_1 = document.createElementNS('http://www.w3.org/2000/svg', 'metadata') + const foreign_1 = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject') + const text_1 = document.createElementNS('http://www.w3.org/2000/svg', 'text') + switch_1.appendChild(metadata_1) + svg_2.appendChild(text_1) + foreign_1.appendChild(svg_2) + switch_1.appendChild(foreign_1) + svg_1.appendChild(switch_1) + document.documentElement.appendChild(svg_1) + }) + </script> +</head> +</html> diff --git a/layout/svg/crashtests/1696505.html b/layout/svg/crashtests/1696505.html new file mode 100644 index 0000000000..92700496e8 --- /dev/null +++ b/layout/svg/crashtests/1696505.html @@ -0,0 +1,9 @@ +<script> +document.addEventListener("DOMContentLoaded", () => { + document.getElementById('a').style.cssText += "grid-row-end:auto" +}) +</script> +<svg> +<polyline marker-mid='url(#a)'/> +<marker id='a'> +<text/> diff --git a/layout/svg/crashtests/1755770-1.html b/layout/svg/crashtests/1755770-1.html new file mode 100644 index 0000000000..ee9cf6fee7 --- /dev/null +++ b/layout/svg/crashtests/1755770-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> + <script> + window.addEventListener("load", () => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("letter-spacing", "65535pc"); + const node = document.createTextNode("\rï¿»ð¨\nó = ð£%?;ð©á©¿ð�"); + text.appendChild(node); + svg.appendChild(text); + document.documentElement.appendChild(svg); + const rect = new DOMRectReadOnly(-128, 256, 0, 1024); + node.convertRectFromNode(rect, document, {}); + }) + </script> +</head> +</html> diff --git a/layout/svg/crashtests/1755770-2.html b/layout/svg/crashtests/1755770-2.html new file mode 100644 index 0000000000..31b42ec452 --- /dev/null +++ b/layout/svg/crashtests/1755770-2.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> + <script> + window.addEventListener("load", () => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); + text.setAttribute("letter-spacing", "65535pc"); + text.setAttribute("writing-mode", "vertical-lr"); + const node = document.createTextNode("\rï¿»ð¨\nó = ð£%?;ð©á©¿ð�"); + text.appendChild(node); + svg.appendChild(text); + document.documentElement.appendChild(svg); + const rect = new DOMRectReadOnly(-128, 256, 0, 1024); + node.convertRectFromNode(rect, document, {}); + }) + </script> +</head> +</html> diff --git a/layout/svg/crashtests/1758029-1.html b/layout/svg/crashtests/1758029-1.html new file mode 100644 index 0000000000..2097e875ee --- /dev/null +++ b/layout/svg/crashtests/1758029-1.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<meta charset="utf-8"> +<style> + body { background: gray; } + canvas { border: 2px solid black;} +</style> + +<img id="img" + onload="go()" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAKElEQVR42u3NQQ0AAAgEoNP+nTWFDzcoQE1udQQCgUAgEAgEAsGTYAGjxAE/G/Q2tQAAAABJRU5ErkJggg=="> +<canvas id="canvas"></canvas> +<script> + const ctx = canvas.getContext("2d", { desynchronized: true }); + const SVG_FILTER = ` + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <filter id="posterize"> + <feComponentTransfer> + <feFuncR type="discrete" tableValues="0,1" /> + <feFuncG type="discrete" tableValues="0,1" /> + <feFuncB type="discrete" tableValues="0,1" /> + <feFuncA type="discrete" tableValues="0,1" /> + </feComponentTransfer> + </filter> + </svg>`; + + const FILTER1 = `url('data:image/svg+xml;utf8,${SVG_FILTER.replace(/\n/g, "") + .replace(/\s+/g, " ") + .trim()}#posterize') grayscale(50%) brightness(50%)`; + function go() { + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + + ctx.imageSmoothingEnabled = true; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.filter = FILTER1; + ctx.drawImage(img, 0, 0); + setTimeout(() => { document.documentElement.removeAttribute("class")}, 0); + } +</script> diff --git a/layout/svg/crashtests/1764936-1.html b/layout/svg/crashtests/1764936-1.html new file mode 100644 index 0000000000..c03789d547 --- /dev/null +++ b/layout/svg/crashtests/1764936-1.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> + <script> + window.addEventListener("load", () => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg") + const image = document.createElementNS("http://www.w3.org/2000/svg", "image") + image.setAttribute("height", "78.04250580135444ch") + image.setAttribute("width", "1024rem") + image.setAttribute("clip-path", "path( evenodd, '\\C' )") + svg.appendChild(image) + document.documentElement.appendChild(svg) + }) + </script> +</head> +</html> diff --git a/layout/svg/crashtests/1804958.html b/layout/svg/crashtests/1804958.html new file mode 100644 index 0000000000..663f621d4c --- /dev/null +++ b/layout/svg/crashtests/1804958.html @@ -0,0 +1,4 @@ +<svg>
+<polygon points="0,2 2,0" marker-end="url(#a)"></polygon>
+<marker id="a" clip-path="">
+<svg style="mix-blend-mode: lighten">
diff --git a/layout/svg/crashtests/1810260.html b/layout/svg/crashtests/1810260.html new file mode 100644 index 0000000000..f555581165 --- /dev/null +++ b/layout/svg/crashtests/1810260.html @@ -0,0 +1,28 @@ +<style> +*:only-child { + display: contents; +} +*:nth-child(1) { + mask: url(data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=) repeat-x view-box; +} +</style> +<script> +document.addEventListener('DOMContentLoaded', () => { + document.execCommand("selectAll", false) + document.designMode = "on" + document.execCommand("backColor", false, "red") + document.execCommand("justifyFull", false) +}) +</script> +A +<q contenteditable="true"> +<link> +<svg> +<line></line> +<text white-space="pre-line"> +A +</text> +<defs> +</svg> +A + diff --git a/layout/svg/crashtests/1826444-1.html b/layout/svg/crashtests/1826444-1.html new file mode 100644 index 0000000000..b41e59928d --- /dev/null +++ b/layout/svg/crashtests/1826444-1.html @@ -0,0 +1,6 @@ +<svg> +<marker id="a"> +<circle stroke="context-fill" r="1em"> +</marker> +<polygon points="1,50 1,511 0,1 0,0 30,71 32768,912 1,52 41146,14 0,120 814,7 868,16 973,83 8192,0 121,242 0,16384 0,1 541,44049 0,0 0,12 1,0 16376,0 2,7 0,1 0,0 0,16 953,15758 2048,992 1,8 33,0 58,0 0,6 26,7" fill="url(#b)" marker-mid="url(#a)" fill-opacity="0"></polygon> +<pattern id="b"> diff --git a/layout/svg/crashtests/1831419.html b/layout/svg/crashtests/1831419.html new file mode 100644 index 0000000000..32a120907c --- /dev/null +++ b/layout/svg/crashtests/1831419.html @@ -0,0 +1,6 @@ +<style> +* { font-size: 5% } +</style> +<svg> +<text y="1,172" clip-path="url()" transform="matrix(0,0,4,1,0,0)"> +AA diff --git a/layout/svg/crashtests/220165-1.svg b/layout/svg/crashtests/220165-1.svg new file mode 100644 index 0000000000..0335f78d41 --- /dev/null +++ b/layout/svg/crashtests/220165-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?>
+
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:html="http://www.w3.org/1999/xhtml" height="500"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="document.documentElement.getBoundingClientRect();
+ document.getElementById('x').textContent = 'New text'">
+
+ <foreignObject x="200" y="180" width="100" height="50" >
+ <html:button id="x">Old long long long text</html:button>
+ </foreignObject>
+
+ <g transform="rotate(10) translate(-100) scale(0.8)">
+ <polygon style="fill:red; fill-opacity:0.5;"
+ points="350, 75 379,161 469,161 397,215
+ 423,301 350,250 277,301 303,215
+ 231,161 321,161" />
+
+ </g>
+
+</svg>
diff --git a/layout/svg/crashtests/267650-1.svg b/layout/svg/crashtests/267650-1.svg new file mode 100644 index 0000000000..3e9c7ecc01 --- /dev/null +++ b/layout/svg/crashtests/267650-1.svg @@ -0,0 +1,4 @@ +<?xml version='1.0'?>
+<svg xmlns='http://www.w3.org/2000/svg'>
+ <text fill='none' stroke='black'>TESTCASE</text>
+</svg>
diff --git a/layout/svg/crashtests/294022-1.svg b/layout/svg/crashtests/294022-1.svg new file mode 100644 index 0000000000..f30b484c83 --- /dev/null +++ b/layout/svg/crashtests/294022-1.svg @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" + "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + > + + <g> + <clipPath id="action_box_cp"> + <rect width="100" height="46"/> + </clipPath> + <text style="clip-path:url(#action_box_cp); " y="10" id="action_boxtext" pointer-events="none" class="TextBoxText"> + <tspan x="0" dy="0">Action</tspan> + </text> + </g> + +</svg> diff --git a/layout/svg/crashtests/307314-1.svg b/layout/svg/crashtests/307314-1.svg new file mode 100644 index 0000000000..5c538df88d --- /dev/null +++ b/layout/svg/crashtests/307314-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait" onload="setTimeout(function() { var g = document.getElementById('N1'), h = document.getElementById('N2'); g.appendChild(h); document.documentElement.removeAttribute("class"); }, 20);"> + + <text id="N1"/> + <text id="N2"> + <tspan> + <textPath/> + </tspan> + </text> +</svg> diff --git a/layout/svg/crashtests/308615-1.svg b/layout/svg/crashtests/308615-1.svg new file mode 100644 index 0000000000..fe5391de38 --- /dev/null +++ b/layout/svg/crashtests/308615-1.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + + <pattern id="pattern1"> + <rect style="fill:url(#pattern1);"/> + </pattern> + + <rect style="fill:url(#pattern1);" /> + +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/308917-1.svg b/layout/svg/crashtests/308917-1.svg new file mode 100644 index 0000000000..7d0ae44f74 --- /dev/null +++ b/layout/svg/crashtests/308917-1.svg @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<svg version="1.1" baseProfile="basic" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 480 360" class="reftest-wait" onload="first();"> + +<script><![CDATA[ + +function first() +{ + document.getElementById("z").appendChild(document.getElementById("pat1")); + setTimeout(second, 30); +} + +function second() +{ + document.getElementById("pat4").appendChild(document.getElementById("z")); + document.documentElement.removeAttribute("class"); +} + +]]></script> + + + <pattern patternUnits="userSpaceOnUse" id="pat1" x="10" y="10" width="20" height="20"> + <rect x="5" y="5" width="10" height="10" fill="red" /> + <rect x="10" y="10" width="10" height="10" fill="green" /> + </pattern> + <rect x="25" y="10" width="430" height="60" stroke="black" fill="url(#pat1)" /> + + <pattern patternUnits="userSpaceOnUse" id="pat4" x="0" y="0" width="20" height="10"> + <rect x="0" y="0" width="10" height="10" fill="red" /> + <rect x="10" y="0" width="10" height="10" fill="blue" /> + </pattern> + <text font-family="Arial" font-size="40" fill="none" stroke="url(#pat4)" stroke-width="2" x="25" y="275" id="z">Pattern on stroke</text> + +</svg> + diff --git a/layout/svg/crashtests/310436-1.svg b/layout/svg/crashtests/310436-1.svg new file mode 100644 index 0000000000..e6dd5680ce --- /dev/null +++ b/layout/svg/crashtests/310436-1.svg @@ -0,0 +1,28 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"><script><![CDATA[ + +function init() { + var docElt = document.documentElement; + var div1 = document.getElementById("div1"); + var div2 = document.getElementById("div2"); + var textNode = div2.childNodes[0]; + + function first() + { + docElt.appendChild(div2); + div2.appendChild(div1); + } + + function second() + { + div2.appendChild(div1); + div1.appendChild(textNode); + document.documentElement.removeAttribute("class"); + } + + first(); + setTimeout(second, 30); +} + +window.addEventListener("load", init, false); + +]]></script><div xmlns="http://www.w3.org/1999/xhtml" id="div1"><div id="div2">A Z</div></div></svg> diff --git a/layout/svg/crashtests/310638.svg b/layout/svg/crashtests/310638.svg new file mode 100644 index 0000000000..e5ee30fb2c --- /dev/null +++ b/layout/svg/crashtests/310638.svg @@ -0,0 +1,35 @@ +<svg xmlns="http://www.w3.org/2000/svg"><div xmlns='http://www.w3.org/1999/xhtml' id="div1">
+<div id="div2">bar</div> +</div>
+<script><![CDATA[ + +function init() +{ + var div2 = document.getElementById("div2"); + var div1 = document.getElementById("div1"); + var docElt = document.documentElement; + var titleText = document.createTextNode("foo baz"); + + docElt.appendChild(div2);
div2.appendChild(titleText); + + function second () + { + div2.appendChild(div1); + removeNode(titleText); + removeNode(div2); + } + + setTimeout(second, 0); +} + + +function removeNode(q1) { q1.parentNode.removeChild(q1); } + + +setTimeout(init, 0); + + +]]></script> + + +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/313737-1.xml b/layout/svg/crashtests/313737-1.xml new file mode 100644 index 0000000000..93421f6077 --- /dev/null +++ b/layout/svg/crashtests/313737-1.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?> +<!DOCTYPE xhtml PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd" [ +]> + +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" xmlns:xlink="http://www.w3.org/1999/xlink"> + <head> + <title>bug 313737</title> + </head> + <body> + + <svg:svg style="position:fixed;"> + <input type="text" style="position:absolute;" /> + </svg:svg> + + </body> +</html> diff --git a/layout/svg/crashtests/314244-1.xhtml b/layout/svg/crashtests/314244-1.xhtml new file mode 100644 index 0000000000..14717f146d --- /dev/null +++ b/layout/svg/crashtests/314244-1.xhtml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?> +<!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=314244 --> +<!-- Just checking for lack of crash, nothing more --> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="GMail" + width="300" height="300" + screenX="10" screenY="10"> + + <hbox> + <svg version="1.0" + xmlns="http://www.w3.org/2000/svg" + width="100px" height="100px" + id="back-button" + class="nav-button" + style="display: -moz-box;"> + <rect x="10" y="10" width="80" height="80" fill="blue" /> + </svg> + <spacer flex="1" /> + </hbox> + + <spacer flex="1" /> + +</window> + diff --git a/layout/svg/crashtests/322185-1.svg b/layout/svg/crashtests/322185-1.svg new file mode 100644 index 0000000000..8a10eb34a4 --- /dev/null +++ b/layout/svg/crashtests/322185-1.svg @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <g id="g" style="display: -moz-box; overflow: hidden;"> + <circle /> + </g> +</svg> diff --git a/layout/svg/crashtests/322215-1.svg b/layout/svg/crashtests/322215-1.svg new file mode 100644 index 0000000000..f872fbcd8f --- /dev/null +++ b/layout/svg/crashtests/322215-1.svg @@ -0,0 +1,31 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" +"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- 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/. --> +<!-- +Copyright Georgi Guninski +--> + + +<svg width="100%" height="100%" version="1.1" +xmlns="http://www.w3.org/2000/svg"> + +<defs> +<filter id="MyFilter" filterUnits="userSpaceOnUse" +x="0" y="0" width="32769" height="32769"> + +<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/> + +</filter> +</defs> + +<rect x="1" y="1" width="198" height="118" fill="#cccccc" /> + +<g filter="url(#MyFilter)"> +<text fill="#FFFFFF" stroke="black" font-size="45" +x="42" y="42">Feck b1ll</text> +</g> + +</svg> diff --git a/layout/svg/crashtests/323704-1.svg b/layout/svg/crashtests/323704-1.svg new file mode 100644 index 0000000000..13b8d52243 --- /dev/null +++ b/layout/svg/crashtests/323704-1.svg @@ -0,0 +1,12 @@ +<svg xmlns='http://www.w3.org/2000/svg' +xmlns:xlink='http://www.w3.org/1999/xlink'> + + <clipPath id='clipPath_0'> + <rect x='10' y='10' width='25' height='25' rx='5' ry='5' fill='none' +clip-path='url(#clipPath_0)'/> + </clipPath> + + <rect x='5' y='5' width='35' height='35' fill='red' +clip-path='url(#clipPath_0)'/> + +</svg> diff --git a/layout/svg/crashtests/325427-1.svg b/layout/svg/crashtests/325427-1.svg new file mode 100644 index 0000000000..1f1c645251 --- /dev/null +++ b/layout/svg/crashtests/325427-1.svg @@ -0,0 +1,20 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <g id="module">
+ <use xlink:href="#module" />
+ <use xlink:href="#module" />
+ <use xlink:href="#module" />
+ <use xlink:href="#module" />
+ <use xlink:href="#baseModule" />
+ <use xlink:href="#baseModule" />
+ <use xlink:href="#extendsModule" />
+ <use xlink:href="#extendsModule" />
+ <use xlink:href="#extendsModule" />
+ <use xlink:href="#extendsModule" />
+ <use xlink:href="#extendsModule" />
+ <use xlink:href="#extendsModule" />
+ </g>
+ <use id="baseModule" xlink:href="#module" />
+ <use id="extendsModule" xlink:href="#module" />
+ </defs>
+</svg>
diff --git a/layout/svg/crashtests/326495-1.svg b/layout/svg/crashtests/326495-1.svg new file mode 100644 index 0000000000..a5bf25b62a --- /dev/null +++ b/layout/svg/crashtests/326495-1.svg @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<svg width="10cm" height="5cm" viewBox="0 0 1000 500" + xmlns="http://www.w3.org/2000/svg" + version="1.1"> + + <script> + function init() + { + document.getElementsByTagName("rect")[0].style.display = "-moz-inline-box"; + } + + window.addEventListener("load", init, false); + </script> + + <rect requiredFeatures="http://www.w3.org/TR/SVG11/feature#SVG-nonexistent-feature"/> +</svg> diff --git a/layout/svg/crashtests/326974-1.svg b/layout/svg/crashtests/326974-1.svg new file mode 100644 index 0000000000..750165b730 --- /dev/null +++ b/layout/svg/crashtests/326974-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?>
+ +<svg xmlns="http://www.w3.org/2000/svg">
+ +<script> + +function init() {
var n2 = document.getElementById("n2"); + var n3 = document.getElementById("n3"); + + n2.appendChild(n3); +}
+ +window.addEventListener("load", init, false); + +</script> + +
+<g id="n2">
<text id="n3" /> +</g>
+ +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/327706-1.svg b/layout/svg/crashtests/327706-1.svg new file mode 100644 index 0000000000..9aae909250 --- /dev/null +++ b/layout/svg/crashtests/327706-1.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<svg xmlns="http://www.w3.org/2000/svg"> + +<script> + +document.documentElement.unsuspendRedraw(6) + +</script> +</svg> diff --git a/layout/svg/crashtests/327711-1.svg b/layout/svg/crashtests/327711-1.svg new file mode 100644 index 0000000000..d919b866f6 --- /dev/null +++ b/layout/svg/crashtests/327711-1.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?>
+ +<svg xmlns="http://www.w3.org/2000/svg"> + +<script>
+ +function init() +{
+ document.documentElement.unsuspendRedrawAll(); + document.getElementsByTagName("text")[0].firstChild.data = "Quux"; +}
+ +window.addEventListener("load", init, false); + +</script> + +<text x="125" y="30">Foo</text>
+ +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/328137-1.svg b/layout/svg/crashtests/328137-1.svg new file mode 100644 index 0000000000..26190b1eb0 --- /dev/null +++ b/layout/svg/crashtests/328137-1.svg @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + +<script> + + +function init() +{ + x = document.getElementsByTagName("stop"); + x[0].appendChild(x[1]); +} + + +window.addEventListener("load", init, false); + +</script> + +<radialGradient> + <stop/> + <stop/> +</radialGradient> + +</svg> diff --git a/layout/svg/crashtests/329848-1.svg b/layout/svg/crashtests/329848-1.svg new file mode 100644 index 0000000000..ac4f0022e1 --- /dev/null +++ b/layout/svg/crashtests/329848-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg">
<polygon transform="?" points="100,100 200,100 150,200"/>
</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/337408-1.xhtml b/layout/svg/crashtests/337408-1.xhtml new file mode 100644 index 0000000000..f56c06ec94 --- /dev/null +++ b/layout/svg/crashtests/337408-1.xhtml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=337408 --> +<!-- Just checking for lack of crash, nothing more --> +<window id="svg-in-xul-stack" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:svg="http://www.w3.org/2000/svg" + style="background-color:white;" + screenX="20" + screenY="20" + width="600" + height="400"> + + <stack> + <box flex="1"> + <label value="foo"/> + </box> + <svg:svg> + <svg:rect width="100%" height="100%" fill="red" fill-opacity="0.5"/> + </svg:svg> + </stack> +</window> diff --git a/layout/svg/crashtests/338301-1.xhtml b/layout/svg/crashtests/338301-1.xhtml new file mode 100644 index 0000000000..3367eedebb --- /dev/null +++ b/layout/svg/crashtests/338301-1.xhtml @@ -0,0 +1,13 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="de"> +<body> + + <svg xmlns="http://www.w3.org/2000/svg"> + <defs> + <linearGradient> + <path/> + </linearGradient> + </defs> + </svg> + +</body> +</html> diff --git a/layout/svg/crashtests/338312-1.xhtml b/layout/svg/crashtests/338312-1.xhtml new file mode 100644 index 0000000000..5a751a2ae0 --- /dev/null +++ b/layout/svg/crashtests/338312-1.xhtml @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> + + +function boom() +{ + document.getElementById("foo").appendChild(document.getElementById("bar")); +} + +window.addEventListener("load", boom, false); + +</script> +</head> + +<body> + + <div id="foo"></div> + + <svg xmlns="http://www.w3.org/2000/svg"> + <defs> + <linearGradient id="grad1"/> + </defs> + <rect id="bar" style="fill:url(#grad1);" /> + </svg> + +</body> +</html> diff --git a/layout/svg/crashtests/340083-1.svg b/layout/svg/crashtests/340083-1.svg new file mode 100644 index 0000000000..7f015b6efe --- /dev/null +++ b/layout/svg/crashtests/340083-1.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%"> + <defs> + <title> + <image x="30" y="0" width="190" height="190" xlink:href="../../../testing/crashtest/images/tree.gif"/> + </title> + </defs> +</svg> diff --git a/layout/svg/crashtests/340945-1.svg b/layout/svg/crashtests/340945-1.svg new file mode 100644 index 0000000000..01ac66fb33 --- /dev/null +++ b/layout/svg/crashtests/340945-1.svg @@ -0,0 +1,2 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="display: table;"> +</svg> diff --git a/layout/svg/crashtests/342923-1.html b/layout/svg/crashtests/342923-1.html new file mode 100644 index 0000000000..bed6e89791 --- /dev/null +++ b/layout/svg/crashtests/342923-1.html @@ -0,0 +1,23 @@ +<html> +<head> +<script> + +function boo() +{ + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + rect.setAttribute("stroke", "blue"); + + document.body.appendChild(rect); +} + +</script> +</head> + +<body class="bodytext" onload="boo();"> + +<div id="c1"></div> + +<p>In a debug trunk build from 2006-006-27, loading this page triggers an assertion. (It also triggers a CSS error in the console, but I think that's a known, separate bug.)</p> + +</body> +</html> diff --git a/layout/svg/crashtests/343221-1.xhtml b/layout/svg/crashtests/343221-1.xhtml new file mode 100644 index 0000000000..890b161dfe --- /dev/null +++ b/layout/svg/crashtests/343221-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script> + +function boo() +{ + document.getElementById("c").style.overflow = "hidden"; + document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="setTimeout(boo, 30);"> + +<svg xmlns="http://www.w3.org/2000/svg"> + <circle id="c" cx="50" cy="50" r="20" /> +</svg> + +</body> +</html> diff --git a/layout/svg/crashtests/344749-1.svg b/layout/svg/crashtests/344749-1.svg new file mode 100644 index 0000000000..1a02d7c180 --- /dev/null +++ b/layout/svg/crashtests/344749-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + +<circle cx="6cm" cy="2cm" r="100" fill="red" transform="translate(0,50)" /> +<circle cx="6cm" cy="2cm" r="100" fill="blue" transform="translate(70,150)" /> +<circle cx="6cm" cy="2cm" r="100" fill="green" transform="translate(-70,150)" /> + +<rect id="rect1" fill="url(#pat0)"/> + +<pattern patternUnits="userSpaceOnUse" id="pat0" x="10" y="10" width="20" height="20">
<rect x="5" y="5" width="10" height="10" fill="red" />
<rect x="10" y="10" width="10" height="10" fill="green" />
</pattern>
+
</svg> diff --git a/layout/svg/crashtests/344887-1.svg b/layout/svg/crashtests/344887-1.svg new file mode 100644 index 0000000000..f0bd21c592 --- /dev/null +++ b/layout/svg/crashtests/344887-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml" onload="setTimeout(foo, 30);" class="reftest-wait"> + +<script> + +var SVG_NS = "http://www.w3.org/2000/svg"; + +function foo() +{ + var rect = document.createElementNS(SVG_NS, 'rect'); + rect.setAttribute('opacity', ".3"); + document.documentElement.appendChild(rect); + document.documentElement.removeAttribute("class"); +} + +</script> + + +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/344892-1.svg b/layout/svg/crashtests/344892-1.svg new file mode 100644 index 0000000000..a38d7eb40f --- /dev/null +++ b/layout/svg/crashtests/344892-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<text stroke-width="50%">foo</text> + +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/344898-1.svg b/layout/svg/crashtests/344898-1.svg new file mode 100644 index 0000000000..34c3f45a4e --- /dev/null +++ b/layout/svg/crashtests/344898-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" onload="setTimeout(removeText, 30);" class="reftest-wait"> + + +<script> + +function removeText() +{ + var x = document.getElementById("textPath"); + x.removeChild(x.firstChild); + document.documentElement.removeAttribute("class"); +} + +</script> + + +<text x="30" y="30"><textPath id="textPath">Foo</textPath></text> + + +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/344904-1.svg b/layout/svg/crashtests/344904-1.svg new file mode 100644 index 0000000000..a2c8d07647 --- /dev/null +++ b/layout/svg/crashtests/344904-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" onload="setTimeout(boom, 30);" class="reftest-wait"> + +<script> + +function boom() +{ + document.getElementById("m").setAttribute("stroke-miterlimit", 1); + document.documentElement.removeAttribute("class"); +} + +</script> + + +<marker> + <path id="m" /> +</marker> + + +</svg> diff --git a/layout/svg/crashtests/345418-1.svg b/layout/svg/crashtests/345418-1.svg new file mode 100644 index 0000000000..2cf8b331fa --- /dev/null +++ b/layout/svg/crashtests/345418-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<tspan>arg</tspan> +
</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/348982-1.xhtml b/layout/svg/crashtests/348982-1.xhtml new file mode 100644 index 0000000000..ad0340689a --- /dev/null +++ b/layout/svg/crashtests/348982-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> +<style> +#div, #svg { display: table; } +#g { display: inline; } +</style> +</head> + +<body> + <div id="div"> + <svg id="svg" xmlns="http://www.w3.org/2000/svg"> + <g id="g"> + <circle cx="6.5cm" cy="2cm" r="100" style="fill: blue;" /> + </g> + </svg> + </div> +</body> + +</html> diff --git a/layout/svg/crashtests/354777-1.xhtml b/layout/svg/crashtests/354777-1.xhtml new file mode 100644 index 0000000000..e82baf34c9 --- /dev/null +++ b/layout/svg/crashtests/354777-1.xhtml @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + +<head> +<script> + +var SVG_NS = "http://www.w3.org/2000/svg"; + +function boom() +{ + var svgElem = document.createElementNS(SVG_NS, "svg"); + var ellipse = document.createElementNS(SVG_NS, "ellipse"); + + svgElem.setAttribute("viewBox", "0 0 30 40"); + document.body.appendChild(svgElem); + document.body.appendChild(ellipse); + ellipse.appendChild(svgElem); + svgElem.removeAttribute("viewBox"); +} + +</script> +</head> + +<body onload="boom()"> + +</body> + +</html> + diff --git a/layout/svg/crashtests/359516-1.svg b/layout/svg/crashtests/359516-1.svg new file mode 100644 index 0000000000..c997eb3a85 --- /dev/null +++ b/layout/svg/crashtests/359516-1.svg @@ -0,0 +1,36 @@ +<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ class="reftest-wait"
+ onload="setTimeout(doStuff, 30);">
+
+<html:script style="display: none;" type="text/javascript">
+
+function doStuff()
+{
+ var svg = document.documentElement;
+ var ellipse = document.getElementById("ellipse");
+ var filter = document.getElementById("filter");
+
+ document.addEventListener("DOMNodeRemoved", foopy, false);
+ filter.removeChild(filter.firstChild);
+ document.removeEventListener("DOMNodeRemoved", foopy, false);
+
+ function foopy()
+ {
+ document.removeEventListener("DOMNodeRemoved", foopy, false);
+ svg.appendChild(filter);
+ }
+
+ // Needed for the crash, but not for the assertion.
+ svg.appendChild(ellipse);
+
+ document.documentElement.removeAttribute("class");
+}
+
+</html:script>
+
+<ellipse id="ellipse" cx="200" cy="150" rx="70" ry="40" style="filter: url(#filter);"/>
+
+<filter id="filter"> </filter>
+
+</svg>
diff --git a/layout/svg/crashtests/361015-1.svg b/layout/svg/crashtests/361015-1.svg new file mode 100644 index 0000000000..8ac4bc56f2 --- /dev/null +++ b/layout/svg/crashtests/361015-1.svg @@ -0,0 +1,33 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + class="reftest-wait" + onload="setTimeout(boom, 30)"> + +<html:script> +<![CDATA[ + +function boom() +{ + var grad = document.getElementById("grad"); + var g = document.getElementById("g"); + grad.appendChild(g); + g.removeAttribute("transform"); + document.documentElement.removeAttribute("class"); +} + +]]> +</html:script> + + + <g id="g" transform="translate(500,0)"> + <text x="25" y="85">Foo</text> + </g> + + + <linearGradient id="grad" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1"> + <stop stop-color="blue" offset="0.2"/> + <stop stop-color="lime" offset="0.4"/> + </linearGradient> + + +</svg> diff --git a/layout/svg/crashtests/361587-1.svg b/layout/svg/crashtests/361587-1.svg new file mode 100644 index 0000000000..52bce9eda7 --- /dev/null +++ b/layout/svg/crashtests/361587-1.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" + onload="setTimeout(boom, 30);" + class="reftest-wait"> + +<script style="display: none" type="text/javascript"> +<![CDATA[ + +function boom() +{ + var oldGrad = document.getElementById("grad"); + oldGrad.parentNode.removeChild(oldGrad); + + var newGrad = document.createElementNS("http://www.w3.org/2000/svg", "radialGradient"); + newGrad.setAttribute("gradientUnits", "userSpaceOnUse"); + newGrad.setAttribute("id", "grad"); + + document.documentElement.appendChild(newGrad); + + document.documentElement.removeAttribute("class"); +} + +]]> +</script> + + <radialGradient id="grad" gradientUnits="userSpaceOnUse" cx="240" cy="210" r="220" fx="240" fy="210"> + <stop stop-color="yellow" offset="0"/> + <stop stop-color="green" offset="1"/> + </radialGradient> + <rect x="20" y="150" width="440" height="80" fill="url(#grad)" stroke-width="40"/> + +</svg> diff --git a/layout/svg/crashtests/363611-1.xhtml b/layout/svg/crashtests/363611-1.xhtml new file mode 100644 index 0000000000..6bc386bcdf --- /dev/null +++ b/layout/svg/crashtests/363611-1.xhtml @@ -0,0 +1,21 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + +<script style="display: none" type="text/javascript"> + +function boom() +{ + var fo = document.createElementNS("http://www.w3.org/2000/svg", 'foreignObject'); + document.getElementById("innerSVG").appendChild(fo); +} + +</script> + +</head> + +<body onload="boom();"> + +<svg xmlns="http://www.w3.org/2000/svg" ><linearGradient><svg id="innerSVG"></svg></linearGradient></svg> + +</body> +</html> diff --git a/layout/svg/crashtests/364688-1.svg b/layout/svg/crashtests/364688-1.svg new file mode 100644 index 0000000000..045061cd2f --- /dev/null +++ b/layout/svg/crashtests/364688-1.svg @@ -0,0 +1,34 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="setTimeout(boom, 30);" + class="reftest-wait"> + +<script> +function boom() +{ + document.getElementById("sss").removeAttribute('value'); + + document.documentElement.removeAttribute("class"); +} +</script> + + +<foreignObject width="500" height="500" y="300"> + +<div xmlns="http://www.w3.org/1999/xhtml"> + +<table border="1"> + <tr> + <td>Foo</td> + </tr> + <tr> + <td><input type="text" value="Baz" id="sss" /></td> + </tr> +</table> + +</div> + +</foreignObject> + + +</svg> diff --git a/layout/svg/crashtests/366956-1.svg b/layout/svg/crashtests/366956-1.svg new file mode 100644 index 0000000000..9836c7ea3a --- /dev/null +++ b/layout/svg/crashtests/366956-1.svg @@ -0,0 +1,61 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml" onload="setTimeout(boom1, 50);" class="reftest-wait"> + +<html:script> + +function boom1() +{ + document.getElementsByTagName("mi")[0].setAttribute('id', "ffff"); + + document.getElementById("fo").appendChild(document.createTextNode(" ")); + + setTimeout(boom2, 50); +} + +function boom2() +{ + var fodiv = document.getElementById("fodiv"); + fodiv.parentNode.removeChild(fodiv); + + document.documentElement.removeAttribute("class"); +} + +</html:script> + + + <g> + <foreignObject width="500" height="500" transform="scale(.7,.7)" id="fo" y="300"> + +<div id="fodiv" xmlns="http://www.w3.org/1999/xhtml"> + + + +<p>0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99</p> + + + +<div><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> +<mrow> + <mi>A</mi> +</mrow> +</math></div> + + +<svg xmlns="http://www.w3.org/2000/svg" id="svg" viewbox="0 0 250 250" width="100" height="100"> + <style type="text/css"> + circle:hover {fill-opacity:0.9;} + </style> + + <g style="fill-opacity:0.7;" transform="scale(.2)"> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:red; stroke:black; stroke-width:0.1cm" transform="translate(0,50)" /> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:blue; stroke:black; stroke-width:0.1cm" transform="translate(70,150)" /> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:green; stroke:black; stroke-width:0.1cm" transform="translate(-70,150)"/> + </g> +</svg> + +</div> +</foreignObject> +</g> + + + +</svg> diff --git a/layout/svg/crashtests/366956-2.svg b/layout/svg/crashtests/366956-2.svg new file mode 100644 index 0000000000..a2ab21ed55 --- /dev/null +++ b/layout/svg/crashtests/366956-2.svg @@ -0,0 +1,61 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml" onload="setTimeout(boom1, 30);" class="reftest-wait"> + +<html:script> + +function boom1() +{ + document.getElementsByTagName("mi")[0].setAttribute('id', "ffff"); + + document.getElementById("fo").appendChild(document.createTextNode(" ")); + + boom2(); +} + +function boom2() +{ + var fodiv = document.getElementById("fodiv"); + fodiv.parentNode.removeChild(fodiv); + + document.documentElement.removeAttribute("class"); +} + +</html:script> + + + <g> + <foreignObject width="500" height="500" transform="scale(.7,.7)" id="fo" y="300"> + +<div id="fodiv" xmlns="http://www.w3.org/1999/xhtml"> + + + +<p>0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 990 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99</p> + + + +<div><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> +<mrow> + <mi>A</mi> +</mrow> +</math></div> + + +<svg xmlns="http://www.w3.org/2000/svg" id="svg" viewbox="0 0 250 250" width="100" height="100"> + <style type="text/css"> + circle:hover {fill-opacity:0.9;} + </style> + + <g style="fill-opacity:0.7;" transform="scale(.2)"> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:red; stroke:black; stroke-width:0.1cm" transform="translate(0,50)" /> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:blue; stroke:black; stroke-width:0.1cm" transform="translate(70,150)" /> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:green; stroke:black; stroke-width:0.1cm" transform="translate(-70,150)"/> + </g> +</svg> + +</div> +</foreignObject> +</g> + + + +</svg> diff --git a/layout/svg/crashtests/367111-1.svg b/layout/svg/crashtests/367111-1.svg new file mode 100644 index 0000000000..dcf6a39bf7 --- /dev/null +++ b/layout/svg/crashtests/367111-1.svg @@ -0,0 +1,29 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="setTimeout(boom, 30);" + class="reftest-wait"> + +<html:script> + +function boom() +{ + document.getElementById("text").appendChild(document.getElementById("fo")); + + document.documentElement.removeAttribute("class"); +} + +</html:script> + +<defs> + <marker> + <text id="text">svg:text</text> + </marker> +</defs> + +<foreignObject id="fo"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <p>HTML in a foreignObject</p> + </div> +</foreignObject> + +</svg> diff --git a/layout/svg/crashtests/367368-1.xhtml b/layout/svg/crashtests/367368-1.xhtml new file mode 100644 index 0000000000..b9bcd3241b --- /dev/null +++ b/layout/svg/crashtests/367368-1.xhtml @@ -0,0 +1,12 @@ +<!-- From https://bugzilla.mozilla.org/show_bug.cgi?id=367368 --> +<!-- Just checking for crash, nothing more --> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <body> + + <svg xmlns="http://www.w3.org/2000/svg"> + <circle cx="6.5cm" cy="2cm" r="100" style="fill:red; stroke:black;" transform="translate(0,50)" /> + </svg> + + </body> +</html> diff --git a/layout/svg/crashtests/369233-1.svg b/layout/svg/crashtests/369233-1.svg new file mode 100644 index 0000000000..22f4aacb37 --- /dev/null +++ b/layout/svg/crashtests/369233-1.svg @@ -0,0 +1,33 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:html="http://www.w3.org/1999/xhtml" + onload="setTimeout(boom, 200);" + class="reftest-wait"> + +<html:script> + +function boom() +{ + try { + document.getElementById("grad2").gradientUnits.baseVal = "y"; + } catch (e) { + } + + document.documentElement.removeAttribute("class"); +} + +</html:script> + + + +<radialGradient id="grad2" gradientUnits="userSpaceOnUse" cx="240" cy="210" r="220" fx="240" fy="210"> + <stop stop-color="black" offset="0"/> + <stop stop-color="yellow" offset="0.2"/> + <stop stop-color="red" offset="0.4"/> + <stop stop-color="blue" offset="0.6"/> + <stop stop-color="white" offset="0.8"/> + <stop stop-color="green" offset="1"/> +</radialGradient> + +<rect x="20" y="150" width="440" height="80" fill="url(#grad2)" stroke-width="40"/> + +</svg> diff --git a/layout/svg/crashtests/369438-1.svg b/layout/svg/crashtests/369438-1.svg new file mode 100644 index 0000000000..78bcb6b54d --- /dev/null +++ b/layout/svg/crashtests/369438-1.svg @@ -0,0 +1,24 @@ +<svg width="100%" height="100%" version="1.1"
+xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml" onload="setTimeout(boom, 30);" class="reftest-wait"><html:script src="data:text/javascript,"></html:script><html:script>
+
+function boom()
+{
+ var defs = document.getElementById("defs");
+ defs.parentNode.removeChild(defs);
+
+ document.documentElement.removeAttribute("class");
+}
+
+</html:script>
+
+<defs id="defs">
+<filter id="Gaussian_Blur">
+<feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
+</filter>
+</defs>
+
+<ellipse cx="200" cy="150" rx="70" ry="40"
+style="fill:#ff0000;stroke:#000000;
+stroke-width:2;filter:url(#Gaussian_Blur)"/>
+
+</svg>
diff --git a/layout/svg/crashtests/369438-2.svg b/layout/svg/crashtests/369438-2.svg new file mode 100644 index 0000000000..92eea9ee0f --- /dev/null +++ b/layout/svg/crashtests/369438-2.svg @@ -0,0 +1,27 @@ +<svg width="100%" height="100%" version="1.1"
+xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml" onload="setTimeout(boom, 30);" class="reftest-wait"><html:script>
+
+function boom()
+{
+ var defs = document.getElementById("defs");
+ var gb = document.getElementById("Gaussian_Blur");
+
+ defs.parentNode.removeChild(defs);
+ gb.removeChild(gb.firstChild); // remove a whitespace text node (!)
+
+ document.documentElement.removeAttribute("class");
+}
+
+</html:script>
+
+<defs id="defs">
+<filter id="Gaussian_Blur">
+<feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
+</filter>
+</defs>
+
+<ellipse cx="200" cy="150" rx="70" ry="40"
+style="fill:#ff0000;stroke:#000000;
+stroke-width:2;filter:url(#Gaussian_Blur)"/>
+
+</svg>
diff --git a/layout/svg/crashtests/371463-1.xhtml b/layout/svg/crashtests/371463-1.xhtml new file mode 100644 index 0000000000..461fb27ba3 --- /dev/null +++ b/layout/svg/crashtests/371463-1.xhtml @@ -0,0 +1,8 @@ +<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+<body>
+
+<select><svg:svg><svg:foreignObject/></svg:svg></select>
+
+</body>
+</html>
diff --git a/layout/svg/crashtests/371563-1.xhtml b/layout/svg/crashtests/371563-1.xhtml new file mode 100644 index 0000000000..0ebdc9bfa1 --- /dev/null +++ b/layout/svg/crashtests/371563-1.xhtml @@ -0,0 +1,32 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script> + +function boom() +{ + document.getElementById("sdiv").style.overflow = "scroll"; + + document.documentElement.removeAttribute("class"); +} + + +</script> +</head> + +<body onload="setTimeout(boom, 30);"> + + <div id="sdiv" style="float: left;"> + + <svg xmlns="http://www.w3.org/2000/svg" height="400px" width="400px" + y="0.0000000" x="0.0000000" version="1.0" > + <defs> + <marker id="Arrow"/> + </defs> + <path style="marker-end:url(#Arrow)" + d="M 12.500000,200.00000 L 387.50000,200.00000" /> + </svg> + + </div> + +</body> +</html> diff --git a/layout/svg/crashtests/375775-1.svg b/layout/svg/crashtests/375775-1.svg new file mode 100644 index 0000000000..cd17c85a94 --- /dev/null +++ b/layout/svg/crashtests/375775-1.svg @@ -0,0 +1,23 @@ +<svg xmlns="http://www.w3.org/2000/svg" onload="setTimeout(boom, 30);" class="reftest-wait"> + +<script type="text/javascript"> + +function boom() +{ + document.getElementById("filter").style.display = "none"; + document.getElementById("path").style.display = "none"; + + document.documentElement.removeAttribute("class"); +} + +</script> + +<filter id="filter" filterUnits="userSpaceOnUse" x="0" y="0" width="200" height="120" /> + +<g filter="url(#filter)"> + <path id="path" + fill="black" + d="M60,80 C30,80 30,40 60,40 L140,40 C170,40 170,80 140,80 z" /> +</g> + +</svg> diff --git a/layout/svg/crashtests/378716.svg b/layout/svg/crashtests/378716.svg new file mode 100644 index 0000000000..b6faa00284 --- /dev/null +++ b/layout/svg/crashtests/378716.svg @@ -0,0 +1,4 @@ +<svg width="100%" height="100%" x="0" y="0" viewBox="0 0 1 1" + xmlns="http://www.w3.org/2000/svg"> + <text id="text_1" x="0.5" y="0.5" font-size="0.05" fill="green">Okay Text</text> +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/380691-1.svg b/layout/svg/crashtests/380691-1.svg new file mode 100644 index 0000000000..ed28552633 --- /dev/null +++ b/layout/svg/crashtests/380691-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <mask id="m"/> + <foreignObject mask="url(#m)"/> +</svg> diff --git a/layout/svg/crashtests/384391-1.xhtml b/layout/svg/crashtests/384391-1.xhtml new file mode 100644 index 0000000000..12c657a48c --- /dev/null +++ b/layout/svg/crashtests/384391-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg" > +<head> +<script> + +function boom() +{ + var circle = document.getElementById("circle"); + document.removeChild(document.documentElement); + document.appendChild(circle); +} + +</script> +</head> + +<body onload="boom()"> + +<svg:circle id="circle" /> + +</body> +</html> diff --git a/layout/svg/crashtests/384499-1.svg b/layout/svg/crashtests/384499-1.svg new file mode 100644 index 0000000000..f448910008 --- /dev/null +++ b/layout/svg/crashtests/384499-1.svg @@ -0,0 +1,20 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:html="http://www.w3.org/1999/xhtml"> + +<html:style> + #mathy { display: table} +</html:style> + +<foreignObject width="500" height="500" y="50"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <p>Foo</p> + <div> + <math xmlns="http://www.w3.org/1998/Math/MathML" id="mathy" display="block"> + <mrow> + <mi>x</mi> + </mrow> + </math> + </div> + </div> +</foreignObject> + +</svg> diff --git a/layout/svg/crashtests/384637-1.svg b/layout/svg/crashtests/384637-1.svg new file mode 100644 index 0000000000..263a2d556a --- /dev/null +++ b/layout/svg/crashtests/384637-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" baseProfile="basic" width="100%" height="100%" viewBox="0 0 480 360">
+
+ <mask id="mask1" maskUnits="userSpaceOnUse" x="60" y="50" width="100" height="60">
+ <rect x="60" y="50" width="100" height="60" fill="yellow" mask="url(#mask1)"/>
+ </mask>
+
+ <rect x="60" y="50" width="100" height="60" fill="lime" mask="url(#mask1)"/>
+
+</svg>
diff --git a/layout/svg/crashtests/384728-1.svg b/layout/svg/crashtests/384728-1.svg new file mode 100644 index 0000000000..ccafc83706 --- /dev/null +++ b/layout/svg/crashtests/384728-1.svg @@ -0,0 +1,21 @@ +<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" onload="boom();">
+
+<script>
+
+function boom()
+{
+ document.getElementById("thhh").setAttributeNS("http://www.w3.org/1999/xlink", 'href', '');
+}
+
+</script>
+
+ <defs>
+ <g id="ch" style="counter-reset: c;">
+ <rect x="75" y="0" width="75" height="75" fill="lightgreen" style="counter-increment: c;"/>
+ </g>
+
+ </defs>
+
+ <use id="thhh" x="0" y="0"><use xlink:href="#ch" x="0" y="0"/></use>
+
+</svg>
diff --git a/layout/svg/crashtests/385246-1.svg b/layout/svg/crashtests/385246-1.svg new file mode 100644 index 0000000000..cddad0c5e4 --- /dev/null +++ b/layout/svg/crashtests/385246-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<foreignObject x="100" y="100" width="-2" height="500"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <p>Foo</p> + </div> +</foreignObject> + +</svg> diff --git a/layout/svg/crashtests/385246-2.svg b/layout/svg/crashtests/385246-2.svg new file mode 100644 index 0000000000..c392f2fc8a --- /dev/null +++ b/layout/svg/crashtests/385246-2.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + +<script type="text/javascript" xlink:href="data:text/javascript,"></script> + + +<foreignObject width="-2" height="500" id="fo" x="300"> + <div xmlns="http://www.w3.org/1999/xhtml"> + <p>Hi!!!</p> + </div> +</foreignObject> + + + + +</svg> diff --git a/layout/svg/crashtests/385552-1.svg b/layout/svg/crashtests/385552-1.svg new file mode 100644 index 0000000000..019e249d77 --- /dev/null +++ b/layout/svg/crashtests/385552-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<script> +document.createElementNS("http://www.w3.org/2000/svg", "svg").unsuspendRedrawAll(); +</script> diff --git a/layout/svg/crashtests/385552-2.svg b/layout/svg/crashtests/385552-2.svg new file mode 100644 index 0000000000..9a93d657fb --- /dev/null +++ b/layout/svg/crashtests/385552-2.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<script> +document.createElementNS("http://www.w3.org/2000/svg", "svg").suspendRedraw(3); +</script> diff --git a/layout/svg/crashtests/385840-1.svg b/layout/svg/crashtests/385840-1.svg new file mode 100644 index 0000000000..cf7ff6949c --- /dev/null +++ b/layout/svg/crashtests/385840-1.svg @@ -0,0 +1,20 @@ +<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" onload="boom();">
+
+<script>
+
+function boom()
+{
+ var SVG_NS = "http://www.w3.org/2000/svg";
+
+ var svgCircle = document.createElementNS(SVG_NS, 'circle');
+ var svgText = document.createElementNS(SVG_NS, 'text');
+ svgText.appendChild(document.createTextNode("foo"));
+ svgCircle.appendChild(svgText);
+
+ document.removeChild(document.documentElement);
+ document.appendChild(svgCircle);
+}
+
+</script>
+
+</svg>
diff --git a/layout/svg/crashtests/385852-1.svg b/layout/svg/crashtests/385852-1.svg new file mode 100644 index 0000000000..17ad99ca8f --- /dev/null +++ b/layout/svg/crashtests/385852-1.svg @@ -0,0 +1,34 @@ +<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" onload="setTimeout(boom, 30)" class="reftest-wait">
+
+<script>
+
+var originalRoot = document.documentElement;
+var svgCircle;
+
+function boom()
+{
+ var SVG_NS = "http://www.w3.org/2000/svg";
+
+ var svgPolyline = document.createElementNS(SVG_NS, 'polyline');
+ svgCircle = document.createElementNS(SVG_NS, 'circle');
+
+ svgCircle.appendChild(svgPolyline);
+
+ document.removeChild(originalRoot);
+ document.appendChild(svgCircle);
+
+ setTimeout(restore, 30);
+}
+
+function restore()
+{
+ // We have to put it the root element back in the document so that reftest.js
+ // sees the event for the removal of class="reftest-wait"!
+ document.removeChild(svgCircle);
+ document.appendChild(originalRoot);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+
+</svg>
diff --git a/layout/svg/crashtests/386475-1.xhtml b/layout/svg/crashtests/386475-1.xhtml new file mode 100644 index 0000000000..4d1b9a2808 --- /dev/null +++ b/layout/svg/crashtests/386475-1.xhtml @@ -0,0 +1,24 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> +<head> +<script> +function boom() +{ + document.body.style.display = "table-header-group"; + document.getElementById("svg").setAttribute('height', 1); +} +</script> +</head> + +<body onload="boom();"> + +<svg:svg width="100%" height="100%" id="svg"> + <svg:g> + <svg:foreignObject width="8205em" height="100%"> + <span>hello</span> <span>world</span> + </svg:foreignObject> + </svg:g> +</svg:svg> + +</body> +</html> diff --git a/layout/svg/crashtests/386690-1.svg b/layout/svg/crashtests/386690-1.svg new file mode 100644 index 0000000000..e206978134 --- /dev/null +++ b/layout/svg/crashtests/386690-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 40"> + <foreignObject width="-2" height="100" /> +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/387290-1.svg b/layout/svg/crashtests/387290-1.svg new file mode 100644 index 0000000000..4ac8463204 --- /dev/null +++ b/layout/svg/crashtests/387290-1.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- 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/. --> +<!-- +Copyright Georgi Guninski +--> + +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" +"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> + +<svg version="1.1" +xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" +> +<defs> +<filter id="dafilter" filterUnits="userSpaceOnUse" +x="0" y="0" width="4194305" height="17"> +<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/> +</filter> +</defs> +<g> + +<rect fill="red" width="256" height="256" filter="url(#dafilter)" transform="scale(262145,9)" /> +</g> + +</svg> diff --git a/layout/svg/crashtests/402408-1.svg b/layout/svg/crashtests/402408-1.svg new file mode 100644 index 0000000000..f442b2171e --- /dev/null +++ b/layout/svg/crashtests/402408-1.svg @@ -0,0 +1,32 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + onload="boom();" + class="reftest-wait"> + +<script> + +function boom() +{ + var grad1 = document.getElementById("grad1"); + var grad2 = document.getElementById("grad2"); + + grad1.appendChild(grad2); + + setTimeout(function() { + grad1.removeChild(grad2); + document.documentElement.removeAttribute("class"); + }, 30); +} + +</script> + +<linearGradient id="grad1" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="0"> + <stop id="green" stop-color="#00dd00" offset="0"/> + <stop id="blue" stop-color="#0000dd" offset="1"/> +</linearGradient> + +<linearGradient id="grad2" xlink:href="#grad1"/> + +<rect x="20" y="20" width="440" height="80" fill="url(#grad2)" /> + +</svg> diff --git a/layout/svg/crashtests/404677-1.xhtml b/layout/svg/crashtests/404677-1.xhtml new file mode 100644 index 0000000000..c1df3869b9 --- /dev/null +++ b/layout/svg/crashtests/404677-1.xhtml @@ -0,0 +1,9 @@ +<html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg"> +<head> +</head> +<body> + +<svg:svg height="-2" width="5" /> + +</body> +</html> diff --git a/layout/svg/crashtests/409565-1.xhtml b/layout/svg/crashtests/409565-1.xhtml new file mode 100644 index 0000000000..2c427ccc8b --- /dev/null +++ b/layout/svg/crashtests/409565-1.xhtml @@ -0,0 +1,3 @@ +<html xmlns="http://www.w3.org/1999/xhtml" style="white-space: pre;"><body style="width: 24px; height: 24px; column-width: 200px;"> + + <svg xmlns="http://www.w3.org/2000/svg" style="float: left;"></svg></body></html> diff --git a/layout/svg/crashtests/420697-1.svg b/layout/svg/crashtests/420697-1.svg new file mode 100644 index 0000000000..d8b7f38340 --- /dev/null +++ b/layout/svg/crashtests/420697-1.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text stroke="black" y="1em" + stroke-dashoffset="1%" + stroke-dasharray="1px"> + m + </text> +</svg> diff --git a/layout/svg/crashtests/420697-2.svg b/layout/svg/crashtests/420697-2.svg new file mode 100644 index 0000000000..8987693e50 --- /dev/null +++ b/layout/svg/crashtests/420697-2.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text stroke="black" y="1em" + stroke-dasharray="1%"> + m + </text> +</svg> diff --git a/layout/svg/crashtests/429774-1.svg b/layout/svg/crashtests/429774-1.svg new file mode 100644 index 0000000000..00b726de6a --- /dev/null +++ b/layout/svg/crashtests/429774-1.svg @@ -0,0 +1,29 @@ +<?xml version="1.0"?> + +<svg width="7.5cm" height="5cm" viewBox="0 0 200 120" + xmlns="http://www.w3.org/2000/svg"> + + <defs> + <filter id="MyFilter" filterUnits="userSpaceOnUse" x="0" y="0" width="200" height="120"> + + <feOffset in="SourceAlpha" result="offset" dx="4" dy="4" y="76"/> + + <feSpecularLighting in="offset" result="specOut" + surfaceScale="5" specularConstant=".75" specularExponent="20"> + <fePointLight x="-5000" y="-10000" z="20000"/> + </feSpecularLighting> + + <feComposite in="SourceAlpha" in2="SourceAlpha" result="litPaint" + operator="arithmetic" k1="0" k2="1" k3="1" k4="0"/> + + <feMerge> + <feMergeNode in="offset"/> + <feMergeNode in="litPaint"/> + </feMerge> + + </filter> + </defs> + + <g filter="url(#MyFilter)"/> + +</svg> diff --git a/layout/svg/crashtests/441368-1.svg b/layout/svg/crashtests/441368-1.svg new file mode 100644 index 0000000000..d0fee7478b --- /dev/null +++ b/layout/svg/crashtests/441368-1.svg @@ -0,0 +1,31 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" +"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<!-- 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/. --> +<!-- +Copyright Georgi Guninski +--> + + +<svg width="100%" height="100%" version="1.1" +xmlns="http://www.w3.org/2000/svg"> + +<defs> +<filter id="MyFilter" filterUnits="userSpaceOnUse" +x="0" y="0" width="32769" height="32769"> + +<feGaussianBlur in="SourceAlpha" stdDeviation="2147483648" result="blur"/> + +</filter> +</defs> + +<rect x="1" y="1" width="198" height="118" fill="#cccccc" /> + +<g filter="url(#MyFilter)"> +<text fill="#FFFFFF" stroke="black" font-size="45" +x="42" y="42">Feck b1ll</text> +</g> + +</svg> diff --git a/layout/svg/crashtests/453754-1.svg b/layout/svg/crashtests/453754-1.svg new file mode 100644 index 0000000000..a32d819281 --- /dev/null +++ b/layout/svg/crashtests/453754-1.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <filter id="f" height="-1"/> + + <rect filter="url(#f)" /> + +</svg> diff --git a/layout/svg/crashtests/455314-1.xhtml b/layout/svg/crashtests/455314-1.xhtml new file mode 100644 index 0000000000..01bb33d653 --- /dev/null +++ b/layout/svg/crashtests/455314-1.xhtml @@ -0,0 +1,16 @@ +<html xmlns="http://www.w3.org/1999/xhtml"><head>
+<script>
+function doe() {
+document.getElementById('a').appendChild(document.body);
+}
+setTimeout(doe, 100);
+</script>
+</head>
+<body>
+<div style="position: absolute; -moz-appearance: button; filter: url(#b); "></div>
+<pre style="position: absolute;">
+<table id="b"></table>
+</pre>
+</body>
+<div id="a"/>
+</html>
\ No newline at end of file diff --git a/layout/svg/crashtests/458453.html b/layout/svg/crashtests/458453.html new file mode 100644 index 0000000000..ab72d46dee --- /dev/null +++ b/layout/svg/crashtests/458453.html @@ -0,0 +1,24 @@ +<html class="reftest-wait"> +<head> +<script type="text/javascript"> + +var i = 0; + +function bouncy() +{ + var body = document.body; + document.documentElement.removeChild(body); + document.documentElement.appendChild(body); + + if (++i < 30) + setTimeout(bouncy, 1); + else + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body onload="bouncy();"><span id="a"></span><span style="filter: url(#a);"><span style="filter: url(#a);">B</span></span></body> + +</html> diff --git a/layout/svg/crashtests/459666-1.html b/layout/svg/crashtests/459666-1.html new file mode 100644 index 0000000000..69074b6028 --- /dev/null +++ b/layout/svg/crashtests/459666-1.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html style="filter: url(#e);"> +<head></head> +<body onload="document.documentElement.style.counterReset = 'a';"> +<div id="e"></div> +</body> +</html> diff --git a/layout/svg/crashtests/459883.xhtml b/layout/svg/crashtests/459883.xhtml new file mode 100644 index 0000000000..e125e71d8a --- /dev/null +++ b/layout/svg/crashtests/459883.xhtml @@ -0,0 +1,13 @@ +<html xmlns="http://www.w3.org/1999/xhtml" style="filter: url(#r);" class="reftest-wait"><head> +<script type="text/javascript"> + +function boom() +{ + document.getElementById("s").setAttribute("style", "display: -moz-box;"); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", function() { setTimeout(boom, 0); }, false); + +</script> +</head><body><ms xmlns="http://www.w3.org/1998/Math/MathML" id="s"><maction id="r"/></ms></body></html> diff --git a/layout/svg/crashtests/461289-1.svg b/layout/svg/crashtests/461289-1.svg new file mode 100644 index 0000000000..82a57f81b0 --- /dev/null +++ b/layout/svg/crashtests/461289-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + +<script type="text/javascript"> + +function boom() +{ + var f = document.getElementById("filter1"); + f.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "feImage")); + f.appendChild(document.getElementById("rect")); +} + +window.addEventListener("load", boom, false); + +</script> + +<filter id="filter1"/><rect id="rect" filter="url(#filter1)"/> + +</svg> diff --git a/layout/svg/crashtests/464374-1.svg b/layout/svg/crashtests/464374-1.svg new file mode 100644 index 0000000000..9844e5187f --- /dev/null +++ b/layout/svg/crashtests/464374-1.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" onload="boom();">
+
+<script type="text/javascript">
+
+function boom()
+{
+ document.getElementById("b").appendChild(document.createElementNS("http://www.w3.org/1998/Math/MathML", "math"));
+ document.getElementById("defs").setAttribute("filter", "url(#a)");
+}
+
+</script>
+
+<defs id="defs"><filter id="a"/><g id="b"><rect/></g></defs>
+
+</svg>
diff --git a/layout/svg/crashtests/466585-1.svg b/layout/svg/crashtests/466585-1.svg new file mode 100644 index 0000000000..22ad862e15 --- /dev/null +++ b/layout/svg/crashtests/466585-1.svg @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<script type="text/javascript"> + +window.addEventListener("load", boom, false); + +function boom() +{ + document.getElementById("rect").setAttribute("filter", "url(#filter)"); + document.getElementById("defs").setAttribute("fill", "red"); +} + +</script> + +<defs id="defs"><filter id="filter"/><rect id="rect"/></defs> + +</svg> diff --git a/layout/svg/crashtests/467323-1.svg b/layout/svg/crashtests/467323-1.svg new file mode 100644 index 0000000000..9d757c349d --- /dev/null +++ b/layout/svg/crashtests/467323-1.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"> + +<filter id="f1" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse"> + <feFlood flood-color="#ff0000" result="flood" x="0" y="0" width="100" height="100"/> + <feDisplacementMap style="color-interpolation-filters:sRGB" + in="SourceGraphic" in2="flood" scale="100" xChannelSelector="R" yChannelSelector="G"/> +</filter> +<g filter="url(#f1)"></g> + +</svg> diff --git a/layout/svg/crashtests/467498-1.svg b/layout/svg/crashtests/467498-1.svg new file mode 100644 index 0000000000..9839e6c30d --- /dev/null +++ b/layout/svg/crashtests/467498-1.svg @@ -0,0 +1,12 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <use id="a" width="100" height="100" xlink:href="#b"/> + <use id="b" x="100" y="100" width="100" height="100" xlink:href="#a"/> + <script> + document.getElementById("a").setAttribute("width", "200"); + </script> +</svg> diff --git a/layout/svg/crashtests/470124-1.svg b/layout/svg/crashtests/470124-1.svg new file mode 100644 index 0000000000..ba3b8aff40 --- /dev/null +++ b/layout/svg/crashtests/470124-1.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<filter id="f7" filterUnits="userSpaceOnUse" primitiveUnits="objectBoundingBox"><feComposite/></filter> + +<g filter="url(#f7)"/> + +</svg> diff --git a/layout/svg/crashtests/472782-1.svg b/layout/svg/crashtests/472782-1.svg new file mode 100644 index 0000000000..7cfeb11a69 --- /dev/null +++ b/layout/svg/crashtests/472782-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="r"> +<text><textPath xlink:href="#r">S</textPath> </text> +</svg> diff --git a/layout/svg/crashtests/474700-1.svg b/layout/svg/crashtests/474700-1.svg new file mode 100644 index 0000000000..141a1b3903 --- /dev/null +++ b/layout/svg/crashtests/474700-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"><filter id="f1" height="-2"/><rect width="50" height="100" filter="url(#f1)"/></svg> diff --git a/layout/svg/crashtests/475181-1.svg b/layout/svg/crashtests/475181-1.svg new file mode 100644 index 0000000000..ef5a638afb --- /dev/null +++ b/layout/svg/crashtests/475181-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><filter id="filter1"><feImage y="4095"/><feTile/></filter><rect width="100%" height="100%" filter="url(#filter1)"/></svg> diff --git a/layout/svg/crashtests/475193-1.html b/layout/svg/crashtests/475193-1.html new file mode 100644 index 0000000000..edc08bcee4 --- /dev/null +++ b/layout/svg/crashtests/475193-1.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html><head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> + + +<style type="text/css"> + +.p { marker: url('#c'); } + +</style> +<script type="text/javascript"> + +function boom() +{ + document.getElementById("a").setAttribute("class", "p"); + document.documentElement.offsetHeight; + document.getElementById("b").setAttribute("id", "c"); +} + +</script> +</head><body onload="boom();"><div class="p" id="a">C</div><div id="c"></div></body></html>
\ No newline at end of file diff --git a/layout/svg/crashtests/475302-1.svg b/layout/svg/crashtests/475302-1.svg new file mode 100644 index 0000000000..4fdaa2213c --- /dev/null +++ b/layout/svg/crashtests/475302-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<style type="text/css"> + tref { filter: url(#filter1); } +</style> + +<filter id="filter1"><feFlood/></filter> + +<tref><polyline points="350,75 379,161 469,161 397,215 423,301 350,250 277,301 303,215 231,161 321,161"/></tref> + +</svg> diff --git a/layout/svg/crashtests/477935-1.html b/layout/svg/crashtests/477935-1.html new file mode 100644 index 0000000000..9c2ac5438a --- /dev/null +++ b/layout/svg/crashtests/477935-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head> +<style type="text/css"> :root { filter: url('#g'); } </style> +<style type="text/css" id="ccs"> .cc { content: 'X'; } </style> +</head> +<body onload="document.getElementById('ccs').disabled = true;"> +<div id="g"></div> +<div class="cc">5</div> +</body> +</html> diff --git a/layout/svg/crashtests/478128-1.svg b/layout/svg/crashtests/478128-1.svg new file mode 100644 index 0000000000..a34552776a --- /dev/null +++ b/layout/svg/crashtests/478128-1.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <script type="text/javascript"> + + function boom() + { + document.documentElement.style.columnCount = '15'; + } + window.onload = function() { setTimeout(boom, 20); }; + + </script> + + <foreignObject width="50" height="50" filter="url(#f1)"/> + + <filter id="f1"/> +</svg> diff --git a/layout/svg/crashtests/478511-1.svg b/layout/svg/crashtests/478511-1.svg new file mode 100644 index 0000000000..75a4aaa9b2 --- /dev/null +++ b/layout/svg/crashtests/478511-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.0"> + <defs> + <pattern id="pattern" + x="0" y="0" width="200" height="200"> + <circle fill="lime" r="100" cx="100" cy="100"/> + </pattern> + </defs> + <rect width="200" height="200" fill="url(#pattern)"/> +</svg> diff --git a/layout/svg/crashtests/483439-1.svg b/layout/svg/crashtests/483439-1.svg new file mode 100644 index 0000000000..c9e9ebae1c --- /dev/null +++ b/layout/svg/crashtests/483439-1.svg @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + + <defs> + <path id="myTextPath" + d="M275,20 + a1,1 0 0,0 100,0 + " + /> + </defs> + + <svg y="15"> + <text x="10" y="100" style="stroke: #000000;"> + <textPath xlink:href="#myTextPath" >Text along a curved path...</textPath> + </text> + </svg> +</svg> diff --git a/layout/svg/crashtests/492186-1.svg b/layout/svg/crashtests/492186-1.svg new file mode 100644 index 0000000000..7f4b6650ab --- /dev/null +++ b/layout/svg/crashtests/492186-1.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<altGlyphDef/> +<script xmlns="http://www.w3.org/1999/xhtml"> +document.documentElement.getBBox(); +</script> +</svg> diff --git a/layout/svg/crashtests/508247-1.svg b/layout/svg/crashtests/508247-1.svg new file mode 100644 index 0000000000..c8b36b905f --- /dev/null +++ b/layout/svg/crashtests/508247-1.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<definition-src> +<path id="a"/> +</definition-src> + +<script id="script" xmlns="http://www.w3.org/1999/xhtml"> +setTimeout(function() {document.getElementById('a').getCTM()},10); +</script> +</svg> diff --git a/layout/svg/crashtests/512890-1.svg b/layout/svg/crashtests/512890-1.svg new file mode 100644 index 0000000000..044f693892 --- /dev/null +++ b/layout/svg/crashtests/512890-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="f" height="1em"/> + <rect width="50" height="50" filter="url(#f)"/> +</svg> diff --git a/layout/svg/crashtests/515288-1.html b/layout/svg/crashtests/515288-1.html new file mode 100644 index 0000000000..d78cbbfbec --- /dev/null +++ b/layout/svg/crashtests/515288-1.html @@ -0,0 +1,5 @@ +<script> +var svgElem = document.createElementNS("http://www.w3.org/2000/svg", "svg"); +document.createElement("script").appendChild(svgElem); +svgElem.getScreenCTM(); +</script>
\ No newline at end of file diff --git a/layout/svg/crashtests/522394-1.svg b/layout/svg/crashtests/522394-1.svg new file mode 100644 index 0000000000..f745c47dd2 --- /dev/null +++ b/layout/svg/crashtests/522394-1.svg @@ -0,0 +1,12 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<svg xmlns="http://www.w3.org/2000/svg"> +<defs> + <filter id="f1" x="0" y="0" width="-100" height="-100" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse"> + <feFlood flood-color="lime" x="0" y="0" width="100%" height="100%"/> + </filter> +</defs> +<rect x="10" y="10" width="10" height="10" filter="url(#f1)"/> +</svg> diff --git a/layout/svg/crashtests/522394-2.svg b/layout/svg/crashtests/522394-2.svg new file mode 100644 index 0000000000..1b6f1f0892 --- /dev/null +++ b/layout/svg/crashtests/522394-2.svg @@ -0,0 +1,12 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<svg xmlns="http://www.w3.org/2000/svg"> +<defs> + <filter id="f1" x="0" y="0" width="100000000" height="100000000" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse"> + <feFlood flood-color="lime" x="0" y="0" width="100%" height="100%"/> + </filter> +</defs> +<rect x="10" y="10" width="10" height="10" filter="url(#f1)"/> +</svg> diff --git a/layout/svg/crashtests/522394-3.svg b/layout/svg/crashtests/522394-3.svg new file mode 100644 index 0000000000..cf3483cfad --- /dev/null +++ b/layout/svg/crashtests/522394-3.svg @@ -0,0 +1,12 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<svg xmlns="http://www.w3.org/2000/svg"> +<defs> + <filter id="f1" x="0" y="0" width="4563402752" height="4563402752" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse"> + <feFlood flood-color="lime" x="0" y="0" width="100%" height="100%"/> + </filter> +</defs> +<rect x="10" y="10" width="10" height="10" filter="url(#f1)"/> +</svg> diff --git a/layout/svg/crashtests/566216-1.svg b/layout/svg/crashtests/566216-1.svg new file mode 100644 index 0000000000..999aaf4f08 --- /dev/null +++ b/layout/svg/crashtests/566216-1.svg @@ -0,0 +1,19 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"><animate id="y"/><script> +<![CDATA[ + +function boom() +{ + var r = document.createRange(); + r.setEnd(document.getElementById('y'), 0); + r.extractContents(); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +</svg> + + diff --git a/layout/svg/crashtests/587336-1.html b/layout/svg/crashtests/587336-1.html new file mode 100644 index 0000000000..811f483dd7 --- /dev/null +++ b/layout/svg/crashtests/587336-1.html @@ -0,0 +1,9 @@ +<html> +<head><script> +function boom() +{ + var b = document.getElementById("b"); + b.setAttributeNS(null, "style", "filter: url(#a);"); +} +</script></head> +<body onload="boom();" id="a"><span id="b">B</span></body></html> diff --git a/layout/svg/crashtests/590291-1.svg b/layout/svg/crashtests/590291-1.svg new file mode 100644 index 0000000000..db26fac3a8 --- /dev/null +++ b/layout/svg/crashtests/590291-1.svg @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="0cm" height="0cm">
+
+<text id="a">a</text>
+
+<script><![CDATA[
+var x=document.getElementById('a').getExtentOfChar(0);
+]]></script>
+</svg>
diff --git a/layout/svg/crashtests/601999-1.html b/layout/svg/crashtests/601999-1.html new file mode 100644 index 0000000000..7e8a3d39de --- /dev/null +++ b/layout/svg/crashtests/601999-1.html @@ -0,0 +1,5 @@ +<html class="reftest-wait"> + <body onload="document.getElementsByTagName('div')[0].id='b'; + document.documentElement.removeAttribute('class');" + ><div style="overflow-x: scroll; filter: url(#b)">abc</div></body> +</html> diff --git a/layout/svg/crashtests/605626-1.svg b/layout/svg/crashtests/605626-1.svg new file mode 100644 index 0000000000..678b011797 --- /dev/null +++ b/layout/svg/crashtests/605626-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> +<image width="10" height="10" xlink:href="data:text/plain,g"/> +</svg> diff --git a/layout/svg/crashtests/606914.xhtml b/layout/svg/crashtests/606914.xhtml new file mode 100644 index 0000000000..fc019af573 --- /dev/null +++ b/layout/svg/crashtests/606914.xhtml @@ -0,0 +1 @@ +<html xmlns="http://www.w3.org/1999/xhtml" style="display: table; position: absolute; left: 2305843009213694000pc; bottom: 2452284pc; padding: 9931442138140%; border-bottom-right-radius: 1152921504606847000pc;">X</html> diff --git a/layout/svg/crashtests/610594-1.html b/layout/svg/crashtests/610594-1.html new file mode 100644 index 0000000000..ee48e762cc --- /dev/null +++ b/layout/svg/crashtests/610594-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<svg><path d="M 0 5 a 2 5 -30 104 -5" marker-mid="url(#q)"></path></svg> +</body> +</html> diff --git a/layout/svg/crashtests/610954-1.html b/layout/svg/crashtests/610954-1.html new file mode 100644 index 0000000000..f5080df8b8 --- /dev/null +++ b/layout/svg/crashtests/610954-1.html @@ -0,0 +1 @@ +<!DOCTYPE html><html><body dir=rtl onload="document.getElementById('g').style.filter = 'url(#filter1)';"><span id="g">‎---</span></body></html> diff --git a/layout/svg/crashtests/612662-1.svg b/layout/svg/crashtests/612662-1.svg new file mode 100644 index 0000000000..73dc98ed19 --- /dev/null +++ b/layout/svg/crashtests/612662-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="background: url(#a); direction: rtl; margin: -32944px;"></svg> diff --git a/layout/svg/crashtests/612662-2.svg b/layout/svg/crashtests/612662-2.svg new file mode 100644 index 0000000000..b46841132f --- /dev/null +++ b/layout/svg/crashtests/612662-2.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" + style="direction: rtl; margin: -32944px; + background: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3C%2Fsvg%3E)"></svg> diff --git a/layout/svg/crashtests/612736-1.svg b/layout/svg/crashtests/612736-1.svg new file mode 100644 index 0000000000..cb3044efd0 --- /dev/null +++ b/layout/svg/crashtests/612736-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + class="reftest-wait"> + <path id="path"/> + <text> + <textPath xlink:href="#path">f</textPath> + <textPath xlink:href="#path">f</textPath> + </text> + + <script> + function boom() + { + var path = document.getElementById("path"); + path.parentNode.removeChild(path); + document.documentElement.removeAttribute("class"); + } + window.addEventListener("load", boom, false); + </script> +</svg> diff --git a/layout/svg/crashtests/612736-2.svg b/layout/svg/crashtests/612736-2.svg new file mode 100644 index 0000000000..30b8245a9f --- /dev/null +++ b/layout/svg/crashtests/612736-2.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <path id="path" d="M0 100 h50" stroke="black"/> + <text> + <textPath xlink:href="#path">abc</textPath> + <textPath xlink:href="#path">def</textPath> + </text> +</svg> diff --git a/layout/svg/crashtests/614367-1.svg b/layout/svg/crashtests/614367-1.svg new file mode 100644 index 0000000000..3af7b491da --- /dev/null +++ b/layout/svg/crashtests/614367-1.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + <polygon id="p" transform="?" /> + <script> + document.getElementById("p").transform.baseVal.removeItem(0); + </script> +</svg> diff --git a/layout/svg/crashtests/620034-1.html b/layout/svg/crashtests/620034-1.html new file mode 100644 index 0000000000..bfffd3ffac --- /dev/null +++ b/layout/svg/crashtests/620034-1.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script> + +function boom() +{ + var f = document.createElementNS("http://www.w3.org/2000/svg", "feFuncB"); + var tvb = f.tableValues.baseVal; + f.setAttribute("tableValues", "3 7 5"); + f.setAttribute("tableValues", "i"); + tvb.numberOfItems; +} + +boom(); + +</script> diff --git a/layout/svg/crashtests/621598-1.svg b/layout/svg/crashtests/621598-1.svg new file mode 100644 index 0000000000..dd47967d35 --- /dev/null +++ b/layout/svg/crashtests/621598-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="m1"> + <rect/> + <marker> + <line id="z" marker-end="url(#m1)"/> + </marker> + </marker> + <script> + function boom() + { + document.getElementById("z").getBoundingClientRect(); + } + window.addEventListener("load", boom, false); + </script> + +</svg> diff --git a/layout/svg/crashtests/648819-1.html b/layout/svg/crashtests/648819-1.html new file mode 100644 index 0000000000..727ca3e55f --- /dev/null +++ b/layout/svg/crashtests/648819-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> +var p = document.createElementNS("http://www.w3.org/2000/svg", "pattern"); +p.setAttribute("patternTransform", "i"); +p.patternTransform.baseVal.clear(); +</script> diff --git a/layout/svg/crashtests/655025-1.svg b/layout/svg/crashtests/655025-1.svg new file mode 100644 index 0000000000..4501bb57fa --- /dev/null +++ b/layout/svg/crashtests/655025-1.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text id="a">a</text> + <script> + document.getElementById("a").firstChild.nodeValue = ""; + </script> +</svg> diff --git a/layout/svg/crashtests/655025-2.svg b/layout/svg/crashtests/655025-2.svg new file mode 100644 index 0000000000..601006e831 --- /dev/null +++ b/layout/svg/crashtests/655025-2.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text id="a">a</text> + <script> + document.getElementById("a").appendChild(document.createTextNode("")); + </script> +</svg> diff --git a/layout/svg/crashtests/655025-3.svg b/layout/svg/crashtests/655025-3.svg new file mode 100644 index 0000000000..43e06b6fc3 --- /dev/null +++ b/layout/svg/crashtests/655025-3.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <script> + var a = document.createElementNS("http://www.w3.org/2000/svg", "text"); + a.appendChild(document.createTextNode("")); + document.documentElement.appendChild(a); + a.getNumberOfChars(); + document.documentElement.removeChild(a); + </script> +</svg> diff --git a/layout/svg/crashtests/657077-1.svg b/layout/svg/crashtests/657077-1.svg new file mode 100644 index 0000000000..b0165bd14a --- /dev/null +++ b/layout/svg/crashtests/657077-1.svg @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<svg xmlns="http://www.w3.org/2000/svg"> + +<svg id="a" requiredExtensions="x"/> + +<script> +<![CDATA[ + +function boom() +{ + document.getElementById("a").unsuspendRedrawAll(); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +</svg> diff --git a/layout/svg/crashtests/669025-1.svg b/layout/svg/crashtests/669025-1.svg new file mode 100644 index 0000000000..eb529da4e0 --- /dev/null +++ b/layout/svg/crashtests/669025-1.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <script type="text/javascript"> + + document.createElementNS("http://www.w3.org/2000/svg", "filter").filterResX; + + </script> +</svg> diff --git a/layout/svg/crashtests/669025-2.svg b/layout/svg/crashtests/669025-2.svg new file mode 100644 index 0000000000..ccecebef3c --- /dev/null +++ b/layout/svg/crashtests/669025-2.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <script type="text/javascript"> + + document.createElementNS("http://www.w3.org/2000/svg", "feGaussianBlur").stdDeviationX; + + </script> +</svg> diff --git a/layout/svg/crashtests/682411-1.svg b/layout/svg/crashtests/682411-1.svg new file mode 100644 index 0000000000..92d0c0a725 --- /dev/null +++ b/layout/svg/crashtests/682411-1.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<svg xmlns="http://www.w3.org/2000/svg" style="width: 0pt; padding: 100px;"> + <filter id="s"/> + <g filter="url(#s)"><text>z</text></g> +</svg> diff --git a/layout/svg/crashtests/692203-1.svg b/layout/svg/crashtests/692203-1.svg new file mode 100644 index 0000000000..8427b84268 --- /dev/null +++ b/layout/svg/crashtests/692203-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="marker" markerWidth="0"/> + <path d="M0,0 L100,100 200,200" marker-mid="url(#marker)"/> +</svg> diff --git a/layout/svg/crashtests/692203-2.svg b/layout/svg/crashtests/692203-2.svg new file mode 100644 index 0000000000..b59926dbba --- /dev/null +++ b/layout/svg/crashtests/692203-2.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="marker" markerHeight="0"/> + <path d="M0,0 L100,100 200,200" marker-mid="url(#marker)"/> +</svg> diff --git a/layout/svg/crashtests/693424-1.svg b/layout/svg/crashtests/693424-1.svg new file mode 100644 index 0000000000..8485f6b617 --- /dev/null +++ b/layout/svg/crashtests/693424-1.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="m"> + <foreignObject/> + </marker> + <line marker-end="url(#m)"/> +</svg> diff --git a/layout/svg/crashtests/709920-1.svg b/layout/svg/crashtests/709920-1.svg new file mode 100644 index 0000000000..5f25155307 --- /dev/null +++ b/layout/svg/crashtests/709920-1.svg @@ -0,0 +1,23 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> + <!-- Test to be sure that a zero-sized-in-one-dimension viewBox doesn't + make us fail assertions. --> + <script> + document.addEventListener("MozReftestInvalidate", waitAndFinish, false); + + function waitAndFinish() { + // Sadly, MozReftestInvalidate fires sooner than PaintPattern here, so + // we need to wait a little bit to give PaintPattern a chance to hit + // this bug. + setTimeout(finish, 100); + } + + function finish() { + document.documentElement.removeAttribute("class"); + } + </script> + <pattern id="test" viewBox="0 0 1 0"> + <rect/> + </pattern> + <rect width="200" height="200" fill="url(#test)"/> +</svg> diff --git a/layout/svg/crashtests/709920-2.svg b/layout/svg/crashtests/709920-2.svg new file mode 100644 index 0000000000..58c51111eb --- /dev/null +++ b/layout/svg/crashtests/709920-2.svg @@ -0,0 +1,23 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> + <!-- Test to be sure that a zero-sized-in-one-dimension viewBox doesn't + make us fail assertions. --> + <script> + document.addEventListener("MozReftestInvalidate", waitAndFinish, false); + + function waitAndFinish() { + // Sadly, MozReftestInvalidate fires sooner than PaintPattern here, so + // we need to wait a little bit to give PaintPattern a chance to hit + // this bug. + setTimeout(finish, 100); + } + + function finish() { + document.documentElement.removeAttribute("class"); + } + </script> + <pattern id="test" viewBox="0 0 0 1"> + <rect/> + </pattern> + <rect width="200" height="200" fill="url(#test)"/> +</svg> diff --git a/layout/svg/crashtests/713413-1.svg b/layout/svg/crashtests/713413-1.svg new file mode 100644 index 0000000000..7131202335 --- /dev/null +++ b/layout/svg/crashtests/713413-1.svg @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="Windows-1252"?> +<svg xmlns="http://www.w3.org/2000/svg"> + +<marker id="m"></marker> + +<script> +window.addEventListener("load", function() { + document.getElementById("m").appendChild(document.createElementNS("http://www.w3.org/2000/svg", "foreignObject")); +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/722003-1.svg b/layout/svg/crashtests/722003-1.svg new file mode 100644 index 0000000000..58e2d57734 --- /dev/null +++ b/layout/svg/crashtests/722003-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<marker><foreignObject><span id="x" xmlns="http://www.w3.org/1999/xhtml"></span></foreignObject></marker> + +<script> + +window.addEventListener("load", function() { + document.getElementById("x").getClientRects(); +}, false); + +</script> + +</svg> diff --git a/layout/svg/crashtests/725918-1.svg b/layout/svg/crashtests/725918-1.svg new file mode 100644 index 0000000000..5ebdf33f69 --- /dev/null +++ b/layout/svg/crashtests/725918-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text stroke="url(#p)">t</text> + <pattern id="p"/> +</svg> diff --git a/layout/svg/crashtests/732836-1.svg b/layout/svg/crashtests/732836-1.svg new file mode 100644 index 0000000000..a9abb46668 --- /dev/null +++ b/layout/svg/crashtests/732836-1.svg @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > + + <symbol id="z"> + <use xlink:href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' id='root' />#root" /> + </symbol> + + <use id="a" xlink:href="#z" width="20"/> + + <script> + window.addEventListener("load", function() { + window.scrollByPages(0); + document.getElementById("a").removeAttribute("width"); + document.elementFromPoint(0, 0); + }, false); + </script> + +</svg> diff --git a/layout/svg/crashtests/740627-1.svg b/layout/svg/crashtests/740627-1.svg new file mode 100644 index 0000000000..b74fcd2ddd --- /dev/null +++ b/layout/svg/crashtests/740627-1.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <pattern id="test" viewBox="0 0 10 10" height="-65%"> + <rect/> + </pattern> + <rect width="100" height="100" fill="url(#test)"/> +</svg> diff --git a/layout/svg/crashtests/740627-2.svg b/layout/svg/crashtests/740627-2.svg new file mode 100644 index 0000000000..6241ddaae5 --- /dev/null +++ b/layout/svg/crashtests/740627-2.svg @@ -0,0 +1,6 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <pattern id="test" viewBox="0 0 10 10" width="-65%"> + <rect/> + </pattern> + <rect width="100" height="100" fill="url(#test)"/> +</svg> diff --git a/layout/svg/crashtests/743469.svg b/layout/svg/crashtests/743469.svg new file mode 100644 index 0000000000..6affc6fab8 --- /dev/null +++ b/layout/svg/crashtests/743469.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="border-width: 51703084143745256mm; border-left-style: dashed; border-top-left-radius: 3%; border-top-style: dashed; border-right-style: solid; border-image-outset: 10;"> +<script> +document.elementFromPoint(20, 20); +</script> +</svg> diff --git a/layout/svg/crashtests/757704-1.svg b/layout/svg/crashtests/757704-1.svg new file mode 100644 index 0000000000..b7e610e0e1 --- /dev/null +++ b/layout/svg/crashtests/757704-1.svg @@ -0,0 +1,17 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"><script> +<![CDATA[ + +function boom() +{ + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + document.documentElement.appendChild(rect); + document.removeChild(document.documentElement); + rect.getScreenCTM(); +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/layout/svg/crashtests/757718-1.svg b/layout/svg/crashtests/757718-1.svg new file mode 100644 index 0000000000..fa948c6677 --- /dev/null +++ b/layout/svg/crashtests/757718-1.svg @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var svgDoc = (new DOMParser).parseFromString("<svg xmlns='http://www.w3.org/2000/svg'></svg>", "image/svg+xml"); + var svgRoot = svgDoc.documentElement; + var rf = svgRoot.requiredFeatures; + document.adoptNode(svgRoot); + Object.getOwnPropertyNames(rf); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/layout/svg/crashtests/757751-1.svg b/layout/svg/crashtests/757751-1.svg new file mode 100644 index 0000000000..7ab51d0d19 --- /dev/null +++ b/layout/svg/crashtests/757751-1.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <defs> + <svg id="x" viewBox=" 0 0 10 10"/> + </defs> + <script> + window.addEventListener("load", function() { document.getElementById("x").setAttribute("width", "2"); }, false); + </script> +</svg> diff --git a/layout/svg/crashtests/767056-1.svg b/layout/svg/crashtests/767056-1.svg new file mode 100644 index 0000000000..b813d784b3 --- /dev/null +++ b/layout/svg/crashtests/767056-1.svg @@ -0,0 +1,21 @@ +<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ width="100%"
+ class="reftest-wait">
+ <script>
+
+function resize() {
+ // Set the viewBox to the same width as the content area, but slightly
+ // higher. This checks that we don't enter an infinite reflow loop. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=767056#c10
+ var viewBox = "0 0 " + window.innerWidth + " " + (window.innerHeight + 1);
+ document.documentElement.setAttribute("viewBox", viewBox);
+ document.documentElement.removeAttribute("class");
+}
+
+document.addEventListener("MozReftestInvalidate", resize, false);
+setTimeout(resize, 3000); // For non-gecko
+
+ </script>
+ <rect width="100%" height="100%"/>
+</svg>
diff --git a/layout/svg/crashtests/767535-1.xhtml b/layout/svg/crashtests/767535-1.xhtml new file mode 100644 index 0000000000..2a923fcc6d --- /dev/null +++ b/layout/svg/crashtests/767535-1.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml" style="columns: 2 auto;" class="reftest-wait"> + <head> + <script> + +function test() { + var r = document.documentElement; + document.removeChild(r); + document.appendChild(r); + document.documentElement.removeAttribute("class"); +} + + </script> + </head> + <body style="filter:url(#f);" onload="setTimeout(test, 0);"> + <div> + </div> + <svg xmlns="http://www.w3.org/2000/svg"> + <filter id="f"/> + </svg> + </body> +</html> + diff --git a/layout/svg/crashtests/768087-1.html b/layout/svg/crashtests/768087-1.html new file mode 100644 index 0000000000..9a7899f9d1 --- /dev/null +++ b/layout/svg/crashtests/768087-1.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<html> +<body onload="setTimeout(function() { document.body.innerHTML = '<span>x<svg viewbox=\'0 0 30 40\' ></svg></span>'; }, 0);"></body> +</html> diff --git a/layout/svg/crashtests/768351.svg b/layout/svg/crashtests/768351.svg new file mode 100644 index 0000000000..50a4b9b9c4 --- /dev/null +++ b/layout/svg/crashtests/768351.svg @@ -0,0 +1,2 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="mask: url('data:text/plain,1#f');" /> + diff --git a/layout/svg/crashtests/772313-1.svg b/layout/svg/crashtests/772313-1.svg new file mode 100644 index 0000000000..c77af02b6e --- /dev/null +++ b/layout/svg/crashtests/772313-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="background: url(#d); width: 0.1px;"></svg> diff --git a/layout/svg/crashtests/778492-1.svg b/layout/svg/crashtests/778492-1.svg new file mode 100644 index 0000000000..76deda594e --- /dev/null +++ b/layout/svg/crashtests/778492-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg width="1cm" viewBox="0 0 1 1" xmlns="http://www.w3.org/2000/svg"> +<line x2="100" y2="100"/> +</svg> diff --git a/layout/svg/crashtests/779971-1.svg b/layout/svg/crashtests/779971-1.svg new file mode 100644 index 0000000000..d57065a0ba --- /dev/null +++ b/layout/svg/crashtests/779971-1.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="r" class="reftest-wait"> +<text id="t"><textPath xlink:href="#r">x</textPath>1</text> +<script> + +window.addEventListener("load", function() { + setTimeout(function() { + document.getElementById("t").lastChild.data = "2"; + + document.documentElement.removeAttribute("class"); + }, 200); +}, false); + +</script> +</svg> diff --git a/layout/svg/crashtests/780764-1.svg b/layout/svg/crashtests/780764-1.svg new file mode 100644 index 0000000000..6f4eb970bb --- /dev/null +++ b/layout/svg/crashtests/780764-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + +<marker id="marker"><path d="M100,0 l100,100 200,200" filter="url(#filter)"/></marker> + +<filter id="filter"/> + +<path d="M100,0 l100,100 200,200" marker-mid="url(#marker)"/> + +<script> +window.addEventListener("load", function() { + setTimeout(function() { + document.getElementById("filter").style.fontWeight = "bold"; + document.documentElement.removeAttribute("class"); + }, 200); +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/780963-1.html b/layout/svg/crashtests/780963-1.html new file mode 100644 index 0000000000..8cbeb1a37f --- /dev/null +++ b/layout/svg/crashtests/780963-1.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> + <head> + <script> + +function tweak() { + document.body.offsetTop; + + var feImage = document.getElementsByTagName("feImage")[0]; + feImage.setAttribute('filter', 'url(#f1)') + document.body.offsetTop; + + var child = document.createElementNS('http://www.w3.org/2000/svg', 'g') + feImage.appendChild(child); +} + + </script> + </head> + <body onload="tweak()"> + <svg xmlns="http://www.w3.org/2000/svg"> + <filter filterUnits="userSpaceOnUse" id="f1"> + <feImage/> + </filter> + <rect height="100" width="100"/> + </svg> + </body> +</html> diff --git a/layout/svg/crashtests/782141-1.svg b/layout/svg/crashtests/782141-1.svg new file mode 100644 index 0000000000..6f0af76ff4 --- /dev/null +++ b/layout/svg/crashtests/782141-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + onload="go()"> + <script> + +function go() { + var f = document.getElementById('f'); + var fm = document.getElementById('fm'); + f.appendChild(fm.cloneNode(1)); +} + + </script> + <filter id="f"> + <feMorphology id="fm" radius="2147483500"/> + </filter> + <rect height="28" width="256" filter="url(#f)" /> +</svg> diff --git a/layout/svg/crashtests/784061-1.svg b/layout/svg/crashtests/784061-1.svg new file mode 100644 index 0000000000..6a9623154d --- /dev/null +++ b/layout/svg/crashtests/784061-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> + +<defs><path id="x"/></defs> + +<script> +function boom() +{ + document.getElementById("x").style.transform = "translate(0px)"; + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/788831-1.svg b/layout/svg/crashtests/788831-1.svg new file mode 100644 index 0000000000..b6202a1324 --- /dev/null +++ b/layout/svg/crashtests/788831-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <pattern id="pattern" width="40" height="40"><stop/></pattern> + <rect id="rect" width="200" height="100"/> + <use xlink:href="#rect" stroke="url(#pattern)" /> +</svg> diff --git a/layout/svg/crashtests/789390-1.html b/layout/svg/crashtests/789390-1.html new file mode 100644 index 0000000000..542037f229 --- /dev/null +++ b/layout/svg/crashtests/789390-1.html @@ -0,0 +1 @@ +<html style="transition: 1s;"><body onload="document.documentElement.style.stroke = '-moz-objectStroke';"></body></html> diff --git a/layout/svg/crashtests/790072.svg b/layout/svg/crashtests/790072.svg new file mode 100644 index 0000000000..b288251a7e --- /dev/null +++ b/layout/svg/crashtests/790072.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"><text style="stroke: -moz-objectfill none;">abc</text></svg> diff --git a/layout/svg/crashtests/791826-1.svg b/layout/svg/crashtests/791826-1.svg new file mode 100644 index 0000000000..f42261a3ad --- /dev/null +++ b/layout/svg/crashtests/791826-1.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="position: fixed;"> +<script> +<![CDATA[ + +function boom() +{ + document.documentElement.setAttribute("preserveAspectRatio", "_"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +</svg> diff --git a/layout/svg/crashtests/803562-1.svg b/layout/svg/crashtests/803562-1.svg new file mode 100644 index 0000000000..e6fc91127e --- /dev/null +++ b/layout/svg/crashtests/803562-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<foreignObject width="500" height="500" style="-moz-appearance: checkbox;"> + <option xmlns="http://www.w3.org/1999/xhtml"></option> +</foreignObject> + +<script> +<![CDATA[ + +window.addEventListener("load", function() { + document.getElementsByTagName("option")[0].appendChild(document.createTextNode("Option 1")); +}, false); + +]]> +</script> + +</svg> + diff --git a/layout/svg/crashtests/808318-1.svg b/layout/svg/crashtests/808318-1.svg new file mode 100644 index 0000000000..48907225cc --- /dev/null +++ b/layout/svg/crashtests/808318-1.svg @@ -0,0 +1,2 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="-moz-transform-style: preserve-3d"></svg> + diff --git a/layout/svg/crashtests/813420-1.svg b/layout/svg/crashtests/813420-1.svg new file mode 100644 index 0000000000..d977c0e982 --- /dev/null +++ b/layout/svg/crashtests/813420-1.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + <svg id="i" style="mask: url("#none");"> + <marker id="markerEnd"/><polygon marker-end="url(#markerEnd)" points="250,150 200,150"/> + </svg> + <script> + +window.addEventListener("MozReftestInvalidate", function() { + document.getElementById("i").style.mask = "url(#none)"; + document.documentElement.removeAttribute("class"); +}, false); + + </script> +</svg> + diff --git a/layout/svg/crashtests/841163-1.svg b/layout/svg/crashtests/841163-1.svg new file mode 100644 index 0000000000..b1bb5198ab --- /dev/null +++ b/layout/svg/crashtests/841163-1.svg @@ -0,0 +1,29 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + +<filter id="f"/> + +<g filter="url(#f)"> + <text>AB</text> +</g> + +<script> + +function forceFrameConstruction() +{ + document.documentElement.getBoundingClientRect(); +} + +function boom() +{ + document.getElementsByTagName("text")[0].firstChild.splitText(1); + forceFrameConstruction(); + document.normalize(); + forceFrameConstruction(); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +</script> + +</svg> diff --git a/layout/svg/crashtests/841812-1.svg b/layout/svg/crashtests/841812-1.svg new file mode 100644 index 0000000000..e5bcaa66ee --- /dev/null +++ b/layout/svg/crashtests/841812-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <text> + <circle> + <textPath id="t" xlink:href="data:text/html,1" /> + </circle> + </text> + + <script> + window.addEventListener("load", function() { document.getElementById("t").removeAttribute('xlink:href'); }, false); + </script> +</svg> diff --git a/layout/svg/crashtests/842009-1.svg b/layout/svg/crashtests/842009-1.svg new file mode 100644 index 0000000000..25656ba530 --- /dev/null +++ b/layout/svg/crashtests/842009-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <title id="hello">hello</title> + <text x="100" y="100"> <tref xlink:href="#hello"/></text> +</svg> diff --git a/layout/svg/crashtests/842630-1.svg b/layout/svg/crashtests/842630-1.svg new file mode 100644 index 0000000000..8d36998be6 --- /dev/null +++ b/layout/svg/crashtests/842630-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"><text dy="20 20">A<tspan style="display: none;">B</tspan></text></svg> diff --git a/layout/svg/crashtests/842909-1.svg b/layout/svg/crashtests/842909-1.svg new file mode 100644 index 0000000000..9a1bc89eb4 --- /dev/null +++ b/layout/svg/crashtests/842909-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <defs> + <text id="t">X</text> + </defs> + + <script> + window.addEventListener("load", function() { + document.getElementById("t").getSubStringLength(0, 0); + }, false); + </script> +</svg> diff --git a/layout/svg/crashtests/843072-1.svg b/layout/svg/crashtests/843072-1.svg new file mode 100644 index 0000000000..590721f058 --- /dev/null +++ b/layout/svg/crashtests/843072-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <defs> + <text id="t"></text> + </defs> + + <script> + window.addEventListener("load", function() { + document.getElementById("t").getExtentOfChar(0); + }, false); + </script> +</svg> diff --git a/layout/svg/crashtests/843917-1.svg b/layout/svg/crashtests/843917-1.svg new file mode 100644 index 0000000000..55cf7ab186 --- /dev/null +++ b/layout/svg/crashtests/843917-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <filter id="f"/> + + <g filter="url(#f)"> + <text>a𞠯</text> + </g> + + <script> + + window.addEventListener("load", function() { + var text = document.getElementsByTagName("text")[0]; + text.firstChild.data = "d"; + text.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "g")); + }, false); + + </script> + +</svg> diff --git a/layout/svg/crashtests/847139-1.svg b/layout/svg/crashtests/847139-1.svg new file mode 100644 index 0000000000..81fffa4be8 --- /dev/null +++ b/layout/svg/crashtests/847139-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<marker><text>x</text></marker> + +<script> + +window.addEventListener("load", function() { + document.caretPositionFromPoint(0, 0); +}, false); + +</script> + +</svg> diff --git a/layout/svg/crashtests/849688-1.svg b/layout/svg/crashtests/849688-1.svg new file mode 100644 index 0000000000..142f04c933 --- /dev/null +++ b/layout/svg/crashtests/849688-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<text></text> + +<script> +window.addEventListener("load", function() { + document.getElementsByTagName('text')[0].getStartPositionOfChar(1); +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/849688-2.svg b/layout/svg/crashtests/849688-2.svg new file mode 100644 index 0000000000..4b71b20c7c --- /dev/null +++ b/layout/svg/crashtests/849688-2.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<text>X</text> + +<script> +window.addEventListener("load", function() { + document.getElementsByTagName('text')[0].getStartPositionOfChar(2); +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/860378-1.svg b/layout/svg/crashtests/860378-1.svg new file mode 100644 index 0000000000..f4ec09bc4c --- /dev/null +++ b/layout/svg/crashtests/860378-1.svg @@ -0,0 +1,24 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<filter id="f"/> +<g filter="url(#f)"><text>ab</text></g> + +<script> + +function boom() +{ + var svgtext = document.getElementsByTagName("text")[0]; + var text1 = svgtext.firstChild ; + var text2 = text1.splitText(1); + + setTimeout(function() { + text1.data = "c"; + svgtext.removeChild(text2); + }, 200); +} + +window.addEventListener("load", boom, false); + +</script> + +</svg> diff --git a/layout/svg/crashtests/868904-1.svg b/layout/svg/crashtests/868904-1.svg new file mode 100644 index 0000000000..c8d7e9437e --- /dev/null +++ b/layout/svg/crashtests/868904-1.svg @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<style> + +* { animation-name: a; animation-duration: 72ms } +@keyframes a { 60% { transform: skewx(30deg); } } + +</style> +</head> +<body> + +<svg></svg> + +</body> +</html> diff --git a/layout/svg/crashtests/873806-1.svg b/layout/svg/crashtests/873806-1.svg new file mode 100644 index 0000000000..e40aff201b --- /dev/null +++ b/layout/svg/crashtests/873806-1.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <foreignObject requiredFeatures="fail"> + <svg> + <text>a</text> + </svg> + </foreignObject> + <script> + document.querySelector("text").getBBox(); + </script> +</svg> diff --git a/layout/svg/crashtests/876831-1.svg b/layout/svg/crashtests/876831-1.svg new file mode 100644 index 0000000000..6b6c01f9e7 --- /dev/null +++ b/layout/svg/crashtests/876831-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<script> + +function boom() +{ + var x = document.getElementById("x").firstChild; + x.data = x.data.slice(1); + document.caretPositionFromPoint(0, 0); +} + +window.addEventListener("load", boom, false); + +</script> + +<text><tspan id="x">@ت</tspan></text> + +</svg> diff --git a/layout/svg/crashtests/877029-1.svg b/layout/svg/crashtests/877029-1.svg new file mode 100644 index 0000000000..1a7bad0f1b --- /dev/null +++ b/layout/svg/crashtests/877029-1.svg @@ -0,0 +1,10 @@ +<!-- + Check that we don't crash due to an nsSVGMarkerFrame having a null + mMarkedFrame and incorrectly calling GetCanvasTM() on the nsSVGMarkerFrame. + --> +<svg xmlns="http://www.w3.org/2000/svg"> + <marker><text>a</text></marker> + <script> + document.querySelector("text").getComputedTextLength(); + </script> +</svg> diff --git a/layout/svg/crashtests/880925-1.svg b/layout/svg/crashtests/880925-1.svg new file mode 100644 index 0000000000..77efd3c0a5 --- /dev/null +++ b/layout/svg/crashtests/880925-1.svg @@ -0,0 +1,26 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> +<script> +<![CDATA[ + +function boom() +{ + var svgText = document.createElementNS("http://www.w3.org/2000/svg", "text"); + document.documentElement.appendChild(svgText); + var text1 = document.createTextNode("A"); + svgText.appendChild(text1); + var text2 = document.createTextNode(""); + svgText.appendChild(text2); + document.caretPositionFromPoint(0, 0); + setTimeout(function() { + text2.data = "B"; + document.caretPositionFromPoint(0, 0); + }, 0); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +</svg> diff --git a/layout/svg/crashtests/881031-1.svg b/layout/svg/crashtests/881031-1.svg new file mode 100644 index 0000000000..0738e1299d --- /dev/null +++ b/layout/svg/crashtests/881031-1.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<script> + +function boom() +{ + var svgText = document.getElementById("t"); + svgText.firstChild.data = "C"; + svgText.appendChild(document.createTextNode("D")); + document.caretPositionFromPoint(0, 0); +} +window.addEventListener("load", boom, false); + +</script> +<text id="t">A</text> +</svg> diff --git a/layout/svg/crashtests/885608-1.svg b/layout/svg/crashtests/885608-1.svg new file mode 100644 index 0000000000..0c96777508 --- /dev/null +++ b/layout/svg/crashtests/885608-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<mask id="m"><text id="t">z</text></mask> + +<rect width="600" height="400" mask="url(#m)"/> + +<script> +window.addEventListener("load", function() { + document.getElementById("t").firstChild.data = "ab"; +}, false); +</script> + +</svg> diff --git a/layout/svg/crashtests/890782-1.svg b/layout/svg/crashtests/890782-1.svg new file mode 100644 index 0000000000..686bc73a8f --- /dev/null +++ b/layout/svg/crashtests/890782-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <foreignObject requiredFeatures="foo" id="f"> + <svg> + <text id="t"/> + </svg> + </foreignObject> + + <script> + window.addEventListener("load", function() { + document.documentElement.appendChild(document.getElementById("f")) + document.getElementById("t").getNumberOfChars(); + }, false); + </script> + +</svg> diff --git a/layout/svg/crashtests/890783-1.svg b/layout/svg/crashtests/890783-1.svg new file mode 100644 index 0000000000..25f54ba2a3 --- /dev/null +++ b/layout/svg/crashtests/890783-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <mask id="m"> + <text> + <tspan id="tspan" /> + </text> + </mask> + + <rect width="600" height="400" mask="url(#m)"/> + + <script> + + window.addEventListener("load", function() { + document.getElementById("tspan").style.dominantBaseline = "alphabetic"; + }, false); + + </script> + +</svg> diff --git a/layout/svg/crashtests/893510-1.svg b/layout/svg/crashtests/893510-1.svg new file mode 100644 index 0000000000..bb58be0450 --- /dev/null +++ b/layout/svg/crashtests/893510-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <g requiredExtensions="foo"> + <text>تz</text> + </g> +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/895311-1.svg b/layout/svg/crashtests/895311-1.svg new file mode 100644 index 0000000000..7b0c728043 --- /dev/null +++ b/layout/svg/crashtests/895311-1.svg @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <mask id="m"> + <text> + <tspan id="ts" /> + </text> + </mask> + + <rect width="600" height="400" mask="url(#m)"/> + + <script> + window.addEventListener("load", function() { + document.getElementById("ts").style.overflow = "hidden"; + }, false); + </script> + +</svg> diff --git a/layout/svg/crashtests/897342-1.svg b/layout/svg/crashtests/897342-1.svg new file mode 100644 index 0000000000..547e919b7d --- /dev/null +++ b/layout/svg/crashtests/897342-1.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"><text textLength="50" lengthAdjust="spacingAndGlyphs">‍</text></svg> diff --git a/layout/svg/crashtests/898909-1.svg b/layout/svg/crashtests/898909-1.svg new file mode 100644 index 0000000000..8a70cd7b8d --- /dev/null +++ b/layout/svg/crashtests/898909-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" requiredFeatures="foo"> + + <text id="t" /> + + <script> + window.addEventListener("load", function() { + document.getElementById("t").getComputedTextLength(); + }, false); + </script> + +</svg> diff --git a/layout/svg/crashtests/898951-1.svg b/layout/svg/crashtests/898951-1.svg new file mode 100644 index 0000000000..f42dbf69f2 --- /dev/null +++ b/layout/svg/crashtests/898951-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text>X<tspan style="display: none;">2</tspan>́</text> +</svg> diff --git a/layout/svg/crashtests/913990.html b/layout/svg/crashtests/913990.html new file mode 100644 index 0000000000..21d8ef3cc3 --- /dev/null +++ b/layout/svg/crashtests/913990.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<body style="filter: url('feed:javascript:5');"> +</body> +</html> diff --git a/layout/svg/crashtests/919371-1.xhtml b/layout/svg/crashtests/919371-1.xhtml new file mode 100644 index 0000000000..b27ba3fa66 --- /dev/null +++ b/layout/svg/crashtests/919371-1.xhtml @@ -0,0 +1,5 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <svg xmlns="http://www.w3.org/2000/svg"> + <marker style="position: absolute;" /> + </svg> +</html> diff --git a/layout/svg/crashtests/950324-1.svg b/layout/svg/crashtests/950324-1.svg new file mode 100644 index 0000000000..a43d84f4d8 --- /dev/null +++ b/layout/svg/crashtests/950324-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text style="font-family: sans-serif;"> ́</text> +</svg> diff --git a/layout/svg/crashtests/951904-1.html b/layout/svg/crashtests/951904-1.html new file mode 100644 index 0000000000..7a1d1d4dd4 --- /dev/null +++ b/layout/svg/crashtests/951904-1.html @@ -0,0 +1,43 @@ +<body onload="go()"> +<svg> + <switch> + <text id="a">Bonjour</text> + <text id="b">Hello</text> + <a><text id="c">Hello</text></a> + <g> + <mask> + <text>Lundi</text> + </mask> + </g> + <switch> + <text id="d">Au revoir</text> + <g> + <mask> + <text>Mercredi</text> + </mask> + </g> + <text id="e">Goodbye</text> + <a><text id="f">Goodbye</text></a> + </switch> + </switch> +</svg> +<svg> + <switch> + <mask> + <text id="g">Vendredi</text> + </mask> + <a></a> + </switch> +</svg> +</body> +<script> +function go() { + a.getComputedTextLength(); + b.getComputedTextLength(); + c.getComputedTextLength(); + d.getComputedTextLength(); + e.getComputedTextLength(); + f.getComputedTextLength(); + g.getComputedTextLength(); +} +</script> diff --git a/layout/svg/crashtests/952270-1.svg b/layout/svg/crashtests/952270-1.svg new file mode 100644 index 0000000000..69bac47d42 --- /dev/null +++ b/layout/svg/crashtests/952270-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + + <path id="path" transform="scale(2,1)" /> + + <text> + <textPath xlink:href="#path">F</textPath> + </text> + +</svg> diff --git a/layout/svg/crashtests/963086-1.svg b/layout/svg/crashtests/963086-1.svg new file mode 100644 index 0000000000..3805b46d75 --- /dev/null +++ b/layout/svg/crashtests/963086-1.svg @@ -0,0 +1,18 @@ +<svg xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 64 64"> + <defs> + <filter id="dropShadow"> + <feGaussianBlur stdDeviation="2" /> + <feOffset + result="offsetBlur" + dy="1073741824"/> + <feMerge> + <feMergeNode + in="offsetBlur" /> + <feMergeNode + in="SourceGraphic" /> + </feMerge> + </filter> + </defs> + <rect height="64" width="64" style="filter:url(#dropShadow)" /> +</svg> diff --git a/layout/svg/crashtests/974746-1.svg b/layout/svg/crashtests/974746-1.svg new file mode 100644 index 0000000000..c619c25f79 --- /dev/null +++ b/layout/svg/crashtests/974746-1.svg @@ -0,0 +1,9 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <pattern id="patternRotated" width="1" patternTransform="rotate(45 50 50)"> + <rect/> + </pattern> + + <rect width="100" height="100" fill="url(#patternRotated)"/> + +</svg> diff --git a/layout/svg/crashtests/975773-1.svg b/layout/svg/crashtests/975773-1.svg new file mode 100644 index 0000000000..dd225eb2ae --- /dev/null +++ b/layout/svg/crashtests/975773-1.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + + <filter id="f"> + <feSpecularLighting style="display: none;"/> + <feComposite in="SourceGraphic"/> + </filter> + + <path d="M0,0 h100 v100 h-100 z M20,20 v60 h60 v-60 z" filter="url(#f)"/> + +</svg> diff --git a/layout/svg/crashtests/979407-1.svg b/layout/svg/crashtests/979407-1.svg new file mode 100644 index 0000000000..b615f3bec2 --- /dev/null +++ b/layout/svg/crashtests/979407-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="marker" viewBox="0 0 10 10" markerHeight="-1px"/> + <path d="M0,0 h10" marker-start="url(#marker)"/> +</svg> diff --git a/layout/svg/crashtests/979407-2.svg b/layout/svg/crashtests/979407-2.svg new file mode 100644 index 0000000000..75aee06345 --- /dev/null +++ b/layout/svg/crashtests/979407-2.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <marker id="marker" viewBox="0 0 10 10" markerWidth="-1px"/> + <path d="M0,0 h10" marker-start="url(#marker)"/> +</svg> diff --git a/layout/svg/crashtests/993443.svg b/layout/svg/crashtests/993443.svg new file mode 100644 index 0000000000..30bd18543c --- /dev/null +++ b/layout/svg/crashtests/993443.svg @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> +<image width="10" height="10" x="17592186044416pt"/> +</svg>
\ No newline at end of file diff --git a/layout/svg/crashtests/blob-merging-and-retained-display-list.html b/layout/svg/crashtests/blob-merging-and-retained-display-list.html new file mode 100644 index 0000000000..56ca743dc9 --- /dev/null +++ b/layout/svg/crashtests/blob-merging-and-retained-display-list.html @@ -0,0 +1,62 @@ +<!doctype html> +<html class="reftest-wait"> +<script> + var r = 40; + var xmlns = "http://www.w3.org/2000/svg"; + const raf = f => requestAnimationFrame(f); + function rect(x, y) { + var r = document.createElementNS (xmlns, "rect"); + r.setAttribute("x", x); + r.setAttribute("y", y); + r.setAttribute("width", "100"); + r.setAttribute("height", "100"); + r.setAttribute("fill", "blue"); + return r; + } + function f1() { + svg = document.getElementById("cnvs"); + svg.appendChild(rect(0, 0)); + svg.appendChild(rect(600, 0)); + svg.appendChild(rect(600, 400)); + svg.appendChild(rect(0, 400)); + let a = rect(110, 110); + let b = rect(120, 120); + let c = rect(130, 130); + let d = rect(140, 140); + let a2 = rect(310, 140); + let b2 = rect(320, 130); + let c2 = rect(330, 120); + let d2 = rect(340, 110); + raf(() => { + svg.appendChild(a); + svg.appendChild(b); + svg.appendChild(c); + svg.appendChild(d); + raf(() => { + // the display list partial update will end up with these items before x,y,w,z + svg.appendChild(d2); + svg.appendChild(c2); + svg.appendChild(b2); + svg.appendChild(a2); + raf(() => { + // this forces all the items to be ordered and makes the new display list + // contain reorded items outside of the invalid area + let mix = rect(220, 220); + svg.insertBefore(mix, d2); + raf(() => { document.documentElement.className = "" }); + }) + }) + }) + } + + function f() { + requestAnimationFrame(f1); + } + + onload = f; +</script> + +<body> +<svg width="700" height="600" id=cnvs> + +</svg> diff --git a/layout/svg/crashtests/conditional-outer-svg-nondirty-reflow-assert.xhtml b/layout/svg/crashtests/conditional-outer-svg-nondirty-reflow-assert.xhtml new file mode 100644 index 0000000000..b23f064a6c --- /dev/null +++ b/layout/svg/crashtests/conditional-outer-svg-nondirty-reflow-assert.xhtml @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg"> +<head> + <title>Test of NS_FRAME_IS_NONDISPLAY / NS_FRAME_IS_DIRTY assertion</title> + <!-- reduced from layout/reftests/svg/svg-integration/conditions-outer-svg-01.xhtml + and modified to occur without bug 1308876 patches. --> + <style> + div { position: relative; height: 100px; width: 100px } + svg { position: absolute; top: 0; left: 0; height: 100%; width: 100% } + </style> + <script> + function run() { + var d = document.getElementById("d"); + d.offsetHeight; + d.style.height = "50px"; + } + </script> +</head> +<body onload="run()"> + <div id="d"> + <svg:svg systemLanguage="x"></svg:svg> + </div> +</body> +</html> diff --git a/layout/svg/crashtests/crashtests.list b/layout/svg/crashtests/crashtests.list new file mode 100644 index 0000000000..9d38f43f7a --- /dev/null +++ b/layout/svg/crashtests/crashtests.list @@ -0,0 +1,260 @@ +load 220165-1.svg +load 267650-1.svg +load 294022-1.svg +load 307314-1.svg +load 308615-1.svg +load 308917-1.svg +load 310436-1.svg +load 310638.svg +load 313737-1.xml +load chrome://reftest/content/crashtests/layout/svg/crashtests/314244-1.xhtml +load 322185-1.svg +load 322215-1.svg +load 323704-1.svg +load 325427-1.svg +load 326495-1.svg +load 326974-1.svg +load 327706-1.svg +load 327711-1.svg +load 328137-1.svg +load 329848-1.svg +load chrome://reftest/content/crashtests/layout/svg/crashtests/337408-1.xhtml +load 338301-1.xhtml +load 338312-1.xhtml +load 340083-1.svg +load 340945-1.svg +load 342923-1.html +load 343221-1.xhtml +load 344749-1.svg +load 344887-1.svg +load 344892-1.svg +load 344898-1.svg +load 344904-1.svg +load 345418-1.svg +load 348982-1.xhtml +load 354777-1.xhtml +load 359516-1.svg +load 361015-1.svg +load 361587-1.svg +load 363611-1.xhtml +load 364688-1.svg +load 366956-1.svg +load 366956-2.svg +load 367111-1.svg +load 367368-1.xhtml +load 369233-1.svg +load 369438-1.svg +load 369438-2.svg +load 371463-1.xhtml +load 371563-1.xhtml +load 375775-1.svg +load 378716.svg +load 380691-1.svg +load 384391-1.xhtml +load 384499-1.svg +load 384637-1.svg +load 384728-1.svg +load 385246-1.svg +load 385246-2.svg +load 385552-1.svg +load 385552-2.svg +load 385840-1.svg +load 385852-1.svg +load 386475-1.xhtml +load 386690-1.svg +load 387290-1.svg +load 402408-1.svg +load 404677-1.xhtml +load 409565-1.xhtml +load 420697-1.svg +load 420697-2.svg +load 429774-1.svg +load 441368-1.svg +load 453754-1.svg +load 455314-1.xhtml +load 458453.html +load 459666-1.html +load 459883.xhtml +load 461289-1.svg +load 464374-1.svg +load 466585-1.svg +load 467323-1.svg +load 467498-1.svg +load 470124-1.svg +load 472782-1.svg +load 474700-1.svg +load 475181-1.svg +load 475193-1.html +load 475302-1.svg +load 477935-1.html +load 478128-1.svg +skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 478511-1.svg +skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 483439-1.svg +load 492186-1.svg +load 508247-1.svg +load 512890-1.svg +load 515288-1.html +load 522394-1.svg +load 522394-2.svg +load 522394-3.svg +load 566216-1.svg +load 587336-1.html +load 590291-1.svg +load 601999-1.html +load 605626-1.svg +asserts(2) load 606914.xhtml # bug 606914, bug 718883 +load 610594-1.html +load 610954-1.html +load 612662-1.svg +load 612662-2.svg +load 612736-1.svg +load 612736-2.svg +load 614367-1.svg +load 620034-1.html +load 621598-1.svg +load 648819-1.html +load 655025-1.svg +load 655025-2.svg +load 655025-3.svg +load 657077-1.svg +load 669025-1.svg +load 669025-2.svg +load 682411-1.svg +load 692203-1.svg +load 692203-2.svg +load 693424-1.svg +load 709920-1.svg +load 709920-2.svg +load 713413-1.svg +load 722003-1.svg +load 725918-1.svg +load 732836-1.svg +load 740627-1.svg +load 740627-2.svg +load 743469.svg +load 757704-1.svg +load 757718-1.svg +load 757751-1.svg +load 767056-1.svg +load 767535-1.xhtml +load 768087-1.html +load 768351.svg +load 772313-1.svg +load 778492-1.svg +load 779971-1.svg +load 780764-1.svg +load 780963-1.html +load 782141-1.svg +load 784061-1.svg +load 788831-1.svg +load 789390-1.html +load 790072.svg +load 791826-1.svg +load 808318-1.svg +load 803562-1.svg +load 813420-1.svg +load 841163-1.svg +load 841812-1.svg +load 842009-1.svg +load 842630-1.svg +load 842909-1.svg +load 843072-1.svg +load 843917-1.svg +load 847139-1.svg +load 849688-1.svg +load 849688-2.svg +load 860378-1.svg +load 868904-1.svg +load 873806-1.svg +load 876831-1.svg +load 877029-1.svg +load 880925-1.svg +load 881031-1.svg +load 885608-1.svg +load 890782-1.svg +load 890783-1.svg +load 893510-1.svg +load 895311-1.svg +load 897342-1.svg +load 898909-1.svg +load 898951-1.svg +load 913990.html +load 919371-1.xhtml +load 950324-1.svg +load 951904-1.html +load 952270-1.svg +load 963086-1.svg +load 974746-1.svg +load 975773-1.svg +load 979407-1.svg +load 979407-2.svg +load 993443.svg +load 1016145.svg +load 1028512.svg +load 1140080-1.svg +load 1149542-1.svg +load 1156581-1.svg +load 1182496-1.html +load 1209525-1.svg +load 1223281-1.svg +load 1234726-1.svg +load 1322537-1.html +load 1322537-2.html +load 1322852.html +load 1348564.svg +load 1402109.html +load 1402124.html +load 1402486.html +load 1403656-1.html +load 1403656-2.html +load 1403656-3.html +load 1403656-4.html +load 1403656-5.html +load 1404086.html +load 1421807-1.html +load 1421807-2.html +load 1422226.html +load 1425434-1.html +load 1443092.html +load 1454201-1.html +load 1467552-1.html +load 1474982.html +load conditional-outer-svg-nondirty-reflow-assert.xhtml +load extref-test-1.xhtml +skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) pref(widget.windows.window_occlusion_tracking.enabled,false) load blob-merging-and-retained-display-list.html # Bug 1819154 +load empty-blob-merging.html +load grouping-empty-bounds.html +load 1480275.html +load 1480224.html +load 1502936.html +load 1504918.svg +load perspective-invalidation.html +load invalid_url.html +load 1535517-1.svg +load 1504072.html +load 1072758.html +load 1424031.html +load 1426575.html +load 1536892.html +load 1539318-1.svg +load 1548985-1.html +load 1548985-2.svg +load 1555851.html +skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) pref(widget.windows.window_occlusion_tracking.enabled,false) load invalidation-of-opacity-0.html # Bug 1819154 +load 1563779.html +load 1600855.html +load 1601824.html +load 1605223-1.html +load 1609663.html +skip-if(Android|/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 1671950.html # No print-preview support on android +load 1678947.html +load 1693032.html +load 1696505.html +load 1758029-1.html +HTTP load 1755770-1.html +HTTP load 1755770-2.html +load 1764936-1.html +load 1804958.html +load 1810260.html +load 1826444-1.html +load 1831419.html diff --git a/layout/svg/crashtests/empty-blob-merging.html b/layout/svg/crashtests/empty-blob-merging.html new file mode 100644 index 0000000000..4b603c19d9 --- /dev/null +++ b/layout/svg/crashtests/empty-blob-merging.html @@ -0,0 +1,48 @@ +<html class="reftest-wait"> +<style> +@keyframes spinnow { + 100% { + transform: rotate(360deg) scale(.2, .2); + } +} + +rect { + transform: rotate(0deg) scale(0.6, 1); + transform-origin: center; + animation: 5s spinnow infinite linear; +} + +</style> +<svg width=400 height=400> + <!-- + onwheel is needed so that we get a hit test info display item + before the transform on the rect + --> + <g onwheel="alert(1)"> + <g id="gr"> + <circle r=30 fill=yellow cx=300 cy=100 /> + <circle r=30 fill=yellow cx=10 cy=100 /> + <circle r=30 fill=yellow cx=300 cy=300 /> + <circle r=30 fill=yellow cx=10 cy=300 /> + </g> + <rect width=100 height=100 fill=blue x=100 y=100 /> + <g opacity=0.5> + <circle r=30 fill=pink cx=300 cy=100 /> + <circle r=30 fill=pink cx=10 cy=100 /> + <circle r=30 fill=pink cx=300 cy=300 /> + <circle r=30 fill=pink cx=10 cy=300 /> + </g> + </g> +</svg> +<script> + function blam() { + let gr = document.getElementById("gr"); + gr.remove(); + document.documentElement.removeAttribute("class"); + } +document.addEventListener("MozReftestInvalidate", function() { + requestAnimationFrame(function() { + blam(); + }); +}); +</script> diff --git a/layout/svg/crashtests/extref-test-1-resource.xhtml b/layout/svg/crashtests/extref-test-1-resource.xhtml new file mode 100644 index 0000000000..cd47ddc2f5 --- /dev/null +++ b/layout/svg/crashtests/extref-test-1-resource.xhtml @@ -0,0 +1,24 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> +<body style="margin:0"> + <embed type="application/x-shockwave-flash" src="data:application/x-shockwave-flash,This is a test"></embed> + <iframe src="date:text/plain,aaa"></iframe> + <div style="mask: url(#m1); width:500px; height:500px; background:lime;"></div> + + <svg:svg height="0"> + <svg:mask id="m1" maskUnits="objectBoundingBox" maskContentUnits="objectBoundingBox"> + <svg:linearGradient id="g" gradientUnits="objectBoundingBox" x2="0" y2="1"> + <svg:stop stop-color="white" offset="0"/> + <svg:stop stop-color="white" stop-opacity="0" offset="1"/> + </svg:linearGradient> + <svg:circle cx="0.25" cy="0.25" r="0.25" id="circle" fill="white"/> + <svg:rect x="0.5" y="0" width="0.5" height="1" fill="url(#g)"/> + </svg:mask> + </svg:svg> +</body> +</html> diff --git a/layout/svg/crashtests/extref-test-1.xhtml b/layout/svg/crashtests/extref-test-1.xhtml new file mode 100644 index 0000000000..932b679b1f --- /dev/null +++ b/layout/svg/crashtests/extref-test-1.xhtml @@ -0,0 +1,11 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/licenses/publicdomain/ +--> +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> +<body style="margin:0"> + <div style="mask: url(extref-test-1-resource.xhtml#m1); width:500px; height:500px; background:lime;"></div> +</body> +</html> diff --git a/layout/svg/crashtests/grouping-empty-bounds.html b/layout/svg/crashtests/grouping-empty-bounds.html new file mode 100644 index 0000000000..c9f688d0f0 --- /dev/null +++ b/layout/svg/crashtests/grouping-empty-bounds.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<html lang="en" class='reftest-wait'> +<meta charset="utf-8"> +<title>This testcase might create a non-empty display list with an empty set of drawing commands / items in the EventRecorder</title> + +<style> + +body { + margin: 0; +} + +.animated-opacity { + animation: opacity-animation 1s linear alternate infinite; +} + +@keyframes opacity-animation { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +</style> + +<svg style="width: 100px; height: 100px;"> + <rect class="animated-opacity" x="0" y="0" width="100" height="100"/> + <rect x="0" y="0" width="10" height="10" id="toremove"/> + <g transform="translate(10 10)"><rect x="120" y="0" width="1" height="1"/></g> +</svg> + +<script> + +window.addEventListener("MozReftestInvalidate", () => { + var elem = document.getElementById("toremove"); + elem.parentNode.removeChild(elem); + document.documentElement.removeAttribute('class'); +}); + +</script> diff --git a/layout/svg/crashtests/invalid_url.html b/layout/svg/crashtests/invalid_url.html new file mode 100644 index 0000000000..ac8671b1ce --- /dev/null +++ b/layout/svg/crashtests/invalid_url.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> +<head></head> +<body> + <!-- + If invalid url isn't correctly handled, this test will crash when + gfx.webrender.enabled=true and gfx.webrender.all=true + --> + <div style="padding: 1px; filter:url('data://not-valid-data');"></div> +</body> +</html> diff --git a/layout/svg/crashtests/invalidation-of-opacity-0.html b/layout/svg/crashtests/invalidation-of-opacity-0.html new file mode 100644 index 0000000000..50700e3908 --- /dev/null +++ b/layout/svg/crashtests/invalidation-of-opacity-0.html @@ -0,0 +1,26 @@ +<html class="reftest-wait"> + <script> + var i = 0; + var opac = [0.3, 0.2, 0, 0.3]; + + function f() { + document.getElementById("rim").setAttribute("opacity", opac[i]); + document.getElementById("circ").setAttribute("r", i + 10); + i++; + if (i > opac.length) { + document.documentElement.className = "" + } else { + requestAnimationFrame(f); + } + } + onload = () => requestAnimationFrame(f); +</script> + +<body> + <svg height="1000" width="1000"> + <circle cx="50" cy="50" r="40" fill="red" /> + <g id=rim clip-path="url(#myClip)" opacity=0> + <circle id="circ" cx="150" cy="150" r="40" fill="red" /> + </g> + <circle cx="250" cy="250" r="40" fill="red" /> + </svg> diff --git a/layout/svg/crashtests/masked-3d-transform.html b/layout/svg/crashtests/masked-3d-transform.html new file mode 100644 index 0000000000..6c70aae7fe --- /dev/null +++ b/layout/svg/crashtests/masked-3d-transform.html @@ -0,0 +1,20 @@ +<style> + svg, +svg * { + perspective: 1000px; +} +</style> +<svg style="max-width: 176px;" viewBox="0 0 279 169"> + <g transform="translate(-2.000000, -2.000000)"> + <g mask="url(#mask-2)"> + <g transform="translate(19.000000, 22.000000)"> + <g fill="#FFFFFF"> + <path d="M43,2.3 C41,2.3 39.9,3.3 39.9,5.6 L39.9,12.4 C39.9,14.7 41,15.7 43.1,15.7 C45.7,15.7 45.9,14.1 46.1,13 C46.1,12.7 46.4,12.4 46.8,12.4 C47.3,12.4 47.5,12.6 47.5,13.3 C47.5,15.3 45.8,17 42.9,17 C40.4,17 38.4,15.7 38.4,12.3 L38.4,5.5 C38.4,2.1 40.5,0.9 43,0.9 C45.9,0.9 47.5,2.6 47.5,4.5 C47.5,5.2 47.3,5.4 46.8,5.4 C46.3,5.4 46.1,5.2 46.1,4.9 C46.1,4 45.6,2.3 43,2.3 L43,2.3 Z"></path> + </g> + </g> + <ellipse fill="#000000" cx="157.9" cy="162.1" rx="157.9" ry="4.3"></ellipse> + </g> + </g> +</svg> + + diff --git a/layout/svg/crashtests/perspective-invalidation.html b/layout/svg/crashtests/perspective-invalidation.html new file mode 100644 index 0000000000..179836d500 --- /dev/null +++ b/layout/svg/crashtests/perspective-invalidation.html @@ -0,0 +1,9 @@ +<span> +<svg id="a" transform="skewX(0)" opacity="0"> +<text> +</span> +<marquee> +A +</marquee> +<svg xml:space="preserve"> +<use style="outline: auto; -webkit-perspective: 1px" xlink:href="#a" mask="url(#x)"> diff --git a/layout/svg/moz.build b/layout/svg/moz.build new file mode 100644 index 0000000000..d32dcc6363 --- /dev/null +++ b/layout/svg/moz.build @@ -0,0 +1,98 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "SVG") + +if CONFIG["ENABLE_TESTS"]: + MOCHITEST_MANIFESTS += [ + "tests/mochitest.ini", + ] + MOCHITEST_CHROME_MANIFESTS += [ + "tests/chrome.ini", + ] + +EXPORTS.mozilla += [ + "CSSClipPathInstance.h", + "DisplaySVGItem.h", + "FilterInstance.h", + "ISVGDisplayableFrame.h", + "ISVGSVGFrame.h", + "SVGClipPathFrame.h", + "SVGContainerFrame.h", + "SVGContextPaint.h", + "SVGFilterInstance.h", + "SVGForeignObjectFrame.h", + "SVGGeometryFrame.h", + "SVGGradientFrame.h", + "SVGImageContext.h", + "SVGImageFrame.h", + "SVGIntegrationUtils.h", + "SVGMaskFrame.h", + "SVGObserverUtils.h", + "SVGOuterSVGFrame.h", + "SVGPaintServerFrame.h", + "SVGTextFrame.h", + "SVGUseFrame.h", + "SVGUtils.h", +] + +UNIFIED_SOURCES += [ + "CSSClipPathInstance.cpp", + "CSSFilterInstance.cpp", + "DisplaySVGItem.cpp", + "FilterInstance.cpp", + "SVGAFrame.cpp", + "SVGClipPathFrame.cpp", + "SVGContainerFrame.cpp", + "SVGContextPaint.cpp", + "SVGFEContainerFrame.cpp", + "SVGFEImageFrame.cpp", + "SVGFELeafFrame.cpp", + "SVGFEUnstyledLeafFrame.cpp", + "SVGFilterFrame.cpp", + "SVGFilterInstance.cpp", + "SVGForeignObjectFrame.cpp", + "SVGGeometryFrame.cpp", + "SVGGFrame.cpp", + "SVGGradientFrame.cpp", + "SVGImageContext.cpp", + "SVGImageFrame.cpp", + "SVGInnerSVGFrame.cpp", + "SVGIntegrationUtils.cpp", + "SVGMarkerFrame.cpp", + "SVGMaskFrame.cpp", + "SVGObserverUtils.cpp", + "SVGOuterSVGFrame.cpp", + "SVGPaintServerFrame.cpp", + "SVGPatternFrame.cpp", + "SVGStopFrame.cpp", + "SVGSwitchFrame.cpp", + "SVGSymbolFrame.cpp", + "SVGTextFrame.cpp", + "SVGUseFrame.cpp", + "SVGUtils.cpp", + "SVGViewFrame.cpp", + "SVGViewportFrame.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" +LOCAL_INCLUDES += [ + "../../widget", + "../base", + "../generic", + "../painting", + "../style", + "../xul", + "/dom/base", + "/dom/svg", +] + +RESOURCE_FILES += [ + "svg.css", +] diff --git a/layout/svg/svg.css b/layout/svg/svg.css new file mode 100644 index 0000000000..af3a55343b --- /dev/null +++ b/layout/svg/svg.css @@ -0,0 +1,104 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * 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/. */ + +@namespace url(http://www.w3.org/2000/svg); +@namespace xml url(http://www.w3.org/XML/1998/namespace); + +style, script { + display: none; +} + +/* + * This is only to be overridden by the rule right below. + * + * NOTE(emilio): NodeCouldBeRendered in SVGUseElement.cpp relies on this. + */ +symbol { + display: none !important; +} + +/* + * From https://svgwg.org/svg2-draft/struct.html#SymbolNotes: + * + * > The generated instance of a 'symbol' that is the direct referenced element + * > of a 'use' element must always have a computed value of inline for the + * > display property. In other words, it must be rendered whenever the host + * > 'use' element is rendered. + * + * NOTE(emilio): other browsers instead just replace the `<symbol>` element by + * an `<svg>` element while cloning, but they don't implement the SVG2 + * selector-matching rules that would make that observable via selectors. + */ +symbol:-moz-use-shadow-tree-root { + display: inline !important; +} + +svg:not(:root), symbol, image, marker, pattern, foreignObject { + overflow: hidden; +} + +@media all and (-moz-is-glyph) { + :root { + fill: context-fill; + fill-opacity: context-fill-opacity; + stroke: context-stroke; + stroke-opacity: context-stroke-opacity; + stroke-width: context-value; + stroke-dasharray: context-value; + stroke-dashoffset: context-value; + } +} + +foreignObject { + appearance: none ! important; + margin: 0 ! important; + padding: 0 ! important; + border-width: 0 ! important; + white-space: normal; +} + +@media all and (-moz-is-resource-document) { + foreignObject *|* { + appearance: none !important; + } +} + +*|*::-moz-svg-foreign-content { + display: block !important; + /* We need to be an absolute and fixed container */ + transform: translate(0) !important; + text-indent: 0; +} + +/* Set |transform-origin:0 0;| for all SVG elements except outer-<svg>, + noting that 'svg' as a child of 'foreignObject' counts as outer-<svg>. +*/ +*:not(svg), +*:not(foreignObject) > svg { + transform-origin:0 0; +} + +*|*::-moz-svg-text { + unicode-bidi: inherit; + vector-effect: inherit; +} + +*[xml|space=preserve] { + white-space: -moz-pre-space; +} + +*|*::-moz-svg-marker-anon-child { + clip-path: inherit; + filter: inherit; + mask: inherit; + opacity: inherit; +} + +/* Make SVG shapes unselectable to avoid triggering AccessibleCaret on tap. + <mesh> will be supported in bug 1238882. */ +circle, ellipse, line, mesh, path, polygon, polyline, rect { + user-select: none; +} diff --git a/layout/svg/tests/chrome.ini b/layout/svg/tests/chrome.ini new file mode 100644 index 0000000000..cc1b542a66 --- /dev/null +++ b/layout/svg/tests/chrome.ini @@ -0,0 +1,8 @@ +[test_disabled_chrome.html] +support-files = + svg_example_test.html + svg_example_script.svg + +[test_context_properties_allowed_domains.html] +support-files = + file_context_fill_fallback_red.html diff --git a/layout/svg/tests/file_black_yellow.svg b/layout/svg/tests/file_black_yellow.svg new file mode 100644 index 0000000000..58c5689838 --- /dev/null +++ b/layout/svg/tests/file_black_yellow.svg @@ -0,0 +1,9 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect height="100%" width="100%" fill="yellow" /> + <rect height="50px" width="100px" fill="black" /> +</svg> diff --git a/layout/svg/tests/file_context_fill_fallback_red.html b/layout/svg/tests/file_context_fill_fallback_red.html new file mode 100644 index 0000000000..ac8b5b4203 --- /dev/null +++ b/layout/svg/tests/file_context_fill_fallback_red.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<style> + img { + -moz-context-properties: fill; + fill: green; + } +</style> +<img style="width: 100px; height: 100px;"> +<script> + let domain = location.search.substring(1); + let img = document.querySelector("img"); + img.src = img.alt = `http://${domain}/tests/layout/svg/tests/file_context_fill_fallback_red.svg`; +</script> diff --git a/layout/svg/tests/file_context_fill_fallback_red.svg b/layout/svg/tests/file_context_fill_fallback_red.svg new file mode 100644 index 0000000000..9088555b53 --- /dev/null +++ b/layout/svg/tests/file_context_fill_fallback_red.svg @@ -0,0 +1,8 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect height="100%" width="100%" fill="context-fill red" /> +</svg> diff --git a/layout/svg/tests/file_disabled_iframe.html b/layout/svg/tests/file_disabled_iframe.html new file mode 100644 index 0000000000..55eda75fde --- /dev/null +++ b/layout/svg/tests/file_disabled_iframe.html @@ -0,0 +1,81 @@ +<!doctype html> +<script> + window.is = window.parent.is; + window.SimpleTest = window.parent.SimpleTest; +</script> +<div id="testnodes"><span>hi</span> there <!-- mon ami --></div> +<script> + let t = document.getElementById('testnodes'); + t.innerHTML = null; + t.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg:svg")); + t.firstChild.textContent = "<foo>"; + is(t.innerHTML, "<svg:svg><foo></svg:svg>"); + + // This test crashes if the style tags are not handled correctly + t.innerHTML = `<svg version="1.1"> + <style> + circle { + fill: currentColor; + } + </style> + <g><circle cx="25.8" cy="9.3" r="1.5"/></g> + </svg> + `; + is(t.firstChild.tagName.toLowerCase(), 'svg'); + + // This test crashes if the script tags are not handled correctly + t.innerHTML = `<svg version="1.1"> + <scri` + `pt> + throw "badment, should never fire."; + </scri` + `pt> + <g><circle cx="25.8" cy="9.3" r="1.5"/></g> + </svg>`; + is(t.firstChild.tagName.toLowerCase(), 'svg'); + + t.innerHTML = null; + t.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); + is(t.firstChild.namespaceURI, "http://www.w3.org/2000/svg"); + t.firstChild.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "script")); + is(t.firstChild.firstChild.namespaceURI, "http://www.w3.org/2000/svg"); + t.firstChild.firstChild.textContent = "1&2<3>4\xA0"; + is(t.innerHTML, '<svg><script>1&2<3>4 \u003C/script></svg>'); + + t.innerHTML = null; + t.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); + is(t.firstChild.namespaceURI, "http://www.w3.org/2000/svg"); + t.firstChild.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "style")); + is(t.firstChild.firstChild.namespaceURI, "http://www.w3.org/2000/svg"); + t.firstChild.firstChild.textContent = "1&2<3>4\xA0"; + is(t.innerHTML, '<svg><style>1&2<3>4 \u003C/style></svg>'); + + // + // Tests for Bug 1673237 + // + + // This test fails if about:blank renders SVGs + t.innerHTML = null; + var iframe = document.createElement("iframe"); + iframe.setAttribute("src", "about:blank") + t.appendChild(iframe); + iframe.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg:svg")); + iframe.firstChild.textContent = "<foo>"; + is(iframe.innerHTML, "<svg:svg><foo></svg:svg>"); + + // This test fails if about:blank renders SVGs + var win = window.open("about:blank"); + win.document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg:svg")) + win.document.body.firstChild.textContent = "<foo>"; + is(win.document.body.innerHTML, "<svg:svg><foo></svg:svg>"); + win.close(); + + // This test fails if about:srcdoc renders SVGs + t.innerHTML = null; + iframe = document.createElement("iframe"); + iframe.srcdoc = "<svg:svg></svg:svg>"; + iframe.onload = function() { + iframe.contentDocument.body.firstChild.textContent = "<foo>"; + is(iframe.contentDocument.body.innerHTML, "<svg:svg><foo></svg:svg>"); + SimpleTest.finish(); + } + t.appendChild(iframe); +</script> diff --git a/layout/svg/tests/file_embed_sizing_both.svg b/layout/svg/tests/file_embed_sizing_both.svg new file mode 100644 index 0000000000..8bc39b7d02 --- /dev/null +++ b/layout/svg/tests/file_embed_sizing_both.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="200" height="400" viewBox="0 0 1 5"></svg> diff --git a/layout/svg/tests/file_embed_sizing_none.svg b/layout/svg/tests/file_embed_sizing_none.svg new file mode 100644 index 0000000000..714efc7ef0 --- /dev/null +++ b/layout/svg/tests/file_embed_sizing_none.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg"></svg> diff --git a/layout/svg/tests/file_embed_sizing_ratio.svg b/layout/svg/tests/file_embed_sizing_ratio.svg new file mode 100644 index 0000000000..b590bb7da6 --- /dev/null +++ b/layout/svg/tests/file_embed_sizing_ratio.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 5"></svg> diff --git a/layout/svg/tests/file_embed_sizing_size.svg b/layout/svg/tests/file_embed_sizing_size.svg new file mode 100644 index 0000000000..76f50b5f08 --- /dev/null +++ b/layout/svg/tests/file_embed_sizing_size.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="200" height="900"></svg> diff --git a/layout/svg/tests/file_filter_crossorigin.svg b/layout/svg/tests/file_filter_crossorigin.svg new file mode 100644 index 0000000000..a1b9ad0cab --- /dev/null +++ b/layout/svg/tests/file_filter_crossorigin.svg @@ -0,0 +1,25 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + xmlns:xlink="http://www.w3.org/1999/xlink"> + + <!-- We include these <use> elements simply to be sure the SVG resource URLs + get a chance to block 'onload', if they can be loaded. --> + <use xlink:href="http://mochi.test:8888/tests/layout/svg/tests/filters.svg#empty" /> + <use xlink:href="http://example.org/tests/layout/svg/tests/filters.svg#empty" /> + + <!-- giant yellow rect in the background, just so you can visually tell + that this SVG file has loaded/rendered. --> + <rect height="100%" width="100%" fill="yellow" /> + + <!-- For both rects below: if it's black, its filter resolved successfully. + If it's yellow, it means we failed to load the resource + (e.g. because it was blocked as a cross-origin resource). --> + <rect height="50px" width="100px" fill="yellow" + filter="url(http://mochi.test:8888/tests/layout/svg/tests/filters.svg#NonWhiteToBlack)"/> + <rect y="50px" + height="50px" width="100px" fill="yellow" + filter="url(http://example.org/tests/layout/svg/tests/filters.svg#NonWhiteToBlack)"/> +</svg> diff --git a/layout/svg/tests/file_yellow_black.svg b/layout/svg/tests/file_yellow_black.svg new file mode 100644 index 0000000000..77c14c9af8 --- /dev/null +++ b/layout/svg/tests/file_yellow_black.svg @@ -0,0 +1,9 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <rect height="100%" width="100%" fill="yellow" /> + <rect y="50px" height="50px" width="100px" fill="black" /> +</svg> diff --git a/layout/svg/tests/filters.svg b/layout/svg/tests/filters.svg new file mode 100644 index 0000000000..213df7fc93 --- /dev/null +++ b/layout/svg/tests/filters.svg @@ -0,0 +1,28 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" version="1.1"> + <defs> + + <!-- so that other documents can svg:use this one and force it to + load before onload --> + <g id="empty" /> + + <!-- Keep all white pixels white, and change any others to black. --> + <!-- NOTE: alpha is preserved, so it will not adjust alpha edges --> + <filter id="NonWhiteToBlack" x="0%" y="0%" width="100%" height="100%"> + <feComponentTransfer> + <feFuncR type="linear" slope="-1" intercept="1" /> + <feFuncG type="linear" slope="-1" intercept="1" /> + <feFuncB type="linear" slope="-1" intercept="1" /> + </feComponentTransfer> + <feColorMatrix type="matrix" values="255 255 255 0 0 + 255 255 255 0 0 + 255 255 255 0 0 + 0 0 0 1 0" /> + <feComponentTransfer> + <feFuncR type="linear" slope="-1" intercept="1" /> + <feFuncG type="linear" slope="-1" intercept="1" /> + <feFuncB type="linear" slope="-1" intercept="1" /> + </feComponentTransfer> + </filter> + </defs> +</svg> diff --git a/layout/svg/tests/mochitest.ini b/layout/svg/tests/mochitest.ini new file mode 100644 index 0000000000..3397e849b5 --- /dev/null +++ b/layout/svg/tests/mochitest.ini @@ -0,0 +1,30 @@ +[DEFAULT] +support-files = + file_disabled_iframe.html + file_context_fill_fallback_red.svg + +[test_disabled.html] +[test_embed_sizing.html] +support-files = + file_embed_sizing_none.svg + file_embed_sizing_size.svg + file_embed_sizing_ratio.svg + file_embed_sizing_both.svg +skip-if = + http3 + +[test_filter_crossorigin.html] +support-files = + filters.svg + file_filter_crossorigin.svg + file_black_yellow.svg + file_yellow_black.svg +# Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" +skip-if = + xorigin + os == "linux" && bits == 64 #Bug 1642198 + win10_2004 && !debug # Bug 1642198 +[test_hover_near_text.html] +[test_multiple_font_size.html] +[test_use_tree_cycle.html] +[test_bug1544209.html] diff --git a/layout/svg/tests/svg_example_script.svg b/layout/svg/tests/svg_example_script.svg new file mode 100644 index 0000000000..5eab758f92 --- /dev/null +++ b/layout/svg/tests/svg_example_script.svg @@ -0,0 +1,7 @@ +<svg version="1.1"> + <script> + document.documentElement.style.backgroundColor = 'rebeccapurple'; + throw "badment, should never fire."; + </script> + <g><circle cx="25.8" cy="9.3" r="1.5"/></g> +</svg> diff --git a/layout/svg/tests/svg_example_test.html b/layout/svg/tests/svg_example_test.html new file mode 100644 index 0000000000..45c31c98b4 --- /dev/null +++ b/layout/svg/tests/svg_example_test.html @@ -0,0 +1,7 @@ +<svg id="layout" viewBox="0 0 120 120" version="1.1" + xmlns="http://www.w3.org/2000/svg"> + <circle cx="60" cy="60" r="50"/> +</svg> + +<svg id="svgel"> +</svg> diff --git a/layout/svg/tests/test_bug1544209.html b/layout/svg/tests/test_bug1544209.html new file mode 100644 index 0000000000..b2226b3ea9 --- /dev/null +++ b/layout/svg/tests/test_bug1544209.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1544209">Mozilla Bug 1544209</a> + +<svg height="800px" width="800px"> + <text transform="scale(3,3)" id="a" x="20px" y="20px">ABC<tspan id="b">ABC</tspan></text> +</svg> + +<script type="application/javascript"> + let a = document.getElementById("a"), + b = document.getElementById("b"); + + let wtext = a.getBoundingClientRect().width, + wtspan = b.getBoundingClientRect().width; + + ok(wtext >= wtspan, "<tspan> should not be wider than <text>"); + isfuzzy((wtext - wtspan) / wtext, 0.5, 0.1, "<tspan> should be approximately half of the <text> width"); +</script> diff --git a/layout/svg/tests/test_context_properties_allowed_domains.html b/layout/svg/tests/test_context_properties_allowed_domains.html new file mode 100644 index 0000000000..26f38a8770 --- /dev/null +++ b/layout/svg/tests/test_context_properties_allowed_domains.html @@ -0,0 +1,95 @@ +<!doctype html> +<meta charset="utf-8"> +<title>Bug 1699892 - SVG context properties for allowed domains</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script src="chrome://mochikit/content/tests/SimpleTest/WindowSnapshot.js"></script> +<script> + /** + * Returns a Promise that resolves when target fires a load event. + */ + function waitForLoad(target) { + return new Promise(resolve => { + target.addEventListener("load", () => { + if (event.target == target) { + resolve(); + }}, { once: true }); + }); + } + + function makeContextFillFrame(source) { + return ` + <style> + img { + -moz-context-properties: fill; + fill: green; + } + </style> + <img src="${source}" style="width: 100px; height: 100px;"> + `; + } + + /** + * Creates an iframe, loads src in it, and waits for the load event + * for the iframe to fire. Then it snapshots the iframe and returns + * the snapshot (and removes the iframe from the document, to clean up). + * + * src can be a URL starting with http, or is otherwise assumed to be + * a srcdoc string. + */ + async function loadSrcImageAndSnapshot({ src, srcdoc }) { + let frame = document.createElement("iframe"); + document.body.appendChild(frame); + + if (src) { + frame.src = src; + } else { + frame.srcdoc = srcdoc; + } + + await waitForLoad(frame); + + let snapshot = await snapshotWindow(frame, false); + document.body.removeChild(frame); + return snapshot; + } + + add_task(async () => { + const ALLOWED_DOMAIN = "example.org"; + const DISALLOWED_DOMAIN = "example.com"; + + await SpecialPowers.pushPrefEnv({ + set: [["svg.context-properties.content.allowed-domains", ALLOWED_DOMAIN]] + }); + + // When the context properties are allowed, we expect a green + // square. When they are not allowed, we expected a red square. + + let redReference = await loadSrcImageAndSnapshot({ + srcdoc: `<div style="width: 100px; height: 100px; background: red"></div>`, + }); + + let greenReference = await loadSrcImageAndSnapshot({ + srcdoc: `<div style="width: 100px; height: 100px; background: green"></div>`, + }); + + let allowedSnapshot = await loadSrcImageAndSnapshot({ + src: `file_context_fill_fallback_red.html?${ALLOWED_DOMAIN}` + }); + + let disallowedSnapshot = await loadSrcImageAndSnapshot({ + src: `file_context_fill_fallback_red.html?${DISALLOWED_DOMAIN}` + }); + + const kNoFuzz = null; + info("Reference snapshots should look different from each other"); + assertSnapshots(redReference, greenReference, false, kNoFuzz, "red", "green"); + + info("Allowed domain should be green"); + assertSnapshots(allowedSnapshot, greenReference, true, kNoFuzz, ALLOWED_DOMAIN, "green"); + + info("Disallowed domain should be red"); + assertSnapshots(disallowedSnapshot, redReference, true, kNoFuzz, DISALLOWED_DOMAIN, "red"); + }); +</script> +<body> +</body> diff --git a/layout/svg/tests/test_disabled.html b/layout/svg/tests/test_disabled.html new file mode 100644 index 0000000000..acf52e73a2 --- /dev/null +++ b/layout/svg/tests/test_disabled.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<!-- +Copied from https://bugzilla.mozilla.org/show_bug.cgi?id=744830 +--> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=166235">Mozilla Bug 166235</a> +<iframe></iframe> +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [["svg.disabled", true]]}, function() { + document.querySelector('iframe').src = "file_disabled_iframe.html"; + }); +</script> diff --git a/layout/svg/tests/test_disabled_chrome.html b/layout/svg/tests/test_disabled_chrome.html new file mode 100644 index 0000000000..e7564f17f2 --- /dev/null +++ b/layout/svg/tests/test_disabled_chrome.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=744830 +--> +<head> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=166235">Mozilla Bug 166235</a> +<div id="testnodes"><span>hi</span> there <!-- mon ami --></div> +<pre id="test"> +<script type="application/javascript"> + add_task(async function() { + const initialPrefValue = SpecialPowers.getBoolPref("svg.disabled"); + SpecialPowers.setBoolPref("svg.disabled", true); + const Cu = SpecialPowers.Components.utils; + const { ContentTaskUtils } = ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm"); + let t = document.getElementById('testnodes'); + + let url = 'chrome://mochitests/content/chrome/layout/svg/tests/svg_example_test.html' + const chromeIframeEl = document.createElement('iframe'); + let chromeLoadPromise = ContentTaskUtils.waitForEvent(chromeIframeEl, 'load', false); + chromeIframeEl.src = url; + t.appendChild(chromeIframeEl); + + await chromeLoadPromise; + const chromeBR = chromeIframeEl.contentDocument.body.getBoundingClientRect(); + + url = "http://mochi.test:8888/chrome/layout/svg/tests/svg_example_test.html"; + const iframeEl = document.createElement('iframe'); + iframeEl.src = url; + let loadPromise = ContentTaskUtils.waitForEvent(iframeEl, 'load', false); + t.appendChild(iframeEl); + await loadPromise; + + const contentBR = iframeEl.contentDocument.body.getBoundingClientRect(); + ok(chromeBR.height > contentBR.height, "Chrome content height should be bigger than content due to layout"); + + url = "http://mochi.test:8888/chrome/layout/svg/tests/svg_example_script.svg"; + const iframeElScript = document.createElement("iframe"); + let loadPromiseScript = ContentTaskUtils.waitForEvent(iframeElScript, "load", false); + iframeElScript.src = url; + t.appendChild(iframeElScript); + await loadPromiseScript; + ok(!iframeElScript.contentDocument.documentElement.style, "Content should not be styled"); + + SpecialPowers.setBoolPref("svg.disabled", initialPrefValue); + }); +</script> +</pre> +</body> +</html> diff --git a/layout/svg/tests/test_embed_sizing.html b/layout/svg/tests/test_embed_sizing.html new file mode 100644 index 0000000000..9afbc7ca7d --- /dev/null +++ b/layout/svg/tests/test_embed_sizing.html @@ -0,0 +1,65 @@ +<!DOCTYPE html> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> + +<p>Test intrinsic sizing of embed elements referencing SVG documents, both same +origin and cross origin.</p> + +<div id="container" style="width: 500px;"></div> + +<script> +const TESTS = [ + { outer: "none", inner: "none", expected: "300x150" }, + { outer: "none", inner: "size", expected: "200x900" }, + { outer: "none", inner: "ratio", expected: "500x2500" }, + { outer: "none", inner: "both", expected: "200x400" }, + { outer: "size", inner: "none", expected: "100x150" }, + { outer: "size", inner: "size", expected: "100x450" }, + { outer: "size", inner: "ratio", expected: "100x500" }, + { outer: "size", inner: "both", expected: "100x200" }, + { outer: "ratio", inner: "none", expected: "500x1500" }, + { outer: "ratio", inner: "size", expected: "200x900" }, + { outer: "ratio", inner: "ratio", expected: "500x1500" }, + { outer: "ratio", inner: "both", expected: "200x400" }, + { outer: "both", inner: "none", expected: "100x300" }, + { outer: "both", inner: "size", expected: "100x300" }, + { outer: "both", inner: "ratio", expected: "100x300" }, + { outer: "both", inner: "both", expected: "100x300" }, +]; + +add_task(async function() { + for (let test of TESTS) { + for (let crossorigin of [false, true]) { + let host = crossorigin ? "http://example.org" : "http://mochi.test:8888"; + let e = document.createElement("embed"); + + switch (test.outer) { + case "none": + break; + case "size": + e.style.width = "100px"; + break; + case "ratio": + e.style.aspectRatio = "1 / 3"; + break; + case "both": + e.style.width = "100px"; + e.style.aspectRatio = "1 / 3"; + break; + default: + throw new Error("unexpected subtest"); + } + + await new Promise(function(resolve) { + e.src = host + location.pathname.replace("test_embed_sizing.html", `file_embed_sizing_${test.inner}.svg`); + e.onload = resolve; + container.append(e); + }); + + let desc = `Subtest (${test.outer}/${test.inner}/${crossorigin ? 'cross' : 'same'} origin)`; + is(`${e.offsetWidth}x${e.offsetHeight}`, test.expected, desc); + e.remove(); + } + } +}); +</script> diff --git a/layout/svg/tests/test_filter_crossorigin.html b/layout/svg/tests/test_filter_crossorigin.html new file mode 100644 index 0000000000..e31c13bee1 --- /dev/null +++ b/layout/svg/tests/test_filter_crossorigin.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=695385 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 695385</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="run()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=695385">Mozilla Bug 695385</a> +<br> +<!-- These iframes' renderings are expected to match: --> +<iframe src="http://mochi.test:8888/tests/layout/svg/tests/file_filter_crossorigin.svg"></iframe> +<iframe src="file_black_yellow.svg"></iframe> +<br> +<!-- These iframes' renderings are expected to match: --> +<iframe src="http://example.org/tests/layout/svg/tests/file_filter_crossorigin.svg"></iframe> +<iframe src="file_yellow_black.svg"></iframe> + +<pre id="test"> +<script type="application/javascript"> + +function promiseExecuteSoon() { + return new Promise(SimpleTest.executeSoon); +} + +// Main Function +async function run() { + SimpleTest.waitForExplicitFinish(); + + // XXXdholbert Wait a few ticks, to give the iframes a little more + // opportunity to load their external resources before we call + // snapshotWindow. This is an attempt at a workaround/diagnostic for + // https://bugzilla.mozilla.org/show_bug.cgi?id=1706542 + for (let i = 0; i < 60; i++) { + await promiseExecuteSoon(); + } + + let snapshots = new Array(4); + for (let i = 0; i < snapshots.length; i++) { + snapshots[i] = await snapshotWindow(frames[i], false); + } + + // Compare mochi.test iframe against its reference: + assertSnapshots(snapshots[0], snapshots[1], true, null, + "Testcase loaded from mochi.test", "Reference: black/yellow"); + + // Compare example.org iframe against its reference: + assertSnapshots(snapshots[2], snapshots[3], true, null, + "Testcase loaded from example.org", "Reference: yellow/black"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/layout/svg/tests/test_hover_near_text.html b/layout/svg/tests/test_hover_near_text.html new file mode 100644 index 0000000000..2bf808e61a --- /dev/null +++ b/layout/svg/tests/test_hover_near_text.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=UTF-8"/> + <title>Test mouse hover near text element inside svg element with viewBox attribute</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + </head> + <body style="margin:0"> + <div> + <svg viewBox="-1 -1 2 2" width="300" height="300"> + <text style="font-size:0.1px" onmouseover="this.setAttribute('fill', 'red')">Hi</text> + </svg> + </div> + <p> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1519144">Mozilla Bug 1519144</a> + </p> + <script type="application/javascript"> + let utils = SpecialPowers.getDOMWindowUtils(window); + utils.sendMouseEvent('mousemove', 155, 125, 0, 0, 0); //hover above the text + utils.sendMouseEvent('mousemove', 125, 155, 0, 0, 0); //hover to the left of the text + requestIdleCallback(() => { + ok(!document.getElementsByTagName('text')[0].hasAttribute('fill'), + 'Text element should not receive an event'); + SimpleTest.finish(); + }); + SimpleTest.waitForExplicitFinish() + </script> + </body> +</html> diff --git a/layout/svg/tests/test_multiple_font_size.html b/layout/svg/tests/test_multiple_font_size.html new file mode 100644 index 0000000000..aca32eac03 --- /dev/null +++ b/layout/svg/tests/test_multiple_font_size.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370646">Mozilla Bug 1370646</a> + +<svg xmlns="http://www.w3.org/2000/svg" width="440" height="100" viewBox="0 0 440 100"> + <text> + <tspan id="a" style="font-size:100px">3</tspan> + </text> + <text> + <tspan id="b" style="font-size:100px">3</tspan> + <tspan style="font-size:0.1px">0</tspan> + </text> +</svg> + +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + let alen = document.getElementById("a").getComputedTextLength(), + blen = document.getElementById("b").getComputedTextLength(); + + SimpleTest.isfuzzy(alen, blen, 5, "lengths should be close"); + + SimpleTest.finish(); +</script> diff --git a/layout/svg/tests/test_use_tree_cycle.html b/layout/svg/tests/test_use_tree_cycle.html new file mode 100644 index 0000000000..e820f0f2a2 --- /dev/null +++ b/layout/svg/tests/test_use_tree_cycle.html @@ -0,0 +1,37 @@ +<!doctype html> +<title>Test for bug 1531333</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<style> + symbol { display: block } +</style> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="10" height="10"> + <symbol id="svg-sprite" viewBox="0 0 133 230866"> + <title>svg-sprite</title> + <symbol id="svg-sprite" viewBox="0 0 133 230866"> + <title>svg-sprite</title> + <use xlink:href="#svg-sprite" width="500" height="500" /> + </symbol> + <use xlink:href="#svg-sprite" y="1601" width="133" height="228958" /> + </symbol> + <use xlink:href="#svg-sprite" y="1601" width="133" height="230866" /> +</svg> +<script> +function countUseElements(subtreeRoot) { + if (!subtreeRoot) + return 0; + + let i = 0; + for (const use of subtreeRoot.querySelectorAll("use")) + i += 1 + countUseElements(SpecialPowers.wrap(use).openOrClosedShadowRoot); + return i; +} +SimpleTest.waitForExplicitFinish(); +onload = function() { + document.body.offsetTop; + // The three in the document, plus the two created from the element that's + // not in the <symbol> subtree. + is(countUseElements(document), 5, "Shouldn't create more than 5 use elements"); + SimpleTest.finish(); +} +</script> |