/* -*- 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/CanvasShutdownManager.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 "mozilla/Logging.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) 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 = CanvasShutdownManager::Get(); if (NS_WARN_IF(!canvasManager)) { mHasShutdown = true; return false; } canvasManager->AddShutdownObserver(this); return true; } void CanvasRenderingContext2D::RemoveShutdownObserver() { auto* const canvasManager = CanvasShutdownManager::MaybeGet(); if (!canvasManager) { return; } canvasManager->RemoveShutdownObserver(this); } void CanvasRenderingContext2D::OnRemoteCanvasLost() { // We only lose context / data if we are using remote canvas, which is only // for accelerated targets. if (!mBufferProvider || !mBufferProvider->IsAccelerated() || mIsContextLost) { return; } // 2. Set context's context lost to true. mIsContextLost = mAllowContextRestore = true; // 3. Reset the rendering context to its default state given context. ClearTarget(); // We dispatch because it isn't safe to call into the script event handlers, // and we don't want to mutate our state in CanvasShutdownManager. NS_DispatchToCurrentThread(NS_NewCancelableRunnableFunction( "CanvasRenderingContext2D::OnRemoteCanvasLost", [self = RefPtr{this}] { // 4. Let shouldRestore be the result of firing an event named // contextlost at canvas, with the cancelable attribute initialized to // true. self->mAllowContextRestore = self->DispatchEvent( u"contextlost"_ns, CanBubble::eNo, Cancelable::eYes); })); } void CanvasRenderingContext2D::OnRemoteCanvasRestored() { // We never lost our context if it was not a remote canvas, nor can we restore // if we have already shutdown. if (mHasShutdown || !mIsContextLost || !mAllowContextRestore) { return; } // We dispatch because it isn't safe to call into the script event handlers, // and we don't want to mutate our state in CanvasShutdownManager. NS_DispatchToCurrentThread(NS_NewCancelableRunnableFunction( "CanvasRenderingContext2D::OnRemoteCanvasRestored", [self = RefPtr{this}] { // 5. If shouldRestore is false, then abort these steps. if (!self->mHasShutdown && self->mIsContextLost && self->mAllowContextRestore) { // 7. Set context's context lost to false. self->mIsContextLost = false; // 6. Attempt to restore context by creating a backing storage using // context's attributes and associating them with context. If this // fails, then abort these steps. if (!self->EnsureTarget()) { self->mIsContextLost = true; return; } // 8. Fire an event named contextrestored at canvas. self->DispatchEvent(u"contextrestored"_ns, CanBubble::eNo, Cancelable::eNo); } })); } 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(ErrorResult& aError, const gfx::Rect* aCoveredRect, bool aWillClear) { if (AlreadyShutDown()) { gfxCriticalNoteOnce << "Attempt to render into a Canvas2d after shutdown."; SetErrorState(); aError.ThrowInvalidStateError( "Cannot use canvas after shutdown initiated."); return false; } // The spec doesn't say what to do in this case, but Chrome silently fails // without throwing an error. We should at least throw if the canvas is // permanently disabled. if (NS_WARN_IF(mIsContextLost)) { if (!mAllowContextRestore) { aError.ThrowInvalidStateError( "Cannot use canvas as context is lost forever."); } return false; } if (mTarget) { if (mTarget == sErrorTarget.get()) { aError.ThrowInvalidStateError("Canvas is already in error state."); return false; } return true; } // Check that the dimensions are sane if (mWidth > StaticPrefs::gfx_canvas_max_size() || mHeight > StaticPrefs::gfx_canvas_max_size()) { SetErrorState(); aError.ThrowInvalidStateError("Canvas exceeds max size."); return false; } if (mWidth < 0 || mHeight < 0) { SetErrorState(); aError.ThrowInvalidStateError("Canvas has invalid size."); 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, aError)) { 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) { MOZ_ASSERT(NS_IsMainThread()); WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); if (NS_WARN_IF(!renderer)) { return false; } aOutProvider = PersistentBufferProviderAccelerated::Create( GetSize(), GetSurfaceFormat(), renderer->AsKnowsCompositor()); } else if (mOffscreenCanvas && StaticPrefs::gfx_canvas_remote_allow_offscreen()) { RefPtr imageBridge = ImageBridgeChild::GetSingleton(); if (NS_WARN_IF(!imageBridge)) { return false; } aOutProvider = PersistentBufferProviderAccelerated::Create( GetSize(), GetSurfaceFormat(), imageBridge); } 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) { MOZ_ASSERT(NS_IsMainThread()); WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); if (NS_WARN_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 = 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, ErrorResult& aError) { aOutDT = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget( GetSize(), GetSurfaceFormat()); if (!aOutDT) { aError.ThrowInvalidStateError("Canvas could not create basic draw target."); return false; } // See Bug 1524554 - this forces DT initialization. aOutDT->ClearRect(gfx::Rect()); if (!aOutDT->IsValid()) { aOutDT = nullptr; aError.ThrowInvalidStateError("Canvas could not init basic draw target."); 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() || mIsContextLost, "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) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); Matrix newMatrix = mTarget->GetTransform(); newMatrix.PreScale(aX, aY); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::Rotate(double aAngle, ErrorResult& aError) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); Matrix newMatrix = Matrix::Rotation(aAngle) * mTarget->GetTransform(); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::Translate(double aX, double aY, ErrorResult& aError) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); 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) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy); newMatrix *= mTarget->GetTransform(); SetTransformInternal(newMatrix); } already_AddRefed CanvasRenderingContext2D::GetTransform( ErrorResult& aError) { if (!EnsureTarget(aError)) { return nullptr; } MOZ_ASSERT(IsTargetValid()); 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) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::SetTransform(const DOMMatrix2DInit& aInit, ErrorResult& aError) { if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); 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(); if (!EnsureTarget(aError)) { return nullptr; } MOZ_ASSERT(IsTargetValid()); 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(); } if (!EnsureTarget(aError)) { return nullptr; } MOZ_ASSERT(IsTargetValid()); // 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"); } if (!EnsureWritablePath()) { return; } // 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; } if (!EnsureWritablePath()) { return; } EnsureActivePath(); mPathBuilder->Arc(Point(aX, aY), aR, aStartAngle, aEndAngle, aAnticlockwise); mPathPruned = false; } void CanvasRenderingContext2D::Rect(double aX, double aY, double aW, double aH) { if (!EnsureWritablePath()) { return; } 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) { if (!EnsureWritablePath()) { return; } 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"); } if (!EnsureWritablePath()) { return; } 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; } bool 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. if (NS_WARN_IF(!mTarget)) { return false; } FillRule fillRule = CurrentState().fillRule; if (mPathTransformDirty) { FlushPathTransform(); } if (mPathBuilder) { return true; } if (!mPath) { mPathBuilder = mTarget->CreatePathBuilder(fillRule); } else { mPathBuilder = mPath->CopyToBuilder(fillRule); } return true; } 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; if (!EnsureTarget(aError)) { return nullptr; } MOZ_ASSERT(IsTargetValid()); 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; if (!EnsureTarget(aError)) { return; } MOZ_ASSERT(IsTargetValid()); 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); if (!EnsureTarget(aError, discardContent ? &drawRect : nullptr)) { return; } MOZ_ASSERT(IsTargetValid()); 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. if (!EnsureTarget(aError, discardContent ? &drawRect : nullptr)) { return; } MOZ_ASSERT(IsTargetValid()); 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); if (!EnsureTarget(aRv, &putRect)) { return; } MOZ_ASSERT(IsTargetValid()); 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