From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- dom/canvas/CanvasRenderingContext2D.cpp | 6847 +++++++++++++++++++++++++++++++ 1 file changed, 6847 insertions(+) create mode 100644 dom/canvas/CanvasRenderingContext2D.cpp (limited to 'dom/canvas/CanvasRenderingContext2D.cpp') diff --git a/dom/canvas/CanvasRenderingContext2D.cpp b/dom/canvas/CanvasRenderingContext2D.cpp new file mode 100644 index 0000000000..28579d4b75 --- /dev/null +++ b/dom/canvas/CanvasRenderingContext2D.cpp @@ -0,0 +1,6847 @@ +/* -*- 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/. */ + +#include "CanvasRenderingContext2D.h" + +#include "mozilla/gfx/Helpers.h" +#include "nsXULElement.h" + +#include "nsMathUtils.h" + +#include "nsContentUtils.h" + +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/GeckoBindings.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/SVGImageContext.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FontFaceSetImpl.h" +#include "mozilla/dom/FontFaceSet.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/GeneratePlaceholderCanvasData.h" +#include "mozilla/dom/VideoFrame.h" +#include "mozilla/gfx/CanvasManagerChild.h" +#include "nsPresContext.h" + +#include "nsIInterfaceRequestorUtils.h" +#include "nsIFrame.h" +#include "nsError.h" + +#include "nsCSSPseudoElements.h" +#include "nsComputedDOMStyle.h" + +#include "nsPrintfCString.h" + +#include "nsRFPService.h" +#include "nsReadableUtils.h" + +#include "nsColor.h" +#include "nsGfxCIID.h" +#include "nsIDocShell.h" +#include "nsPIDOMWindow.h" +#include "nsDisplayList.h" +#include "nsFocusManager.h" + +#include "nsTArray.h" + +#include "ImageEncoder.h" +#include "ImageRegion.h" + +#include "gfxContext.h" +#include "gfxPlatform.h" +#include "gfxFont.h" +#include "gfxBlur.h" +#include "gfxTextRun.h" +#include "gfxUtils.h" + +#include "nsFrameLoader.h" +#include "nsBidiPresUtils.h" +#include "LayerUserData.h" +#include "CanvasUtils.h" +#include "nsIMemoryReporter.h" +#include "nsStyleUtil.h" +#include "CanvasImageCache.h" + +#include + +#include "jsapi.h" +#include "jsfriendapi.h" +#include "js/Array.h" // JS::GetArrayLength +#include "js/Conversions.h" +#include "js/experimental/TypedData.h" // JS_NewUint8ClampedArray, JS_GetUint8ClampedArrayData +#include "js/HeapAPI.h" +#include "js/PropertyAndElement.h" // JS_GetElement +#include "js/Warnings.h" // JS::WarnASCII + +#include "mozilla/Alignment.h" +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/dom/CanvasGradient.h" +#include "mozilla/dom/CanvasPattern.h" +#include "mozilla/dom/DOMMatrix.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/ImageData.h" +#include "mozilla/dom/PBrowserParent.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/dom/TypedArray.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/FilterInstance.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Tools.h" +#include "mozilla/gfx/PathHelpers.h" +#include "mozilla/gfx/DataSurfaceHelpers.h" +#include "mozilla/gfx/PatternHelpers.h" +#include "mozilla/gfx/Swizzle.h" +#include "mozilla/layers/ImageBridgeChild.h" +#include "mozilla/layers/PersistentBufferProvider.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Preferences.h" +#include "mozilla/RestyleManager.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_gfx.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Unused.h" +#include "nsCCUncollectableMarker.h" +#include "nsWrapperCacheInlines.h" +#include "mozilla/dom/CanvasRenderingContext2DBinding.h" +#include "mozilla/dom/CanvasPath.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/SVGImageElement.h" +#include "mozilla/dom/TextMetrics.h" +#include "mozilla/FloatingPoint.h" +#include "nsGlobalWindowInner.h" +#include "nsDeviceContext.h" +#include "nsFontMetrics.h" +#include "nsLayoutUtils.h" +#include "Units.h" +#include "mozilla/CycleCollectedJSRuntime.h" +#include "mozilla/ServoCSSParser.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/layers/CanvasClient.h" +#include "mozilla/layers/WebRenderUserData.h" +#include "mozilla/layers/WebRenderCanvasRenderer.h" +#include "WindowRenderer.h" +#include "GeckoBindings.h" + +#undef free // apparently defined by some windows header, clashing with a + // free() method in SkTypes.h + +#ifdef XP_WIN +# include "gfxWindowsPlatform.h" +#endif + +// windows.h (included by chromium code) defines this, in its infinite wisdom +#undef DrawText + +using namespace mozilla; +using namespace mozilla::CanvasUtils; +using namespace mozilla::css; +using namespace mozilla::gfx; +using namespace mozilla::image; +using namespace mozilla::ipc; +using namespace mozilla::layers; + +namespace mozilla::dom { + +// Cap sigma to avoid overly large temp surfaces. +const Float SIGMA_MAX = 100; + +const size_t MAX_STYLE_STACK_SIZE = 1024; + +/* Memory reporter stuff */ +static Atomic gCanvasAzureMemoryUsed(0); + +// Adds Save() / Restore() calls to the scope. +class MOZ_RAII AutoSaveRestore { + public: + explicit AutoSaveRestore(CanvasRenderingContext2D* aCtx) : mCtx(aCtx) { + mCtx->Save(); + } + ~AutoSaveRestore() { mCtx->Restore(); } + + private: + RefPtr mCtx; +}; + +// This is KIND_OTHER because it's not always clear where in memory the pixels +// of a canvas are stored. Furthermore, this memory will be tracked by the +// underlying surface implementations. See bug 655638 for details. +class Canvas2dPixelsReporter final : public nsIMemoryReporter { + ~Canvas2dPixelsReporter() = default; + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) override { + MOZ_COLLECT_REPORT("canvas-2d-pixels", KIND_OTHER, UNITS_BYTES, + gCanvasAzureMemoryUsed, + "Memory used by 2D canvases. Each canvas requires " + "(width * height * 4) bytes."); + + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(Canvas2dPixelsReporter, nsIMemoryReporter) + +class CanvasConicGradient : public CanvasGradient { + public: + CanvasConicGradient(CanvasRenderingContext2D* aContext, Float aAngle, + const Point& aCenter) + : CanvasGradient(aContext, Type::CONIC), + mAngle(aAngle), + mCenter(aCenter) {} + + const Float mAngle; + const Point mCenter; +}; + +class CanvasRadialGradient : public CanvasGradient { + public: + CanvasRadialGradient(CanvasRenderingContext2D* aContext, + const Point& aBeginOrigin, Float aBeginRadius, + const Point& aEndOrigin, Float aEndRadius) + : CanvasGradient(aContext, Type::RADIAL), + mCenter1(aBeginOrigin), + mCenter2(aEndOrigin), + mRadius1(aBeginRadius), + mRadius2(aEndRadius) {} + + Point mCenter1; + Point mCenter2; + Float mRadius1; + Float mRadius2; +}; + +class CanvasLinearGradient : public CanvasGradient { + public: + CanvasLinearGradient(CanvasRenderingContext2D* aContext, const Point& aBegin, + const Point& aEnd) + : CanvasGradient(aContext, Type::LINEAR), mBegin(aBegin), mEnd(aEnd) {} + + protected: + friend struct CanvasBidiProcessor; + friend class CanvasGeneralPattern; + + // Beginning of linear gradient. + Point mBegin; + // End of linear gradient. + Point mEnd; +}; + +bool CanvasRenderingContext2D::PatternIsOpaque( + CanvasRenderingContext2D::Style aStyle, bool* aIsColor) const { + const ContextState& state = CurrentState(); + bool opaque = false; + bool color = false; + if (state.globalAlpha >= 1.0) { + if (state.patternStyles[aStyle] && state.patternStyles[aStyle]->mSurface) { + opaque = IsOpaque(state.patternStyles[aStyle]->mSurface->GetFormat()); + } else if (!state.gradientStyles[aStyle]) { + // TODO: for gradient patterns we could check that all stops are opaque + // colors. + // it's a color pattern. + opaque = sRGBColor::FromABGR(state.colorStyles[aStyle]).a >= 1.0; + color = true; + } + } + if (aIsColor) { + *aIsColor = color; + } + return opaque; +} + +// This class is named 'GeneralCanvasPattern' instead of just +// 'GeneralPattern' to keep Windows PGO builds from confusing the +// GeneralPattern class in gfxContext.cpp with this one. +class CanvasGeneralPattern { + public: + using Style = CanvasRenderingContext2D::Style; + using ContextState = CanvasRenderingContext2D::ContextState; + + Pattern& ForStyle(CanvasRenderingContext2D* aCtx, Style aStyle, + DrawTarget* aRT) { + // This should only be called once or the mPattern destructor will + // not be executed. + NS_ASSERTION( + !mPattern.GetPattern(), + "ForStyle() should only be called once on CanvasGeneralPattern!"); + + const ContextState& state = aCtx->CurrentState(); + + if (state.StyleIsColor(aStyle)) { + mPattern.InitColorPattern(ToDeviceColor(state.colorStyles[aStyle])); + } else if (state.gradientStyles[aStyle] && + state.gradientStyles[aStyle]->GetType() == + CanvasGradient::Type::LINEAR) { + auto gradient = static_cast( + state.gradientStyles[aStyle].get()); + + mPattern.InitLinearGradientPattern( + gradient->mBegin, gradient->mEnd, + gradient->GetGradientStopsForTarget(aRT)); + } else if (state.gradientStyles[aStyle] && + state.gradientStyles[aStyle]->GetType() == + CanvasGradient::Type::RADIAL) { + auto gradient = static_cast( + state.gradientStyles[aStyle].get()); + + mPattern.InitRadialGradientPattern( + gradient->mCenter1, gradient->mCenter2, gradient->mRadius1, + gradient->mRadius2, gradient->GetGradientStopsForTarget(aRT)); + } else if (state.gradientStyles[aStyle] && + state.gradientStyles[aStyle]->GetType() == + CanvasGradient::Type::CONIC) { + auto gradient = + static_cast(state.gradientStyles[aStyle].get()); + + mPattern.InitConicGradientPattern( + gradient->mCenter, gradient->mAngle, 0, 1, + gradient->GetGradientStopsForTarget(aRT)); + } else if (state.patternStyles[aStyle]) { + aCtx->DoSecurityCheck(state.patternStyles[aStyle]->mPrincipal, + state.patternStyles[aStyle]->mForceWriteOnly, + state.patternStyles[aStyle]->mCORSUsed); + + ExtendMode mode; + if (state.patternStyles[aStyle]->mRepeat == + CanvasPattern::RepeatMode::NOREPEAT) { + mode = ExtendMode::CLAMP; + } else { + mode = ExtendMode::REPEAT; + } + + SamplingFilter samplingFilter; + if (state.imageSmoothingEnabled) { + samplingFilter = SamplingFilter::GOOD; + } else { + samplingFilter = SamplingFilter::POINT; + } + + mPattern.InitSurfacePattern(state.patternStyles[aStyle]->mSurface, mode, + state.patternStyles[aStyle]->mTransform, + samplingFilter); + } + + return *mPattern.GetPattern(); + } + + GeneralPattern mPattern; +}; + +/* This is an RAII based class that can be used as a drawtarget for + * operations that need to have a filter applied to their results. + * All coordinates passed to the constructor are in device space. + */ +class AdjustedTargetForFilter { + public: + using ContextState = CanvasRenderingContext2D::ContextState; + + AdjustedTargetForFilter(CanvasRenderingContext2D* aCtx, + DrawTarget* aFinalTarget, + const gfx::IntPoint& aFilterSpaceToTargetOffset, + const gfx::IntRect& aPreFilterBounds, + const gfx::IntRect& aPostFilterBounds, + gfx::CompositionOp aCompositionOp) + : mFinalTarget(aFinalTarget), + mCtx(aCtx), + mPostFilterBounds(aPostFilterBounds), + mOffset(aFilterSpaceToTargetOffset), + mCompositionOp(aCompositionOp) { + nsIntRegion sourceGraphicNeededRegion; + nsIntRegion fillPaintNeededRegion; + nsIntRegion strokePaintNeededRegion; + + FilterSupport::ComputeSourceNeededRegions( + aCtx->CurrentState().filter, mPostFilterBounds, + sourceGraphicNeededRegion, fillPaintNeededRegion, + strokePaintNeededRegion); + + mSourceGraphicRect = sourceGraphicNeededRegion.GetBounds(); + mFillPaintRect = fillPaintNeededRegion.GetBounds(); + mStrokePaintRect = strokePaintNeededRegion.GetBounds(); + + mSourceGraphicRect = mSourceGraphicRect.Intersect(aPreFilterBounds); + + if (mSourceGraphicRect.IsEmpty()) { + // The filter might not make any use of the source graphic. We need to + // create a DrawTarget that we can return from DT() anyway, so we'll + // just use a 1x1-sized one. + mSourceGraphicRect.SizeTo(1, 1); + } + + if (!mFinalTarget->CanCreateSimilarDrawTarget(mSourceGraphicRect.Size(), + SurfaceFormat::B8G8R8A8)) { + mTarget = mFinalTarget; + mCtx = nullptr; + mFinalTarget = nullptr; + return; + } + + mTarget = mFinalTarget->CreateSimilarDrawTarget(mSourceGraphicRect.Size(), + SurfaceFormat::B8G8R8A8); + + if (mTarget) { + // See bug 1524554. + mTarget->ClearRect(gfx::Rect()); + } + + if (!mTarget || !mTarget->IsValid()) { + // XXX - Deal with the situation where our temp size is too big to + // fit in a texture (bug 1066622). + mTarget = mFinalTarget; + mCtx = nullptr; + mFinalTarget = nullptr; + return; + } + + mTarget->SetTransform(mFinalTarget->GetTransform().PostTranslate( + -mSourceGraphicRect.TopLeft() + mOffset)); + } + + // Return a SourceSurface that contains the FillPaint or StrokePaint source. + already_AddRefed DoSourcePaint( + gfx::IntRect& aRect, CanvasRenderingContext2D::Style aStyle) { + if (aRect.IsEmpty()) { + return nullptr; + } + + RefPtr dt = mFinalTarget->CreateSimilarDrawTarget( + aRect.Size(), SurfaceFormat::B8G8R8A8); + + if (dt) { + // See bug 1524554. + dt->ClearRect(gfx::Rect()); + } + + if (!dt || !dt->IsValid()) { + aRect.SetEmpty(); + return nullptr; + } + + Matrix transform = + mFinalTarget->GetTransform().PostTranslate(-aRect.TopLeft() + mOffset); + + dt->SetTransform(transform); + + if (transform.Invert()) { + gfx::Rect dtBounds(0, 0, aRect.width, aRect.height); + gfx::Rect fillRect = transform.TransformBounds(dtBounds); + dt->FillRect(fillRect, CanvasGeneralPattern().ForStyle(mCtx, aStyle, dt)); + } + return dt->Snapshot(); + } + + ~AdjustedTargetForFilter() { + if (!mCtx) { + return; + } + + RefPtr snapshot = mTarget->Snapshot(); + + RefPtr fillPaint = + DoSourcePaint(mFillPaintRect, CanvasRenderingContext2D::Style::FILL); + RefPtr strokePaint = DoSourcePaint( + mStrokePaintRect, CanvasRenderingContext2D::Style::STROKE); + + AutoRestoreTransform autoRestoreTransform(mFinalTarget); + mFinalTarget->SetTransform(Matrix()); + + MOZ_RELEASE_ASSERT(!mCtx->CurrentState().filter.mPrimitives.IsEmpty()); + gfx::FilterSupport::RenderFilterDescription( + mFinalTarget, mCtx->CurrentState().filter, gfx::Rect(mPostFilterBounds), + snapshot, mSourceGraphicRect, fillPaint, mFillPaintRect, strokePaint, + mStrokePaintRect, mCtx->CurrentState().filterAdditionalImages, + mPostFilterBounds.TopLeft() - mOffset, + DrawOptions(1.0f, mCompositionOp)); + + const gfx::FilterDescription& filter = mCtx->CurrentState().filter; + MOZ_RELEASE_ASSERT(!filter.mPrimitives.IsEmpty()); + if (filter.mPrimitives.LastElement().IsTainted()) { + if (mCtx->mCanvasElement) { + mCtx->mCanvasElement->SetWriteOnly(); + } else if (mCtx->mOffscreenCanvas) { + mCtx->mOffscreenCanvas->SetWriteOnly(); + } + } + } + + DrawTarget* DT() { return mTarget; } + + private: + RefPtr mTarget; + RefPtr mFinalTarget; + CanvasRenderingContext2D* mCtx; + gfx::IntRect mSourceGraphicRect; + gfx::IntRect mFillPaintRect; + gfx::IntRect mStrokePaintRect; + gfx::IntRect mPostFilterBounds; + gfx::IntPoint mOffset; + gfx::CompositionOp mCompositionOp; +}; + +/* This is an RAII based class that can be used as a drawtarget for + * operations that need to have a shadow applied to their results. + * All coordinates passed to the constructor are in device space. + */ +class AdjustedTargetForShadow { + public: + using ContextState = CanvasRenderingContext2D::ContextState; + + AdjustedTargetForShadow(CanvasRenderingContext2D* aCtx, + DrawTarget* aFinalTarget, const gfx::Rect& aBounds, + gfx::CompositionOp aCompositionOp) + : mFinalTarget(aFinalTarget), mCtx(aCtx), mCompositionOp(aCompositionOp) { + const ContextState& state = mCtx->CurrentState(); + mSigma = state.ShadowBlurSigma(); + + // We actually include the bounds of the shadow blur, this makes it + // easier to execute the actual blur on hardware, and shouldn't affect + // the amount of pixels that need to be touched. + gfx::Rect bounds = aBounds; + int32_t blurRadius = state.ShadowBlurRadius(); + bounds.Inflate(blurRadius); + bounds.RoundOut(); + if (!bounds.ToIntRect(&mTempRect) || + !mFinalTarget->CanCreateSimilarDrawTarget(mTempRect.Size(), + SurfaceFormat::B8G8R8A8)) { + mTarget = mFinalTarget; + mCtx = nullptr; + mFinalTarget = nullptr; + return; + } + + mTarget = mFinalTarget->CreateShadowDrawTarget( + mTempRect.Size(), SurfaceFormat::B8G8R8A8, mSigma); + + if (mTarget) { + // See bug 1524554. + mTarget->ClearRect(gfx::Rect()); + } + + if (!mTarget || !mTarget->IsValid()) { + // XXX - Deal with the situation where our temp size is too big to + // fit in a texture (bug 1066622). + mTarget = mFinalTarget; + mCtx = nullptr; + mFinalTarget = nullptr; + } else { + mTarget->SetTransform( + mFinalTarget->GetTransform().PostTranslate(-mTempRect.TopLeft())); + } + } + + ~AdjustedTargetForShadow() { + if (!mCtx) { + return; + } + + RefPtr snapshot = mTarget->Snapshot(); + + mFinalTarget->DrawSurfaceWithShadow( + snapshot, mTempRect.TopLeft(), + ShadowOptions(ToDeviceColor(mCtx->CurrentState().shadowColor), + mCtx->CurrentState().shadowOffset, mSigma), + mCompositionOp); + } + + DrawTarget* DT() { return mTarget; } + + gfx::IntPoint OffsetToFinalDT() { return mTempRect.TopLeft(); } + + private: + RefPtr mTarget; + RefPtr mFinalTarget; + CanvasRenderingContext2D* mCtx; + Float mSigma; + gfx::IntRect mTempRect; + gfx::CompositionOp mCompositionOp; +}; + +/* + * This is an RAII based class that can be used as a drawtarget for + * operations that need a shadow or a filter drawn. It will automatically + * provide a temporary target when needed, and if so blend it back with a + * shadow, filter, or both. + * If both a shadow and a filter are needed, the filter is applied first, + * and the shadow is applied to the filtered results. + * + * aBounds specifies the bounds of the drawing operation that will be + * drawn to the target, it is given in device space! If this is nullptr the + * drawing operation will be assumed to cover the whole canvas. + */ +class AdjustedTarget { + public: + using ContextState = CanvasRenderingContext2D::ContextState; + + explicit AdjustedTarget(CanvasRenderingContext2D* aCtx, + const gfx::Rect* aBounds = nullptr, + bool aAllowOptimization = false) + : mCtx(aCtx), + mOptimizeShadow(false), + mUsedOperation(aCtx->CurrentState().op) { + // All rects in this function are in the device space of ctx->mTarget. + + // In order to keep our temporary surfaces as small as possible, we first + // calculate what their maximum required bounds would need to be if we + // were to fill the whole canvas. Everything outside those bounds we don't + // need to render. + gfx::Rect r(0, 0, aCtx->mWidth, aCtx->mHeight); + gfx::Rect maxSourceNeededBoundsForShadow = + MaxSourceNeededBoundsForShadow(r, aCtx); + gfx::Rect maxSourceNeededBoundsForFilter = + MaxSourceNeededBoundsForFilter(maxSourceNeededBoundsForShadow, aCtx); + if (!aCtx->IsTargetValid()) { + return; + } + + gfx::Rect bounds = maxSourceNeededBoundsForFilter; + if (aBounds) { + bounds = bounds.Intersect(*aBounds); + } + gfx::Rect boundsAfterFilter = BoundsAfterFilter(bounds, aCtx); + if (!aCtx->IsTargetValid() || !boundsAfterFilter.IsFinite()) { + return; + } + + gfx::IntPoint offsetToFinalDT; + + // First set up the shadow draw target, because the shadow goes outside. + // It applies to the post-filter results, if both a filter and a shadow + // are used. + const bool applyFilter = aCtx->NeedToApplyFilter(); + if (aCtx->NeedToDrawShadow()) { + if (aAllowOptimization && !applyFilter) { + // If only drawing a shadow and no filter, then avoid buffering to an + // intermediate target while drawing the shadow directly to the final + // target. When doing so, we want to use the actual composition op + // instead of OP_OVER. + mTarget = aCtx->mTarget; + if (mTarget && mTarget->IsValid()) { + mOptimizeShadow = true; + return; + } + } + mShadowTarget = MakeUnique( + aCtx, aCtx->mTarget, boundsAfterFilter, mUsedOperation); + mTarget = mShadowTarget->DT(); + offsetToFinalDT = mShadowTarget->OffsetToFinalDT(); + + // If we also have a filter, the filter needs to be drawn with OP_OVER + // because shadow drawing already applies op on the result. + mUsedOperation = CompositionOp::OP_OVER; + } + + // Now set up the filter draw target. + if (!aCtx->IsTargetValid()) { + return; + } + if (applyFilter) { + bounds.RoundOut(); + + if (!mTarget) { + mTarget = aCtx->mTarget; + } + gfx::IntRect intBounds; + if (!bounds.ToIntRect(&intBounds)) { + return; + } + mFilterTarget = MakeUnique( + aCtx, mTarget, offsetToFinalDT, intBounds, + gfx::RoundedToInt(boundsAfterFilter), mUsedOperation); + mTarget = mFilterTarget->DT(); + mUsedOperation = CompositionOp::OP_OVER; + } + if (!mTarget) { + mTarget = aCtx->mTarget; + } + } + + ~AdjustedTarget() { + // The order in which the targets are finalized is important. + // Filters are inside, any shadow applies to the post-filter results. + mFilterTarget.reset(); + mShadowTarget.reset(); + } + + operator DrawTarget*() { return mTarget; } + + DrawTarget* operator->() MOZ_NO_ADDREF_RELEASE_ON_RETURN { return mTarget; } + + CompositionOp UsedOperation() const { return mUsedOperation; } + + ShadowOptions ShadowParams() const { + const ContextState& state = mCtx->CurrentState(); + return ShadowOptions(ToDeviceColor(state.shadowColor), state.shadowOffset, + state.ShadowBlurSigma()); + } + + void Fill(const Path* aPath, const Pattern& aPattern, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + mTarget->DrawShadow(aPath, aPattern, ShadowParams(), aOptions); + } + mTarget->Fill(aPath, aPattern, aOptions); + } + + void FillRect(const Rect& aRect, const Pattern& aPattern, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + RefPtr path = MakePathForRect(*mTarget, aRect); + mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions); + } + mTarget->FillRect(aRect, aPattern, aOptions); + } + + void Stroke(const Path* aPath, const Pattern& aPattern, + const StrokeOptions& aStrokeOptions, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + mTarget->DrawShadow(aPath, aPattern, ShadowParams(), aOptions, + &aStrokeOptions); + } + mTarget->Stroke(aPath, aPattern, aStrokeOptions, aOptions); + } + + void StrokeRect(const Rect& aRect, const Pattern& aPattern, + const StrokeOptions& aStrokeOptions, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + RefPtr path = MakePathForRect(*mTarget, aRect); + mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions, + &aStrokeOptions); + } + mTarget->StrokeRect(aRect, aPattern, aStrokeOptions, aOptions); + } + + void StrokeLine(const Point& aStart, const Point& aEnd, + const Pattern& aPattern, const StrokeOptions& aStrokeOptions, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + RefPtr builder = mTarget->CreatePathBuilder(); + builder->MoveTo(aStart); + builder->LineTo(aEnd); + RefPtr path = builder->Finish(); + mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions, + &aStrokeOptions); + } + mTarget->StrokeLine(aStart, aEnd, aPattern, aStrokeOptions, aOptions); + } + + void DrawSurface(SourceSurface* aSurface, const Rect& aDest, + const Rect& aSource, const DrawSurfaceOptions& aSurfOptions, + const DrawOptions& aOptions) { + if (mOptimizeShadow) { + RefPtr path = MakePathForRect(*mTarget, aSource); + ShadowOptions shadowParams(ShadowParams()); + SurfacePattern pattern(aSurface, ExtendMode::CLAMP, Matrix(), + shadowParams.BlurRadius() > 1 + ? SamplingFilter::POINT + : aSurfOptions.mSamplingFilter); + Matrix matrix = Matrix::Scaling(aDest.width / aSource.width, + aDest.height / aSource.height); + matrix.PreTranslate(-aSource.x, -aSource.y); + matrix.PostTranslate(aDest.x, aDest.y); + AutoRestoreTransform autoRestoreTransform(mTarget); + mTarget->ConcatTransform(matrix); + mTarget->DrawShadow(path, pattern, shadowParams, aOptions); + } + mTarget->DrawSurface(aSurface, aDest, aSource, aSurfOptions, aOptions); + } + + private: + gfx::Rect MaxSourceNeededBoundsForFilter(const gfx::Rect& aDestBounds, + CanvasRenderingContext2D* aCtx) { + const bool applyFilter = aCtx->NeedToApplyFilter(); + if (!aCtx->IsTargetValid()) { + return aDestBounds; + } + if (!applyFilter) { + return aDestBounds; + } + + nsIntRegion sourceGraphicNeededRegion; + nsIntRegion fillPaintNeededRegion; + nsIntRegion strokePaintNeededRegion; + + FilterSupport::ComputeSourceNeededRegions( + aCtx->CurrentState().filter, gfx::RoundedToInt(aDestBounds), + sourceGraphicNeededRegion, fillPaintNeededRegion, + strokePaintNeededRegion); + + return gfx::Rect(sourceGraphicNeededRegion.GetBounds()); + } + + gfx::Rect MaxSourceNeededBoundsForShadow(const gfx::Rect& aDestBounds, + CanvasRenderingContext2D* aCtx) { + if (!aCtx->NeedToDrawShadow()) { + return aDestBounds; + } + + const ContextState& state = aCtx->CurrentState(); + gfx::Rect sourceBounds = aDestBounds - state.shadowOffset; + sourceBounds.Inflate(state.ShadowBlurRadius()); + + // Union the shadow source with the original rect because we're going to + // draw both. + return sourceBounds.Union(aDestBounds); + } + + gfx::Rect BoundsAfterFilter(const gfx::Rect& aBounds, + CanvasRenderingContext2D* aCtx) { + const bool applyFilter = aCtx->NeedToApplyFilter(); + if (!aCtx->IsTargetValid()) { + return aBounds; + } + if (!applyFilter) { + return aBounds; + } + + gfx::Rect bounds(aBounds); + bounds.RoundOut(); + + gfx::IntRect intBounds; + if (!bounds.ToIntRect(&intBounds)) { + return gfx::Rect(); + } + + nsIntRegion extents = gfx::FilterSupport::ComputePostFilterExtents( + aCtx->CurrentState().filter, intBounds); + return gfx::Rect(extents.GetBounds()); + } + + CanvasRenderingContext2D* mCtx; + bool mOptimizeShadow; + CompositionOp mUsedOperation; + RefPtr mTarget; + UniquePtr mShadowTarget; + UniquePtr mFilterTarget; +}; + +void CanvasPattern::SetTransform(const DOMMatrix2DInit& aInit, + ErrorResult& aError) { + RefPtr matrix = + DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError); + if (aError.Failed()) { + return; + } + const auto* matrix2D = matrix->GetInternal2D(); + if (!matrix2D->IsFinite()) { + return; + } + mTransform = Matrix(*matrix2D); +} + +void CanvasGradient::AddColorStop(float aOffset, const nsACString& aColorstr, + ErrorResult& aRv) { + if (aOffset < 0.0 || aOffset > 1.0) { + return aRv.ThrowIndexSizeError("Offset out of 0-1.0 range"); + } + + if (!mContext) { + return aRv.ThrowSyntaxError("No canvas context"); + } + + auto color = mContext->ParseColor( + aColorstr, CanvasRenderingContext2D::ResolveCurrentColor::No); + if (!color) { + return aRv.ThrowSyntaxError("Invalid color"); + } + + GradientStop newStop; + + newStop.offset = aOffset; + newStop.color = ToDeviceColor(*color); + + mRawStops.AppendElement(newStop); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasGradient, mContext) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPattern, mContext) + +class CanvasShutdownObserver final : public nsIObserver { + public: + explicit CanvasShutdownObserver(CanvasRenderingContext2D* aCanvas) + : mCanvas(aCanvas) {} + + void OnShutdown() { + if (!mCanvas) { + return; + } + + mCanvas = nullptr; + nsContentUtils::UnregisterShutdownObserver(this); + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + private: + ~CanvasShutdownObserver() = default; + + CanvasRenderingContext2D* mCanvas; +}; + +NS_IMPL_ISUPPORTS(CanvasShutdownObserver, nsIObserver) + +NS_IMETHODIMP +CanvasShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (mCanvas && strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + mCanvas->OnShutdown(); + OnShutdown(); + } + + return NS_OK; +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(CanvasRenderingContext2D) +NS_IMPL_CYCLE_COLLECTING_RELEASE(CanvasRenderingContext2D) + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(CanvasRenderingContext2D) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CanvasRenderingContext2D) + // Make sure we remove ourselves from the list of demotable contexts (raw + // pointers), since we're logically destructed at this point. + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCanvasElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOffscreenCanvas) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell) + for (uint32_t i = 0; i < tmp->mStyleStack.Length(); i++) { + ImplCycleCollectionUnlink(tmp->mStyleStack[i].patternStyles[Style::STROKE]); + ImplCycleCollectionUnlink(tmp->mStyleStack[i].patternStyles[Style::FILL]); + ImplCycleCollectionUnlink( + tmp->mStyleStack[i].gradientStyles[Style::STROKE]); + ImplCycleCollectionUnlink(tmp->mStyleStack[i].gradientStyles[Style::FILL]); + auto autoSVGFiltersObserver = + tmp->mStyleStack[i].autoSVGFiltersObserver.get(); + if (autoSVGFiltersObserver) { + // XXXjwatt: I don't think this call achieves anything. See the comment + // that documents this function. + SVGObserverUtils::DetachFromCanvasContext(autoSVGFiltersObserver); + } + ImplCycleCollectionUnlink(tmp->mStyleStack[i].autoSVGFiltersObserver); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CanvasRenderingContext2D) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCanvasElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOffscreenCanvas) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell) + for (uint32_t i = 0; i < tmp->mStyleStack.Length(); i++) { + ImplCycleCollectionTraverse( + cb, tmp->mStyleStack[i].patternStyles[Style::STROKE], + "Stroke CanvasPattern"); + ImplCycleCollectionTraverse(cb, + tmp->mStyleStack[i].patternStyles[Style::FILL], + "Fill CanvasPattern"); + ImplCycleCollectionTraverse( + cb, tmp->mStyleStack[i].gradientStyles[Style::STROKE], + "Stroke CanvasGradient"); + ImplCycleCollectionTraverse(cb, + tmp->mStyleStack[i].gradientStyles[Style::FILL], + "Fill CanvasGradient"); + ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].autoSVGFiltersObserver, + "RAII SVG Filters Observer"); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(CanvasRenderingContext2D) + if (nsCCUncollectableMarker::sGeneration && tmp->HasKnownLiveWrapper()) { + dom::Element* canvasElement = tmp->mCanvasElement; + if (canvasElement) { + if (canvasElement->IsPurple()) { + canvasElement->RemovePurple(); + } + dom::Element::MarkNodeChildren(canvasElement); + } + return true; + } +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(CanvasRenderingContext2D) + return nsCCUncollectableMarker::sGeneration && tmp->HasKnownLiveWrapper(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END + +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(CanvasRenderingContext2D) + return nsCCUncollectableMarker::sGeneration && tmp->HasKnownLiveWrapper(); +NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasRenderingContext2D) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsICanvasRenderingContextInternal) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CanvasRenderingContext2D::ContextState::ContextState() = default; + +CanvasRenderingContext2D::ContextState::ContextState(const ContextState& aOther) + : fontGroup(aOther.fontGroup), + fontLanguage(aOther.fontLanguage), + fontFont(aOther.fontFont), + gradientStyles(aOther.gradientStyles), + patternStyles(aOther.patternStyles), + colorStyles(aOther.colorStyles), + font(aOther.font), + textAlign(aOther.textAlign), + textBaseline(aOther.textBaseline), + textDirection(aOther.textDirection), + fontKerning(aOther.fontKerning), + fontStretch(aOther.fontStretch), + fontVariantCaps(aOther.fontVariantCaps), + textRendering(aOther.textRendering), + letterSpacing(aOther.letterSpacing), + wordSpacing(aOther.wordSpacing), + letterSpacingStr(aOther.letterSpacingStr), + wordSpacingStr(aOther.wordSpacingStr), + shadowColor(aOther.shadowColor), + transform(aOther.transform), + shadowOffset(aOther.shadowOffset), + lineWidth(aOther.lineWidth), + miterLimit(aOther.miterLimit), + globalAlpha(aOther.globalAlpha), + shadowBlur(aOther.shadowBlur), + dash(aOther.dash.Clone()), + dashOffset(aOther.dashOffset), + op(aOther.op), + fillRule(aOther.fillRule), + lineCap(aOther.lineCap), + lineJoin(aOther.lineJoin), + filterString(aOther.filterString), + filterChain(aOther.filterChain), + autoSVGFiltersObserver(aOther.autoSVGFiltersObserver), + filter(aOther.filter), + filterAdditionalImages(aOther.filterAdditionalImages.Clone()), + filterSourceGraphicTainted(aOther.filterSourceGraphicTainted), + imageSmoothingEnabled(aOther.imageSmoothingEnabled), + fontExplicitLanguage(aOther.fontExplicitLanguage) {} + +CanvasRenderingContext2D::ContextState::~ContextState() = default; + +void CanvasRenderingContext2D::ContextState::SetColorStyle(Style aWhichStyle, + nscolor aColor) { + colorStyles[aWhichStyle] = aColor; + gradientStyles[aWhichStyle] = nullptr; + patternStyles[aWhichStyle] = nullptr; +} + +void CanvasRenderingContext2D::ContextState::SetPatternStyle( + Style aWhichStyle, CanvasPattern* aPat) { + gradientStyles[aWhichStyle] = nullptr; + patternStyles[aWhichStyle] = aPat; +} + +void CanvasRenderingContext2D::ContextState::SetGradientStyle( + Style aWhichStyle, CanvasGradient* aGrad) { + gradientStyles[aWhichStyle] = aGrad; + patternStyles[aWhichStyle] = nullptr; +} + +/** + ** CanvasRenderingContext2D impl + **/ + +// Initialize our static variables. +MOZ_THREAD_LOCAL(uintptr_t) CanvasRenderingContext2D::sNumLivingContexts; +MOZ_THREAD_LOCAL(DrawTarget*) CanvasRenderingContext2D::sErrorTarget; + +// Helpers to map Canvas2D WebIDL enum values to gfx constants for rendering. +static JoinStyle CanvasToGfx(CanvasLineJoin aJoin) { + switch (aJoin) { + case CanvasLineJoin::Round: + return JoinStyle::ROUND; + case CanvasLineJoin::Bevel: + return JoinStyle::BEVEL; + case CanvasLineJoin::Miter: + return JoinStyle::MITER_OR_BEVEL; + default: + MOZ_CRASH("unknown lineJoin!"); + } +} + +static CapStyle CanvasToGfx(CanvasLineCap aCap) { + switch (aCap) { + case CanvasLineCap::Butt: + return CapStyle::BUTT; + case CanvasLineCap::Round: + return CapStyle::ROUND; + case CanvasLineCap::Square: + return CapStyle::SQUARE; + default: + MOZ_CRASH("unknown lineCap!"); + } +} + +static uint8_t CanvasToGfx(CanvasFontKerning aKerning) { + switch (aKerning) { + case CanvasFontKerning::Auto: + return NS_FONT_KERNING_AUTO; + case CanvasFontKerning::Normal: + return NS_FONT_KERNING_NORMAL; + case CanvasFontKerning::None: + return NS_FONT_KERNING_NONE; + default: + MOZ_CRASH("unknown kerning!"); + } +} + +CanvasRenderingContext2D::CanvasRenderingContext2D( + layers::LayersBackend aCompositorBackend) + : // these are the default values from the Canvas spec + mWidth(0), + mHeight(0), + mZero(false), + mOpaqueAttrValue(false), + mContextAttributesHasAlpha(true), + mOpaque(false), + mResetLayer(true), + mIPC(false), + mHasPendingStableStateCallback(false), + mIsEntireFrameInvalid(false), + mPredictManyRedrawCalls(false), + mFrameCaptureState(FrameCaptureState::CLEAN, + "CanvasRenderingContext2D::mFrameCaptureState"), + mInvalidateCount(0), + mWriteOnly(false) { + sNumLivingContexts.infallibleInit(); + sErrorTarget.infallibleInit(); + sNumLivingContexts.set(sNumLivingContexts.get() + 1); +} + +CanvasRenderingContext2D::~CanvasRenderingContext2D() { + RemovePostRefreshObserver(); + RemoveShutdownObserver(); + ResetBitmap(); + + sNumLivingContexts.set(sNumLivingContexts.get() - 1); + if (sNumLivingContexts.get() == 0 && sErrorTarget.get()) { + RefPtr target = dont_AddRef(sErrorTarget.get()); + sErrorTarget.set(nullptr); + } +} + +nsresult CanvasRenderingContext2D::Initialize() { + if (NS_WARN_IF(!AddShutdownObserver())) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +JSObject* CanvasRenderingContext2D::WrapObject( + JSContext* aCx, JS::Handle aGivenProto) { + return CanvasRenderingContext2D_Binding::Wrap(aCx, this, aGivenProto); +} + +void CanvasRenderingContext2D::GetContextAttributes( + CanvasRenderingContext2DSettings& aSettings) const { + aSettings = CanvasRenderingContext2DSettings(); + + aSettings.mAlpha = mContextAttributesHasAlpha; + aSettings.mWillReadFrequently = mWillReadFrequently; + + // We don't support the 'desynchronized' and 'colorSpace' attributes, so + // those just keep their default values. +} + +CanvasRenderingContext2D::ColorStyleCacheEntry +CanvasRenderingContext2D::ParseColorSlow(const nsACString& aString) { + ColorStyleCacheEntry result{nsCString(aString)}; + Document* document = mCanvasElement ? mCanvasElement->OwnerDoc() : nullptr; + css::Loader* loader = document ? document->CSSLoader() : nullptr; + + PresShell* presShell = GetPresShell(); + ServoStyleSet* set = presShell ? presShell->StyleSet() : nullptr; + bool wasCurrentColor = false; + nscolor color; + if (ServoCSSParser::ComputeColor(set, NS_RGB(0, 0, 0), aString, &color, + &wasCurrentColor, loader)) { + result.mWasCurrentColor = wasCurrentColor; + result.mColor.emplace(color); + } + + return result; +} + +Maybe CanvasRenderingContext2D::ParseColor( + const nsACString& aString, ResolveCurrentColor aResolveCurrentColor) { + auto entry = mColorStyleCache.Lookup(aString); + if (!entry) { + entry.Set(ParseColorSlow(aString)); + } + + const auto& data = entry.Data(); + if (data.mWasCurrentColor && mCanvasElement && + aResolveCurrentColor == ResolveCurrentColor::Yes) { + // If it was currentColor, get the value of the color property, flushing + // style if necessary. + RefPtr canvasStyle = + nsComputedDOMStyle::GetComputedStyle(mCanvasElement); + if (canvasStyle) { + return Some(canvasStyle->StyleText()->mColor.ToColor()); + } + } + return data.mColor; +} + +void CanvasRenderingContext2D::ResetBitmap(bool aFreeBuffer) { + if (mCanvasElement) { + mCanvasElement->InvalidateCanvas(); + } + + // only do this for non-docshell created contexts, + // since those are the ones that we created a surface for + if (mTarget && IsTargetValid() && !mDocShell) { + gCanvasAzureMemoryUsed -= mWidth * mHeight * 4; + } + + bool forceReset = true; + ReturnTarget(forceReset); + mTarget = nullptr; + if (aFreeBuffer) { + mBufferProvider = nullptr; + } else if (mBufferProvider) { + // Try to keep the buffer around. However, we still need to clear the + // contents as if it was recreated before next use. + mBufferNeedsClear = true; + } + + // Since the target changes the backing texture will change, and this will + // no longer be valid. + mIsEntireFrameInvalid = false; + mPredictManyRedrawCalls = false; + mFrameCaptureState = FrameCaptureState::CLEAN; +} + +void CanvasRenderingContext2D::OnShutdown() { + RefPtr provider = mBufferProvider; + + ResetBitmap(); + + if (provider) { + provider->OnShutdown(); + } + + if (mOffscreenCanvas) { + mOffscreenCanvas->Destroy(); + } + + mHasShutdown = true; +} + +bool CanvasRenderingContext2D::AddShutdownObserver() { + auto* const canvasManager = CanvasManagerChild::Get(); + if (NS_WARN_IF(!canvasManager)) { + if (NS_IsMainThread()) { + mShutdownObserver = new CanvasShutdownObserver(this); + nsContentUtils::RegisterShutdownObserver(mShutdownObserver); + return true; + } + + mHasShutdown = true; + return false; + } + + canvasManager->AddShutdownObserver(this); + return true; +} + +void CanvasRenderingContext2D::RemoveShutdownObserver() { + if (mShutdownObserver) { + mShutdownObserver->OnShutdown(); + mShutdownObserver = nullptr; + return; + } + + auto* const canvasManager = CanvasManagerChild::MaybeGet(); + if (!canvasManager) { + return; + } + + canvasManager->RemoveShutdownObserver(this); +} + +void CanvasRenderingContext2D::SetStyleFromString(const nsACString& aStr, + Style aWhichStyle) { + MOZ_ASSERT(!aStr.IsVoid()); + + Maybe color = ParseColor(aStr); + if (!color) { + return; + } + + CurrentState().SetColorStyle(aWhichStyle, *color); +} + +void CanvasRenderingContext2D::GetStyleAsUnion( + OwningUTF8StringOrCanvasGradientOrCanvasPattern& aValue, + Style aWhichStyle) { + const ContextState& state = CurrentState(); + if (state.patternStyles[aWhichStyle]) { + aValue.SetAsCanvasPattern() = state.patternStyles[aWhichStyle]; + } else if (state.gradientStyles[aWhichStyle]) { + aValue.SetAsCanvasGradient() = state.gradientStyles[aWhichStyle]; + } else { + StyleColorToString(state.colorStyles[aWhichStyle], + aValue.SetAsUTF8String()); + } +} + +// static +void CanvasRenderingContext2D::StyleColorToString(const nscolor& aColor, + nsACString& aStr) { + aStr.Truncate(); + // We can't reuse the normal CSS color stringification code, + // because the spec calls for a different algorithm for canvas. + if (NS_GET_A(aColor) == 255) { + aStr.AppendPrintf("#%02x%02x%02x", NS_GET_R(aColor), NS_GET_G(aColor), + NS_GET_B(aColor)); + } else { + aStr.AppendPrintf("rgba(%d, %d, %d, ", NS_GET_R(aColor), NS_GET_G(aColor), + NS_GET_B(aColor)); + aStr.AppendFloat(nsStyleUtil::ColorComponentToFloat(NS_GET_A(aColor))); + aStr.Append(')'); + } +} + +nsresult CanvasRenderingContext2D::Redraw() { + mFrameCaptureState = FrameCaptureState::DIRTY; + + if (mIsEntireFrameInvalid) { + return NS_OK; + } + + mIsEntireFrameInvalid = true; + + if (mCanvasElement) { + SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement); + mCanvasElement->InvalidateCanvasContent(nullptr); + } else if (mOffscreenCanvas) { + mOffscreenCanvas->QueueCommitToCompositor(); + } else { + NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!"); + } + + return NS_OK; +} + +void CanvasRenderingContext2D::Redraw(const gfx::Rect& aR) { + mFrameCaptureState = FrameCaptureState::DIRTY; + + ++mInvalidateCount; + + if (mIsEntireFrameInvalid) { + return; + } + + if (mPredictManyRedrawCalls || mInvalidateCount > kCanvasMaxInvalidateCount) { + Redraw(); + return; + } + + if (mCanvasElement) { + SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement); + mCanvasElement->InvalidateCanvasContent(&aR); + } else if (mOffscreenCanvas) { + mOffscreenCanvas->QueueCommitToCompositor(); + } else { + NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!"); + } +} + +void CanvasRenderingContext2D::DidRefresh() {} + +void CanvasRenderingContext2D::RedrawUser(const gfxRect& aR) { + mFrameCaptureState = FrameCaptureState::DIRTY; + + if (mIsEntireFrameInvalid) { + ++mInvalidateCount; + return; + } + + gfx::Rect newr = mTarget->GetTransform().TransformBounds(ToRect(aR)); + Redraw(newr); +} + +bool CanvasRenderingContext2D::CopyBufferProvider( + PersistentBufferProvider& aOld, DrawTarget& aTarget, IntRect aCopyRect) { + // Borrowing the snapshot must be done after ReturnTarget. + RefPtr snapshot = aOld.BorrowSnapshot(); + + if (!snapshot) { + return false; + } + + aTarget.CopySurface(snapshot, aCopyRect, IntPoint()); + + aOld.ReturnSnapshot(snapshot.forget()); + return true; +} + +void CanvasRenderingContext2D::Demote() {} + +void CanvasRenderingContext2D::ScheduleStableStateCallback() { + if (mHasPendingStableStateCallback) { + return; + } + mHasPendingStableStateCallback = true; + + nsContentUtils::RunInStableState( + NewRunnableMethod("dom::CanvasRenderingContext2D::OnStableState", this, + &CanvasRenderingContext2D::OnStableState)); +} + +void CanvasRenderingContext2D::OnStableState() { + if (!mHasPendingStableStateCallback) { + return; + } + + ReturnTarget(); + + mHasPendingStableStateCallback = false; +} + +void CanvasRenderingContext2D::RestoreClipsAndTransformToTarget() { + // Restore clips and transform. + mTarget->SetTransform(Matrix()); + + if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) { + // Cairo doesn't play well with huge clips. When given a very big clip it + // will try to allocate big mask surface without taking the target + // size into account which can cause OOM. See bug 1034593. + // This limits the clip extents to the size of the canvas. + // A fix in Cairo would probably be preferable, but requires somewhat + // invasive changes. + mTarget->PushClipRect(gfx::Rect(0, 0, mWidth, mHeight)); + } + + for (auto& style : mStyleStack) { + for (auto& clipOrTransform : style.clipsAndTransforms) { + if (clipOrTransform.IsClip()) { + if (mClipsNeedConverting) { + // We have possibly changed backends, so we need to convert the clips + // in case they are no longer compatible with mTarget. + RefPtr pathBuilder = mTarget->CreatePathBuilder(); + clipOrTransform.clip->StreamToSink(pathBuilder); + clipOrTransform.clip = pathBuilder->Finish(); + } + mTarget->PushClip(clipOrTransform.clip); + } else { + mTarget->SetTransform(clipOrTransform.transform); + } + } + } + + mClipsNeedConverting = false; +} + +bool CanvasRenderingContext2D::BorrowTarget(const IntRect& aPersistedRect, + bool aNeedsClear) { + // We are attempting to request a DrawTarget from the current + // PersistentBufferProvider. However, if the provider needs to be refreshed, + // or if it is accelerated and the application has requested that we disallow + // acceleration, then we skip trying to use this provider so that it will be + // recreated by EnsureTarget later. + if (!mBufferProvider || mBufferProvider->RequiresRefresh() || + (mBufferProvider->IsAccelerated() && GetEffectiveWillReadFrequently())) { + return false; + } + mTarget = mBufferProvider->BorrowDrawTarget(aPersistedRect); + if (!mTarget || !mTarget->IsValid()) { + if (mTarget) { + mBufferProvider->ReturnDrawTarget(mTarget.forget()); + } + return false; + } + if (mBufferNeedsClear) { + if (mBufferProvider->PreservesDrawingState()) { + // If the buffer provider preserves the clip and transform state, then + // we must ensure it is cleared before reusing the target. + if (!mTarget->RemoveAllClips()) { + mBufferProvider->ReturnDrawTarget(mTarget.forget()); + return false; + } + mTarget->SetTransform(Matrix()); + } + // If the canvas was reset, then we need to clear the target in case its + // contents was somehow preserved. We only need to clear the target if + // the operation doesn't fill the entire canvas. + if (aNeedsClear) { + mTarget->ClearRect(gfx::Rect(mTarget->GetRect())); + } + } + if (!mBufferProvider->PreservesDrawingState() || mBufferNeedsClear) { + RestoreClipsAndTransformToTarget(); + } + mBufferNeedsClear = false; + return true; +} + +bool CanvasRenderingContext2D::EnsureTarget(const gfx::Rect* aCoveredRect, + bool aWillClear) { + if (AlreadyShutDown()) { + gfxCriticalNoteOnce << "Attempt to render into a Canvas2d after shutdown."; + SetErrorState(); + return false; + } + + if (mTarget) { + return mTarget != sErrorTarget.get(); + } + + // Check that the dimensions are sane + if (mWidth > StaticPrefs::gfx_canvas_max_size() || + mHeight > StaticPrefs::gfx_canvas_max_size() || mWidth < 0 || + mHeight < 0) { + SetErrorState(); + return false; + } + + // If the next drawing command covers the entire canvas, we can skip copying + // from the previous frame and/or clearing the canvas. + gfx::Rect canvasRect(0, 0, mWidth, mHeight); + bool canDiscardContent = + aCoveredRect && CurrentState() + .transform.TransformBounds(*aCoveredRect) + .Contains(canvasRect); + + // If a clip is active we don't know for sure that the next drawing command + // will really cover the entire canvas. + for (const auto& style : mStyleStack) { + if (!canDiscardContent) { + break; + } + for (const auto& clipOrTransform : style.clipsAndTransforms) { + if (clipOrTransform.IsClip()) { + canDiscardContent = false; + break; + } + } + } + + ScheduleStableStateCallback(); + + IntRect persistedRect = canDiscardContent || mBufferNeedsClear + ? IntRect() + : IntRect(0, 0, mWidth, mHeight); + + // Attempt to reuse the existing buffer provider. + if (BorrowTarget(persistedRect, !canDiscardContent)) { + return true; + } + + RefPtr newTarget; + RefPtr newProvider; + + if (!TryAcceleratedTarget(newTarget, newProvider) && + !TrySharedTarget(newTarget, newProvider) && + !TryBasicTarget(newTarget, newProvider)) { + gfxCriticalError( + CriticalLog::DefaultOptions(Factory::ReasonableSurfaceSize(GetSize()))) + << "Failed borrow shared and basic targets."; + + SetErrorState(); + return false; + } + + MOZ_ASSERT(newTarget); + MOZ_ASSERT(newProvider); + + bool needsClear = + !canDiscardContent || (mBufferProvider && mBufferNeedsClear); + if (newTarget->GetBackendType() == gfx::BackendType::SKIA && + (needsClear || !aWillClear)) { + // Skia expects the unused X channel to contains 0xFF even for opaque + // operations so we can't skip clearing in that case, even if we are going + // to cover the entire canvas in the next drawing operation. + newTarget->ClearRect(canvasRect); + needsClear = false; + } + + // Try to copy data from the previous buffer provider if there is one. + if (!canDiscardContent && mBufferProvider && !mBufferNeedsClear && + CopyBufferProvider(*mBufferProvider, *newTarget, persistedRect)) { + needsClear = false; + } + + if (needsClear) { + newTarget->ClearRect(canvasRect); + } + + mTarget = std::move(newTarget); + mBufferProvider = std::move(newProvider); + mBufferNeedsClear = false; + + RegisterAllocation(); + AddZoneWaitingForGC(); + + RestoreClipsAndTransformToTarget(); + + // Force a full layer transaction since we didn't have a layer before + // and now we might need one. + if (mCanvasElement) { + mCanvasElement->InvalidateCanvas(); + } + // EnsureTarget hasn't drawn anything. Preserve mFrameCaptureState. + FrameCaptureState captureState = mFrameCaptureState; + // Calling Redraw() tells our invalidation machinery that the entire + // canvas is already invalid, which can speed up future drawing. + Redraw(); + mFrameCaptureState = captureState; + + return true; +} + +void CanvasRenderingContext2D::SetInitialState() { + // Set up the initial canvas defaults + mPathBuilder = nullptr; + mPath = nullptr; + mPathPruned = false; + mPathTransform = Matrix(); + mPathTransformDirty = false; + + mStyleStack.Clear(); + ContextState* state = mStyleStack.AppendElement(); + state->globalAlpha = 1.0; + + state->colorStyles[Style::FILL] = NS_RGB(0, 0, 0); + state->colorStyles[Style::STROKE] = NS_RGB(0, 0, 0); + state->shadowColor = NS_RGBA(0, 0, 0, 0); +} + +void CanvasRenderingContext2D::SetErrorState() { + EnsureErrorTarget(); + + if (mTarget && mTarget != sErrorTarget.get()) { + gCanvasAzureMemoryUsed -= mWidth * mHeight * 4; + } + + mTarget = sErrorTarget.get(); + mBufferProvider = nullptr; + + // clear transforms, clips, etc. + SetInitialState(); +} + +void CanvasRenderingContext2D::RegisterAllocation() { + // XXX - It would make more sense to track the allocation in + // PeristentBufferProvider, rather than here. + static bool registered = false; + // FIXME: Disable the reporter for now, see bug 1241865 + if (!registered && false) { + registered = true; + RegisterStrongMemoryReporter(new Canvas2dPixelsReporter()); + } +} + +void CanvasRenderingContext2D::AddZoneWaitingForGC() { + JSObject* wrapper = GetWrapperPreserveColor(); + if (wrapper) { + CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC( + JS::GetObjectZone(wrapper)); + } +} + +static WindowRenderer* WindowRendererFromCanvasElement( + nsINode* aCanvasElement) { + if (!aCanvasElement) { + return nullptr; + } + + return nsContentUtils::WindowRendererForDocument(aCanvasElement->OwnerDoc()); +} + +bool CanvasRenderingContext2D::TryAcceleratedTarget( + RefPtr& aOutDT, + RefPtr& aOutProvider) { + if (!XRE_IsContentProcess()) { + // Only allow accelerated contexts to be created in a content process to + // ensure it is remoted appropriately and run on the correct parent or + // GPU process threads. + return false; + } + if (mBufferProvider && mBufferProvider->IsAccelerated() && + mBufferProvider->RequiresRefresh()) { + // If there is already a provider and we got here, then the provider needs + // to be refreshed and we should avoid using acceleration in the future. + mAllowAcceleration = false; + } + // Don't try creating an accelerate DrawTarget if either acceleration failed + // previously or if the application expects acceleration to be slow. + if (!mAllowAcceleration || GetEffectiveWillReadFrequently()) { + return false; + } + + if (!mCanvasElement) { + return false; + } + WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); + if (!renderer) { + return false; + } + aOutProvider = PersistentBufferProviderAccelerated::Create( + GetSize(), GetSurfaceFormat(), renderer->AsKnowsCompositor()); + if (!aOutProvider) { + return false; + } + aOutDT = aOutProvider->BorrowDrawTarget(IntRect()); + MOZ_ASSERT(aOutDT); + return !!aOutDT; +} + +bool CanvasRenderingContext2D::TrySharedTarget( + RefPtr& aOutDT, + RefPtr& aOutProvider) { + aOutDT = nullptr; + aOutProvider = nullptr; + + if (mBufferProvider && mBufferProvider->IsShared()) { + // we are already using a shared buffer provider, we are allocating a new + // one because the current one failed so let's just fall back to the basic + // provider. + mClipsNeedConverting = true; + return false; + } + + if (mCanvasElement) { + WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); + if (!renderer) { + return false; + } + + aOutProvider = renderer->CreatePersistentBufferProvider( + GetSize(), GetSurfaceFormat(), + !mAllowAcceleration || GetEffectiveWillReadFrequently()); + } else if (mOffscreenCanvas) { + if (!StaticPrefs::gfx_offscreencanvas_shared_provider()) { + return false; + } + + RefPtr imageBridge = + layers::ImageBridgeChild::GetSingleton(); + if (NS_WARN_IF(!imageBridge)) { + return false; + } + + aOutProvider = layers::PersistentBufferProviderShared::Create( + GetSize(), GetSurfaceFormat(), imageBridge, + !mAllowAcceleration || GetEffectiveWillReadFrequently(), + mOffscreenCanvas->GetWindowID()); + } + + if (!aOutProvider) { + return false; + } + + // We can pass an empty persisted rect since we just created the buffer + // provider (nothing to restore). + aOutDT = aOutProvider->BorrowDrawTarget(IntRect()); + MOZ_ASSERT(aOutDT); + + return !!aOutDT; +} + +bool CanvasRenderingContext2D::TryBasicTarget( + RefPtr& aOutDT, + RefPtr& aOutProvider) { + aOutDT = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget( + GetSize(), GetSurfaceFormat()); + if (!aOutDT) { + return false; + } + + // See Bug 1524554 - this forces DT initialization. + aOutDT->ClearRect(gfx::Rect()); + + if (!aOutDT->IsValid()) { + aOutDT = nullptr; + return false; + } + + aOutProvider = new PersistentBufferProviderBasic(aOutDT); + return true; +} + +PersistentBufferProvider* CanvasRenderingContext2D::GetBufferProvider() { + if (mBufferProvider && mBufferNeedsClear) { + // Force the buffer to clear before it is used. + EnsureTarget(); + } + return mBufferProvider; +} + +Maybe CanvasRenderingContext2D::GetFrontBuffer( + WebGLFramebufferJS*, const bool webvr) { + if (auto* provider = GetBufferProvider()) { + return provider->GetFrontBuffer(); + } + return Nothing(); +} + +already_AddRefed +CanvasRenderingContext2D::UseCompositableForwarder( + layers::CompositableForwarder* aForwarder) { + if (mBufferProvider) { + return mBufferProvider->UseCompositableForwarder(aForwarder); + } + return nullptr; +} + +PresShell* CanvasRenderingContext2D::GetPresShell() { + if (mCanvasElement) { + return mCanvasElement->OwnerDoc()->GetPresShell(); + } + if (mDocShell) { + return mDocShell->GetPresShell(); + } + return nullptr; +} + +NS_IMETHODIMP +CanvasRenderingContext2D::SetDimensions(int32_t aWidth, int32_t aHeight) { + // Zero sized surfaces can cause problems. + mZero = false; + if (aHeight == 0) { + aHeight = 1; + mZero = true; + } + if (aWidth == 0) { + aWidth = 1; + mZero = true; + } + + ClearTarget(aWidth, aHeight); + + return NS_OK; +} + +void CanvasRenderingContext2D::AddAssociatedMemory() { + JSObject* wrapper = GetWrapperMaybeDead(); + if (wrapper) { + JS::AddAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this), + JS::MemoryUse::DOMBinding); + } +} + +void CanvasRenderingContext2D::RemoveAssociatedMemory() { + JSObject* wrapper = GetWrapperMaybeDead(); + if (wrapper) { + JS::RemoveAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this), + JS::MemoryUse::DOMBinding); + } +} + +void CanvasRenderingContext2D::ClearTarget(int32_t aWidth, int32_t aHeight) { + // Only free the buffer provider if the size no longer matches. + bool freeBuffer = aWidth != mWidth || aHeight != mHeight; + ResetBitmap(freeBuffer); + + mResetLayer = true; + + SetInitialState(); + + // Update dimensions only if new (strictly positive) values were passed. + if (aWidth > 0 && aHeight > 0) { + // Update the memory size associated with the wrapper object when we change + // the dimensions. Note that we need to keep updating dying wrappers before + // they are finalized so that the memory accounting balances out. + RemoveAssociatedMemory(); + mWidth = aWidth; + mHeight = aHeight; + AddAssociatedMemory(); + } + + if (mOffscreenCanvas) { + OffscreenCanvasDisplayData data; + data.mSize = {mWidth, mHeight}; + data.mIsOpaque = mOpaque; + data.mIsAlphaPremult = true; + data.mDoPaintCallbacks = true; + mOffscreenCanvas->UpdateDisplayData(data); + } + + if (!mCanvasElement || !mCanvasElement->IsInComposedDoc()) { + return; + } + + // For vertical writing-mode, unless text-orientation is sideways, + // we'll modify the initial value of textBaseline to 'middle'. + RefPtr canvasStyle = + nsComputedDOMStyle::GetComputedStyle(mCanvasElement); + if (canvasStyle) { + WritingMode wm(canvasStyle); + if (wm.IsVertical() && !wm.IsSideways()) { + CurrentState().textBaseline = CanvasTextBaseline::Middle; + } + } +} + +void CanvasRenderingContext2D::ReturnTarget(bool aForceReset) { + if (mTarget && mBufferProvider && mTarget != sErrorTarget.get()) { + CurrentState().transform = mTarget->GetTransform(); + if (aForceReset || !mBufferProvider->PreservesDrawingState()) { + for (const auto& style : mStyleStack) { + for (const auto& clipOrTransform : style.clipsAndTransforms) { + if (clipOrTransform.IsClip()) { + mTarget->PopClip(); + } + } + } + + if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) { + // With the cairo backend we pushed an extra clip rect which we have to + // balance out here. See the comment in + // RestoreClipsAndTransformToTarget. + mTarget->PopClip(); + } + + mTarget->SetTransform(Matrix()); + } + + mBufferProvider->ReturnDrawTarget(mTarget.forget()); + } +} + +NS_IMETHODIMP +CanvasRenderingContext2D::InitializeWithDrawTarget( + nsIDocShell* aShell, NotNull aTarget) { + if (NS_WARN_IF(!AddShutdownObserver())) { + return NS_ERROR_FAILURE; + } + + RemovePostRefreshObserver(); + mDocShell = aShell; + AddPostRefreshObserverIfNecessary(); + + IntSize size = aTarget->GetSize(); + SetDimensions(size.width, size.height); + + mTarget = aTarget; + mBufferProvider = new PersistentBufferProviderBasic(aTarget); + + RestoreClipsAndTransformToTarget(); + + return NS_OK; +} + +void CanvasRenderingContext2D::SetOpaqueValueFromOpaqueAttr( + bool aOpaqueAttrValue) { + if (aOpaqueAttrValue != mOpaqueAttrValue) { + mOpaqueAttrValue = aOpaqueAttrValue; + UpdateIsOpaque(); + } +} + +void CanvasRenderingContext2D::UpdateIsOpaque() { + mOpaque = !mContextAttributesHasAlpha || mOpaqueAttrValue; + ClearTarget(); +} + +NS_IMETHODIMP +CanvasRenderingContext2D::SetContextOptions(JSContext* aCx, + JS::Handle aOptions, + ErrorResult& aRvForDictionaryInit) { + if (aOptions.isNullOrUndefined()) { + return NS_OK; + } + + // This shouldn't be called before drawing starts, so there should be no + // drawtarget yet + MOZ_ASSERT(!mTarget); + + CanvasRenderingContext2DSettings attributes; + if (!attributes.Init(aCx, aOptions)) { + aRvForDictionaryInit.Throw(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + mWillReadFrequently = attributes.mWillReadFrequently; + + mContextAttributesHasAlpha = attributes.mAlpha; + UpdateIsOpaque(); + + return NS_OK; +} + +UniquePtr CanvasRenderingContext2D::GetImageBuffer( + int32_t* out_format, gfx::IntSize* out_imageSize) { + UniquePtr ret; + + *out_format = 0; + *out_imageSize = {}; + + if (!GetBufferProvider() && !EnsureTarget()) { + return nullptr; + } + + RefPtr snapshot = mBufferProvider->BorrowSnapshot(); + if (snapshot) { + RefPtr data = snapshot->GetDataSurface(); + if (data && data->GetSize() == GetSize()) { + *out_format = imgIEncoder::INPUT_FORMAT_HOSTARGB; + *out_imageSize = data->GetSize(); + ret = SurfaceToPackedBGRA(data); + } + } + + mBufferProvider->ReturnSnapshot(snapshot.forget()); + + if (ret && ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) { + nsRFPService::RandomizePixels( + GetCookieJarSettings(), ret.get(), out_imageSize->width, + out_imageSize->height, out_imageSize->width * out_imageSize->height * 4, + SurfaceFormat::A8R8G8B8_UINT32); + } + + return ret; +} + +NS_IMETHODIMP +CanvasRenderingContext2D::GetInputStream(const char* aMimeType, + const nsAString& aEncoderOptions, + nsIInputStream** aStream) { + nsCString enccid("@mozilla.org/image/encoder;2?type="); + enccid += aMimeType; + nsCOMPtr encoder = do_CreateInstance(enccid.get()); + if (!encoder) { + return NS_ERROR_FAILURE; + } + + int32_t format = 0; + gfx::IntSize imageSize = {}; + UniquePtr imageBuffer = GetImageBuffer(&format, &imageSize); + if (!imageBuffer) { + return NS_ERROR_FAILURE; + } + + return ImageEncoder::GetInputStream(imageSize.width, imageSize.height, + imageBuffer.get(), format, encoder, + aEncoderOptions, aStream); +} + +already_AddRefed +CanvasRenderingContext2D::GetOptimizedSnapshot(DrawTarget* aTarget, + gfxAlphaType* aOutAlphaType) { + if (aOutAlphaType) { + *aOutAlphaType = (mOpaque ? gfxAlphaType::Opaque : gfxAlphaType::Premult); + } + + // For GetSurfaceSnapshot we always call EnsureTarget even if mBufferProvider + // already exists, otherwise we get performance issues. See bug 1567054. + if (!EnsureTarget()) { + MOZ_ASSERT( + mTarget == sErrorTarget.get(), + "On EnsureTarget failure mTarget should be set to sErrorTarget."); + // In rare circumstances we may have failed to create an error target. + return mTarget ? mTarget->Snapshot() : nullptr; + } + + // The concept of BorrowSnapshot seems a bit broken here, but the original + // code in GetSurfaceSnapshot just returned a snapshot from mTarget, which + // amounts to breaking the concept implicitly. + RefPtr snapshot = mBufferProvider->BorrowSnapshot(aTarget); + RefPtr retSurface = snapshot; + mBufferProvider->ReturnSnapshot(snapshot.forget()); + return retSurface.forget(); +} + +SurfaceFormat CanvasRenderingContext2D::GetSurfaceFormat() const { + return mOpaque ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::B8G8R8A8; +} + +// +// state +// + +void CanvasRenderingContext2D::Save() { + EnsureTarget(); + if (MOZ_UNLIKELY(!mTarget || mStyleStack.IsEmpty())) { + SetErrorState(); + return; + } + mStyleStack[mStyleStack.Length() - 1].transform = mTarget->GetTransform(); + mStyleStack.SetCapacity(mStyleStack.Length() + 1); + mStyleStack.AppendElement(CurrentState()); + + if (mStyleStack.Length() > MAX_STYLE_STACK_SIZE) { + // This is not fast, but is better than OOMing and shouldn't be hit by + // reasonable code. + mStyleStack.RemoveElementAt(0); + } +} + +void CanvasRenderingContext2D::Restore() { + if (MOZ_UNLIKELY(mStyleStack.Length() < 2)) { + return; + } + + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + for (const auto& clipOrTransform : CurrentState().clipsAndTransforms) { + if (clipOrTransform.IsClip()) { + mTarget->PopClip(); + } + } + + mStyleStack.RemoveLastElement(); + + mTarget->SetTransform(CurrentState().transform); + mPathTransformDirty = true; +} + +// +// transformations +// + +void CanvasRenderingContext2D::Scale(double aX, double aY, + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Matrix newMatrix = mTarget->GetTransform(); + newMatrix.PreScale(aX, aY); + SetTransformInternal(newMatrix); +} + +void CanvasRenderingContext2D::Rotate(double aAngle, ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Matrix newMatrix = Matrix::Rotation(aAngle) * mTarget->GetTransform(); + SetTransformInternal(newMatrix); +} + +void CanvasRenderingContext2D::Translate(double aX, double aY, + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Matrix newMatrix = mTarget->GetTransform(); + newMatrix.PreTranslate(aX, aY); + SetTransformInternal(newMatrix); +} + +void CanvasRenderingContext2D::Transform(double aM11, double aM12, double aM21, + double aM22, double aDx, double aDy, + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy); + newMatrix *= mTarget->GetTransform(); + SetTransformInternal(newMatrix); +} + +already_AddRefed CanvasRenderingContext2D::GetTransform( + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + RefPtr matrix = + new DOMMatrix(GetParentObject(), mTarget->GetTransform()); + return matrix.forget(); +} + +void CanvasRenderingContext2D::SetTransform(double aM11, double aM12, + double aM21, double aM22, + double aDx, double aDy, + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy); + SetTransformInternal(newMatrix); +} + +void CanvasRenderingContext2D::SetTransform(const DOMMatrix2DInit& aInit, + ErrorResult& aError) { + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr matrix = + DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError); + if (!aError.Failed()) { + Matrix newMatrix = Matrix(*(matrix->GetInternal2D())); + SetTransformInternal(newMatrix); + } +} + +void CanvasRenderingContext2D::SetTransformInternal(const Matrix& aTransform) { + if (!aTransform.IsFinite()) { + return; + } + + // Save the transform in the clip stack to be able to replay clips properly. + auto& clipsAndTransforms = CurrentState().clipsAndTransforms; + if (clipsAndTransforms.IsEmpty() || + clipsAndTransforms.LastElement().IsClip()) { + clipsAndTransforms.AppendElement(ClipState(aTransform)); + } else { + // If the last item is a transform we can replace it instead of appending + // a new item. + clipsAndTransforms.LastElement().transform = aTransform; + } + + mTarget->SetTransform(aTransform); + mPathTransformDirty = true; +} + +void CanvasRenderingContext2D::ResetTransform(ErrorResult& aError) { + SetTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, aError); +} + +// +// colors +// + +void CanvasRenderingContext2D::SetStyleFromUnion( + const UTF8StringOrCanvasGradientOrCanvasPattern& aValue, + Style aWhichStyle) { + if (aValue.IsUTF8String()) { + SetStyleFromString(aValue.GetAsUTF8String(), aWhichStyle); + return; + } + + if (aValue.IsCanvasGradient()) { + SetStyleFromGradient(aValue.GetAsCanvasGradient(), aWhichStyle); + return; + } + + if (aValue.IsCanvasPattern()) { + CanvasPattern& pattern = aValue.GetAsCanvasPattern(); + SetStyleFromPattern(pattern, aWhichStyle); + if (pattern.mForceWriteOnly) { + SetWriteOnly(); + } + return; + } + + MOZ_ASSERT_UNREACHABLE("Invalid union value"); +} + +void CanvasRenderingContext2D::SetFillRule(const nsAString& aString) { + FillRule rule; + + if (aString.EqualsLiteral("evenodd")) + rule = FillRule::FILL_EVEN_ODD; + else if (aString.EqualsLiteral("nonzero")) + rule = FillRule::FILL_WINDING; + else + return; + + CurrentState().fillRule = rule; +} + +void CanvasRenderingContext2D::GetFillRule(nsAString& aString) { + switch (CurrentState().fillRule) { + case FillRule::FILL_WINDING: + aString.AssignLiteral("nonzero"); + break; + case FillRule::FILL_EVEN_ODD: + aString.AssignLiteral("evenodd"); + break; + } +} +// +// gradients and patterns +// +already_AddRefed CanvasRenderingContext2D::CreateLinearGradient( + double aX0, double aY0, double aX1, double aY1) { + RefPtr grad = + new CanvasLinearGradient(this, Point(aX0, aY0), Point(aX1, aY1)); + + return grad.forget(); +} + +already_AddRefed CanvasRenderingContext2D::CreateRadialGradient( + double aX0, double aY0, double aR0, double aX1, double aY1, double aR1, + ErrorResult& aError) { + if (aR0 < 0.0 || aR1 < 0.0) { + aError.ThrowIndexSizeError("Negative radius"); + return nullptr; + } + + RefPtr grad = new CanvasRadialGradient( + this, Point(aX0, aY0), aR0, Point(aX1, aY1), aR1); + + return grad.forget(); +} + +already_AddRefed CanvasRenderingContext2D::CreateConicGradient( + double aAngle, double aCx, double aCy) { + double adjustedStartAngle = aAngle + M_PI / 2.0; + return MakeAndAddRef(this, adjustedStartAngle, + Point(aCx, aCy)); +} + +already_AddRefed CanvasRenderingContext2D::CreatePattern( + const CanvasImageSource& aSource, const nsAString& aRepeat, + ErrorResult& aError) { + CanvasPattern::RepeatMode repeatMode = CanvasPattern::RepeatMode::NOREPEAT; + + if (aRepeat.IsEmpty() || aRepeat.EqualsLiteral("repeat")) { + repeatMode = CanvasPattern::RepeatMode::REPEAT; + } else if (aRepeat.EqualsLiteral("repeat-x")) { + repeatMode = CanvasPattern::RepeatMode::REPEATX; + } else if (aRepeat.EqualsLiteral("repeat-y")) { + repeatMode = CanvasPattern::RepeatMode::REPEATY; + } else if (aRepeat.EqualsLiteral("no-repeat")) { + repeatMode = CanvasPattern::RepeatMode::NOREPEAT; + } else { + aError.ThrowSyntaxError("Invalid pattern keyword"); + return nullptr; + } + + Element* element = nullptr; + OffscreenCanvas* offscreenCanvas = nullptr; + VideoFrame* videoFrame = nullptr; + + if (aSource.IsHTMLCanvasElement()) { + HTMLCanvasElement* canvas = &aSource.GetAsHTMLCanvasElement(); + element = canvas; + + nsIntSize size = canvas->GetSize(); + if (size.width == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has width 0"); + return nullptr; + } + + if (size.height == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has height 0"); + return nullptr; + } + + // Special case for Canvas, which could be an Azure canvas! + nsICanvasRenderingContextInternal* srcCanvas = canvas->GetCurrentContext(); + if (srcCanvas) { + // This might not be an Azure canvas! + RefPtr srcSurf = srcCanvas->GetSurfaceSnapshot(); + if (!srcSurf) { + aError.ThrowInvalidStateError( + "CanvasRenderingContext2D.createPattern() failed to snapshot source" + "canvas."); + return nullptr; + } + + RefPtr pat = + new CanvasPattern(this, srcSurf, repeatMode, element->NodePrincipal(), + canvas->IsWriteOnly(), false); + + return pat.forget(); + } + } else if (aSource.IsHTMLImageElement()) { + HTMLImageElement* img = &aSource.GetAsHTMLImageElement(); + element = img; + } else if (aSource.IsSVGImageElement()) { + SVGImageElement* img = &aSource.GetAsSVGImageElement(); + element = img; + } else if (aSource.IsHTMLVideoElement()) { + auto& video = aSource.GetAsHTMLVideoElement(); + video.LogVisibility( + mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_PATTERN); + element = &video; + } else if (aSource.IsOffscreenCanvas()) { + offscreenCanvas = &aSource.GetAsOffscreenCanvas(); + + nsIntSize size = offscreenCanvas->GetWidthHeight(); + if (size.width == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has width 0"); + return nullptr; + } + + if (size.height == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has height 0"); + return nullptr; + } + + nsICanvasRenderingContextInternal* srcCanvas = + offscreenCanvas->GetContext(); + if (srcCanvas) { + RefPtr srcSurf = srcCanvas->GetSurfaceSnapshot(); + if (!srcSurf) { + aError.ThrowInvalidStateError( + "Passed-in canvas failed to create snapshot"); + return nullptr; + } + + RefPtr pat = new CanvasPattern( + this, srcSurf, repeatMode, srcCanvas->PrincipalOrNull(), + offscreenCanvas->IsWriteOnly(), false); + + return pat.forget(); + } + } else if (aSource.IsVideoFrame()) { + videoFrame = &aSource.GetAsVideoFrame(); + + if (videoFrame->CodedWidth() == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has width 0"); + return nullptr; + } + + if (videoFrame->CodedHeight() == 0) { + aError.ThrowInvalidStateError("Passed-in canvas has height 0"); + return nullptr; + } + } else { + // Special case for ImageBitmap + ImageBitmap& imgBitmap = aSource.GetAsImageBitmap(); + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + RefPtr srcSurf = imgBitmap.PrepareForDrawTarget(mTarget); + if (!srcSurf) { + aError.ThrowInvalidStateError( + "Passed-in ImageBitmap has been transferred"); + return nullptr; + } + + // An ImageBitmap never taints others so we set principalForSecurityCheck to + // nullptr and set CORSUsed to true for passing the security check in + // CanvasUtils::DoDrawImageSecurityCheck(). + RefPtr pat = new CanvasPattern( + this, srcSurf, repeatMode, nullptr, imgBitmap.IsWriteOnly(), true); + + return pat.forget(); + } + + EnsureTarget(); + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + // The canvas spec says that createPattern should use the first frame + // of animated images + auto flags = nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE | + nsLayoutUtils::SFE_EXACT_SIZE_SURFACE; + SurfaceFromElementResult res; + if (offscreenCanvas) { + res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, flags, + mTarget); + } else if (videoFrame) { + res = nsLayoutUtils::SurfaceFromVideoFrame(videoFrame, flags, mTarget); + } else { + res = nsLayoutUtils::SurfaceFromElement(element, flags, mTarget); + } + + // Per spec, we should throw here for the HTMLImageElement and SVGImageElement + // cases if the image request state is "broken". In terms of the infromation + // in "res", the "broken" state corresponds to not having a size and not being + // still-loading (so there is no size forthcoming). + if (aSource.IsHTMLImageElement() || aSource.IsSVGImageElement()) { + if (!res.mIsStillLoading && !res.mHasSize) { + aError.ThrowInvalidStateError( + "Passed-in image's current request's state is \"broken\""); + return nullptr; + } + + if (res.mSize.width == 0 || res.mSize.height == 0) { + return nullptr; + } + + // Is the "fully decodable" check already done in SurfaceFromElement? It's + // not clear how to do it from here, exactly. + } + + RefPtr surface = res.GetSourceSurface(); + if (!surface) { + return nullptr; + } + + RefPtr pat = + new CanvasPattern(this, surface, repeatMode, res.mPrincipal, + res.mIsWriteOnly, res.mCORSUsed); + return pat.forget(); +} + +// +// shadows +// +void CanvasRenderingContext2D::SetShadowColor(const nsACString& aShadowColor) { + Maybe color = ParseColor(aShadowColor); + if (!color) { + return; + } + + CurrentState().shadowColor = *color; +} + +// +// filters +// + +static already_AddRefed CreateDeclarationForServo( + nsCSSPropertyID aProperty, const nsACString& aPropertyValue, + Document* aDocument) { + ServoCSSParser::ParsingEnvironment env{aDocument->DefaultStyleAttrURLData(), + aDocument->GetCompatibilityMode(), + aDocument->CSSLoader()}; + RefPtr servoDeclarations = + ServoCSSParser::ParseProperty(aProperty, aPropertyValue, env, + StyleParsingMode::DEFAULT); + + if (!servoDeclarations) { + // We got a syntax error. The spec says this value must be ignored. + return nullptr; + } + + // From canvas spec, force to set line-height property to 'normal' font + // property. + if (aProperty == eCSSProperty_font) { + const nsCString normalString = "normal"_ns; + Servo_DeclarationBlock_SetPropertyById( + servoDeclarations, eCSSProperty_line_height, &normalString, false, + env.mUrlExtraData, StyleParsingMode::DEFAULT, env.mCompatMode, + env.mLoader, env.mRuleType, {}); + } + + return servoDeclarations.forget(); +} + +static already_AddRefed +CreateFontDeclarationForServo(const nsACString& aFont, Document* aDocument) { + return CreateDeclarationForServo(eCSSProperty_font, aFont, aDocument); +} + +static already_AddRefed GetFontStyleForServo( + Element* aElement, const nsACString& aFont, PresShell* aPresShell, + nsACString& aOutUsedFont, ErrorResult& aError) { + RefPtr declarations = + CreateFontDeclarationForServo(aFont, aPresShell->GetDocument()); + if (!declarations) { + // We got a syntax error. The spec says this value must be ignored. + return nullptr; + } + + // In addition to unparseable values, the spec says we need to reject + // 'inherit' and 'initial'. The easiest way to check for this is to look + // at font-size-adjust, which the font shorthand resets to 'none'. + if (Servo_DeclarationBlock_HasCSSWideKeyword(declarations, + eCSSProperty_font_size_adjust)) { + return nullptr; + } + + ServoStyleSet* styleSet = aPresShell->StyleSet(); + + // Have to get a parent ComputedStyle for inherit-like relative values (2em, + // bolder, etc.) + RefPtr parentStyle; + if (aElement) { + parentStyle = nsComputedDOMStyle::GetComputedStyle(aElement); + if (NS_WARN_IF(aPresShell->IsDestroying())) { + // The flush might've killed the shell. + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + } + if (!parentStyle) { + RefPtr declarations = + CreateFontDeclarationForServo("10px sans-serif"_ns, + aPresShell->GetDocument()); + MOZ_ASSERT(declarations); + + parentStyle = + aPresShell->StyleSet()->ResolveForDeclarations(nullptr, declarations); + } + + MOZ_RELEASE_ASSERT(parentStyle, "Should have a valid parent style"); + + MOZ_ASSERT(!aPresShell->IsDestroying(), + "We should have returned an error above if the presshell is " + "being destroyed."); + + RefPtr sc = + styleSet->ResolveForDeclarations(parentStyle, declarations); + + // https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-font + // The font-size component must be converted to CSS px for reserialization, + // so we update the declarations with the value from the computed style. + if (!sc->StyleFont()->mFont.family.is_system_font) { + nsAutoCString computedFontSize; + sc->GetComputedPropertyValue(eCSSProperty_font_size, computedFontSize); + Servo_DeclarationBlock_SetPropertyById( + declarations, eCSSProperty_font_size, &computedFontSize, false, nullptr, + StyleParsingMode::DEFAULT, eCompatibility_FullStandards, nullptr, + StyleCssRuleType::Style, {}); + } + + // The font getter is required to be reserialized based on what we + // parsed (including having line-height removed). + // If we failed to reserialize, ignore this attempt to set the value. + Servo_SerializeFontValueForCanvas(declarations, &aOutUsedFont); + if (aOutUsedFont.IsEmpty()) { + return nullptr; + } + + return sc.forget(); +} + +static already_AddRefed +CreateFilterDeclarationForServo(const nsACString& aFilter, + Document* aDocument) { + return CreateDeclarationForServo(eCSSProperty_filter, aFilter, aDocument); +} + +static already_AddRefed ResolveFilterStyleForServo( + const nsACString& aFilterString, const ComputedStyle* aParentStyle, + PresShell* aPresShell, ErrorResult& aError) { + RefPtr declarations = + CreateFilterDeclarationForServo(aFilterString, aPresShell->GetDocument()); + if (!declarations) { + // Refuse to accept the filter, but do not throw an error. + return nullptr; + } + + // In addition to unparseable values, the spec says we need to reject + // 'inherit' and 'initial'. + if (Servo_DeclarationBlock_HasCSSWideKeyword(declarations, + eCSSProperty_filter)) { + return nullptr; + } + + ServoStyleSet* styleSet = aPresShell->StyleSet(); + RefPtr computedValues = + styleSet->ResolveForDeclarations(aParentStyle, declarations); + + return computedValues.forget(); +} + +bool CanvasRenderingContext2D::ParseFilter( + const nsACString& aString, StyleOwnedSlice& aFilterChain, + ErrorResult& aError) { + RefPtr presShell = GetPresShell(); + if (!presShell) { + nsIGlobalObject* global = GetParentObject(); + FontFaceSet* fontFaceSet = global ? global->GetFonts() : nullptr; + FontFaceSetImpl* fontFaceSetImpl = + fontFaceSet ? fontFaceSet->GetImpl() : nullptr; + RefPtr urlExtraData = + fontFaceSetImpl ? fontFaceSetImpl->GetURLExtraData() : nullptr; + + if (NS_WARN_IF(!urlExtraData)) { + // Provided we have a FontFaceSetImpl object, this should only happen on + // worker threads, where we failed to initialize the worker before it was + // shutdown. + aError.ThrowInvalidStateError("Missing URLExtraData"); + return false; + } + + if (NS_WARN_IF(!Servo_ParseFilters(&aString, /* aIgnoreUrls */ true, + urlExtraData, &aFilterChain))) { + return false; + } + + return true; + } + + nsAutoCString usedFont; // unused + + RefPtr parentStyle = GetFontStyleForServo( + mCanvasElement, GetFont(), presShell, usedFont, aError); + if (!parentStyle) { + return false; + } + + RefPtr style = + ResolveFilterStyleForServo(aString, parentStyle, presShell, aError); + if (!style) { + return false; + } + + aFilterChain = style->StyleEffects()->mFilters; + return true; +} + +void CanvasRenderingContext2D::SetFilter(const nsACString& aFilter, + ErrorResult& aError) { + StyleOwnedSlice filterChain; + if (ParseFilter(aFilter, filterChain, aError)) { + CurrentState().filterString = aFilter; + CurrentState().filterChain = std::move(filterChain); + if (mCanvasElement) { + CurrentState().autoSVGFiltersObserver = + SVGObserverUtils::ObserveFiltersForCanvasContext( + this, mCanvasElement, CurrentState().filterChain.AsSpan()); + } + UpdateFilter(/* aFlushIfNeeded = */ true); + } +} + +static already_AddRefed ResolveStyleForServo( + nsCSSPropertyID aProperty, const nsACString& aString, + const ComputedStyle* aParentStyle, PresShell* aPresShell, + ErrorResult& aError) { + RefPtr declarations = + CreateDeclarationForServo(aProperty, aString, aPresShell->GetDocument()); + if (!declarations) { + return nullptr; + } + + // In addition to unparseable values, reject 'inherit' and 'initial'. + if (Servo_DeclarationBlock_HasCSSWideKeyword(declarations, aProperty)) { + return nullptr; + } + + ServoStyleSet* styleSet = aPresShell->StyleSet(); + return styleSet->ResolveForDeclarations(aParentStyle, declarations); +} + +already_AddRefed +CanvasRenderingContext2D::ResolveStyleForProperty(nsCSSPropertyID aProperty, + const nsACString& aValue) { + RefPtr presShell = GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return nullptr; + } + + nsAutoCString usedFont; + IgnoredErrorResult err; + RefPtr parentStyle = + GetFontStyleForServo(mCanvasElement, GetFont(), presShell, usedFont, err); + if (!parentStyle) { + return nullptr; + } + + return ResolveStyleForServo(aProperty, aValue, parentStyle, presShell, err); +} + +void CanvasRenderingContext2D::GetLetterSpacing(nsACString& aLetterSpacing) { + if (CurrentState().letterSpacingStr.IsEmpty()) { + aLetterSpacing.AssignLiteral("0px"); + } else { + aLetterSpacing = CurrentState().letterSpacingStr; + } +} + +void CanvasRenderingContext2D::SetLetterSpacing( + const nsACString& aLetterSpacing) { + ParseSpacing(aLetterSpacing, &CurrentState().letterSpacing, + CurrentState().letterSpacingStr); +} + +void CanvasRenderingContext2D::GetWordSpacing(nsACString& aWordSpacing) { + if (CurrentState().wordSpacingStr.IsEmpty()) { + aWordSpacing.AssignLiteral("0px"); + } else { + aWordSpacing = CurrentState().wordSpacingStr; + } +} + +void CanvasRenderingContext2D::SetWordSpacing(const nsACString& aWordSpacing) { + ParseSpacing(aWordSpacing, &CurrentState().wordSpacing, + CurrentState().wordSpacingStr); +} + +static GeckoFontMetrics GetFontMetricsFromCanvas(void* aContext) { + auto* ctx = static_cast(aContext); + auto* fontGroup = ctx->GetCurrentFontStyle(); + if (!fontGroup) { + // Shouldn't happen, but just in case... return plausible values for a + // 10px font (canvas default size). + return {Length::FromPixels(5.0), + Length::FromPixels(5.0), + Length::FromPixels(8.0), + Length::FromPixels(10.0), + Length::FromPixels(8.0), + Length::FromPixels(10.0), + 0.0f, + 0.0f}; + } + auto metrics = fontGroup->GetMetricsForCSSUnits(nsFontMetrics::eHorizontal); + return {Length::FromPixels(metrics.xHeight), + Length::FromPixels(metrics.zeroWidth), + Length::FromPixels(metrics.capHeight), + Length::FromPixels(metrics.ideographicWidth), + Length::FromPixels(metrics.maxAscent), + Length::FromPixels(fontGroup->GetStyle()->size), + 0.0f, + 0.0f}; +} + +void CanvasRenderingContext2D::ParseSpacing(const nsACString& aSpacing, + float* aValue, + nsACString& aNormalized) { + // Normalize whitespace in the string before trying to parse it, as we want + // to store it in normalized form, and this allows a simple check against the + // 'normal' keyword, which is not accepted. + nsAutoCString normalized(aSpacing); + normalized.CompressWhitespace(true, true); + ToLowerCase(normalized); + if (normalized.EqualsLiteral("normal")) { + return; + } + float value; + if (!Servo_ParseLengthWithoutStyleContext(&normalized, &value, + GetFontMetricsFromCanvas, this)) { + if (!GetPresShell()) { + return; + } + RefPtr style = + ResolveStyleForProperty(eCSSProperty_letter_spacing, aSpacing); + if (!style) { + return; + } + value = style->StyleText()->mLetterSpacing.ToCSSPixels(); + } + aNormalized = normalized; + *aValue = value; +} + +class CanvasUserSpaceMetrics : public UserSpaceMetricsWithSize { + public: + CanvasUserSpaceMetrics(const gfx::IntSize& aSize, const nsFont& aFont, + const ComputedStyle* aCanvasStyle, + nsPresContext* aPresContext) + : mSize(aSize), + mFont(aFont), + mCanvasStyle(aCanvasStyle), + mPresContext(aPresContext) {} + + float GetEmLength(Type aType) const override { + switch (aType) { + case Type::This: + return mFont.size.ToCSSPixels(); + case Type::Root: + return SVGContentUtils::GetFontSize( + mPresContext->Document()->GetRootElement()); + default: + MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); + return 1.0f; + } + } + gfx::Size GetSize() const override { return Size(mSize); } + + CSSSize GetCSSViewportSize() const override { + return GetCSSViewportSizeFromContext(mPresContext); + } + + private: + GeckoFontMetrics GetFontMetricsForType(Type aType) const override { + switch (aType) { + case Type::This: { + if (!mCanvasStyle) { + return DefaultFontMetrics(); + } + return Gecko_GetFontMetrics( + mPresContext, WritingMode(mCanvasStyle).IsVertical(), + mCanvasStyle->StyleFont(), mCanvasStyle->StyleFont()->mFont.size, + /* aUseUserFontSet = */ true, + /* aRetrieveMathScales */ false); + } + case Type::Root: + return GetFontMetrics(mPresContext->Document()->GetRootElement()); + default: + MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); + return DefaultFontMetrics(); + } + } + + WritingMode GetWritingModeForType(Type aType) const override { + switch (aType) { + case Type::This: + return mCanvasStyle ? WritingMode(mCanvasStyle) : WritingMode(); + case Type::Root: + return GetWritingMode(mPresContext->Document()->GetRootElement()); + default: + MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); + return WritingMode(); + } + } + + gfx::IntSize mSize; + const nsFont& mFont; + RefPtr mCanvasStyle; + nsPresContext* mPresContext; +}; + +// The filter might reference an SVG filter that is declared inside this +// document. Flush frames so that we'll have a SVGFilterFrame to work +// with. +static bool FiltersNeedFrameFlush(Span aFilters) { + for (const auto& filter : aFilters) { + if (filter.IsUrl()) { + return true; + } + } + return false; +} + +void CanvasRenderingContext2D::UpdateFilter(bool aFlushIfNeeded) { + const bool writeOnly = IsWriteOnly() || + (mCanvasElement && mCanvasElement->IsWriteOnly()) || + (mOffscreenCanvas && mOffscreenCanvas->IsWriteOnly()); + + RefPtr presShell = GetPresShell(); + if (!mOffscreenCanvas && (!presShell || presShell->IsDestroying())) { + // Ensure we set an empty filter and update the state to + // reflect the current "taint" status of the canvas + CurrentState().filter = FilterDescription(); + CurrentState().filterSourceGraphicTainted = writeOnly; + return; + } + + // The PresContext is only used with URL filters and we don't allow those to + // be used on worker threads. + nsPresContext* presContext = nullptr; + if (presShell) { + if (aFlushIfNeeded && + FiltersNeedFrameFlush(CurrentState().filterChain.AsSpan())) { + presShell->FlushPendingNotifications(FlushType::Frames); + } + + if (MOZ_UNLIKELY(presShell->IsDestroying())) { + return; + } + + presContext = presShell->GetPresContext(); + } + RefPtr canvasStyle; + if (mCanvasElement) { + canvasStyle = nsComputedDOMStyle::GetComputedStyleNoFlush(mCanvasElement); + } + + MOZ_RELEASE_ASSERT(!mStyleStack.IsEmpty()); + + CurrentState().filter = FilterInstance::GetFilterDescription( + mCanvasElement, CurrentState().filterChain.AsSpan(), + CurrentState().autoSVGFiltersObserver, writeOnly, + CanvasUserSpaceMetrics(GetSize(), CurrentState().fontFont, canvasStyle, + presContext), + gfxRect(0, 0, mWidth, mHeight), CurrentState().filterAdditionalImages); + CurrentState().filterSourceGraphicTainted = writeOnly; +} + +// +// rects +// + +static bool ValidateRect(double& aX, double& aY, double& aWidth, + double& aHeight, bool aIsZeroSizeValid) { + if (!aIsZeroSizeValid && (aWidth == 0.0 || aHeight == 0.0)) { + return false; + } + + // bug 1018527 + // The values of canvas API input are in double precision, but Moz2D APIs are + // using float precision. Bypass canvas API calls when the input is out of + // float precision to avoid precision problem + if (!std::isfinite((float)aX) || !std::isfinite((float)aY) || + !std::isfinite((float)aWidth) || !std::isfinite((float)aHeight)) { + return false; + } + + // bug 1074733 + // The canvas spec does not forbid rects with negative w or h, so given + // corners (x, y), (x+w, y), (x+w, y+h), and (x, y+h) we must generate + // the appropriate rect by flipping negative dimensions. This prevents + // draw targets from receiving "empty" rects later on. + if (aWidth < 0) { + aWidth = -aWidth; + aX -= aWidth; + } + if (aHeight < 0) { + aHeight = -aHeight; + aY -= aHeight; + } + return true; +} + +void CanvasRenderingContext2D::ClearRect(double aX, double aY, double aW, + double aH) { + // Do not allow zeros - it's a no-op at that point per spec. + if (!ValidateRect(aX, aY, aW, aH, false)) { + return; + } + + gfx::Rect clearRect(aX, aY, aW, aH); + + EnsureTarget(&clearRect, true); + if (!IsTargetValid()) { + return; + } + + mTarget->ClearRect(clearRect); + + RedrawUser(gfxRect(aX, aY, aW, aH)); +} + +void CanvasRenderingContext2D::FillRect(double aX, double aY, double aW, + double aH) { + mFeatureUsage |= CanvasFeatureUsage::FillRect; + + if (!ValidateRect(aX, aY, aW, aH, true)) { + return; + } + + const ContextState* state = &CurrentState(); + if (state->patternStyles[Style::FILL]) { + auto& style = state->patternStyles[Style::FILL]; + CanvasPattern::RepeatMode repeat = style->mRepeat; + // In the FillRect case repeat modes are easy to deal with. + bool limitx = repeat == CanvasPattern::RepeatMode::NOREPEAT || + repeat == CanvasPattern::RepeatMode::REPEATY; + bool limity = repeat == CanvasPattern::RepeatMode::NOREPEAT || + repeat == CanvasPattern::RepeatMode::REPEATX; + if ((limitx || limity) && style->mTransform.IsRectilinear()) { + // For rectilinear transforms, we can just get the transformed pattern + // bounds and intersect them with the fill rectangle bounds. + // TODO: If the transform is not rectilinear, then we would need a fully + // general clip path to represent the X and Y clip planes bounding the + // pattern. For such cases, it would be more efficient to rely on Skia's + // Decal tiling mode rather than trying to generate a path. Until then, + // just punt to relying on the default Clamp mode. + gfx::Rect patternBounds(style->mSurface->GetRect()); + patternBounds = style->mTransform.TransformBounds(patternBounds); + if (style->mTransform.HasNonAxisAlignedTransform()) { + // If there is an rotation (90 or 270 degrees), the X axis of the + // pattern projects onto the Y axis of the geometry, and vice versa. + std::swap(limitx, limity); + } + // We always need to execute painting for non-over operators, even if + // we end up with w/h = 0. The default Rect::Intersect can cause both + // dimensions to become empty if either dimension individually fails + // to overlap, which is unsuitable. Instead, we need to independently + // limit the supplied rectangle on each dimension as required. + if (limitx) { + double x2 = aX + aW; + aX = std::max(aX, double(patternBounds.x)); + aW = std::max(std::min(x2, double(patternBounds.XMost())) - aX, 0.0); + } + if (limity) { + double y2 = aY + aH; + aY = std::max(aY, double(patternBounds.y)); + aH = std::max(std::min(y2, double(patternBounds.YMost())) - aY, 0.0); + } + } + } + state = nullptr; + + bool isColor; + bool discardContent = PatternIsOpaque(Style::FILL, &isColor) && + (CurrentState().op == CompositionOp::OP_OVER || + CurrentState().op == CompositionOp::OP_SOURCE); + const gfx::Rect fillRect(aX, aY, aW, aH); + EnsureTarget(discardContent ? &fillRect : nullptr, discardContent && isColor); + if (!IsTargetValid()) { + return; + } + + gfx::Rect bounds; + const bool needBounds = NeedToCalculateBounds(); + if (!IsTargetValid()) { + return; + } + if (needBounds) { + bounds = mTarget->GetTransform().TransformBounds(fillRect); + } + + AntialiasMode antialiasMode = CurrentState().imageSmoothingEnabled + ? AntialiasMode::DEFAULT + : AntialiasMode::NONE; + + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + CompositionOp op = target.UsedOperation(); + if (!target) { + return; + } + target.FillRect(gfx::Rect(aX, aY, aW, aH), + CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget), + DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); + + RedrawUser(gfxRect(aX, aY, aW, aH)); +} + +void CanvasRenderingContext2D::StrokeRect(double aX, double aY, double aW, + double aH) { + if (!aW && !aH) { + return; + } + + if (!ValidateRect(aX, aY, aW, aH, true)) { + return; + } + + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + const bool needBounds = NeedToCalculateBounds(); + if (!IsTargetValid()) { + return; + } + + gfx::Rect bounds; + if (needBounds) { + const ContextState& state = CurrentState(); + bounds = gfx::Rect(aX - state.lineWidth / 2.0f, aY - state.lineWidth / 2.0f, + aW + state.lineWidth, aH + state.lineWidth); + bounds = mTarget->GetTransform().TransformBounds(bounds); + } + + if (!IsTargetValid()) { + return; + } + + if (!aH) { + CapStyle cap = CapStyle::BUTT; + if (CurrentState().lineJoin == CanvasLineJoin::Round) { + cap = CapStyle::ROUND; + } + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + auto op = target.UsedOperation(); + if (!target) { + return; + } + + const ContextState& state = CurrentState(); + target.StrokeLine( + Point(aX, aY), Point(aX + aW, aY), + CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), + StrokeOptions(state.lineWidth, CanvasToGfx(state.lineJoin), cap, + state.miterLimit, state.dash.Length(), + state.dash.Elements(), state.dashOffset), + DrawOptions(state.globalAlpha, op)); + return; + } + + if (!aW) { + CapStyle cap = CapStyle::BUTT; + if (CurrentState().lineJoin == CanvasLineJoin::Round) { + cap = CapStyle::ROUND; + } + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + auto op = target.UsedOperation(); + if (!target) { + return; + } + + const ContextState& state = CurrentState(); + target.StrokeLine( + Point(aX, aY), Point(aX, aY + aH), + CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), + StrokeOptions(state.lineWidth, CanvasToGfx(state.lineJoin), cap, + state.miterLimit, state.dash.Length(), + state.dash.Elements(), state.dashOffset), + DrawOptions(state.globalAlpha, op)); + return; + } + + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + auto op = target.UsedOperation(); + if (!target) { + return; + } + + const ContextState& state = CurrentState(); + target.StrokeRect( + gfx::Rect(aX, aY, aW, aH), + CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), + StrokeOptions(state.lineWidth, CanvasToGfx(state.lineJoin), + CanvasToGfx(state.lineCap), state.miterLimit, + state.dash.Length(), state.dash.Elements(), + state.dashOffset), + DrawOptions(state.globalAlpha, op)); + + Redraw(); +} + +// +// path bits +// + +void CanvasRenderingContext2D::BeginPath() { + mPath = nullptr; + mPathBuilder = nullptr; + mPathPruned = false; +} + +void CanvasRenderingContext2D::FillImpl(const gfx::Path& aPath) { + MOZ_ASSERT(IsTargetValid()); + if (aPath.IsEmpty()) { + return; + } + + const bool needBounds = NeedToCalculateBounds(); + gfx::Rect bounds; + if (needBounds) { + bounds = aPath.GetBounds(mTarget->GetTransform()); + } + + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + if (!target) { + return; + } + + auto op = target.UsedOperation(); + if (!IsTargetValid() || !target) { + return; + } + target.Fill(&aPath, + CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget), + DrawOptions(CurrentState().globalAlpha, op)); + Redraw(); +} + +void CanvasRenderingContext2D::Fill(const CanvasWindingRule& aWinding) { + EnsureUserSpacePath(aWinding); + if (!IsTargetValid()) { + return; + } + + if (mPath) { + FillImpl(*mPath); + } +} + +void CanvasRenderingContext2D::Fill(const CanvasPath& aPath, + const CanvasWindingRule& aWinding) { + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + RefPtr gfxpath = aPath.GetPath(aWinding, mTarget); + if (gfxpath) { + FillImpl(*gfxpath); + } +} + +void CanvasRenderingContext2D::StrokeImpl(const gfx::Path& aPath) { + MOZ_ASSERT(IsTargetValid()); + if (aPath.IsEmpty()) { + return; + } + + const ContextState* state = &CurrentState(); + StrokeOptions strokeOptions(state->lineWidth, CanvasToGfx(state->lineJoin), + CanvasToGfx(state->lineCap), state->miterLimit, + state->dash.Length(), state->dash.Elements(), + state->dashOffset); + state = nullptr; + + const bool needBounds = NeedToCalculateBounds(); + if (!IsTargetValid()) { + return; + } + gfx::Rect bounds; + if (needBounds) { + bounds = aPath.GetStrokedBounds(strokeOptions, mTarget->GetTransform()); + } + + AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); + if (!target) { + return; + } + + auto op = target.UsedOperation(); + if (!IsTargetValid() || !target) { + return; + } + target.Stroke(&aPath, + CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), + strokeOptions, DrawOptions(CurrentState().globalAlpha, op)); + Redraw(); +} + +void CanvasRenderingContext2D::Stroke() { + mFeatureUsage |= CanvasFeatureUsage::Stroke; + + EnsureUserSpacePath(); + if (!IsTargetValid()) { + return; + } + + if (mPath) { + StrokeImpl(*mPath); + } +} + +void CanvasRenderingContext2D::Stroke(const CanvasPath& aPath) { + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + RefPtr gfxpath = + aPath.GetPath(CanvasWindingRule::Nonzero, mTarget); + if (gfxpath) { + StrokeImpl(*gfxpath); + } +} + +void CanvasRenderingContext2D::DrawFocusIfNeeded( + mozilla::dom::Element& aElement, ErrorResult& aRv) { + EnsureUserSpacePath(); + if (!mPath) { + return; + } + + if (DrawCustomFocusRing(aElement)) { + AutoSaveRestore asr(this); + + // set state to conforming focus state + ContextState* state = &CurrentState(); + state->globalAlpha = 1.0; + state->shadowBlur = 0; + state->shadowOffset.x = 0; + state->shadowOffset.y = 0; + state->op = mozilla::gfx::CompositionOp::OP_OVER; + + state->lineCap = CanvasLineCap::Butt; + state->lineJoin = CanvasLineJoin::Miter; + state->lineWidth = 1; + state->dash.Clear(); + + // color and style of the rings is the same as for image maps + // set the background focus color + state->SetColorStyle(Style::STROKE, NS_RGBA(255, 255, 255, 255)); + state = nullptr; + + // draw the focus ring + Stroke(); + if (!mPath) { + return; + } + + // set dashing for foreground + nsTArray& dash = CurrentState().dash; + for (uint32_t i = 0; i < 2; ++i) { + if (!dash.AppendElement(1, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + + // set the foreground focus color + CurrentState().SetColorStyle(Style::STROKE, NS_RGBA(0, 0, 0, 255)); + // draw the focus ring + Stroke(); + if (!mPath) { + return; + } + } +} + +bool CanvasRenderingContext2D::DrawCustomFocusRing(Element& aElement) { + if (!aElement.State().HasState(ElementState::FOCUSRING)) { + return false; + } + + HTMLCanvasElement* canvas = GetCanvas(); + if (!canvas || !aElement.IsInclusiveDescendantOf(canvas)) { + return false; + } + + EnsureUserSpacePath(); + return true; +} + +void CanvasRenderingContext2D::Clip(const CanvasWindingRule& aWinding) { + EnsureUserSpacePath(aWinding); + + if (!mPath) { + return; + } + + mTarget->PushClip(mPath); + CurrentState().clipsAndTransforms.AppendElement(ClipState(mPath)); +} + +void CanvasRenderingContext2D::Clip(const CanvasPath& aPath, + const CanvasWindingRule& aWinding) { + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + RefPtr gfxpath = aPath.GetPath(aWinding, mTarget); + + if (!gfxpath) { + return; + } + + mTarget->PushClip(gfxpath); + CurrentState().clipsAndTransforms.AppendElement(ClipState(gfxpath)); +} + +void CanvasRenderingContext2D::ArcTo(double aX1, double aY1, double aX2, + double aY2, double aRadius, + ErrorResult& aError) { + if (aRadius < 0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + + EnsureWritablePath(); + + // Current point in user space! + Point p0 = mPathBuilder->CurrentPoint(); + + Point p1(aX1, aY1); + Point p2(aX2, aY2); + + if (!p1.IsFinite() || !p2.IsFinite() || !std::isfinite(aRadius)) { + return; + } + + // Execute these calculations in double precision to avoid cumulative + // rounding errors. + double dir, a2, b2, c2, cosx, sinx, d, anx, any, bnx, bny, x3, y3, x4, y4, cx, + cy, angle0, angle1; + bool anticlockwise; + + if (p0 == p1 || p1 == p2 || aRadius == 0) { + LineTo(p1); + return; + } + + // Check for colinearity + dir = (p2.x.value - p1.x.value) * (p0.y.value - p1.y.value) + + (p2.y.value - p1.y.value) * (p1.x.value - p0.x.value); + if (dir == 0) { + LineTo(p1); + return; + } + + // XXX - Math for this code was already available from the non-azure code + // and would be well tested. Perhaps converting to bezier directly might + // be more efficient longer run. + a2 = (p0.x - aX1) * (p0.x - aX1) + (p0.y - aY1) * (p0.y - aY1); + b2 = (aX1 - aX2) * (aX1 - aX2) + (aY1 - aY2) * (aY1 - aY2); + c2 = (p0.x - aX2) * (p0.x - aX2) + (p0.y - aY2) * (p0.y - aY2); + cosx = (a2 + b2 - c2) / (2 * sqrt(a2 * b2)); + + sinx = sqrt(1 - cosx * cosx); + d = aRadius / ((1 - cosx) / sinx); + + anx = (aX1 - p0.x) / sqrt(a2); + any = (aY1 - p0.y) / sqrt(a2); + bnx = (aX1 - aX2) / sqrt(b2); + bny = (aY1 - aY2) / sqrt(b2); + x3 = aX1 - anx * d; + y3 = aY1 - any * d; + x4 = aX1 - bnx * d; + y4 = aY1 - bny * d; + anticlockwise = (dir < 0); + cx = x3 + any * aRadius * (anticlockwise ? 1 : -1); + cy = y3 - anx * aRadius * (anticlockwise ? 1 : -1); + angle0 = atan2((y3 - cy), (x3 - cx)); + angle1 = atan2((y4 - cy), (x4 - cx)); + + LineTo(x3, y3); + + Arc(cx, cy, aRadius, angle0, angle1, anticlockwise, aError); +} + +void CanvasRenderingContext2D::Arc(double aX, double aY, double aR, + double aStartAngle, double aEndAngle, + bool aAnticlockwise, ErrorResult& aError) { + if (aR < 0.0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + if (aStartAngle == aEndAngle) { + LineTo(aX + aR * cos(aStartAngle), aY + aR * sin(aStartAngle)); + return; + } + + EnsureWritablePath(); + + EnsureActivePath(); + + mPathBuilder->Arc(Point(aX, aY), aR, aStartAngle, aEndAngle, aAnticlockwise); + mPathPruned = false; +} + +void CanvasRenderingContext2D::Rect(double aX, double aY, double aW, + double aH) { + EnsureWritablePath(); + + if (!std::isfinite(aX) || !std::isfinite(aY) || !std::isfinite(aW) || + !std::isfinite(aH)) { + return; + } + + EnsureCapped(); + mPathBuilder->MoveTo(Point(aX, aY)); + if (aW == 0 && aH == 0) { + return; + } + mPathBuilder->LineTo(Point(aX + aW, aY)); + mPathBuilder->LineTo(Point(aX + aW, aY + aH)); + mPathBuilder->LineTo(Point(aX, aY + aH)); + mPathBuilder->Close(); +} + +// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect +static void RoundRectImpl( + PathBuilder* aPathBuilder, const Maybe& aTransform, double aX, + double aY, double aW, double aH, + const UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence& + aRadii, + ErrorResult& aError) { + // Step 1. If any of x, y, w, or h are infinite or NaN, then return. + if (!std::isfinite(aX) || !std::isfinite(aY) || !std::isfinite(aW) || + !std::isfinite(aH)) { + return; + } + + nsTArray radii; + // Step 2. If radii is an unrestricted double or DOMPointInit, then set radii + // to « radii ». + if (aRadii.IsUnrestrictedDouble()) { + radii.AppendElement()->SetAsUnrestrictedDouble() = + aRadii.GetAsUnrestrictedDouble(); + } else if (aRadii.IsDOMPointInit()) { + radii.AppendElement()->SetAsDOMPointInit() = aRadii.GetAsDOMPointInit(); + } else { + radii = aRadii.GetAsUnrestrictedDoubleOrDOMPointInitSequence(); + // Step 3. If radii is not a list of size one, two, three, or + // four, then throw a RangeError. + if (radii.Length() < 1 || radii.Length() > 4) { + aError.ThrowRangeError("Can have between 1 and 4 radii"); + return; + } + } + + // Step 4. Let normalizedRadii be an empty list. + AutoTArray normalizedRadii; + + // Step 5. For each radius of radii: + for (const auto& radius : radii) { + // Step 5.1. If radius is a DOMPointInit: + if (radius.IsDOMPointInit()) { + const DOMPointInit& point = radius.GetAsDOMPointInit(); + // Step 5.1.1. If radius["x"] or radius["y"] is infinite or NaN, then + // return. + if (!std::isfinite(point.mX) || !std::isfinite(point.mY)) { + return; + } + + // Step 5.1.2. If radius["x"] or radius["y"] is negative, then + // throw a RangeError. + if (point.mX < 0 || point.mY < 0) { + aError.ThrowRangeError("Radius can not be negative"); + return; + } + + // Step 5.1.3. Otherwise, append radius to + // normalizedRadii. + normalizedRadii.AppendElement( + Size(gfx::Float(point.mX), gfx::Float(point.mY))); + continue; + } + + // Step 5.2. If radius is a unrestricted double: + double r = radius.GetAsUnrestrictedDouble(); + // Step 5.2.1. If radius is infinite or NaN, then return. + if (!std::isfinite(r)) { + return; + } + + // Step 5.2.2. If radius is negative, then throw a RangeError. + if (r < 0) { + aError.ThrowRangeError("Radius can not be negative"); + return; + } + + // Step 5.2.3. Otherwise append «[ "x" → radius, "y" → radius ]» to + // normalizedRadii. + normalizedRadii.AppendElement(Size(gfx::Float(r), gfx::Float(r))); + } + + // Step 6. Let upperLeft, upperRight, lowerRight, and lowerLeft be null. + Size upperLeft, upperRight, lowerRight, lowerLeft; + + if (normalizedRadii.Length() == 4) { + // Step 7. If normalizedRadii's size is 4, then set upperLeft to + // normalizedRadii[0], set upperRight to normalizedRadii[1], set lowerRight + // to normalizedRadii[2], and set lowerLeft to normalizedRadii[3]. + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + lowerLeft = normalizedRadii[3]; + } else if (normalizedRadii.Length() == 3) { + // Step 8. If normalizedRadii's size is 3, then set upperLeft to + // normalizedRadii[0], set upperRight and lowerLeft to normalizedRadii[1], + // and set lowerRight to normalizedRadii[2]. + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerRight = normalizedRadii[2]; + lowerLeft = normalizedRadii[1]; + } else if (normalizedRadii.Length() == 2) { + // Step 9. If normalizedRadii's size is 2, then set upperLeft and lowerRight + // to normalizedRadii[0] and set upperRight and lowerLeft to + // normalizedRadii[1]. + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[1]; + lowerRight = normalizedRadii[0]; + lowerLeft = normalizedRadii[1]; + } else { + // Step 10. If normalizedRadii's size is 1, then set upperLeft, upperRight, + // lowerRight, and lowerLeft to normalizedRadii[0]. + MOZ_ASSERT(normalizedRadii.Length() == 1); + upperLeft = normalizedRadii[0]; + upperRight = normalizedRadii[0]; + lowerRight = normalizedRadii[0]; + lowerLeft = normalizedRadii[0]; + } + + // This is not as specified but copied from Chrome. + // XXX Maybe if we implemented Step 12 (the path algorithm) per + // spec this wouldn't be needed? + Float x(aX), y(aY), w(aW), h(aH); + bool clockwise = true; + if (w < 0) { + // Horizontal flip + clockwise = false; + x += w; + w = -w; + std::swap(upperLeft, upperRight); + std::swap(lowerLeft, lowerRight); + } + + if (h < 0) { + // Vertical flip + clockwise = !clockwise; + y += h; + h = -h; + std::swap(upperLeft, lowerLeft); + std::swap(upperRight, lowerRight); + } + + // Step 11. Corner curves must not overlap. Scale all radii to prevent this: + // Step 11.1. Let top be upperLeft["x"] + upperRight["x"]. + Float top = upperLeft.width + upperRight.width; + // Step 11.2. Let right be upperRight["y"] + lowerRight["y"]. + Float right = upperRight.height + lowerRight.height; + // Step 11.3. Let bottom be lowerRight["x"] + lowerLeft["x"]. + Float bottom = lowerRight.width + lowerLeft.width; + // Step 11.4. Let left be upperLeft["y"] + lowerLeft["y"]. + Float left = upperLeft.height + lowerLeft.height; + // Step 11.5. Let scale be the minimum value of the ratios w / top, h / right, + // w / bottom, h / left. + Float scale = std::min({w / top, h / right, w / bottom, h / left}); + // Step 11.6. If scale is less than 1, then set the x and y members of + // upperLeft, upperRight, lowerLeft, and lowerRight to their current values + // multiplied by scale. + if (scale < 1.0f) { + upperLeft = upperLeft * scale; + upperRight = upperRight * scale; + lowerLeft = lowerLeft * scale; + lowerRight = lowerRight * scale; + } + + // Step 12. Create a new subpath: + // Step 13. Mark the subpath as closed. + // Note: Implemented by AppendRoundedRectToPath, which is shared with CSS + // borders etc. + gfx::Rect rect{x, y, w, h}; + RectCornerRadii cornerRadii(upperLeft, upperRight, lowerRight, lowerLeft); + AppendRoundedRectToPath(aPathBuilder, rect, cornerRadii, clockwise, + aTransform); + + // Step 14. Create a new subpath with the point (x, y) as the only point in + // the subpath. + // XXX We don't seem to be doing this for ::Rect either? +} + +void CanvasRenderingContext2D::RoundRect( + double aX, double aY, double aW, double aH, + const UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence& + aRadii, + ErrorResult& aError) { + EnsureWritablePath(); + + PathBuilder* builder = mPathBuilder; + Maybe transform = Nothing(); + + EnsureCapped(); + RoundRectImpl(builder, transform, aX, aY, aW, aH, aRadii, aError); +} + +void CanvasRenderingContext2D::Ellipse(double aX, double aY, double aRadiusX, + double aRadiusY, double aRotation, + double aStartAngle, double aEndAngle, + bool aAnticlockwise, + ErrorResult& aError) { + if (aRadiusX < 0.0 || aRadiusY < 0.0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + + EnsureWritablePath(); + + ArcToBezier(this, Point(aX, aY), Size(aRadiusX, aRadiusY), aStartAngle, + aEndAngle, aAnticlockwise, aRotation); + mPathPruned = false; +} + +void CanvasRenderingContext2D::FlushPathTransform() { + if (!mPathTransformDirty) { + return; + } + if (mPath || mPathBuilder) { + Matrix inverse = mTarget->GetTransform(); + if (!inverse.ExactlyEquals(mPathTransform) && inverse.Invert()) { + TransformCurrentPath(mPathTransform * inverse); + } + } + mPathTransform = mTarget->GetTransform(); + mPathTransformDirty = false; +} + +void CanvasRenderingContext2D::EnsureWritablePath() { + EnsureTarget(); + // NOTE: IsTargetValid() may be false here (mTarget == sErrorTarget) but we + // go ahead and create a path anyway since callers depend on that. + + FillRule fillRule = CurrentState().fillRule; + + if (mPathTransformDirty) { + FlushPathTransform(); + } + + if (mPathBuilder) { + return; + } + + if (!mPath) { + mPathBuilder = mTarget->CreatePathBuilder(fillRule); + } else { + mPathBuilder = mPath->CopyToBuilder(fillRule); + } +} + +void CanvasRenderingContext2D::EnsureUserSpacePath( + const CanvasWindingRule& aWinding) { + FillRule fillRule = CurrentState().fillRule; + if (aWinding == CanvasWindingRule::Evenodd) { + fillRule = FillRule::FILL_EVEN_ODD; + } + + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + if (mPathTransformDirty) { + FlushPathTransform(); + } + + if (!mPath && !mPathBuilder) { + mPathBuilder = mTarget->CreatePathBuilder(fillRule); + } + + if (mPathBuilder) { + EnsureCapped(); + mPath = mPathBuilder->Finish(); + mPathBuilder = nullptr; + } + + if (mPath && mPath->GetFillRule() != fillRule) { + mPathBuilder = mPath->CopyToBuilder(fillRule); + mPath = mPathBuilder->Finish(); + mPathBuilder = nullptr; + } + + NS_ASSERTION(mPath, "mPath should exist"); +} + +void CanvasRenderingContext2D::TransformCurrentPath(const Matrix& aTransform) { + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + if (mPathBuilder) { + RefPtr path = mPathBuilder->Finish(); + mPathBuilder = path->TransformedCopyToBuilder(aTransform); + } else if (mPath) { + mPathBuilder = mPath->TransformedCopyToBuilder(aTransform); + mPath = nullptr; + } +} + +// +// text +// + +void CanvasRenderingContext2D::SetFont(const nsACString& aFont, + ErrorResult& aError) { + mFeatureUsage |= CanvasFeatureUsage::SetFont; + + SetFontInternal(aFont, aError); + if (aError.Failed()) { + return; + } + + // Setting the font attribute magically resets fontVariantCaps and + // fontStretch to normal. + // (spec unclear, cf. https://github.com/whatwg/html/issues/8103) + SetFontVariantCaps(CanvasFontVariantCaps::Normal); + SetFontStretch(CanvasFontStretch::Normal); + + // If letterSpacing or wordSpacing is present, recompute to account for + // changes to font-relative dimensions. + UpdateSpacing(); +} + +static float QuantizeFontSize(float aSize) { + // Based on the Veltkamp-Dekker float-splitting algorithm, see e.g. + // https://indico.cern.ch/event/313684/contributions/1687773/attachments/600513/826490/FPArith-Part2.pdf + // A 32-bit float has 24 bits of precision (23 stored, plus an implicit 1 bit + // at the start of the mantissa). + constexpr int bitsToDrop = 17; // leaving 7 bits of precision + constexpr int scale = 1 << bitsToDrop; + float d = aSize * (scale + 1); + float t = d - aSize; + return d - t; +} + +bool CanvasRenderingContext2D::SetFontInternal(const nsACString& aFont, + ErrorResult& aError) { + RefPtr presShell = GetPresShell(); + if (!presShell) { + return SetFontInternalDisconnected(aFont, aError); + } + + nsPresContext* c = presShell->GetPresContext(); + FontStyleCacheKey key{aFont, c->RestyleManager()->GetRestyleGeneration()}; + auto entry = mFontStyleCache.Lookup(key); + if (!entry) { + FontStyleData newData; + newData.mKey = key; + newData.mStyle = GetFontStyleForServo(mCanvasElement, aFont, presShell, + newData.mUsedFont, aError); + entry.Set(newData); + } + + const auto& data = entry.Data(); + if (!data.mStyle) { + return false; + } + + const nsStyleFont* fontStyle = data.mStyle->StyleFont(); + + // Purposely ignore the font size that respects the user's minimum + // font preference (fontStyle->mFont.size) in favor of the computed + // size (fontStyle->mSize). See + // https://bugzilla.mozilla.org/show_bug.cgi?id=698652. + // FIXME: Nobody initializes mAllowZoom for servo? + // MOZ_ASSERT(!fontStyle->mAllowZoom, + // "expected text zoom to be disabled on this nsStyleFont"); + nsFont resizedFont(fontStyle->mFont); + // Create a font group working in units of CSS pixels instead of the usual + // device pixels, to avoid being affected by page zoom. nsFontMetrics will + // convert nsFont size in app units to device pixels for the font group, so + // here we first apply to the size the equivalent of a conversion from device + // pixels to CSS pixels, to adjust for the difference in expectations from + // other nsFontMetrics clients. + resizedFont.size = + fontStyle->mSize.ScaledBy(1.0f / c->CSSToDevPixelScale().scale); + + // Quantize font size to avoid filling caches with thousands of fonts that + // differ by imperceptibly-tiny size deltas. + resizedFont.size = StyleCSSPixelLength::FromPixels( + QuantizeFontSize(resizedFont.size.ToCSSPixels())); + + resizedFont.kerning = CanvasToGfx(CurrentState().fontKerning); + + // fontStretch handling: if fontStretch is not 'normal', apply it; + // if it is normal, then use whatever the shorthand set. + // XXX(jfkthame) The interaction between the shorthand and the separate attr + // here is not clearly spec'd, and we may want to reconsider it (or revise + // the available values); see https://github.com/whatwg/html/issues/8103. + switch (CurrentState().fontStretch) { + case CanvasFontStretch::Normal: + // Leave whatever the shorthand set. + break; + case CanvasFontStretch::Ultra_condensed: + resizedFont.stretch = StyleFontStretch::ULTRA_CONDENSED; + break; + case CanvasFontStretch::Extra_condensed: + resizedFont.stretch = StyleFontStretch::EXTRA_CONDENSED; + break; + case CanvasFontStretch::Condensed: + resizedFont.stretch = StyleFontStretch::CONDENSED; + break; + case CanvasFontStretch::Semi_condensed: + resizedFont.stretch = StyleFontStretch::SEMI_CONDENSED; + break; + case CanvasFontStretch::Semi_expanded: + resizedFont.stretch = StyleFontStretch::SEMI_EXPANDED; + break; + case CanvasFontStretch::Expanded: + resizedFont.stretch = StyleFontStretch::EXPANDED; + break; + case CanvasFontStretch::Extra_expanded: + resizedFont.stretch = StyleFontStretch::EXTRA_EXPANDED; + break; + case CanvasFontStretch::Ultra_expanded: + resizedFont.stretch = StyleFontStretch::ULTRA_EXPANDED; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown stretch value"); + break; + } + + // fontVariantCaps handling: if fontVariantCaps is not 'normal', apply it; + // if it is, then use the smallCaps boolean from the shorthand. + // XXX(jfkthame) The interaction between the shorthand and the separate attr + // here is not clearly spec'd, and we may want to reconsider it (or revise + // the available values); see https://github.com/whatwg/html/issues/8103. + switch (CurrentState().fontVariantCaps) { + case CanvasFontVariantCaps::Normal: + // Leave whatever the shorthand set. + break; + case CanvasFontVariantCaps::Small_caps: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_SMALLCAPS; + break; + case CanvasFontVariantCaps::All_small_caps: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_ALLSMALL; + break; + case CanvasFontVariantCaps::Petite_caps: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_PETITECAPS; + break; + case CanvasFontVariantCaps::All_petite_caps: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_ALLPETITE; + break; + case CanvasFontVariantCaps::Unicase: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_UNICASE; + break; + case CanvasFontVariantCaps::Titling_caps: + resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_TITLING; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown caps value"); + break; + } + + c->Document()->FlushUserFontSet(); + + nsFontMetrics::Params params; + params.language = fontStyle->mLanguage; + params.explicitLanguage = fontStyle->mExplicitLanguage; + params.userFontSet = c->GetUserFontSet(); + params.textPerf = c->GetTextPerfMetrics(); + RefPtr metrics = c->GetMetricsFor(resizedFont, params); + + gfxFontGroup* newFontGroup = metrics->GetThebesFontGroup(); + CurrentState().fontGroup = newFontGroup; + NS_ASSERTION(CurrentState().fontGroup, "Could not get font group"); + CurrentState().font = data.mUsedFont; + CurrentState().fontFont = fontStyle->mFont; + CurrentState().fontFont.size = fontStyle->mSize; + CurrentState().fontLanguage = fontStyle->mLanguage; + CurrentState().fontExplicitLanguage = fontStyle->mExplicitLanguage; + + return true; +} + +static nsAutoCString FamilyListToString( + const StyleFontFamilyList& aFamilyList) { + return StringJoin(", "_ns, aFamilyList.list.AsSpan(), + [](nsACString& dst, const StyleSingleFontFamily& name) { + name.AppendToString(dst); + }); +} + +static void SerializeFontForCanvas(const StyleFontFamilyList& aList, + const gfxFontStyle& aStyle, + nsACString& aUsedFont) { + // Re-serialize the font shorthand as required by the canvas spec. + aUsedFont.Truncate(); + + if (!aStyle.style.IsNormal()) { + aStyle.style.ToString(aUsedFont); + aUsedFont.Append(" "); + } + + // font-weight is serialized as a number + if (!aStyle.weight.IsNormal()) { + aUsedFont.AppendFloat(aStyle.weight.ToFloat()); + aUsedFont.Append(" "); + } + + // font-stretch is serialized using CSS Fonts 3 keywords, not percentages. + if (!aStyle.stretch.IsNormal() && + Servo_FontStretch_SerializeKeyword(&aStyle.stretch, &aUsedFont)) { + aUsedFont.Append(" "); + } + + if (aStyle.variantCaps == NS_FONT_VARIANT_CAPS_SMALLCAPS) { + aUsedFont.Append("small-caps "); + } + + // Serialize the computed (not specified) size, and the family name(s). + aUsedFont.AppendFloat(aStyle.size); + aUsedFont.Append("px "); + aUsedFont.Append(FamilyListToString(aList)); +} + +bool CanvasRenderingContext2D::SetFontInternalDisconnected( + const nsACString& aFont, ErrorResult& aError) { + FontFaceSet* fontFaceSet = nullptr; + if (mCanvasElement) { + fontFaceSet = mCanvasElement->OwnerDoc()->Fonts(); + } else { + nsIGlobalObject* global = GetParentObject(); + fontFaceSet = global ? global->GetFonts() : nullptr; + } + + FontFaceSetImpl* fontFaceSetImpl = + fontFaceSet ? fontFaceSet->GetImpl() : nullptr; + RefPtr urlExtraData = + fontFaceSetImpl ? fontFaceSetImpl->GetURLExtraData() : nullptr; + + if (NS_WARN_IF(!urlExtraData)) { + // Provided we have a FontFaceSetImpl object, this should only happen on + // worker threads, where we failed to initialize the worker before it was + // shutdown. + aError.ThrowInvalidStateError("Missing URLExtraData"); + return false; + } + + if (fontFaceSetImpl) { + fontFaceSetImpl->FlushUserFontSet(); + } + + // In the OffscreenCanvas case we don't have the context necessary to call + // GetFontStyleForServo(), as we do in the main-thread canvas context, so + // instead we borrow ParseFontShorthandForMatching to parse the attribute. + StyleComputedFontStyleDescriptor style( + StyleComputedFontStyleDescriptor::Normal()); + StyleFontFamilyList list; + gfxFontStyle fontStyle; + float size = 0.0f; + bool smallCaps = false; + if (!ServoCSSParser::ParseFontShorthandForMatching( + aFont, urlExtraData, list, fontStyle.style, fontStyle.stretch, + fontStyle.weight, &size, &smallCaps)) { + return false; + } + + fontStyle.size = QuantizeFontSize(size); + + switch (CurrentState().fontStretch) { + case CanvasFontStretch::Normal: + // Leave whatever the shorthand set. + break; + case CanvasFontStretch::Ultra_condensed: + fontStyle.stretch = StyleFontStretch::ULTRA_CONDENSED; + break; + case CanvasFontStretch::Extra_condensed: + fontStyle.stretch = StyleFontStretch::EXTRA_CONDENSED; + break; + case CanvasFontStretch::Condensed: + fontStyle.stretch = StyleFontStretch::CONDENSED; + break; + case CanvasFontStretch::Semi_condensed: + fontStyle.stretch = StyleFontStretch::SEMI_CONDENSED; + break; + case CanvasFontStretch::Semi_expanded: + fontStyle.stretch = StyleFontStretch::SEMI_EXPANDED; + break; + case CanvasFontStretch::Expanded: + fontStyle.stretch = StyleFontStretch::EXPANDED; + break; + case CanvasFontStretch::Extra_expanded: + fontStyle.stretch = StyleFontStretch::EXTRA_EXPANDED; + break; + case CanvasFontStretch::Ultra_expanded: + fontStyle.stretch = StyleFontStretch::ULTRA_EXPANDED; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown stretch value"); + break; + } + + // fontVariantCaps handling: if fontVariantCaps is not 'normal', apply it; + // if it is, then use the smallCaps boolean from the shorthand. + // XXX(jfkthame) The interaction between the shorthand and the separate attr + // here is not clearly spec'd, and we may want to reconsider it (or revise + // the available values); see https://github.com/whatwg/html/issues/8103. + switch (CurrentState().fontVariantCaps) { + case CanvasFontVariantCaps::Normal: + fontStyle.variantCaps = smallCaps ? NS_FONT_VARIANT_CAPS_SMALLCAPS + : NS_FONT_VARIANT_CAPS_NORMAL; + break; + case CanvasFontVariantCaps::Small_caps: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_SMALLCAPS; + break; + case CanvasFontVariantCaps::All_small_caps: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_ALLSMALL; + break; + case CanvasFontVariantCaps::Petite_caps: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_PETITECAPS; + break; + case CanvasFontVariantCaps::All_petite_caps: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_ALLPETITE; + break; + case CanvasFontVariantCaps::Unicase: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_UNICASE; + break; + case CanvasFontVariantCaps::Titling_caps: + fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_TITLING; + break; + default: + MOZ_ASSERT_UNREACHABLE("unknown caps value"); + break; + } + // If variantCaps is set, we need to disable a gfxFont fast-path. + fontStyle.noFallbackVariantFeatures = + (fontStyle.variantCaps == NS_FONT_VARIANT_CAPS_NORMAL); + + // Set the kerning feature, if required by the fontKerning attribute. + gfxFontFeature setting{TRUETYPE_TAG('k', 'e', 'r', 'n'), 0}; + switch (CurrentState().fontKerning) { + case CanvasFontKerning::None: + setting.mValue = 0; + fontStyle.featureSettings.AppendElement(setting); + break; + case CanvasFontKerning::Normal: + setting.mValue = 1; + fontStyle.featureSettings.AppendElement(setting); + break; + default: + // auto case implies use user agent default + break; + } + + // If we have a canvas element, get its lang (if known). + RefPtr language; + bool explicitLanguage = false; + if (mCanvasElement) { + language = mCanvasElement->FragmentOrElement::GetLang(); + if (language) { + explicitLanguage = true; + } else { + language = mCanvasElement->OwnerDoc()->GetLanguageForStyle(); + } + } else { + // Pass the OS default language, to behave similarly to HTML or canvas- + // element content with no language tag. + language = nsLanguageAtomService::GetService()->GetLocaleLanguage(); + } + + // TODO: Cache fontGroups in the Worker (use an nsFontCache?) + gfxFontGroup* fontGroup = + new gfxFontGroup(nullptr, // aPresContext + list, // aFontFamilyList + &fontStyle, // aStyle + language, // aLanguage + explicitLanguage, // aExplicitLanguage + nullptr, // aTextPerf + fontFaceSetImpl, // aUserFontSet + 1.0, // aDevToCssSize + StyleFontVariantEmoji::Normal); + CurrentState().fontGroup = fontGroup; + SerializeFontForCanvas(list, fontStyle, CurrentState().font); + CurrentState().fontFont = nsFont(StyleFontFamily{list, false, false}, + StyleCSSPixelLength::FromPixels(size)); + CurrentState().fontFont.variantCaps = fontStyle.variantCaps; + CurrentState().fontLanguage = nullptr; + CurrentState().fontExplicitLanguage = false; + return true; +} + +void CanvasRenderingContext2D::UpdateSpacing() { + auto state = CurrentState(); + if (!state.letterSpacingStr.IsEmpty()) { + SetLetterSpacing(state.letterSpacingStr); + } + if (!state.wordSpacingStr.IsEmpty()) { + SetWordSpacing(state.wordSpacingStr); + } +} + +/* + * Helper function that replaces the whitespace characters in a string + * with U+0020 SPACE. The whitespace characters are defined as U+0020 SPACE, + * U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF), U+000B LINE + * TABULATION, U+000C FORM FEED (FF), and U+000D CARRIAGE RETURN (CR). + * We also replace characters with Bidi type Segment Separator or Block + * Separator. + * @param str The string whose whitespace characters to replace. + */ +static inline void TextReplaceWhitespaceCharacters(nsAutoString& aStr) { + aStr.ReplaceChar(u"\x09\x0A\x0B\x0C\x0D\x1C\x1D\x1E\x1F\x85\x2029", + char16_t(' ')); +} + +void CanvasRenderingContext2D::FillText(const nsAString& aText, double aX, + double aY, + const Optional& aMaxWidth, + ErrorResult& aError) { + // We try to match the most commonly observed strings used by canvas + // fingerprinting scripts. We do a prefix match, because that means having to + // match fewer bytes and sometimes the strings is followed by a few random + // characters. + // - Cwm fjordbank gly + // Used by FingerprintJS + // (https://github.com/fingerprintjs/fingerprintjs/blob/4c4b2c8455e701b8341b2b766d1939cf5de4b615/src/sources/canvas.ts#L119) + // and others + // - Hel$&?6%){mZ+#@ + // - <@nv45. F1n63r,Pr1n71n6! + // Usually there are at most a handful (usually ~1/2) fillText calls by + // fingerprinters + if (mFillTextCalls <= 5) { + if (StringBeginsWith(aText, u"Cwm fjord"_ns) || + StringBeginsWith(aText, u"Hel$&?6%"_ns) || + StringBeginsWith(aText, u"<@nv45. "_ns)) { + mFeatureUsage |= CanvasFeatureUsage::KnownFingerprintText; + } + mFillTextCalls++; + } + + DebugOnly> metrics = DrawOrMeasureText( + aText, aX, aY, aMaxWidth, TextDrawOperation::FILL, aError); + MOZ_ASSERT( + !metrics.inspect()); // drawing operation never returns TextMetrics +} + +void CanvasRenderingContext2D::StrokeText(const nsAString& aText, double aX, + double aY, + const Optional& aMaxWidth, + ErrorResult& aError) { + DebugOnly> metrics = DrawOrMeasureText( + aText, aX, aY, aMaxWidth, TextDrawOperation::STROKE, aError); + MOZ_ASSERT( + !metrics.inspect()); // drawing operation never returns TextMetrics +} + +UniquePtr CanvasRenderingContext2D::MeasureText( + const nsAString& aRawText, ErrorResult& aError) { + Optional maxWidth; + return DrawOrMeasureText(aRawText, 0, 0, maxWidth, TextDrawOperation::MEASURE, + aError); +} + +/** + * Used for nsBidiPresUtils::ProcessText + */ +struct MOZ_STACK_CLASS CanvasBidiProcessor final + : public nsBidiPresUtils::BidiProcessor { + using Style = CanvasRenderingContext2D::Style; + + explicit CanvasBidiProcessor(mozilla::gfx::PaletteCache& aPaletteCache) + : mPaletteCache(aPaletteCache) { + if (StaticPrefs::gfx_missing_fonts_notify()) { + mMissingFonts = MakeUnique(); + } + } + + ~CanvasBidiProcessor() { + // notify front-end code if we encountered missing glyphs in any script + if (mMissingFonts) { + mMissingFonts->Flush(); + } + } + + class PropertyProvider : public gfxTextRun::PropertyProvider { + public: + explicit PropertyProvider(const CanvasBidiProcessor& aProcessor) + : mProcessor(aProcessor) {} + + void GetSpacing(gfxTextRun::Range aRange, + gfxFont::Spacing* aSpacing) const { + for (auto i = aRange.start; i < aRange.end; ++i) { + auto* charGlyphs = mProcessor.mTextRun->GetCharacterGlyphs(); + if (i == mProcessor.mTextRun->GetLength() - 1 || + (charGlyphs[i + 1].IsClusterStart() && + charGlyphs[i + 1].IsLigatureGroupStart())) { + // Currently we add all the letterspacing to the right of the glyph, + // which is similar to Chrome's behavior, though the LTR vs RTL + // asymmetry seems unfortunate. + if (mProcessor.mTextRun->IsRightToLeft()) { + aSpacing->mAfter = 0; + aSpacing->mBefore = mProcessor.mLetterSpacing; + } else { + aSpacing->mBefore = 0; + aSpacing->mAfter = mProcessor.mLetterSpacing; + } + } else { + aSpacing->mBefore = 0; + aSpacing->mAfter = 0; + } + if (charGlyphs[i].CharIsSpace()) { + if (mProcessor.mTextRun->IsRightToLeft()) { + aSpacing->mBefore += mProcessor.mWordSpacing; + } else { + aSpacing->mAfter += mProcessor.mWordSpacing; + } + } + aSpacing++; + } + } + + mozilla::StyleHyphens GetHyphensOption() const { + return mozilla::StyleHyphens::None; + } + + // Methods only used when hyphenation is active, not relevant to canvas2d: + void GetHyphenationBreaks(gfxTextRun::Range aRange, + gfxTextRun::HyphenType* aBreakBefore) const { + MOZ_ASSERT_UNREACHABLE("no hyphenation in canvas2d text!"); + } + gfxFloat GetHyphenWidth() const { + MOZ_ASSERT_UNREACHABLE("no hyphenation in canvas2d text!"); + return 0.0; + } + already_AddRefed GetDrawTarget() const { + MOZ_ASSERT_UNREACHABLE("no hyphenation in canvas2d text!"); + return nullptr; + } + uint32_t GetAppUnitsPerDevUnit() const { + MOZ_ASSERT_UNREACHABLE("no hyphenation in canvas2d text!"); + return 60; + } + gfx::ShapedTextFlags GetShapedTextFlags() const { + MOZ_ASSERT_UNREACHABLE("no hyphenation in canvas2d text!"); + return gfx::ShapedTextFlags(); + } + + private: + const CanvasBidiProcessor& mProcessor; + }; + + using ContextState = CanvasRenderingContext2D::ContextState; + + void SetText(const char16_t* aText, int32_t aLength, + intl::BidiDirection aDirection) override { + if (mIgnoreSetText) { + // We've been told to ignore SetText because the processor is only ever + // handling a single, fixed string. + MOZ_ASSERT(mTextRun && mTextRun->GetLength() == uint32_t(aLength)); + return; + } + mSetTextCount++; + auto* pfl = gfxPlatformFontList::PlatformFontList(); + pfl->Lock(); + mFontgrp->CheckForUpdatedPlatformList(); + mFontgrp->UpdateUserFonts(); // ensure user font generation is current + // adjust flags for current direction run + gfx::ShapedTextFlags flags = mTextRunFlags; + if (aDirection == intl::BidiDirection::RTL) { + flags |= gfx::ShapedTextFlags::TEXT_IS_RTL; + } else { + flags &= ~gfx::ShapedTextFlags::TEXT_IS_RTL; + } + mTextRun = mFontgrp->MakeTextRun( + aText, aLength, mDrawTarget, mAppUnitsPerDevPixel, flags, + nsTextFrameUtils::Flags::DontSkipDrawingForPendingUserFonts, + mMissingFonts.get()); + pfl->Unlock(); + } + + nscoord GetWidth() override { + PropertyProvider provider(*this); + gfxTextRun::Metrics textRunMetrics = mTextRun->MeasureText( + mDoMeasureBoundingBox ? gfxFont::TIGHT_INK_EXTENTS + : gfxFont::LOOSE_INK_EXTENTS, + mDrawTarget, &provider); + + // this only measures the height; the total width is gotten from the + // the return value of ProcessText. + if (mDoMeasureBoundingBox) { + textRunMetrics.mBoundingBox.Scale(1.0 / mAppUnitsPerDevPixel); + mBoundingBox = mBoundingBox.Union(textRunMetrics.mBoundingBox); + } + + return NSToCoordRound(textRunMetrics.mAdvanceWidth); + } + + already_AddRefed GetGradientFor(Style aStyle) { + RefPtr pattern; + CanvasGradient* gradient = mCtx->CurrentState().gradientStyles[aStyle]; + CanvasGradient::Type type = gradient->GetType(); + + switch (type) { + case CanvasGradient::Type::CONIC: { + auto conic = static_cast(gradient); + pattern = new gfxPattern(conic->mCenter.x, conic->mCenter.y, + conic->mAngle, 0, 1); + break; + } + case CanvasGradient::Type::RADIAL: { + auto radial = static_cast(gradient); + pattern = new gfxPattern(radial->mCenter1.x, radial->mCenter1.y, + radial->mRadius1, radial->mCenter2.x, + radial->mCenter2.y, radial->mRadius2); + break; + } + case CanvasGradient::Type::LINEAR: { + auto linear = static_cast(gradient); + pattern = new gfxPattern(linear->mBegin.x, linear->mBegin.y, + linear->mEnd.x, linear->mEnd.y); + break; + } + default: + MOZ_ASSERT(false, "Should be linear, radial or conic gradient."); + return nullptr; + } + + for (auto stop : gradient->mRawStops) { + pattern->AddColorStop(stop.offset, stop.color); + } + + return pattern.forget(); + } + + gfx::ExtendMode CvtCanvasRepeatToGfxRepeat( + CanvasPattern::RepeatMode aRepeatMode) { + switch (aRepeatMode) { + case CanvasPattern::RepeatMode::REPEAT: + return gfx::ExtendMode::REPEAT; + case CanvasPattern::RepeatMode::REPEATX: + return gfx::ExtendMode::REPEAT_X; + case CanvasPattern::RepeatMode::REPEATY: + return gfx::ExtendMode::REPEAT_Y; + case CanvasPattern::RepeatMode::NOREPEAT: + return gfx::ExtendMode::CLAMP; + default: + return gfx::ExtendMode::CLAMP; + } + } + + already_AddRefed GetPatternFor(Style aStyle) { + const CanvasPattern* pat = mCtx->CurrentState().patternStyles[aStyle]; + RefPtr pattern = new gfxPattern(pat->mSurface, pat->mTransform); + pattern->SetExtend(CvtCanvasRepeatToGfxRepeat(pat->mRepeat)); + return pattern.forget(); + } + + void DrawText(nscoord aXOffset) override { + gfx::Point point = mPt; + bool rtl = mTextRun->IsRightToLeft(); + bool verticalRun = mTextRun->IsVertical(); + RefPtr pattern; + + float& inlineCoord = verticalRun ? point.y.value : point.x.value; + inlineCoord += aXOffset; + + PropertyProvider provider(*this); + + // offset is given in terms of left side of string + if (rtl) { + // Bug 581092 - don't use rounded pixel width to advance to + // right-hand end of run, because this will cause different + // glyph positioning for LTR vs RTL drawing of the same + // glyph string on OS X and DWrite where textrun widths may + // involve fractional pixels. + gfxTextRun::Metrics textRunMetrics = mTextRun->MeasureText( + mDoMeasureBoundingBox ? gfxFont::TIGHT_INK_EXTENTS + : gfxFont::LOOSE_INK_EXTENTS, + mDrawTarget, &provider); + inlineCoord += textRunMetrics.mAdvanceWidth; + // old code was: + // point.x += width * mAppUnitsPerDevPixel; + // TODO: restore this if/when we move to fractional coords + // throughout the text layout process + } + + mCtx->EnsureTarget(); + if (!mCtx->IsTargetValid()) { + return; + } + + // Defer the tasks to gfxTextRun which will handle color/svg-in-ot fonts + // appropriately. + StrokeOptions strokeOpts; + DrawOptions drawOpts; + Style style = (mOp == CanvasRenderingContext2D::TextDrawOperation::FILL) + ? Style::FILL + : Style::STROKE; + const ContextState& state = mCtx->CurrentState(); + + gfx::Rect bounds; + if (mCtx->NeedToCalculateBounds()) { + bounds = ToRect(mBoundingBox); + bounds.MoveBy(mPt / mAppUnitsPerDevPixel); + if (style == Style::STROKE) { + bounds.Inflate(state.lineWidth / 2.0); + } + bounds = mDrawTarget->GetTransform().TransformBounds(bounds); + } + + AdjustedTarget target(mCtx, bounds.IsEmpty() ? nullptr : &bounds, false); + if (!target) { + return; + } + + gfxContext thebes(target, /* aPreserveTransform */ true); + gfxTextRun::DrawParams params(&thebes, mPaletteCache); + + params.allowGDI = false; + + if (state.StyleIsColor(style)) { // Color + nscolor fontColor = state.colorStyles[style]; + if (style == Style::FILL) { + params.context->SetColor(sRGBColor::FromABGR(fontColor)); + } else { + params.textStrokeColor = fontColor; + } + } else { + if (state.gradientStyles[style]) { // Gradient + pattern = GetGradientFor(style); + } else if (state.patternStyles[style]) { // Pattern + pattern = GetPatternFor(style); + } else { + MOZ_ASSERT(false, "Should never reach here."); + return; + } + MOZ_ASSERT(pattern, "No valid pattern."); + + if (style == Style::FILL) { + params.context->SetPattern(pattern); + } else { + params.textStrokePattern = pattern; + } + } + + drawOpts.mAlpha = state.globalAlpha; + drawOpts.mCompositionOp = target.UsedOperation(); + if (!mCtx->IsTargetValid()) { + return; + } + + params.drawOpts = &drawOpts; + params.provider = &provider; + + if (style == Style::STROKE) { + strokeOpts.mLineWidth = state.lineWidth; + strokeOpts.mLineJoin = CanvasToGfx(state.lineJoin); + strokeOpts.mLineCap = CanvasToGfx(state.lineCap); + strokeOpts.mMiterLimit = state.miterLimit; + strokeOpts.mDashLength = state.dash.Length(); + strokeOpts.mDashPattern = + (strokeOpts.mDashLength > 0) ? state.dash.Elements() : 0; + strokeOpts.mDashOffset = state.dashOffset; + + params.drawMode = DrawMode::GLYPH_STROKE; + params.strokeOpts = &strokeOpts; + } + + mTextRun->Draw(gfxTextRun::Range(mTextRun.get()), point, params); + } + + // current text run + RefPtr mTextRun; + + // pointer to a screen reference context used to measure text and such + RefPtr mDrawTarget; + + // Pointer to the draw target we should fill our text to + CanvasRenderingContext2D* mCtx = nullptr; + + // position of the left side of the string, alphabetic baseline + gfx::Point mPt; + + // current font + gfxFontGroup* mFontgrp = nullptr; + + // palette cache for COLR font rendering + mozilla::gfx::PaletteCache& mPaletteCache; + + // spacing adjustments to be applied + gfx::Float mLetterSpacing = 0.0f; + gfx::Float mWordSpacing = 0.0f; + + // to record any unsupported characters found in the text, + // and notify front-end if it is interested + UniquePtr mMissingFonts; + + // dev pixel conversion factor + int32_t mAppUnitsPerDevPixel = 0; + + // operation (fill or stroke) + CanvasRenderingContext2D::TextDrawOperation mOp = + CanvasRenderingContext2D::TextDrawOperation::FILL; + + // union of bounding boxes of all runs, needed for shadows + gfxRect mBoundingBox; + + // flags to use when creating textrun, based on CSS style + gfx::ShapedTextFlags mTextRunFlags = gfx::ShapedTextFlags(); + + // Count of how many times SetText has been called on this processor. + uint32_t mSetTextCount = 0; + + // true iff the bounding box should be measured + bool mDoMeasureBoundingBox = false; + + // true if future SetText calls should be ignored + bool mIgnoreSetText = false; +}; + +UniquePtr CanvasRenderingContext2D::DrawOrMeasureText( + const nsAString& aText, float aX, float aY, + const Optional& aMaxWidth, TextDrawOperation aOp, + ErrorResult& aError) { + RefPtr currentFontStyle = GetCurrentFontStyle(); + if (NS_WARN_IF(!currentFontStyle)) { + aError = NS_ERROR_FAILURE; + return nullptr; + } + + RefPtr presShell = GetPresShell(); + RefPtr document = presShell ? presShell->GetDocument() : nullptr; + + // replace all the whitespace characters with U+0020 SPACE + nsAutoString textToDraw(aText); + TextReplaceWhitespaceCharacters(textToDraw); + + // According to spec, the API should return an empty array if maxWidth was + // provided but is less than or equal to zero or equal to NaN. + if (aMaxWidth.WasPassed() && + (aMaxWidth.Value() <= 0 || std::isnan(aMaxWidth.Value()))) { + textToDraw.Truncate(); + } + + RefPtr canvasStyle; + if (mCanvasElement) { + canvasStyle = nsComputedDOMStyle::GetComputedStyle(mCanvasElement); + } + + // Get text direction, either from the property or inherited from context. + const ContextState& state = CurrentState(); + bool isRTL; + switch (state.textDirection) { + case CanvasDirection::Ltr: + isRTL = false; + break; + case CanvasDirection::Rtl: + isRTL = true; + break; + case CanvasDirection::Inherit: + if (canvasStyle) { + isRTL = + canvasStyle->StyleVisibility()->mDirection == StyleDirection::Rtl; + } else if (document) { + isRTL = GET_BIDI_OPTION_DIRECTION(document->GetBidiOptions()) == + IBMBIDI_TEXTDIRECTION_RTL; + } else { + isRTL = false; + } + break; + default: + MOZ_CRASH("unknown direction!"); + } + + // This is only needed to know if we can know the drawing bounding box easily. + const bool doCalculateBounds = NeedToCalculateBounds(); + if (presShell && presShell->IsDestroying()) { + aError = NS_ERROR_FAILURE; + return nullptr; + } + + nsPresContext* presContext = + presShell ? presShell->GetPresContext() : nullptr; + + if (presContext) { + // ensure user font set is up to date + presContext->Document()->FlushUserFontSet(); + currentFontStyle->SetUserFontSet(presContext->GetUserFontSet()); + } + + if (currentFontStyle->GetStyle()->size == 0.0F) { + aError = NS_OK; + if (aOp == TextDrawOperation::MEASURE) { + return MakeUnique(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0); + } + return nullptr; + } + + if (!std::isfinite(aX) || !std::isfinite(aY)) { + aError = NS_OK; + // This may not be correct - what should TextMetrics contain in the case of + // infinite width or height? + if (aOp == TextDrawOperation::MEASURE) { + return MakeUnique(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0); + } + return nullptr; + } + + CanvasBidiProcessor processor(mPaletteCache); + + // If we don't have a ComputedStyle, we can't set up vertical-text flags + // (for now, at least; perhaps we need new Canvas API to control this). + processor.mTextRunFlags = + canvasStyle ? nsLayoutUtils::GetTextRunFlagsForStyle( + canvasStyle, presContext, canvasStyle->StyleFont(), + canvasStyle->StyleText(), 0) + : gfx::ShapedTextFlags(); + + switch (state.textRendering) { + case CanvasTextRendering::Auto: + if (state.fontFont.size.ToCSSPixels() < + StaticPrefs::browser_display_auto_quality_min_font_size()) { + processor.mTextRunFlags |= gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; + } else { + processor.mTextRunFlags &= ~gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; + } + break; + case CanvasTextRendering::OptimizeSpeed: + processor.mTextRunFlags |= gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; + break; + case CanvasTextRendering::OptimizeLegibility: + case CanvasTextRendering::GeometricPrecision: + processor.mTextRunFlags &= ~gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; + break; + default: + MOZ_CRASH("unknown textRendering!"); + } + + GetAppUnitsValues(&processor.mAppUnitsPerDevPixel, nullptr); + processor.mPt = gfx::Point(aX, aY); + processor.mDrawTarget = gfxPlatform::ThreadLocalScreenReferenceDrawTarget(); + + // If we don't have a target then we don't have a transform. A target won't + // be needed in the case where we're measuring the text size. This allows + // to avoid creating a target if it's only being used to measure text sizes. + if (mTarget) { + processor.mDrawTarget->SetTransform(mTarget->GetTransform()); + } + processor.mCtx = this; + processor.mOp = aOp; + processor.mBoundingBox = gfxRect(0, 0, 0, 0); + processor.mDoMeasureBoundingBox = doCalculateBounds || + !mIsEntireFrameInvalid || + aOp == TextDrawOperation::MEASURE; + processor.mFontgrp = currentFontStyle; + + if (state.letterSpacing != 0.0 || state.wordSpacing != 0.0) { + processor.mLetterSpacing = + state.letterSpacing * processor.mAppUnitsPerDevPixel; + processor.mWordSpacing = state.wordSpacing * processor.mAppUnitsPerDevPixel; + processor.mTextRunFlags |= gfx::ShapedTextFlags::TEXT_ENABLE_SPACING; + if (state.letterSpacing != 0.0) { + processor.mTextRunFlags |= + gfx::ShapedTextFlags::TEXT_DISABLE_OPTIONAL_LIGATURES; + } + } + + nscoord totalWidthCoord; + + processor.mFontgrp + ->UpdateUserFonts(); // ensure user font generation is current + RefPtr font = processor.mFontgrp->GetFirstValidFont(); + const gfxFont::Metrics& fontMetrics = + font->GetMetrics(nsFontMetrics::eHorizontal); + + // calls bidi algo twice since it needs the full text width and the + // bounding boxes before rendering anything + aError = nsBidiPresUtils::ProcessText( + textToDraw.get(), textToDraw.Length(), + isRTL ? intl::BidiEmbeddingLevel::RTL() : intl::BidiEmbeddingLevel::LTR(), + presContext, processor, nsBidiPresUtils::MODE_MEASURE, nullptr, 0, + &totalWidthCoord, mBidiEngine); + if (aError.Failed()) { + return nullptr; + } + + // If ProcessText only called SetText once, we're dealing with a single run, + // and so we don't need to repeat SetText and textRun construction at drawing + // time below; we can just re-use the existing textRun. + if (processor.mSetTextCount == 1) { + processor.mIgnoreSetText = true; + } + + float totalWidth = float(totalWidthCoord) / processor.mAppUnitsPerDevPixel; + + // offset pt.x based on text align + gfxFloat anchorX; + + if (state.textAlign == CanvasTextAlign::Center) { + anchorX = .5; + } else if (state.textAlign == CanvasTextAlign::Left || + (!isRTL && state.textAlign == CanvasTextAlign::Start) || + (isRTL && state.textAlign == CanvasTextAlign::End)) { + anchorX = 0; + } else { + anchorX = 1; + } + + float offsetX = anchorX * totalWidth; + processor.mPt.x -= offsetX; + + gfx::ShapedTextFlags runOrientation = + (processor.mTextRunFlags & gfx::ShapedTextFlags::TEXT_ORIENT_MASK); + nsFontMetrics::FontOrientation fontOrientation = + (runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED || + runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT) + ? nsFontMetrics::eVertical + : nsFontMetrics::eHorizontal; + + // offset pt.y (or pt.x, for vertical text) based on text baseline + gfxFloat baselineAnchor; + + switch (state.textBaseline) { + case CanvasTextBaseline::Hanging: + baselineAnchor = font->GetBaselines(fontOrientation).mHanging; + break; + case CanvasTextBaseline::Top: + baselineAnchor = fontMetrics.emAscent; + break; + case CanvasTextBaseline::Middle: + baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f; + break; + case CanvasTextBaseline::Alphabetic: + baselineAnchor = font->GetBaselines(fontOrientation).mAlphabetic; + break; + case CanvasTextBaseline::Ideographic: + baselineAnchor = font->GetBaselines(fontOrientation).mIdeographic; + break; + case CanvasTextBaseline::Bottom: + baselineAnchor = -fontMetrics.emDescent; + break; + default: + MOZ_CRASH("GFX: unexpected TextBaseline"); + } + + // We can't query the textRun directly, as it may not have been created yet; + // so instead we check the flags that will be used to initialize it. + if (runOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL) { + if (fontOrientation == nsFontMetrics::eVertical) { + // Adjust to account for mTextRun being shaped using center baseline + // rather than alphabetic. + baselineAnchor -= (fontMetrics.emAscent - fontMetrics.emDescent) * .5f; + } + processor.mPt.x -= baselineAnchor; + } else { + processor.mPt.y += baselineAnchor; + } + + // if only measuring, don't need to do any more work + if (aOp == TextDrawOperation::MEASURE) { + aError = NS_OK; + // Note that actualBoundingBoxLeft measures the distance in the leftward + // direction, so its sign is reversed from our usual physical coordinates. + double actualBoundingBoxLeft = offsetX - processor.mBoundingBox.X(); + double actualBoundingBoxRight = processor.mBoundingBox.XMost() - offsetX; + double actualBoundingBoxAscent = + -processor.mBoundingBox.Y() - baselineAnchor; + double actualBoundingBoxDescent = + processor.mBoundingBox.YMost() + baselineAnchor; + auto baselines = font->GetBaselines(fontOrientation); + return MakeUnique( + totalWidth, actualBoundingBoxLeft, actualBoundingBoxRight, + fontMetrics.maxAscent - baselineAnchor, // fontBBAscent + fontMetrics.maxDescent + baselineAnchor, // fontBBDescent + actualBoundingBoxAscent, actualBoundingBoxDescent, + fontMetrics.emAscent - baselineAnchor, // emHeightAscent + fontMetrics.emDescent + baselineAnchor, // emHeightDescent + baselines.mHanging - baselineAnchor, + baselines.mAlphabetic - baselineAnchor, + baselines.mIdeographic - baselineAnchor); + } + + // If we did not actually calculate bounds, set up a simple bounding box + // based on the text position and advance. + if (!doCalculateBounds) { + processor.mBoundingBox.width = totalWidth; + processor.mBoundingBox.MoveBy(gfxPoint(processor.mPt.x, processor.mPt.y)); + } + + processor.mPt.x *= processor.mAppUnitsPerDevPixel; + processor.mPt.y *= processor.mAppUnitsPerDevPixel; + + EnsureTarget(); + if (!IsTargetValid()) { + aError = NS_ERROR_FAILURE; + return nullptr; + } + + Matrix oldTransform = mTarget->GetTransform(); + bool restoreTransform = false; + // if text is over aMaxWidth, then scale the text horizontally such that its + // width is precisely aMaxWidth + if (aMaxWidth.WasPassed() && aMaxWidth.Value() > 0 && + totalWidth > aMaxWidth.Value()) { + Matrix newTransform = oldTransform; + + // Translate so that the anchor point is at 0,0, then scale and then + // translate back. + newTransform.PreTranslate(aX, 0); + newTransform.PreScale(aMaxWidth.Value() / totalWidth, 1); + newTransform.PreTranslate(-aX, 0); + /* we do this to avoid an ICE in the android compiler */ + Matrix androidCompilerBug = newTransform; + mTarget->SetTransform(androidCompilerBug); + restoreTransform = true; + } + + // save the previous bounding box + gfxRect boundingBox = processor.mBoundingBox; + + // don't ever need to measure the bounding box twice + processor.mDoMeasureBoundingBox = false; + + aError = nsBidiPresUtils::ProcessText( + textToDraw.get(), textToDraw.Length(), + isRTL ? intl::BidiEmbeddingLevel::RTL() : intl::BidiEmbeddingLevel::LTR(), + presContext, processor, nsBidiPresUtils::MODE_DRAW, nullptr, 0, nullptr, + mBidiEngine); + + if (aError.Failed()) { + return nullptr; + } + + if (restoreTransform) { + mTarget->SetTransform(oldTransform); + } + + if (aOp == CanvasRenderingContext2D::TextDrawOperation::FILL && + !doCalculateBounds) { + RedrawUser(boundingBox); + } else { + Redraw(); + } + + aError = NS_OK; + return nullptr; +} + +gfxFontGroup* CanvasRenderingContext2D::GetCurrentFontStyle() { + // Use lazy (re)initialization for the fontGroup since it's rather expensive. + + RefPtr presShell = GetPresShell(); + nsPresContext* presContext = + presShell ? presShell->GetPresContext() : nullptr; + + // If we have a cached fontGroup, check that it is valid for the current + // prescontext; if not, we need to discard and re-create it. + RefPtr& fontGroup = CurrentState().fontGroup; + if (fontGroup) { + if (fontGroup->GetPresContext() != presContext) { + fontGroup = nullptr; + } + } + + if (!fontGroup) { + ErrorResult err; + constexpr auto kDefaultFontStyle = "10px sans-serif"_ns; + const float kDefaultFontSize = 10.0; + // If the font has already been set, we're re-creating the fontGroup + // and should re-use the existing font attribute; if not, we initialize + // it to the canvas default. + const nsCString& currentFont = CurrentState().font; + bool fontUpdated = SetFontInternal( + currentFont.IsEmpty() ? kDefaultFontStyle : currentFont, err); + if (err.Failed() || !fontUpdated) { + err.SuppressException(); + // XXX Should we get a default lang from the prescontext or something? + nsAtom* language = nsGkAtoms::x_western; + bool explicitLanguage = false; + gfxFontStyle style; + style.size = kDefaultFontSize; + int32_t perDevPixel, perCSSPixel; + GetAppUnitsValues(&perDevPixel, &perCSSPixel); + gfxFloat devToCssSize = gfxFloat(perDevPixel) / gfxFloat(perCSSPixel); + const auto* sans = + Servo_FontFamily_Generic(StyleGenericFontFamily::SansSerif); + fontGroup = new gfxFontGroup( + presContext, sans->families, &style, language, explicitLanguage, + presContext ? presContext->GetTextPerfMetrics() : nullptr, nullptr, + devToCssSize, StyleFontVariantEmoji::Normal); + if (fontGroup) { + CurrentState().font = kDefaultFontStyle; + } else { + NS_ERROR("Default canvas font is invalid"); + } + } + } else { + // The fontgroup needs to check if its cached families/faces are valid. + fontGroup->CheckForUpdatedPlatformList(); + } + + return fontGroup; +} + +// +// line dash styles +// + +void CanvasRenderingContext2D::SetLineDash(const Sequence& aSegments, + ErrorResult& aRv) { + nsTArray dash; + + for (uint32_t x = 0; x < aSegments.Length(); x++) { + if (aSegments[x] < 0.0) { + // Pattern elements must be finite "numbers" >= 0, with "finite" + // taken care of by WebIDL + return; + } + + if (!dash.AppendElement(aSegments[x], fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + if (aSegments.Length() % + 2) { // If the number of elements is odd, concatenate again + for (uint32_t x = 0; x < aSegments.Length(); x++) { + if (!dash.AppendElement(aSegments[x], fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } + } + + CurrentState().dash = std::move(dash); +} + +void CanvasRenderingContext2D::GetLineDash(nsTArray& aSegments) const { + const nsTArray& dash = CurrentState().dash; + aSegments.Clear(); + + for (uint32_t x = 0; x < dash.Length(); x++) { + aSegments.AppendElement(dash[x]); + } +} + +void CanvasRenderingContext2D::SetLineDashOffset(double aOffset) { + CurrentState().dashOffset = aOffset; +} + +double CanvasRenderingContext2D::LineDashOffset() const { + return CurrentState().dashOffset; +} + +bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, double aX, + double aY, + const CanvasWindingRule& aWinding, + nsIPrincipal& aSubjectPrincipal) { + if (!FloatValidate(aX, aY)) { + return false; + } + + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx, + aSubjectPrincipal)) { + return false; + } + } else if (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting( + RFPTarget::CanvasImageExtractionPrompt)) { + return false; + } + + EnsureUserSpacePath(aWinding); + if (!mPath) { + return false; + } + + return mPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform()); +} + +bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, + const CanvasPath& aPath, double aX, + double aY, + const CanvasWindingRule& aWinding, + nsIPrincipal& aSubjectPrincipal) { + if (!FloatValidate(aX, aY)) { + return false; + } + + EnsureTarget(); + if (!IsTargetValid()) { + return false; + } + + RefPtr tempPath = aPath.GetPath(aWinding, mTarget); + + return tempPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform()); +} + +bool CanvasRenderingContext2D::IsPointInStroke( + JSContext* aCx, double aX, double aY, nsIPrincipal& aSubjectPrincipal) { + if (!FloatValidate(aX, aY)) { + return false; + } + + // Check for site-specific permission and return false if no permission. + if (mCanvasElement) { + nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); + if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx, + aSubjectPrincipal)) { + return false; + } + } else if (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting( + RFPTarget::CanvasImageExtractionPrompt)) { + return false; + } + + EnsureUserSpacePath(); + if (!mPath) { + return false; + } + + const ContextState& state = CurrentState(); + + StrokeOptions strokeOptions(state.lineWidth, CanvasToGfx(state.lineJoin), + CanvasToGfx(state.lineCap), state.miterLimit, + state.dash.Length(), state.dash.Elements(), + state.dashOffset); + + return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), + mTarget->GetTransform()); +} + +bool CanvasRenderingContext2D::IsPointInStroke( + JSContext* aCx, const CanvasPath& aPath, double aX, double aY, + nsIPrincipal& aSubjectPrincipal) { + if (!FloatValidate(aX, aY)) { + return false; + } + + EnsureTarget(); + if (!IsTargetValid()) { + return false; + } + + RefPtr tempPath = + aPath.GetPath(CanvasWindingRule::Nonzero, mTarget); + + const ContextState& state = CurrentState(); + + StrokeOptions strokeOptions(state.lineWidth, CanvasToGfx(state.lineJoin), + CanvasToGfx(state.lineCap), state.miterLimit, + state.dash.Length(), state.dash.Elements(), + state.dashOffset); + + return tempPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), + mTarget->GetTransform()); +} + +// Returns a surface that contains only the part needed to draw aSourceRect. +// On entry, aSourceRect is relative to aSurface, and on return aSourceRect is +// relative to the returned surface. +static already_AddRefed ExtractSubrect(SourceSurface* aSurface, + gfx::Rect* aSourceRect, + DrawTarget* aTargetDT) { + gfx::Rect roundedOutSourceRect = *aSourceRect; + roundedOutSourceRect.RoundOut(); + gfx::IntRect roundedOutSourceRectInt; + if (!roundedOutSourceRect.ToIntRect(&roundedOutSourceRectInt)) { + RefPtr surface(aSurface); + return surface.forget(); + } + + // Try to extract an optimized sub-surface. + if (RefPtr surface = + aSurface->ExtractSubrect(roundedOutSourceRectInt)) { + *aSourceRect -= roundedOutSourceRect.TopLeft(); + return surface.forget(); + } + + RefPtr subrectDT = aTargetDT->CreateSimilarDrawTarget( + roundedOutSourceRectInt.Size(), SurfaceFormat::B8G8R8A8); + + if (subrectDT) { + // See bug 1524554. + subrectDT->ClearRect(gfx::Rect()); + } + + if (!subrectDT || !subrectDT->IsValid()) { + RefPtr surface(aSurface); + return surface.forget(); + } + + *aSourceRect -= roundedOutSourceRect.TopLeft(); + + subrectDT->CopySurface(aSurface, roundedOutSourceRectInt, IntPoint()); + return subrectDT->Snapshot(); +} + +// +// image +// + +static void ClipImageDimension(double& aSourceCoord, double& aSourceSize, + double& aClipOriginCoord, double& aClipSize, + double& aDestCoord, double& aDestSize) { + double scale = aDestSize / aSourceSize; + double relativeCoord = aSourceCoord - aClipOriginCoord; + if (relativeCoord < 0.0) { + double destEnd = aDestCoord + aDestSize; + aDestCoord -= relativeCoord * scale; + aDestSize = destEnd - aDestCoord; + aSourceSize += relativeCoord; + aSourceCoord = aClipOriginCoord; + relativeCoord = 0.0; + } + double delta = aClipSize - (relativeCoord + aSourceSize); + if (delta < 0.0) { + aDestSize += delta * scale; + aSourceSize = aClipSize - relativeCoord; + } +} + +// Acts like nsLayoutUtils::SurfaceFromElement, but it'll attempt +// to pull a SourceSurface from our cache. This allows us to avoid +// reoptimizing surfaces if content and canvas backends are different. +SurfaceFromElementResult CanvasRenderingContext2D::CachedSurfaceFromElement( + Element* aElement) { + SurfaceFromElementResult res; + nsCOMPtr imageLoader = do_QueryInterface(aElement); + if (!imageLoader) { + return res; + } + + nsCOMPtr imgRequest; + imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(imgRequest)); + if (!imgRequest) { + return res; + } + + uint32_t status = 0; + if (NS_FAILED(imgRequest->GetImageStatus(&status)) || + !(status & imgIRequest::STATUS_LOAD_COMPLETE)) { + return res; + } + + nsCOMPtr principal; + if (NS_FAILED(imgRequest->GetImagePrincipal(getter_AddRefs(principal))) || + !principal) { + return res; + } + + if (NS_FAILED(imgRequest->GetHadCrossOriginRedirects( + &res.mHadCrossOriginRedirects))) { + return res; + } + + res.mSourceSurface = CanvasImageCache::LookupAllCanvas(aElement, mTarget); + if (!res.mSourceSurface) { + return res; + } + + res.mCORSUsed = nsLayoutUtils::ImageRequestUsesCORS(imgRequest); + res.mSize = res.mIntrinsicSize = res.mSourceSurface->GetSize(); + res.mPrincipal = std::move(principal); + res.mImageRequest = std::move(imgRequest); + res.mIsWriteOnly = CheckWriteOnlySecurity(res.mCORSUsed, res.mPrincipal, + res.mHadCrossOriginRedirects); + + return res; +} + +static void SwapScaleWidthHeightForRotation(gfx::Rect& aRect, + VideoRotation aDegrees) { + if (aDegrees == VideoRotation::kDegree_90 || + aDegrees == VideoRotation::kDegree_270) { + std::swap(aRect.width, aRect.height); + } +} + +static Matrix ComputeRotationMatrix(gfxFloat aRotatedWidth, + gfxFloat aRotatedHeight, + VideoRotation aDegrees) { + Point shiftVideoCenterToOrigin(-aRotatedWidth / 2.0, -aRotatedHeight / 2.0); + if (aDegrees == VideoRotation::kDegree_90 || + aDegrees == VideoRotation::kDegree_270) { + std::swap(shiftVideoCenterToOrigin.x, shiftVideoCenterToOrigin.y); + } + auto angle = static_cast(aDegrees) / 180.0 * M_PI; + Matrix rotation = Matrix::Rotation(static_cast(angle)); + Point shiftLeftTopToOrigin(aRotatedWidth / 2.0, aRotatedHeight / 2.0); + return rotation.PreTranslate(shiftVideoCenterToOrigin) + .PostTranslate(shiftLeftTopToOrigin); +} + +// drawImage(in HTMLImageElement image, in float dx, in float dy); +// -- render image from 0,0 at dx,dy top-left coords +// drawImage(in HTMLImageElement image, in float dx, in float dy, in float dw, +// in float dh); +// -- render image from 0,0 at dx,dy top-left coords clipping it to dw,dh +// drawImage(in HTMLImageElement image, in float sx, in float sy, in float sw, +// in float sh, in float dx, in float dy, in float dw, in float dh); +// -- render the region defined by (sx,sy,sw,wh) in image-local space into the +// region (dx,dy,dw,dh) on the canvas + +// If only dx and dy are passed in then optional_argc should be 0. If only +// dx, dy, dw and dh are passed in then optional_argc should be 2. The only +// other valid value for optional_argc is 6 if sx, sy, sw, sh, dx, dy, dw and dh +// are all passed in. + +void CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage, + double aSx, double aSy, double aSw, + double aSh, double aDx, double aDy, + double aDw, double aDh, + uint8_t aOptional_argc, + ErrorResult& aError) { + MOZ_ASSERT(aOptional_argc == 0 || aOptional_argc == 2 || aOptional_argc == 6); + + if (!ValidateRect(aDx, aDy, aDw, aDh, true)) { + return; + } + if (aOptional_argc == 6) { + if (!ValidateRect(aSx, aSy, aSw, aSh, true)) { + return; + } + } + + RefPtr srcSurf; + gfx::IntSize imgSize; + gfx::IntSize intrinsicImgSize; + Maybe cropRect; + Element* element = nullptr; + OffscreenCanvas* offscreenCanvas = nullptr; + VideoFrame* videoFrame = nullptr; + + EnsureTarget(); + if (!IsTargetValid()) { + return; + } + + if (aImage.IsHTMLCanvasElement()) { + HTMLCanvasElement* canvas = &aImage.GetAsHTMLCanvasElement(); + element = canvas; + nsIntSize size = canvas->GetSize(); + if (size.width == 0 || size.height == 0) { + return aError.ThrowInvalidStateError("Passed-in canvas is empty"); + } + + if (canvas->IsWriteOnly()) { + SetWriteOnly(); + } + } else if (aImage.IsOffscreenCanvas()) { + offscreenCanvas = &aImage.GetAsOffscreenCanvas(); + nsIntSize size = offscreenCanvas->GetWidthHeight(); + if (size.IsEmpty()) { + return aError.ThrowInvalidStateError("Passed-in canvas is empty"); + } + + srcSurf = offscreenCanvas->GetSurfaceSnapshot(); + if (srcSurf) { + imgSize = intrinsicImgSize = srcSurf->GetSize(); + } + + if (offscreenCanvas->IsWriteOnly()) { + SetWriteOnly(); + } + } else if (aImage.IsImageBitmap()) { + ImageBitmap& imageBitmap = aImage.GetAsImageBitmap(); + srcSurf = imageBitmap.PrepareForDrawTarget(mTarget); + + if (!srcSurf) { + if (imageBitmap.IsClosed()) { + aError.ThrowInvalidStateError("Passed-in ImageBitmap is closed"); + } + return; + } + + if (imageBitmap.IsWriteOnly()) { + SetWriteOnly(); + } + + imgSize = intrinsicImgSize = + gfx::IntSize(imageBitmap.Width(), imageBitmap.Height()); + } else if (aImage.IsVideoFrame()) { + videoFrame = &aImage.GetAsVideoFrame(); + } else { + if (aImage.IsHTMLImageElement()) { + HTMLImageElement* img = &aImage.GetAsHTMLImageElement(); + element = img; + } else if (aImage.IsSVGImageElement()) { + SVGImageElement* img = &aImage.GetAsSVGImageElement(); + element = img; + } else { + HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement(); + video->LogVisibility( + mozilla::dom::HTMLVideoElement::CallerAPI::DRAW_IMAGE); + element = video; + } + + srcSurf = + CanvasImageCache::LookupCanvas(element, mCanvasElement, mTarget, + &imgSize, &intrinsicImgSize, &cropRect); + } + + DirectDrawInfo drawInfo; + + if (!srcSurf) { + // The canvas spec says that drawImage should draw the first frame + // of animated images. We also don't want to rasterize vector images. + uint32_t sfeFlags = nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE | + nsLayoutUtils::SFE_NO_RASTERIZING_VECTORS | + nsLayoutUtils::SFE_ALLOW_UNCROPPED_UNSCALED; + + SurfaceFromElementResult res; + if (offscreenCanvas) { + res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, sfeFlags, + mTarget); + } else if (videoFrame) { + res = nsLayoutUtils::SurfaceFromVideoFrame(videoFrame, sfeFlags, mTarget); + } else { + res = CanvasRenderingContext2D::CachedSurfaceFromElement(element); + if (!res.mSourceSurface) { + res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget); + } + } + + srcSurf = res.GetSourceSurface(); + if (!srcSurf && !res.mDrawInfo.mImgContainer) { + // https://html.spec.whatwg.org/#check-the-usability-of-the-image-argument: + // + // Only throw if the request is broken and the element is an + // HTMLImageElement / SVGImageElement. Note that even for those the spec + // says to silently do nothing in the following cases: + // - The element is still loading. + // - The image is bad, but it's not in the broken state (i.e., we could + // decode the headers and get the size). + if (!res.mIsStillLoading && !res.mHasSize && + (aImage.IsHTMLImageElement() || aImage.IsSVGImageElement())) { + aError.ThrowInvalidStateError("Passed-in image is \"broken\""); + } else if (videoFrame) { + aError.ThrowInvalidStateError("Passed-in video frame is \"broken\""); + } + return; + } + + imgSize = res.mSize; + intrinsicImgSize = res.mIntrinsicSize; + cropRect = res.mCropRect; + DoSecurityCheck(res.mPrincipal, res.mIsWriteOnly, res.mCORSUsed); + + if (srcSurf) { + if (res.mImageRequest) { + CanvasImageCache::NotifyDrawImage(element, mCanvasElement, mTarget, + srcSurf, imgSize, intrinsicImgSize, + cropRect); + } + } else { + drawInfo = res.mDrawInfo; + } + } + + double clipOriginX, clipOriginY, clipWidth, clipHeight; + if (cropRect) { + clipOriginX = cropRect.ref().X(); + clipOriginY = cropRect.ref().Y(); + clipWidth = cropRect.ref().Width(); + clipHeight = cropRect.ref().Height(); + } else { + clipOriginX = clipOriginY = 0.0; + clipWidth = imgSize.width; + clipHeight = imgSize.height; + } + + // Any provided coordinates are in the display space, or the same as the + // intrinsic size. In order to get to the surface coordinate space, we may + // need to adjust for scaling and/or cropping. If no source coordinates are + // provided, then we can just directly use the actual surface size. + if (aOptional_argc == 0) { + aSx = clipOriginX; + aSy = clipOriginY; + aSw = clipWidth; + aSh = clipHeight; + aDw = (double)intrinsicImgSize.width; + aDh = (double)intrinsicImgSize.height; + } else if (aOptional_argc == 2) { + aSx = clipOriginX; + aSy = clipOriginY; + aSw = clipWidth; + aSh = clipHeight; + } else if (cropRect || intrinsicImgSize != imgSize) { + // We need to first scale between the cropped size and the intrinsic size, + // and then adjust for the offset from the crop rect. + double scaleXToCrop = clipWidth / intrinsicImgSize.width; + double scaleYToCrop = clipHeight / intrinsicImgSize.height; + aSx = aSx * scaleXToCrop + clipOriginX; + aSy = aSy * scaleYToCrop + clipOriginY; + aSw = aSw * scaleXToCrop; + aSh = aSh * scaleYToCrop; + } + + if (aSw == 0.0 || aSh == 0.0) { + return; + } + + ClipImageDimension(aSx, aSw, clipOriginX, clipWidth, aDx, aDw); + ClipImageDimension(aSy, aSh, clipOriginY, clipHeight, aDy, aDh); + + if (aSw <= 0.0 || aSh <= 0.0 || aDw <= 0.0 || aDh <= 0.0) { + // source and/or destination are fully clipped, so nothing is painted + return; + } + + // Per spec, the smoothing setting applies only to scaling up a bitmap image. + // When down-scaling the user agent is free to choose whether or not to smooth + // the image. Nearest sampling when down-scaling is rarely desirable and + // smoothing when down-scaling matches chromium's behavior. + // If any dimension is up-scaled, we consider the image as being up-scaled. + auto scale = mTarget->GetTransform().ScaleFactors(); + bool isDownScale = + aDw * Abs(scale.xScale) < aSw && aDh * Abs(scale.yScale) < aSh; + + SamplingFilter samplingFilter; + AntialiasMode antialiasMode; + + if (CurrentState().imageSmoothingEnabled || isDownScale) { + samplingFilter = gfx::SamplingFilter::LINEAR; + antialiasMode = AntialiasMode::DEFAULT; + } else { + samplingFilter = gfx::SamplingFilter::POINT; + antialiasMode = AntialiasMode::NONE; + } + + const bool needBounds = NeedToCalculateBounds(); + if (!IsTargetValid()) { + return; + } + gfx::Rect bounds; + if (needBounds) { + bounds = gfx::Rect(aDx, aDy, aDw, aDh); + bounds = mTarget->GetTransform().TransformBounds(bounds); + } + + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + if (srcSurf) { + gfx::Rect sourceRect(aSx, aSy, aSw, aSh); + if ((element && element == mCanvasElement) || + (offscreenCanvas && offscreenCanvas == mOffscreenCanvas)) { + // srcSurf is a snapshot of mTarget. If we draw to mTarget now, we'll + // trigger a COW copy of the whole canvas into srcSurf. That's a huge + // waste if sourceRect doesn't cover the whole canvas. + // We avoid copying the whole canvas by manually copying just the part + // that we need. + srcSurf = ExtractSubrect(srcSurf, &sourceRect, mTarget); + } + + AdjustedTarget tempTarget(this, bounds.IsEmpty() ? nullptr : &bounds, true); + if (!tempTarget) { + gfxWarning() << "Invalid adjusted target in Canvas2D " + << gfx::hexa((DrawTarget*)mTarget) << ", " + << NeedToDrawShadow() << NeedToApplyFilter(); + return; + } + + auto op = tempTarget.UsedOperation(); + if (!IsTargetValid() || !tempTarget) { + return; + } + + VideoRotation rotationDeg = VideoRotation::kDegree_0; + if (HTMLVideoElement* video = HTMLVideoElement::FromNodeOrNull(element)) { + rotationDeg = video->RotationDegrees(); + } + + gfx::Rect destRect(aDx, aDy, aDw, aDh); + + Matrix currentTransform = tempTarget->GetTransform(); + if (rotationDeg != VideoRotation::kDegree_0) { + tempTarget->ConcatTransform( + ComputeRotationMatrix(aDw, aDh, rotationDeg).PostTranslate(aDx, aDy)); + + SwapScaleWidthHeightForRotation(destRect, rotationDeg); + // When rotation exists, aDx, aDy is handled by transform, Since aDest.x + // aDest.y handling of DrawSurface() does not care about the rotation. + destRect.x = 0; + destRect.y = 0; + } + + tempTarget.DrawSurface( + srcSurf, destRect, sourceRect, + DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED), + DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); + + if (rotationDeg != VideoRotation::kDegree_0) { + tempTarget->SetTransform(currentTransform); + } + + } else { + DrawDirectlyToCanvas(drawInfo, &bounds, gfx::Rect(aDx, aDy, aDw, aDh), + gfx::Rect(aSx, aSy, aSw, aSh), imgSize); + } + + RedrawUser(gfxRect(aDx, aDy, aDw, aDh)); +} + +void CanvasRenderingContext2D::DrawDirectlyToCanvas( + const DirectDrawInfo& aImage, gfx::Rect* aBounds, gfx::Rect aDest, + gfx::Rect aSrc, gfx::IntSize aImgSize) { + MOZ_ASSERT(aSrc.width > 0 && aSrc.height > 0, + "Need positive source width and height"); + + AdjustedTarget tempTarget(this, aBounds->IsEmpty() ? nullptr : aBounds); + if (!tempTarget || !tempTarget->IsValid()) { + return; + } + + // Get any existing transforms on the context, including transformations used + // for context shadow. + Matrix matrix = tempTarget->GetTransform(); + gfxMatrix contextMatrix = ThebesMatrix(matrix); + MatrixScalesDouble contextScale = contextMatrix.ScaleFactors(); + + // Scale the dest rect to include the context scale. + aDest.Scale((float)contextScale.xScale, (float)contextScale.yScale); + + // Scale the image size to the dest rect, and adjust the source rect to match. + MatrixScalesDouble scale(aDest.width / aSrc.width, + aDest.height / aSrc.height); + IntSize scaledImageSize = + IntSize::Ceil(static_cast(scale.xScale * aImgSize.width), + static_cast(scale.yScale * aImgSize.height)); + aSrc.Scale(static_cast(scale.xScale), + static_cast(scale.yScale)); + + // We're wrapping tempTarget's (our) DrawTarget here, so we need to restore + // the matrix even though this is a temp gfxContext. + AutoRestoreTransform autoRestoreTransform(mTarget); + + gfxContext context(tempTarget); + context.SetMatrixDouble( + contextMatrix + .PreScale(1.0 / contextScale.xScale, 1.0 / contextScale.yScale) + .PreTranslate(aDest.x - aSrc.x, aDest.y - aSrc.y)); + + context.SetOp(tempTarget.UsedOperation()); + + // FLAG_CLAMP is added for increased performance, since we never tile here. + uint32_t modifiedFlags = aImage.mDrawingFlags | imgIContainer::FLAG_CLAMP; + + // XXX hmm is scaledImageSize really in CSS pixels? + CSSIntSize sz(scaledImageSize.width, scaledImageSize.height); + SVGImageContext svgContext(Some(sz)); + + auto result = aImage.mImgContainer->Draw( + &context, scaledImageSize, + ImageRegion::Create(gfxRect(aSrc.x, aSrc.y, aSrc.width, aSrc.height)), + aImage.mWhichFrame, SamplingFilter::GOOD, svgContext, modifiedFlags, + CurrentState().globalAlpha); + + if (result != ImgDrawResult::SUCCESS) { + NS_WARNING("imgIContainer::Draw failed"); + } +} + +void CanvasRenderingContext2D::SetGlobalCompositeOperation( + const nsAString& aOp, ErrorResult& aError) { + CompositionOp comp_op; + +#define CANVAS_OP_TO_GFX_OP(cvsop, op2d) \ + if (aOp.EqualsLiteral(cvsop)) comp_op = CompositionOp::OP_##op2d; + + CANVAS_OP_TO_GFX_OP("clear", CLEAR) + else CANVAS_OP_TO_GFX_OP("copy", SOURCE) + else CANVAS_OP_TO_GFX_OP("source-atop", ATOP) + else CANVAS_OP_TO_GFX_OP("source-in", IN) + else CANVAS_OP_TO_GFX_OP("source-out", OUT) + else CANVAS_OP_TO_GFX_OP("source-over", OVER) + else CANVAS_OP_TO_GFX_OP("destination-in", DEST_IN) + else CANVAS_OP_TO_GFX_OP("destination-out", DEST_OUT) + else CANVAS_OP_TO_GFX_OP("destination-over", DEST_OVER) + else CANVAS_OP_TO_GFX_OP("destination-atop", DEST_ATOP) + else CANVAS_OP_TO_GFX_OP("lighter", ADD) + else CANVAS_OP_TO_GFX_OP("xor", XOR) + else CANVAS_OP_TO_GFX_OP("multiply", MULTIPLY) + else CANVAS_OP_TO_GFX_OP("screen", SCREEN) + else CANVAS_OP_TO_GFX_OP("overlay", OVERLAY) + else CANVAS_OP_TO_GFX_OP("darken", DARKEN) + else CANVAS_OP_TO_GFX_OP("lighten", LIGHTEN) + else CANVAS_OP_TO_GFX_OP("color-dodge", COLOR_DODGE) + else CANVAS_OP_TO_GFX_OP("color-burn", COLOR_BURN) + else CANVAS_OP_TO_GFX_OP("hard-light", HARD_LIGHT) + else CANVAS_OP_TO_GFX_OP("soft-light", SOFT_LIGHT) + else CANVAS_OP_TO_GFX_OP("difference", DIFFERENCE) + else CANVAS_OP_TO_GFX_OP("exclusion", EXCLUSION) + else CANVAS_OP_TO_GFX_OP("hue", HUE) + else CANVAS_OP_TO_GFX_OP("saturation", SATURATION) + else CANVAS_OP_TO_GFX_OP("color", COLOR) + else CANVAS_OP_TO_GFX_OP("luminosity", LUMINOSITY) + // XXX ERRMSG we need to report an error to developers here! (bug 329026) + else return; + +#undef CANVAS_OP_TO_GFX_OP + CurrentState().op = comp_op; +} + +void CanvasRenderingContext2D::GetGlobalCompositeOperation( + nsAString& aOp, ErrorResult& aError) { + CompositionOp comp_op = CurrentState().op; + +#define CANVAS_OP_TO_GFX_OP(cvsop, op2d) \ + if (comp_op == CompositionOp::OP_##op2d) aOp.AssignLiteral(cvsop); + + CANVAS_OP_TO_GFX_OP("clear", CLEAR) + else CANVAS_OP_TO_GFX_OP("copy", SOURCE) + else CANVAS_OP_TO_GFX_OP("destination-atop", DEST_ATOP) + else CANVAS_OP_TO_GFX_OP("destination-in", DEST_IN) + else CANVAS_OP_TO_GFX_OP("destination-out", DEST_OUT) + else CANVAS_OP_TO_GFX_OP("destination-over", DEST_OVER) + else CANVAS_OP_TO_GFX_OP("lighter", ADD) + else CANVAS_OP_TO_GFX_OP("source-atop", ATOP) + else CANVAS_OP_TO_GFX_OP("source-in", IN) + else CANVAS_OP_TO_GFX_OP("source-out", OUT) + else CANVAS_OP_TO_GFX_OP("source-over", OVER) + else CANVAS_OP_TO_GFX_OP("xor", XOR) + else CANVAS_OP_TO_GFX_OP("multiply", MULTIPLY) + else CANVAS_OP_TO_GFX_OP("screen", SCREEN) + else CANVAS_OP_TO_GFX_OP("overlay", OVERLAY) + else CANVAS_OP_TO_GFX_OP("darken", DARKEN) + else CANVAS_OP_TO_GFX_OP("lighten", LIGHTEN) + else CANVAS_OP_TO_GFX_OP("color-dodge", COLOR_DODGE) + else CANVAS_OP_TO_GFX_OP("color-burn", COLOR_BURN) + else CANVAS_OP_TO_GFX_OP("hard-light", HARD_LIGHT) + else CANVAS_OP_TO_GFX_OP("soft-light", SOFT_LIGHT) + else CANVAS_OP_TO_GFX_OP("difference", DIFFERENCE) + else CANVAS_OP_TO_GFX_OP("exclusion", EXCLUSION) + else CANVAS_OP_TO_GFX_OP("hue", HUE) + else CANVAS_OP_TO_GFX_OP("saturation", SATURATION) + else CANVAS_OP_TO_GFX_OP("color", COLOR) + else CANVAS_OP_TO_GFX_OP("luminosity", LUMINOSITY) + else { + aError.Throw(NS_ERROR_FAILURE); + } + +#undef CANVAS_OP_TO_GFX_OP +} + +void CanvasRenderingContext2D::DrawWindow(nsGlobalWindowInner& aWindow, + double aX, double aY, double aW, + double aH, const nsACString& aBgColor, + uint32_t aFlags, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aError) { + if (int32_t(aW) == 0 || int32_t(aH) == 0) { + return; + } + + // protect against too-large surfaces that will cause allocation + // or overflow issues + if (!Factory::CheckSurfaceSize(IntSize(int32_t(aW), int32_t(aH)), 0xffff)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Document* doc = aWindow.GetExtantDoc(); + if (doc && aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) { + doc->WarnOnceAbout( + DeprecatedOperations::eDrawWindowCanvasRenderingContext2D); + } + + // Flush layout updates + if (!(aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DO_NOT_FLUSH)) { + nsContentUtils::FlushLayoutForTree(aWindow.GetOuterWindow()); + } + + CompositionOp op = CurrentState().op; + bool discardContent = + GlobalAlpha() == 1.0f && + (op == CompositionOp::OP_OVER || op == CompositionOp::OP_SOURCE); + const gfx::Rect drawRect(aX, aY, aW, aH); + EnsureTarget(discardContent ? &drawRect : nullptr); + if (!IsTargetValid()) { + return; + } + + RefPtr presContext; + nsIDocShell* docshell = aWindow.GetDocShell(); + if (docshell) { + presContext = docshell->GetPresContext(); + } + if (!presContext) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + Maybe backgroundColor = ParseColor(aBgColor); + if (!backgroundColor) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + nsRect r(nsPresContext::CSSPixelsToAppUnits((float)aX), + nsPresContext::CSSPixelsToAppUnits((float)aY), + nsPresContext::CSSPixelsToAppUnits((float)aW), + nsPresContext::CSSPixelsToAppUnits((float)aH)); + RenderDocumentFlags renderDocFlags = + (RenderDocumentFlags::IgnoreViewportScrolling | + RenderDocumentFlags::DocumentRelative); + if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DRAW_CARET) { + renderDocFlags |= RenderDocumentFlags::DrawCaret; + } + if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DRAW_VIEW) { + renderDocFlags &= ~(RenderDocumentFlags::IgnoreViewportScrolling | + RenderDocumentFlags::DocumentRelative); + } + if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_USE_WIDGET_LAYERS) { + renderDocFlags |= RenderDocumentFlags::UseWidgetLayers; + } + if (aFlags & + CanvasRenderingContext2D_Binding::DRAWWINDOW_ASYNC_DECODE_IMAGES) { + renderDocFlags |= RenderDocumentFlags::AsyncDecodeImages; + } + if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DO_NOT_FLUSH) { + renderDocFlags |= RenderDocumentFlags::DrawWindowNotFlushing; + } + + // gfxContext-over-Azure may modify the DrawTarget's transform, so + // save and restore it + Matrix matrix = mTarget->GetTransform(); + double sw = matrix._11 * aW; + double sh = matrix._22 * aH; + if (!sw || !sh) { + return; + } + + Maybe thebes; + RefPtr drawDT; + // Rendering directly is faster and can be done if mTarget supports Azure + // and does not need alpha blending. + // Since the pre-transaction callback calls ReturnTarget, we can't have a + // gfxContext wrapped around it when using a shared buffer provider because + // the DrawTarget's shared buffer may be unmapped in ReturnTarget. + op = CompositionOp::OP_ADD; + if (gfxPlatform::GetPlatform()->SupportsAzureContentForDrawTarget(mTarget) && + GlobalAlpha() == 1.0f) { + op = CurrentState().op; + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + } + if (op == CompositionOp::OP_OVER && + (!mBufferProvider || !mBufferProvider->IsShared())) { + thebes.emplace(mTarget); + thebes.ref().SetMatrix(matrix); + } else { + IntSize dtSize = IntSize::Ceil(sw, sh); + if (!Factory::AllowedSurfaceSize(dtSize)) { + // attempt to limit the DT to what will actually cover the target + Size limitSize(mTarget->GetSize()); + limitSize.Scale(matrix._11, matrix._22); + dtSize = Min(dtSize, IntSize::Ceil(limitSize)); + // if the DT is still too big, then error + if (!Factory::AllowedSurfaceSize(dtSize)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + } + drawDT = gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget( + dtSize, SurfaceFormat::B8G8R8A8); + if (!drawDT || !drawDT->IsValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + thebes.emplace(drawDT); + thebes.ref().SetMatrix(Matrix::Scaling(matrix._11, matrix._22)); + } + MOZ_ASSERT(thebes.isSome()); + + RefPtr presShell = presContext->PresShell(); + + Unused << presShell->RenderDocument(r, renderDocFlags, *backgroundColor, + &thebes.ref()); + // If this canvas was contained in the drawn window, the pre-transaction + // callback may have returned its DT. If so, we must reacquire it here. + EnsureTarget(discardContent ? &drawRect : nullptr); + + if (drawDT) { + RefPtr snapshot = drawDT->Snapshot(); + if (NS_WARN_IF(!snapshot)) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + + op = CurrentState().op; + if (!IsTargetValid()) { + aError.Throw(NS_ERROR_FAILURE); + return; + } + gfx::Rect destRect(0, 0, aW, aH); + gfx::Rect sourceRect(0, 0, sw, sh); + mTarget->DrawSurface(snapshot, destRect, sourceRect, + DrawSurfaceOptions(gfx::SamplingFilter::POINT), + DrawOptions(GlobalAlpha(), op, AntialiasMode::NONE)); + } else { + mTarget->SetTransform(matrix); + } + + // note that x and y are coordinates in the document that + // we're drawing; x and y are drawn to 0,0 in current user + // space. + RedrawUser(gfxRect(0, 0, aW, aH)); +} + +// +// device pixel getting/setting +// + +already_AddRefed CanvasRenderingContext2D::GetImageData( + JSContext* aCx, int32_t aSx, int32_t aSy, int32_t aSw, int32_t aSh, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { + if (!mCanvasElement && !mDocShell && !mOffscreenCanvas) { + NS_ERROR("No canvas element and no docshell in GetImageData!!!"); + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + // Check only if we have a canvas element; if we were created with a docshell, + // then it's special internal use. + if (IsWriteOnly() || + (mCanvasElement && !mCanvasElement->CallerCanRead(aSubjectPrincipal)) || + (mOffscreenCanvas && + !mOffscreenCanvas->CallerCanRead(aSubjectPrincipal))) { + // XXX ERRMSG we need to report an error to developers here! (bug 329026) + aError.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + if (!aSw || !aSh) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + // Handle negative width and height by flipping the rectangle over in the + // relevant direction. + uint32_t w, h; + if (aSw < 0) { + w = -aSw; + aSx -= w; + } else { + w = aSw; + } + if (aSh < 0) { + h = -aSh; + aSy -= h; + } else { + h = aSh; + } + + if (w == 0) { + w = 1; + } + if (h == 0) { + h = 1; + } + + JS::Rooted array(aCx); + aError = GetImageDataArray(aCx, aSx, aSy, w, h, aSubjectPrincipal, + array.address()); + if (aError.Failed()) { + return nullptr; + } + MOZ_ASSERT(array); + return MakeAndAddRef(w, h, *array); +} + +static IntRect ClipImageDataTransfer(IntRect& aSrc, const IntPoint& aDestOffset, + const IntSize& aDestBounds) { + IntRect dest = aSrc; + dest.SafeMoveBy(aDestOffset); + dest = IntRect(IntPoint(0, 0), aDestBounds).SafeIntersect(dest); + + aSrc = aSrc.SafeIntersect(dest - aDestOffset); + return aSrc + aDestOffset; +} + +nsresult CanvasRenderingContext2D::GetImageDataArray( + JSContext* aCx, int32_t aX, int32_t aY, uint32_t aWidth, uint32_t aHeight, + nsIPrincipal& aSubjectPrincipal, JSObject** aRetval) { + MOZ_ASSERT(aWidth && aHeight); + + // Restrict the typed array length to INT32_MAX because that's all we support. + CheckedInt len = CheckedInt(aWidth) * aHeight * 4; + if (!len.isValid() || len.value() > INT32_MAX) { + return NS_ERROR_DOM_INDEX_SIZE_ERR; + } + + CheckedInt rightMost = CheckedInt(aX) + aWidth; + CheckedInt bottomMost = CheckedInt(aY) + aHeight; + + if (!rightMost.isValid() || !bottomMost.isValid()) { + return NS_ERROR_DOM_SYNTAX_ERR; + } + + JS::Rooted darray(aCx, JS_NewUint8ClampedArray(aCx, len.value())); + if (!darray) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (mZero) { + *aRetval = darray; + return NS_OK; + } + + IntRect dstWriteRect(0, 0, aWidth, aHeight); + IntRect srcReadRect = ClipImageDataTransfer(dstWriteRect, IntPoint(aX, aY), + IntSize(mWidth, mHeight)); + if (srcReadRect.IsEmpty()) { + *aRetval = darray; + return NS_OK; + } + + if (!GetBufferProvider() && !EnsureTarget()) { + return NS_ERROR_FAILURE; + } + + RefPtr snapshot = mBufferProvider->BorrowSnapshot(); + if (!snapshot) { + return NS_ERROR_OUT_OF_MEMORY; + } + + RefPtr readback = snapshot->GetDataSurface(); + mBufferProvider->ReturnSnapshot(snapshot.forget()); + + // Check for site-specific permission. + CanvasUtils::ImageExtraction permission = + CanvasUtils::ImageExtraction::Unrestricted; + if (mCanvasElement) { + permission = CanvasUtils::ImageExtractionResult(mCanvasElement, aCx, + aSubjectPrincipal); + } else if (mOffscreenCanvas) { + permission = CanvasUtils::ImageExtractionResult(mOffscreenCanvas, aCx, + aSubjectPrincipal); + } + + // Clone the data source surface if canvas randomization is enabled. We need + // to do this because we don't want to alter the actual image buffer. + // Otherwise, we will provide inconsistent image data with multiple calls. + // + // Note that we don't need to clone if we will use the place holder because + // the place holder doesn't use actual image data. + if (permission == CanvasUtils::ImageExtraction::Randomize) { + if (readback) { + readback = CreateDataSourceSurfaceByCloning(readback); + } + } + + DataSourceSurface::MappedSurface rawData; + if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + do { + uint8_t* randomData; + if (permission == CanvasUtils::ImageExtraction::Placeholder) { + // Since we cannot call any GC-able functions (like requesting the RNG + // service) after we call JS_GetUint8ClampedArrayData, we will + // pre-generate the randomness required for GeneratePlaceholderCanvasData. + randomData = TryToGenerateRandomDataForPlaceholderCanvasData(); + } else if (permission == CanvasUtils::ImageExtraction::Randomize) { + // Apply the random noises if canvan randomization is enabled. We don't + // need to calculate random noises if we are going to use the place + // holder. + + const IntSize size = readback->GetSize(); + nsRFPService::RandomizePixels( + GetCookieJarSettings(), rawData.mData, size.width, size.height, + size.height * size.width * 4, SurfaceFormat::A8R8G8B8_UINT32); + } + + JS::AutoCheckCannotGC nogc; + bool isShared; + uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc); + MOZ_ASSERT(!isShared); // Should not happen, data was created above + + if (permission == CanvasUtils::ImageExtraction::Placeholder) { + FillPlaceholderCanvas(randomData, len.value(), data); + break; + } + + uint32_t srcStride = rawData.mStride; + uint8_t* src = + rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4; + + uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4; + + if (mOpaque) { + SwizzleData(src, srcStride, SurfaceFormat::X8R8G8B8_UINT32, dst, + aWidth * 4, SurfaceFormat::R8G8B8A8, dstWriteRect.Size()); + } else { + UnpremultiplyData(src, srcStride, SurfaceFormat::A8R8G8B8_UINT32, dst, + aWidth * 4, SurfaceFormat::R8G8B8A8, + dstWriteRect.Size()); + } + } while (false); + + readback->Unmap(); + *aRetval = darray; + return NS_OK; +} + +void CanvasRenderingContext2D::EnsureErrorTarget() { + if (sErrorTarget.get()) { + return; + } + + RefPtr errorTarget = + gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget( + IntSize(1, 1), SurfaceFormat::B8G8R8A8); + MOZ_ASSERT(errorTarget, "Failed to allocate the error target!"); + + sErrorTarget.set(errorTarget.forget().take()); +} + +void CanvasRenderingContext2D::FillRuleChanged() { + if (mPath) { + mPathBuilder = mPath->CopyToBuilder(CurrentState().fillRule); + mPath = nullptr; + } +} + +void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx, + int32_t aDy, ErrorResult& aRv) { + RootedSpiderMonkeyInterface arr(RootingCx()); + PutImageData_explicit(aDx, aDy, aImageData, false, 0, 0, 0, 0, aRv); +} + +void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx, + int32_t aDy, int32_t aDirtyX, + int32_t aDirtyY, + int32_t aDirtyWidth, + int32_t aDirtyHeight, + ErrorResult& aRv) { + PutImageData_explicit(aDx, aDy, aImageData, true, aDirtyX, aDirtyY, + aDirtyWidth, aDirtyHeight, aRv); +} + +void CanvasRenderingContext2D::PutImageData_explicit( + int32_t aX, int32_t aY, ImageData& aImageData, bool aHasDirtyRect, + int32_t aDirtyX, int32_t aDirtyY, int32_t aDirtyWidth, int32_t aDirtyHeight, + ErrorResult& aRv) { + RootedSpiderMonkeyInterface arr(RootingCx()); + if (!arr.Init(aImageData.GetDataObject())) { + return aRv.ThrowInvalidStateError( + "Failed to extract Uint8ClampedArray from ImageData (security check " + "failed?)"); + } + + const uint32_t width = aImageData.Width(); + const uint32_t height = aImageData.Height(); + if (width == 0 || height == 0) { + return aRv.ThrowInvalidStateError("Passed-in image is empty"); + } + + IntRect dirtyRect; + IntRect imageDataRect(0, 0, width, height); + + if (aHasDirtyRect) { + // fix up negative dimensions + if (aDirtyWidth < 0) { + if (aDirtyWidth == INT_MIN) { + return aRv.ThrowInvalidStateError("Dirty width is invalid"); + } + + CheckedInt32 checkedDirtyX = CheckedInt32(aDirtyX) + aDirtyWidth; + + if (!checkedDirtyX.isValid()) { + return aRv.ThrowInvalidStateError("Dirty width is invalid"); + } + + aDirtyX = checkedDirtyX.value(); + aDirtyWidth = -aDirtyWidth; + } + + if (aDirtyHeight < 0) { + if (aDirtyHeight == INT_MIN) { + return aRv.ThrowInvalidStateError("Dirty height is invalid"); + } + + CheckedInt32 checkedDirtyY = CheckedInt32(aDirtyY) + aDirtyHeight; + + if (!checkedDirtyY.isValid()) { + return aRv.ThrowInvalidStateError("Dirty height is invalid"); + } + + aDirtyY = checkedDirtyY.value(); + aDirtyHeight = -aDirtyHeight; + } + + // bound the dirty rect within the imageData rectangle + dirtyRect = imageDataRect.Intersect( + IntRect(aDirtyX, aDirtyY, aDirtyWidth, aDirtyHeight)); + + if (dirtyRect.Width() <= 0 || dirtyRect.Height() <= 0) { + return; + } + } else { + dirtyRect = imageDataRect; + } + + IntRect srcRect = dirtyRect; + dirtyRect = ClipImageDataTransfer(srcRect, IntPoint(aX, aY), + IntSize(mWidth, mHeight)); + if (dirtyRect.IsEmpty()) { + return; + } + + RefPtr sourceSurface; + uint8_t* lockedBits = nullptr; + + // The canvas spec says that the current path, transformation matrix, + // shadow attributes, global alpha, the clipping region, and global + // composition operator must not affect the getImageData() and + // putImageData() methods. + const gfx::Rect putRect(dirtyRect); + EnsureTarget(&putRect); + + if (!IsTargetValid()) { + return aRv.Throw(NS_ERROR_FAILURE); + } + + DataSourceSurface::MappedSurface map; + uint8_t* dstData; + IntSize dstSize; + int32_t dstStride; + SurfaceFormat dstFormat; + if (mTarget->LockBits(&lockedBits, &dstSize, &dstStride, &dstFormat)) { + dstData = lockedBits + dirtyRect.y * dstStride + dirtyRect.x * 4; + } else { + sourceSurface = Factory::CreateDataSourceSurface( + dirtyRect.Size(), SurfaceFormat::B8G8R8A8, false); + + // In certain scenarios, requesting larger than 8k image fails. Bug + // 803568 covers the details of how to run into it, but the full + // detailed investigation hasn't been done to determine the + // underlying cause. We will just handle the failure to allocate + // the surface to avoid a crash. + if (!sourceSurface) { + return aRv.Throw(NS_ERROR_FAILURE); + } + if (!sourceSurface->Map(DataSourceSurface::READ_WRITE, &map)) { + return aRv.Throw(NS_ERROR_FAILURE); + } + + dstData = map.mData; + if (!dstData) { + return aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } + dstStride = map.mStride; + dstFormat = sourceSurface->GetFormat(); + } + + arr.ProcessData( + [&](const Span& aData, JS::AutoCheckCannotGC&& nogc) { + // Verify that the length hasn't changed. + if (aData.Length() != width * height * 4) { + // FIXME Should this call ReleaseBits/Unmap? + return aRv.ThrowInvalidStateError("Invalid width or height"); + } + + uint8_t* srcData = + aData.Elements() + srcRect.y * (width * 4) + srcRect.x * 4; + + PremultiplyData(srcData, width * 4, SurfaceFormat::R8G8B8A8, dstData, + dstStride, + mOpaque ? SurfaceFormat::X8R8G8B8_UINT32 + : SurfaceFormat::A8R8G8B8_UINT32, + dirtyRect.Size()); + }); + + if (aRv.Failed()) { + return; + } + + if (lockedBits) { + mTarget->ReleaseBits(lockedBits); + } else if (sourceSurface) { + sourceSurface->Unmap(); + mTarget->CopySurface(sourceSurface, dirtyRect - dirtyRect.TopLeft(), + dirtyRect.TopLeft()); + } + + Redraw( + gfx::Rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height)); +} + +static already_AddRefed CreateImageData( + JSContext* aCx, CanvasRenderingContext2D* aContext, uint32_t aW, + uint32_t aH, ErrorResult& aError) { + if (aW == 0) aW = 1; + if (aH == 0) aH = 1; + + // Restrict the typed array length to INT32_MAX because that's all we support + // in dom::TypedArray::ComputeState. + CheckedInt len = CheckedInt(aW) * aH * 4; + if (!len.isValid() || len.value() > INT32_MAX) { + aError.ThrowIndexSizeError("Invalid width or height"); + return nullptr; + } + + // Create the fast typed array; it's initialized to 0 by default. + JSObject* darray = + Uint8ClampedArray::Create(aCx, aContext, len.value(), aError); + if (aError.Failed()) { + return nullptr; + } + + return do_AddRef(new ImageData(aW, aH, *darray)); +} + +already_AddRefed CanvasRenderingContext2D::CreateImageData( + JSContext* aCx, int32_t aSw, int32_t aSh, ErrorResult& aError) { + if (!aSw || !aSh) { + aError.ThrowIndexSizeError("Invalid width or height"); + return nullptr; + } + + uint32_t w = Abs(aSw); + uint32_t h = Abs(aSh); + return dom::CreateImageData(aCx, this, w, h, aError); +} + +already_AddRefed CanvasRenderingContext2D::CreateImageData( + JSContext* aCx, ImageData& aImagedata, ErrorResult& aError) { + return dom::CreateImageData(aCx, this, aImagedata.Width(), + aImagedata.Height(), aError); +} + +void CanvasRenderingContext2D::OnMemoryPressure() { + if (mBufferProvider) { + mBufferProvider->OnMemoryPressure(); + } +} + +void CanvasRenderingContext2D::OnBeforePaintTransaction() { + if (!mTarget) return; + OnStableState(); +} + +void CanvasRenderingContext2D::OnDidPaintTransaction() { MarkContextClean(); } + +bool CanvasRenderingContext2D::UpdateWebRenderCanvasData( + nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) { + if (mOpaque) { + // If we're opaque then make sure we have a surface so we paint black + // instead of transparent. + EnsureTarget(); + } + + // Don't call EnsureTarget() ... if there isn't already a surface, then + // we have nothing to paint and there is no need to create a surface just + // to paint nothing. Also, EnsureTarget() can cause creation of a persistent + // layer manager which must NOT happen during a paint. + if (!mBufferProvider && !IsTargetValid()) { + // No DidTransactionCallback will be received, so mark the context clean + // now so future invalidations will be dispatched. + MarkContextClean(); + // Clear CanvasRenderer of WebRenderCanvasData + aCanvasData->ClearCanvasRenderer(); + return false; + } + + auto renderer = aCanvasData->GetCanvasRenderer(); + + if (!mResetLayer && renderer) { + CanvasRendererData data; + data.mContext = this; + data.mSize = GetSize(); + + if (renderer->IsDataValid(data)) { + return true; + } + } + + renderer = aCanvasData->CreateCanvasRenderer(); + if (!InitializeCanvasRenderer(aBuilder, renderer)) { + // Clear CanvasRenderer of WebRenderCanvasData + aCanvasData->ClearCanvasRenderer(); + return false; + } + + MOZ_ASSERT(renderer); + mResetLayer = false; + return true; +} + +bool CanvasRenderingContext2D::InitializeCanvasRenderer( + nsDisplayListBuilder* aBuilder, CanvasRenderer* aRenderer) { + CanvasRendererData data; + data.mContext = this; + data.mSize = GetSize(); + data.mIsOpaque = mOpaque; + data.mDoPaintCallbacks = true; + + if (!mBufferProvider) { + // Force the creation of a buffer provider. + EnsureTarget(); + ReturnTarget(); + if (!mBufferProvider) { + MarkContextClean(); + return false; + } + } + + aRenderer->Initialize(data); + aRenderer->SetDirty(); + return true; +} + +void CanvasRenderingContext2D::MarkContextClean() { + if (mInvalidateCount > 0) { + mPredictManyRedrawCalls = mInvalidateCount > kCanvasMaxInvalidateCount; + } + mIsEntireFrameInvalid = false; + mInvalidateCount = 0; +} + +void CanvasRenderingContext2D::GetAppUnitsValues(int32_t* aPerDevPixel, + int32_t* aPerCSSPixel) { + // If we don't have a canvas element, we just return something generic. + if (aPerDevPixel) { + *aPerDevPixel = 60; + } + if (aPerCSSPixel) { + *aPerCSSPixel = 60; + } + PresShell* presShell = GetPresShell(); + if (!presShell) { + return; + } + nsPresContext* presContext = presShell->GetPresContext(); + if (!presContext) { + return; + } + if (aPerDevPixel) { + *aPerDevPixel = presContext->AppUnitsPerDevPixel(); + } + if (aPerCSSPixel) { + *aPerCSSPixel = AppUnitsPerCSSPixel(); + } +} + +void CanvasRenderingContext2D::SetWriteOnly() { + mWriteOnly = true; + if (mCanvasElement) { + mCanvasElement->SetWriteOnly(); + } else if (mOffscreenCanvas) { + mOffscreenCanvas->SetWriteOnly(); + } +} + +bool CanvasRenderingContext2D::GetEffectiveWillReadFrequently() const { + return StaticPrefs::gfx_canvas_willreadfrequently_enabled_AtStartup() && + mWillReadFrequently; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPath, mParent) + +CanvasPath::CanvasPath(nsISupports* aParent) : mParent(aParent) { + mPathBuilder = + gfxPlatform::ThreadLocalScreenReferenceDrawTarget()->CreatePathBuilder(); +} + +CanvasPath::CanvasPath(nsISupports* aParent, + already_AddRefed aPathBuilder) + : mParent(aParent), mPathBuilder(aPathBuilder) { + if (!mPathBuilder) { + mPathBuilder = gfxPlatform::ThreadLocalScreenReferenceDrawTarget() + ->CreatePathBuilder(); + } +} + +JSObject* CanvasPath::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return Path2D_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed CanvasPath::Constructor( + const GlobalObject& aGlobal) { + RefPtr path = new CanvasPath(aGlobal.GetAsSupports()); + return path.forget(); +} + +already_AddRefed CanvasPath::Constructor( + const GlobalObject& aGlobal, CanvasPath& aCanvasPath) { + RefPtr drawTarget = + gfxPlatform::ThreadLocalScreenReferenceDrawTarget(); + RefPtr tempPath = + aCanvasPath.GetPath(CanvasWindingRule::Nonzero, drawTarget.get()); + + RefPtr path = + new CanvasPath(aGlobal.GetAsSupports(), tempPath->CopyToBuilder()); + return path.forget(); +} + +already_AddRefed CanvasPath::Constructor( + const GlobalObject& aGlobal, const nsAString& aPathString) { + RefPtr tempPath = SVGContentUtils::GetPath(aPathString); + if (!tempPath) { + return Constructor(aGlobal); + } + + RefPtr path = + new CanvasPath(aGlobal.GetAsSupports(), tempPath->CopyToBuilder()); + return path.forget(); +} + +void CanvasPath::ClosePath() { + EnsurePathBuilder(); + + mPathBuilder->Close(); + mPruned = false; +} + +inline void CanvasPath::EnsureCapped() const { + // If there were zero-length segments emitted that were pruned, we need to + // emit a LineTo to ensure that caps are generated for the segment. + if (mPruned) { + mPathBuilder->LineTo(mPathBuilder->CurrentPoint()); + mPruned = false; + } +} + +inline void CanvasPath::EnsureActive() const { + // If the path is not active, then adding an op to the path may cause the path + // to add the first point of the op as the initial point instead of the actual + // current point. + if (mPruned && !mPathBuilder->IsActive()) { + mPathBuilder->MoveTo(mPathBuilder->CurrentPoint()); + mPruned = false; + } +} + +void CanvasPath::MoveTo(double aX, double aY) { + EnsurePathBuilder(); + + Point pos(ToFloat(aX), ToFloat(aY)); + if (!pos.IsFinite()) { + return; + } + + EnsureCapped(); + mPathBuilder->MoveTo(pos); +} + +void CanvasPath::LineTo(double aX, double aY) { + LineTo(Point(ToFloat(aX), ToFloat(aY))); +} + +void CanvasPath::QuadraticCurveTo(double aCpx, double aCpy, double aX, + double aY) { + EnsurePathBuilder(); + + Point cp1(ToFloat(aCpx), ToFloat(aCpy)); + Point cp2(ToFloat(aX), ToFloat(aY)); + if (!cp1.IsFinite() || !cp2.IsFinite()) { + return; + } + if (cp1 == mPathBuilder->CurrentPoint() && cp1 == cp2) { + mPruned = true; + return; + } + + EnsureActive(); + + mPathBuilder->QuadraticBezierTo(cp1, cp2); + mPruned = false; +} + +void CanvasPath::BezierCurveTo(double aCp1x, double aCp1y, double aCp2x, + double aCp2y, double aX, double aY) { + BezierTo(gfx::Point(ToFloat(aCp1x), ToFloat(aCp1y)), + gfx::Point(ToFloat(aCp2x), ToFloat(aCp2y)), + gfx::Point(ToFloat(aX), ToFloat(aY))); +} + +void CanvasPath::ArcTo(double aX1, double aY1, double aX2, double aY2, + double aRadius, ErrorResult& aError) { + if (aRadius < 0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + + EnsurePathBuilder(); + + // Current point in user space! + Point p0 = mPathBuilder->CurrentPoint(); + Point p1(aX1, aY1); + Point p2(aX2, aY2); + + if (!p1.IsFinite() || !p2.IsFinite() || !std::isfinite(aRadius)) { + return; + } + + // Execute these calculations in double precision to avoid cumulative + // rounding errors. + double dir, a2, b2, c2, cosx, sinx, d, anx, any, bnx, bny, x3, y3, x4, y4, cx, + cy, angle0, angle1; + bool anticlockwise; + + if (p0 == p1 || p1 == p2 || aRadius == 0) { + LineTo(p1); + return; + } + + // Check for colinearity + dir = (p2.x.value - p1.x.value) * (p0.y.value - p1.y.value) + + (p2.y.value - p1.y.value) * (p1.x.value - p0.x.value); + if (dir == 0) { + LineTo(p1); + return; + } + + // XXX - Math for this code was already available from the non-azure code + // and would be well tested. Perhaps converting to bezier directly might + // be more efficient longer run. + a2 = (p0.x - aX1) * (p0.x - aX1) + (p0.y - aY1) * (p0.y - aY1); + b2 = (aX1 - aX2) * (aX1 - aX2) + (aY1 - aY2) * (aY1 - aY2); + c2 = (p0.x - aX2) * (p0.x - aX2) + (p0.y - aY2) * (p0.y - aY2); + cosx = (a2 + b2 - c2) / (2 * sqrt(a2 * b2)); + + sinx = sqrt(1 - cosx * cosx); + d = aRadius / ((1 - cosx) / sinx); + + anx = (aX1 - p0.x) / sqrt(a2); + any = (aY1 - p0.y) / sqrt(a2); + bnx = (aX1 - aX2) / sqrt(b2); + bny = (aY1 - aY2) / sqrt(b2); + x3 = aX1 - anx * d; + y3 = aY1 - any * d; + x4 = aX1 - bnx * d; + y4 = aY1 - bny * d; + anticlockwise = (dir < 0); + cx = x3 + any * aRadius * (anticlockwise ? 1 : -1); + cy = y3 - anx * aRadius * (anticlockwise ? 1 : -1); + angle0 = atan2((y3 - cy), (x3 - cx)); + angle1 = atan2((y4 - cy), (x4 - cx)); + + LineTo(x3, y3); + + Arc(cx, cy, aRadius, angle0, angle1, anticlockwise, aError); +} + +void CanvasPath::Rect(double aX, double aY, double aW, double aH) { + EnsurePathBuilder(); + + if (!std::isfinite(aX) || !std::isfinite(aY) || !std::isfinite(aW) || + !std::isfinite(aH)) { + return; + } + + MoveTo(aX, aY); + if (aW == 0 && aH == 0) { + return; + } + LineTo(aX + aW, aY); + LineTo(aX + aW, aY + aH); + LineTo(aX, aY + aH); + ClosePath(); +} + +void CanvasPath::RoundRect( + double aX, double aY, double aW, double aH, + const UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence& + aRadii, + ErrorResult& aError) { + EnsurePathBuilder(); + + EnsureCapped(); + RoundRectImpl(mPathBuilder, Nothing(), aX, aY, aW, aH, aRadii, aError); +} + +void CanvasPath::Arc(double aX, double aY, double aRadius, double aStartAngle, + double aEndAngle, bool aAnticlockwise, + ErrorResult& aError) { + if (aRadius < 0.0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + if (aStartAngle == aEndAngle) { + LineTo(aX + aRadius * cos(aStartAngle), aY + aRadius * sin(aStartAngle)); + return; + } + + EnsurePathBuilder(); + + EnsureActive(); + + mPathBuilder->Arc(Point(aX, aY), aRadius, aStartAngle, aEndAngle, + aAnticlockwise); + mPruned = false; +} + +void CanvasPath::Ellipse(double x, double y, double radiusX, double radiusY, + double rotation, double startAngle, double endAngle, + bool anticlockwise, ErrorResult& aError) { + if (radiusX < 0.0 || radiusY < 0.0) { + return aError.ThrowIndexSizeError("Negative radius"); + } + + EnsurePathBuilder(); + + ArcToBezier(this, Point(x, y), Size(radiusX, radiusY), startAngle, endAngle, + anticlockwise, rotation); + mPruned = false; +} + +void CanvasPath::LineTo(const gfx::Point& aPoint) { + EnsurePathBuilder(); + + if (!aPoint.IsFinite()) { + return; + } + if (aPoint == mPathBuilder->CurrentPoint()) { + mPruned = true; + return; + } + + EnsureActive(); + + mPathBuilder->LineTo(aPoint); + mPruned = false; +} + +void CanvasPath::BezierTo(const gfx::Point& aCP1, const gfx::Point& aCP2, + const gfx::Point& aCP3) { + EnsurePathBuilder(); + + if (!aCP1.IsFinite() || !aCP2.IsFinite() || !aCP3.IsFinite()) { + return; + } + if (aCP1 == mPathBuilder->CurrentPoint() && aCP1 == aCP2 && aCP1 == aCP3) { + mPruned = true; + return; + } + + EnsureActive(); + + mPathBuilder->BezierTo(aCP1, aCP2, aCP3); + mPruned = false; +} + +void CanvasPath::AddPath(CanvasPath& aCanvasPath, const DOMMatrix2DInit& aInit, + ErrorResult& aError) { + RefPtr drawTarget = + gfxPlatform::ThreadLocalScreenReferenceDrawTarget(); + RefPtr tempPath = + aCanvasPath.GetPath(CanvasWindingRule::Nonzero, drawTarget.get()); + + RefPtr matrix = + DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError); + if (aError.Failed()) { + return; + } + + Matrix transform(*(matrix->GetInternal2D())); + + if (!transform.IsFinite()) { + return; + } + + if (!transform.IsIdentity()) { + RefPtr tempBuilder = + tempPath->TransformedCopyToBuilder(transform, FillRule::FILL_WINDING); + tempPath = tempBuilder->Finish(); + } + + EnsurePathBuilder(); // in case a path is added to itself + EnsureCapped(); + tempPath->StreamToSink(mPathBuilder); +} + +already_AddRefed CanvasPath::GetPath( + const CanvasWindingRule& aWinding, const DrawTarget* aTarget) const { + FillRule fillRule = FillRule::FILL_WINDING; + if (aWinding == CanvasWindingRule::Evenodd) { + fillRule = FillRule::FILL_EVEN_ODD; + } + + if (mPath && (mPath->GetBackendType() == aTarget->GetBackendType()) && + (mPath->GetFillRule() == fillRule)) { + RefPtr path(mPath); + return path.forget(); + } + + if (!mPath) { + // if there is no path, there must be a pathbuilder + MOZ_ASSERT(mPathBuilder); + EnsureCapped(); + mPath = mPathBuilder->Finish(); + if (!mPath) { + RefPtr path(mPath); + return path.forget(); + } + + mPathBuilder = nullptr; + } + + // retarget our backend if we're used with a different backend + if (mPath->GetBackendType() != aTarget->GetBackendType()) { + RefPtr tmpPathBuilder = aTarget->CreatePathBuilder(fillRule); + mPath->StreamToSink(tmpPathBuilder); + mPath = tmpPathBuilder->Finish(); + } else if (mPath->GetFillRule() != fillRule) { + RefPtr tmpPathBuilder = mPath->CopyToBuilder(fillRule); + mPath = tmpPathBuilder->Finish(); + } + + RefPtr path(mPath); + return path.forget(); +} + +void CanvasPath::EnsurePathBuilder() const { + if (mPathBuilder) { + return; + } + + // if there is not pathbuilder, there must be a path + MOZ_ASSERT(mPath); + mPathBuilder = mPath->CopyToBuilder(); + mPath = nullptr; +} + +size_t BindingJSObjectMallocBytes(CanvasRenderingContext2D* aContext) { + IntSize size = aContext->GetSize(); + + // TODO: Bug 1552137: No memory will be allocated if either dimension is + // greater than gfxPrefs::gfx_canvas_max_size(). We should check this here + // too. + + CheckedInt bytes = + CheckedInt(size.width) * size.height * 4; + if (!bytes.isValid()) { + return 0; + } + + return bytes.value(); +} + +} // namespace mozilla::dom -- cgit v1.2.3