/* -*- 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/PresShell.h" #include "mozilla/PresShellInlines.h" #include "mozilla/SVGImageContext.h" #include "mozilla/SVGObserverUtils.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/HTMLCanvasElement.h" #include "mozilla/dom/GeneratePlaceholderCanvasData.h" #include "nsPresContext.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIFrame.h" #include "nsError.h" #include "nsCSSPseudoElements.h" #include "nsComputedDOMStyle.h" #include "nsPrintfCString.h" #include "nsReadableUtils.h" #include "nsColor.h" #include "nsGfxCIID.h" #include "nsIDocShell.h" #include "nsPIDOMWindow.h" #include "nsDisplayList.h" #include "nsFocusManager.h" #include "nsContentUtils.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 "Layers.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/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/Helpers.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/PersistentBufferProvider.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/Preferences.h" #include "mozilla/ServoBindings.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 "nsGlobalWindow.h" #include "nsDeviceContext.h" #include "nsFontMetrics.h" #include "nsLayoutUtils.h" #include "Units.h" #include "CanvasUtils.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" #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 int64_t 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: typedef CanvasRenderingContext2D::Style Style; typedef CanvasRenderingContext2D::ContextState 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]) { if (aCtx->mCanvasElement) { CanvasUtils::DoDrawImageSecurityCheck( aCtx->mCanvasElement, 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: typedef CanvasRenderingContext2D::ContextState 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() && mCtx->mCanvasElement) { mCtx->mCanvasElement->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: typedef CanvasRenderingContext2D::ContextState 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(); bounds.ToIntRect(&mTempRect); if (!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(), 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: typedef CanvasRenderingContext2D::ContextState ContextState; explicit AdjustedTarget(CanvasRenderingContext2D* aCtx, const gfx::Rect* aBounds = nullptr) { // 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()) { return; } mozilla::gfx::CompositionOp op = aCtx->CurrentState().op; 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. if (aCtx->NeedToDrawShadow()) { mShadowTarget = MakeUnique( aCtx, aCtx->mTarget, boundsAfterFilter, op); 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. op = gfx::CompositionOp::OP_OVER; } // Now set up the filter draw target. const bool applyFilter = aCtx->NeedToApplyFilter(); 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), op); mTarget = mFilterTarget->DT(); } 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; } 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()); } 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"); } PresShell* presShell = mContext ? mContext->GetPresShell() : nullptr; ServoStyleSet* styleSet = presShell ? presShell->StyleSet() : nullptr; nscolor color; bool ok = ServoCSSParser::ComputeColor(styleSet, NS_RGB(0, 0, 0), aColorstr, &color); if (!ok) { return aRv.ThrowSyntaxError("Invalid color"); } mStops = nullptr; GradientStop newStop; newStop.offset = aOffset; newStop.color = ToDeviceColor(color); mRawStops.AppendElement(newStop); } NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasGradient, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasGradient, Release) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasGradient, mContext) NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasPattern, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasPattern, Release) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPattern, mContext) class CanvasShutdownObserver final : public nsIObserver { public: explicit CanvasShutdownObserver(CanvasRenderingContext2D* aCanvas) : mCanvas(aCanvas) {} void OnShutdown() { if (!mCanvas) { return; } mCanvas = nullptr; nsContentUtils::UnregisterShutdownObserver(this); } NS_DECL_ISUPPORTS NS_DECL_NSIOBSERVER private: ~CanvasShutdownObserver() = default; CanvasRenderingContext2D* mCanvas; }; NS_IMPL_ISUPPORTS(CanvasShutdownObserver, nsIObserver) NS_IMETHODIMP CanvasShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (mCanvas && strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { mCanvas->OnShutdown(); OnShutdown(); } return NS_OK; } NS_IMPL_CYCLE_COLLECTING_ADDREF(CanvasRenderingContext2D) NS_IMPL_CYCLE_COLLECTING_RELEASE(CanvasRenderingContext2D) NS_IMPL_CYCLE_COLLECTION_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(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); } for (size_t x = 0; x < tmp->mHitRegionsOptions.Length(); x++) { RegionInfo& info = tmp->mHitRegionsOptions[x]; if (info.mElement) { ImplCycleCollectionUnlink(info.mElement); } } NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CanvasRenderingContext2D) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCanvasElement) 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"); } for (size_t x = 0; x < tmp->mHitRegionsOptions.Length(); x++) { RegionInfo& info = tmp->mHitRegionsOptions[x]; if (info.mElement) { ImplCycleCollectionTraverse(cb, info.mElement, "Hit region fallback element"); } } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(CanvasRenderingContext2D) 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), 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. uintptr_t CanvasRenderingContext2D::sNumLivingContexts = 0; DrawTarget* CanvasRenderingContext2D::sErrorTarget = nullptr; 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), mIsCapturedFrameInvalid(false), mPathTransformWillUpdate(false), mInvalidateCount(0), mWriteOnly(false) { sNumLivingContexts++; mShutdownObserver = new CanvasShutdownObserver(this); nsContentUtils::RegisterShutdownObserver(mShutdownObserver); } CanvasRenderingContext2D::~CanvasRenderingContext2D() { RemovePostRefreshObserver(); RemoveShutdownObserver(); Reset(); sNumLivingContexts--; if (!sNumLivingContexts) { NS_IF_RELEASE(sErrorTarget); } } JSObject* CanvasRenderingContext2D::WrapObject( JSContext* aCx, JS::Handle aGivenProto) { return CanvasRenderingContext2D_Binding::Wrap(aCx, this, aGivenProto); } bool CanvasRenderingContext2D::ParseColor(const nsACString& aString, nscolor* aColor) { Document* document = mCanvasElement ? mCanvasElement->OwnerDoc() : nullptr; css::Loader* loader = document ? document->CSSLoader() : nullptr; PresShell* presShell = GetPresShell(); ServoStyleSet* set = presShell ? presShell->StyleSet() : nullptr; // First, try computing the color without handling currentcolor. bool wasCurrentColor = false; if (!ServoCSSParser::ComputeColor(set, NS_RGB(0, 0, 0), aString, aColor, &wasCurrentColor, loader)) { return false; } if (wasCurrentColor && mCanvasElement) { // Otherwise, get the value of the color property, flushing style // if necessary. RefPtr canvasStyle = nsComputedDOMStyle::GetComputedStyle(mCanvasElement, nullptr); if (canvasStyle) { *aColor = canvasStyle->StyleText()->mColor.ToColor(); } // Beware that the presShell could be gone here. } return true; } nsresult CanvasRenderingContext2D::Reset() { 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; mBufferProvider = nullptr; // reset hit regions mHitRegionsOptions.ClearAndRetainStorage(); // Since the target changes the backing texture will change, and this will // no longer be valid. mIsEntireFrameInvalid = false; mPredictManyRedrawCalls = false; mIsCapturedFrameInvalid = false; return NS_OK; } void CanvasRenderingContext2D::OnShutdown() { mShutdownObserver = nullptr; RefPtr provider = mBufferProvider; Reset(); if (provider) { provider->OnShutdown(); } } void CanvasRenderingContext2D::RemoveShutdownObserver() { if (mShutdownObserver) { mShutdownObserver->OnShutdown(); mShutdownObserver = nullptr; } } void CanvasRenderingContext2D::SetStyleFromString(const nsACString& aStr, Style aWhichStyle) { MOZ_ASSERT(!aStr.IsVoid()); nscolor color; if (!ParseColor(aStr, &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() { mIsCapturedFrameInvalid = true; if (mIsEntireFrameInvalid) { return NS_OK; } mIsEntireFrameInvalid = true; if (!mCanvasElement) { NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!"); return NS_OK; } SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement); mCanvasElement->InvalidateCanvasContent(nullptr); return NS_OK; } void CanvasRenderingContext2D::Redraw(const gfx::Rect& aR) { mIsCapturedFrameInvalid = true; ++mInvalidateCount; if (mIsEntireFrameInvalid) { return; } if (mPredictManyRedrawCalls || mInvalidateCount > kCanvasMaxInvalidateCount) { Redraw(); return; } if (!mCanvasElement) { NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!"); return; } SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement); mCanvasElement->InvalidateCanvasContent(&aR); } void CanvasRenderingContext2D::DidRefresh() {} void CanvasRenderingContext2D::RedrawUser(const gfxRect& aR) { mIsCapturedFrameInvalid = true; 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.begin(); clipOrTransform != style.clipsAndTransforms.end(); clipOrTransform++) { 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::EnsureTarget(const gfx::Rect* aCoveredRect, bool aWillClear) { if (AlreadyShutDown()) { gfxCriticalError() << "Attempt to render into a Canvas2d after shutdown."; SetErrorState(); return false; } if (mTarget) { return mTarget != sErrorTarget; } // Check that the dimensions are sane if (mWidth > StaticPrefs::gfx_canvas_max_size() || mHeight > StaticPrefs::gfx_canvas_max_size() || mWidth < 0 || mHeight < 0) { SetErrorState(); return false; } // If the next drawing command covers the entire canvas, we can skip copying // from the previous frame and/or clearing the canvas. gfx::Rect canvasRect(0, 0, mWidth, mHeight); bool canDiscardContent = aCoveredRect && CurrentState() .transform.TransformBounds(*aCoveredRect) .Contains(canvasRect); // If a clip is active we don't know for sure that the next drawing command // will really cover the entire canvas. for (const auto& style : mStyleStack) { if (!canDiscardContent) { break; } for (const auto& clipOrTransform : style.clipsAndTransforms) { if (clipOrTransform.IsClip()) { canDiscardContent = false; break; } } } ScheduleStableStateCallback(); IntRect persistedRect = canDiscardContent ? IntRect() : IntRect(0, 0, mWidth, mHeight); if (mBufferProvider) { mTarget = mBufferProvider->BorrowDrawTarget(persistedRect); if (mTarget && !mBufferProvider->PreservesDrawingState()) { RestoreClipsAndTransformToTarget(); } if (mTarget && mTarget->IsValid()) { return true; } } RefPtr newTarget; RefPtr newProvider; if (!TrySharedTarget(newTarget, newProvider) && !TryBasicTarget(newTarget, newProvider)) { gfxCriticalError( CriticalLog::DefaultOptions(Factory::ReasonableSurfaceSize(GetSize()))) << "Failed borrow shared and basic targets."; SetErrorState(); return false; } MOZ_ASSERT(newTarget); MOZ_ASSERT(newProvider); bool needsClear = !canDiscardContent; 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 && CopyBufferProvider(*mBufferProvider, *newTarget, persistedRect)) { needsClear = false; } if (needsClear) { newTarget->ClearRect(canvasRect); } mTarget = std::move(newTarget); mBufferProvider = std::move(newProvider); RegisterAllocation(); 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 mIsCapturedFrameInvalid. bool capturedFrameInvalid = mIsCapturedFrameInvalid; // Calling Redraw() tells our invalidation machinery that the entire // canvas is already invalid, which can speed up future drawing. Redraw(); mIsCapturedFrameInvalid = capturedFrameInvalid; return true; } void CanvasRenderingContext2D::SetInitialState() { // Set up the initial canvas defaults mPathBuilder = nullptr; mPath = nullptr; mDSPathBuilder = nullptr; mPathTransformWillUpdate = 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) { gCanvasAzureMemoryUsed -= mWidth * mHeight * 4; } mTarget = sErrorTarget; 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()); } JSObject* wrapper = GetWrapperPreserveColor(); if (wrapper) { CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC( JS::GetObjectZone(wrapper)); } } static already_AddRefed LayerManagerFromCanvasElement( nsINode* aCanvasElement) { if (!aCanvasElement) { return nullptr; } return nsContentUtils::PersistentLayerManagerForDocument( aCanvasElement->OwnerDoc()); } bool CanvasRenderingContext2D::TrySharedTarget( RefPtr& aOutDT, RefPtr& aOutProvider) { aOutDT = nullptr; aOutProvider = nullptr; if (!mCanvasElement) { return false; } if (mBufferProvider && (mBufferProvider->GetType() == LayersBackend::LAYERS_CLIENT || mBufferProvider->GetType() == LayersBackend::LAYERS_WR)) { // 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; } RefPtr layerManager = LayerManagerFromCanvasElement(mCanvasElement); if (!layerManager) { return false; } aOutProvider = layerManager->CreatePersistentBufferProvider( GetSize(), GetSurfaceFormat()); if (!aOutProvider) { return false; } // We can pass an empty persisted rect since we just created the buffer // provider (nothing to restore). aOutDT = aOutProvider->BorrowDrawTarget(IntRect()); MOZ_ASSERT(aOutDT); return !!aOutDT; } bool CanvasRenderingContext2D::TryBasicTarget( RefPtr& aOutDT, RefPtr& aOutProvider) { aOutDT = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget( GetSize(), GetSurfaceFormat()); if (!aOutDT) { return false; } // See Bug 1524554 - this forces DT initialization. aOutDT->ClearRect(gfx::Rect()); if (!aOutDT->IsValid()) { aOutDT = nullptr; return false; } aOutProvider = new PersistentBufferProviderBasic(aOutDT); return true; } 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::ClearTarget(int32_t aWidth, int32_t aHeight) { Reset(); 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. JSObject* wrapper = GetWrapperMaybeDead(); if (wrapper) { JS::RemoveAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this), JS::MemoryUse::DOMBinding); } mWidth = aWidth; mHeight = aHeight; if (wrapper) { JS::AddAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this), JS::MemoryUse::DOMBinding); } } 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, nullptr); if (canvasStyle) { WritingMode wm(canvasStyle); if (wm.IsVertical() && !wm.IsSideways()) { CurrentState().textBaseline = TextBaseline::MIDDLE; } } } void CanvasRenderingContext2D::ReturnTarget(bool aForceReset) { if (mTarget && mBufferProvider && mTarget != sErrorTarget) { 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) { RemovePostRefreshObserver(); mDocShell = aShell; AddPostRefreshObserverIfNecessary(); IntSize size = aTarget->GetSize(); SetDimensions(size.width, size.height); mTarget = aTarget; mBufferProvider = new PersistentBufferProviderBasic(aTarget); if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) { // Cf comment in EnsureTarget mTarget->PushClipRect(gfx::Rect(Point(0, 0), Size(mWidth, mHeight))); } return NS_OK; } void CanvasRenderingContext2D::SetOpaqueValueFromOpaqueAttr( bool aOpaqueAttrValue) { if (aOpaqueAttrValue != mOpaqueAttrValue) { mOpaqueAttrValue = aOpaqueAttrValue; UpdateIsOpaque(); } } void CanvasRenderingContext2D::UpdateIsOpaque() { mOpaque = !mContextAttributesHasAlpha || mOpaqueAttrValue; ClearTarget(); } NS_IMETHODIMP CanvasRenderingContext2D::SetIsIPC(bool aIsIPC) { if (aIsIPC != mIPC) { mIPC = aIsIPC; ClearTarget(); } return NS_OK; } 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); ContextAttributes2D attributes; if (!attributes.Init(aCx, aOptions)) { aRvForDictionaryInit.Throw(NS_ERROR_UNEXPECTED); return NS_ERROR_UNEXPECTED; } mContextAttributesHasAlpha = attributes.mAlpha; UpdateIsOpaque(); return NS_OK; } UniquePtr CanvasRenderingContext2D::GetImageBuffer( int32_t* aFormat) { UniquePtr ret; *aFormat = 0; if (!mBufferProvider) { if (!EnsureTarget()) { return nullptr; } } RefPtr snapshot = mBufferProvider->BorrowSnapshot(); if (snapshot) { RefPtr data = snapshot->GetDataSurface(); if (data && data->GetSize() == GetSize()) { *aFormat = imgIEncoder::INPUT_FORMAT_HOSTARGB; ret = SurfaceToPackedBGRA(data); } } mBufferProvider->ReturnSnapshot(snapshot.forget()); return ret; } nsString CanvasRenderingContext2D::GetHitRegion( const mozilla::gfx::Point& aPoint) { for (size_t x = 0; x < mHitRegionsOptions.Length(); x++) { RegionInfo& info = mHitRegionsOptions[x]; if (info.mPath->ContainsPoint(aPoint, Matrix())) { return info.mId; } } return nsString(); } 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; UniquePtr imageBuffer = GetImageBuffer(&format); if (!imageBuffer) { return NS_ERROR_FAILURE; } return ImageEncoder::GetInputStream(mWidth, mHeight, imageBuffer.get(), format, encoder, aEncoderOptions, aStream); } already_AddRefed CanvasRenderingContext2D::GetSurfaceSnapshot(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, "On EnsureTarget failure mTarget should be set to sErrorTarget."); return mTarget->Snapshot(); } // 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(); 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; } TransformWillUpdate(); if (!IsTargetValid()) { return; } for (const auto& clipOrTransform : CurrentState().clipsAndTransforms) { if (clipOrTransform.IsClip()) { mTarget->PopClip(); } } mStyleStack.RemoveLastElement(); mTarget->SetTransform(CurrentState().transform); } // // transformations // void CanvasRenderingContext2D::Scale(double aX, double aY, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newMatrix = mTarget->GetTransform(); newMatrix.PreScale(aX, aY); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::Rotate(double aAngle, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newMatrix = Matrix::Rotation(aAngle) * mTarget->GetTransform(); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::Translate(double aX, double aY, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newMatrix = mTarget->GetTransform(); newMatrix.PreTranslate(aX, aY); SetTransformInternal(newMatrix); } void CanvasRenderingContext2D::Transform(double aM11, double aM12, double aM21, double aM22, double aDx, double aDy, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy); newMatrix *= mTarget->GetTransform(); SetTransformInternal(newMatrix); } already_AddRefed CanvasRenderingContext2D::GetTransform( ErrorResult& aError) { EnsureTarget(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return nullptr; } RefPtr matrix = new DOMMatrix(GetParentObject(), mTarget->GetTransform()); return matrix.forget(); } void CanvasRenderingContext2D::SetTransform(double aM11, double aM12, double aM21, double aM22, double aDx, double aDy, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } SetTransformInternal(Matrix(aM11, aM12, aM21, aM22, aDx, aDy)); } void CanvasRenderingContext2D::SetTransform(const DOMMatrix2DInit& aInit, ErrorResult& aError) { TransformWillUpdate(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } RefPtr matrix = DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError); if (!aError.Failed()) { SetTransformInternal(Matrix(*(matrix->GetInternal2D()))); } } 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); } void CanvasRenderingContext2D::ResetTransform(ErrorResult& aError) { SetTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, aError); } static void MatrixToJSObject(JSContext* aCx, const Matrix& aMatrix, JS::MutableHandle aResult, ErrorResult& aError) { double elts[6] = {aMatrix._11, aMatrix._12, aMatrix._21, aMatrix._22, aMatrix._31, aMatrix._32}; // XXX Should we enter GetWrapper()'s compartment? JS::Rooted val(aCx); if (!ToJSValue(aCx, elts, &val)) { aError.Throw(NS_ERROR_OUT_OF_MEMORY); } else { aResult.set(&val.toObject()); } } static bool ObjectToMatrix(JSContext* aCx, JS::Handle aObj, Matrix& aMatrix, ErrorResult& aError) { uint32_t length; if (!JS::GetArrayLength(aCx, aObj, &length) || length != 6) { // Not an array-like thing or wrong size aError.Throw(NS_ERROR_INVALID_ARG); return false; } Float* elts[] = {&aMatrix._11, &aMatrix._12, &aMatrix._21, &aMatrix._22, &aMatrix._31, &aMatrix._32}; for (uint32_t i = 0; i < 6; ++i) { JS::Rooted elt(aCx); double d; if (!JS_GetElement(aCx, aObj, i, &elt)) { aError.Throw(NS_ERROR_FAILURE); return false; } if (!CoerceDouble(elt, &d)) { aError.Throw(NS_ERROR_INVALID_ARG); return false; } if (!FloatValidate(d)) { // This is weird, but it's the behavior of SetTransform() return false; } *elts[i] = Float(d); } return true; } void CanvasRenderingContext2D::SetMozCurrentTransform( JSContext* aCx, JS::Handle aCurrentTransform, ErrorResult& aError) { EnsureTarget(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newCTM; if (ObjectToMatrix(aCx, aCurrentTransform, newCTM, aError) && newCTM.IsFinite()) { mTarget->SetTransform(newCTM); } } void CanvasRenderingContext2D::GetMozCurrentTransform( JSContext* aCx, JS::MutableHandle aResult, ErrorResult& aError) { EnsureTarget(); MatrixToJSObject(aCx, mTarget ? mTarget->GetTransform() : Matrix(), aResult, aError); } void CanvasRenderingContext2D::SetMozCurrentTransformInverse( JSContext* aCx, JS::Handle aCurrentTransform, ErrorResult& aError) { EnsureTarget(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } Matrix newCTMInverse; if (ObjectToMatrix(aCx, aCurrentTransform, newCTMInverse, aError)) { // XXX ERRMSG we need to report an error to developers here! (bug 329026) if (newCTMInverse.Invert() && newCTMInverse.IsFinite()) { mTarget->SetTransform(newCTMInverse); } } } void CanvasRenderingContext2D::GetMozCurrentTransformInverse( JSContext* aCx, JS::MutableHandle aResult, ErrorResult& aError) { EnsureTarget(); if (!mTarget) { MatrixToJSObject(aCx, Matrix(), aResult, aError); return; } Matrix ctm = mTarget->GetTransform(); if (!ctm.Invert()) { double NaN = JS::GenericNaN(); ctm = Matrix(NaN, NaN, NaN, NaN, NaN, NaN); } MatrixToJSObject(aCx, ctm, aResult, 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) { return MakeAndAddRef(this, aAngle, 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; 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.MarkAsContentSource( mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_PATTERN); element = &video; } else { // Special case for ImageBitmap ImageBitmap& imgBitmap = aSource.GetAsImageBitmap(); EnsureTarget(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } RefPtr srcSurf = imgBitmap.PrepareForDrawTarget(mTarget); if (!srcSurf) { aError.ThrowInvalidStateError( "Passed-in ImageBitmap has been transferred"); return nullptr; } // An ImageBitmap never taints others so we set principalForSecurityCheck to // nullptr and set CORSUsed to true for passing the security check in // CanvasUtils::DoDrawImageSecurityCheck(). RefPtr pat = new CanvasPattern( this, srcSurf, repeatMode, nullptr, imgBitmap.IsWriteOnly(), true); return pat.forget(); } EnsureTarget(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); return nullptr; } // The canvas spec says that createPattern should use the first frame // of animated images SurfaceFromElementResult res = nsLayoutUtils::SurfaceFromElement( element, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE, 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) { nscolor color; if (!ParseColor(aShadowColor, &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); 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, ParsingMode::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(); RefPtr parentStyle; // have to get a parent ComputedStyle for inherit-like relative // values (2em, bolder, etc.) if (aElement && aElement->IsInComposedDoc()) { parentStyle = nsComputedDOMStyle::GetComputedStyle(aElement, nullptr); if (!parentStyle) { // The flush killed the shell, so we couldn't get any meaningful style // back. aError.Throw(NS_ERROR_FAILURE); return nullptr; } } else { 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); // The font getter is required to be reserialized based on what we // parsed (including having line-height removed). (Older drafts of // the spec required font sizes be converted to pixels, but that no // longer seems to be required.) Servo_SerializeFontValueForCanvas(declarations, &aOutUsedFont); 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) { if (!mCanvasElement && !mDocShell) { NS_WARNING( "Canvas element must be non-null or a docshell must be provided"); aError.Throw(NS_ERROR_FAILURE); return false; } RefPtr presShell = GetPresShell(); if (NS_WARN_IF(!presShell)) { aError.Throw(NS_ERROR_FAILURE); return false; } 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(); } } } class CanvasUserSpaceMetrics : public UserSpaceMetricsWithSize { public: CanvasUserSpaceMetrics(const gfx::IntSize& aSize, const nsFont& aFont, nsAtom* aFontLanguage, bool aExplicitLanguage, nsPresContext* aPresContext) : mSize(aSize), mFont(aFont), mFontLanguage(aFontLanguage), mExplicitLanguage(aExplicitLanguage), mPresContext(aPresContext) {} virtual float GetEmLength() const override { return mFont.size.ToCSSPixels(); } virtual float GetExLength() const override { nsDeviceContext* dc = mPresContext->DeviceContext(); nsFontMetrics::Params params; params.language = mFontLanguage; params.explicitLanguage = mExplicitLanguage; params.textPerf = mPresContext->GetTextPerfMetrics(); params.fontStats = mPresContext->GetFontMatchingStats(); params.featureValueLookup = mPresContext->GetFontFeatureValuesLookup(); RefPtr fontMetrics = dc->GetMetricsFor(mFont, params); return NSAppUnitsToFloatPixels(fontMetrics->XHeight(), AppUnitsPerCSSPixel()); } virtual gfx::Size GetSize() const override { return Size(mSize); } private: gfx::IntSize mSize; const nsFont& mFont; nsAtom* mFontLanguage; bool mExplicitLanguage; 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() { RefPtr presShell = GetPresShell(); if (!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 = mCanvasElement && mCanvasElement->IsWriteOnly(); return; } if (FiltersNeedFrameFlush(CurrentState().filterChain.AsSpan())) { presShell->FlushPendingNotifications(FlushType::Frames); } MOZ_RELEASE_ASSERT(!mStyleStack.IsEmpty()); if (MOZ_UNLIKELY(presShell->IsDestroying())) { return; } const bool sourceGraphicIsTainted = mCanvasElement && mCanvasElement->IsWriteOnly(); CurrentState().filter = FilterInstance::GetFilterDescription( mCanvasElement, CurrentState().filterChain.AsSpan(), sourceGraphicIsTainted, CanvasUserSpaceMetrics( GetSize(), CurrentState().fontFont, CurrentState().fontLanguage, CurrentState().fontExplicitLanguage, presShell->GetPresContext()), gfxRect(0, 0, mWidth, mHeight), CurrentState().filterAdditionalImages); CurrentState().filterSourceGraphicTainted = sourceGraphicIsTainted; } // // 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) { if (!ValidateRect(aX, aY, aW, aH, true)) { return; } const ContextState* state = &CurrentState(); if (state->patternStyles[Style::FILL]) { CanvasPattern::RepeatMode repeat = state->patternStyles[Style::FILL]->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; IntSize patternSize = state->patternStyles[Style::FILL]->mSurface->GetSize(); // We always need to execute painting for non-over operators, even if // we end up with w/h = 0. if (limitx) { if (aX < 0) { aW += aX; if (aW < 0) { aW = 0; } aX = 0; } if (aX + aW > patternSize.width) { aW = patternSize.width - aX; if (aW < 0) { aW = 0; } } } if (limity) { if (aY < 0) { aH += aY; if (aH < 0) { aH = 0; } aY = 0; } if (aY + aH > patternSize.height) { aH = patternSize.height - aY; if (aH < 0) { aH = 0; } } } } state = nullptr; CompositionOp op = UsedOperation(); bool isColor; bool discardContent = PatternIsOpaque(Style::FILL, &isColor) && (op == CompositionOp::OP_OVER || 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); 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); } auto op = UsedOperation(); if (!IsTargetValid()) { return; } if (!aH) { CapStyle cap = CapStyle::BUTT; if (CurrentState().lineJoin == JoinStyle::ROUND) { cap = CapStyle::ROUND; } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); 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, 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 == JoinStyle::ROUND) { cap = CapStyle::ROUND; } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); 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, 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); if (!target) { return; } const ContextState& state = CurrentState(); target->StrokeRect( gfx::Rect(aX, aY, aW, aH), CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), StrokeOptions(state.lineWidth, state.lineJoin, 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; mDSPathBuilder = nullptr; mPathTransformWillUpdate = false; } void CanvasRenderingContext2D::Fill(const CanvasWindingRule& aWinding) { EnsureUserSpacePath(aWinding); if (!mPath) { return; } const bool needBounds = NeedToCalculateBounds(); if (!IsTargetValid()) { return; } gfx::Rect bounds; if (needBounds) { bounds = mPath->GetBounds(mTarget->GetTransform()); } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); if (!target) { return; } auto op = UsedOperation(); if (!IsTargetValid() || !target) { return; } target->Fill(mPath, CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget), DrawOptions(CurrentState().globalAlpha, op)); Redraw(); } void CanvasRenderingContext2D::Fill(const CanvasPath& aPath, const CanvasWindingRule& aWinding) { EnsureTarget(); if (!IsTargetValid()) { return; } RefPtr gfxpath = aPath.GetPath(aWinding, mTarget); if (!gfxpath) { return; } const bool needBounds = NeedToCalculateBounds(); if (!IsTargetValid()) { return; } gfx::Rect bounds; if (needBounds) { bounds = gfxpath->GetBounds(mTarget->GetTransform()); } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); if (!target) { return; } auto op = UsedOperation(); if (!IsTargetValid() || !target) { return; } target->Fill(gfxpath, CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget), DrawOptions(CurrentState().globalAlpha, op)); Redraw(); } void CanvasRenderingContext2D::Stroke() { EnsureUserSpacePath(); if (!mPath) { return; } const ContextState* state = &CurrentState(); StrokeOptions strokeOptions(state->lineWidth, state->lineJoin, 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 = mPath->GetStrokedBounds(strokeOptions, mTarget->GetTransform()); } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); if (!target) { return; } auto op = UsedOperation(); if (!IsTargetValid() || !target) { return; } target->Stroke(mPath, CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), strokeOptions, DrawOptions(CurrentState().globalAlpha, op)); Redraw(); } void CanvasRenderingContext2D::Stroke(const CanvasPath& aPath) { EnsureTarget(); if (!IsTargetValid()) { return; } RefPtr gfxpath = aPath.GetPath(CanvasWindingRule::Nonzero, mTarget); if (!gfxpath) { return; } const ContextState* state = &CurrentState(); StrokeOptions strokeOptions(state->lineWidth, state->lineJoin, 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 = gfxpath->GetStrokedBounds(strokeOptions, mTarget->GetTransform()); } AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds); if (!target) { return; } auto op = UsedOperation(); if (!IsTargetValid() || !target) { return; } target->Stroke(gfxpath, CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget), strokeOptions, DrawOptions(CurrentState().globalAlpha, op)); Redraw(); } 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 = CapStyle::BUTT; state->lineJoin = mozilla::gfx::JoinStyle::MITER_OR_BEVEL; 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(NS_EVENT_STATE_FOCUSRING)) { return false; } HTMLCanvasElement* canvas = GetCanvas(); if (!canvas || !aElement.IsInclusiveDescendantOf(canvas)) { return false; } EnsureUserSpacePath(); return true; } void CanvasRenderingContext2D::Clip(const CanvasWindingRule& aWinding) { EnsureUserSpacePath(aWinding); if (!mPath) { return; } mTarget->PushClip(mPath); CurrentState().clipsAndTransforms.AppendElement(ClipState(mPath)); } void CanvasRenderingContext2D::Clip(const CanvasPath& aPath, const CanvasWindingRule& aWinding) { EnsureTarget(); if (!IsTargetValid()) { return; } RefPtr gfxpath = aPath.GetPath(aWinding, mTarget); if (!gfxpath) { return; } mTarget->PushClip(gfxpath); CurrentState().clipsAndTransforms.AppendElement(ClipState(gfxpath)); } void CanvasRenderingContext2D::ArcTo(double aX1, double aY1, double aX2, double aY2, double aRadius, ErrorResult& aError) { if (aRadius < 0) { return aError.ThrowIndexSizeError("Negative radius"); } EnsureWritablePath(); // Current point in user space! Point p0; if (mPathBuilder) { p0 = mPathBuilder->CurrentPoint(); } else { Matrix invTransform = mTarget->GetTransform(); if (!invTransform.Invert()) { return; } p0 = invTransform.TransformPoint(mDSPathBuilder->CurrentPoint()); } Point p1(aX1, aY1); Point p2(aX2, aY2); // 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.x, p1.y); return; } // Check for colinearity dir = (p2.x - p1.x) * (p0.y - p1.y) + (p2.y - p1.y) * (p1.x - p0.x); if (dir == 0) { LineTo(p1.x, p1.y); 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"); } EnsureWritablePath(); ArcToBezier(this, Point(aX, aY), Size(aR, aR), aStartAngle, aEndAngle, aAnticlockwise); } void CanvasRenderingContext2D::Rect(double aX, double aY, double aW, double aH) { EnsureWritablePath(); if (mPathBuilder) { mPathBuilder->MoveTo(Point(aX, aY)); mPathBuilder->LineTo(Point(aX + aW, aY)); mPathBuilder->LineTo(Point(aX + aW, aY + aH)); mPathBuilder->LineTo(Point(aX, aY + aH)); mPathBuilder->Close(); } else { mDSPathBuilder->MoveTo( mTarget->GetTransform().TransformPoint(Point(aX, aY))); mDSPathBuilder->LineTo( mTarget->GetTransform().TransformPoint(Point(aX + aW, aY))); mDSPathBuilder->LineTo( mTarget->GetTransform().TransformPoint(Point(aX + aW, aY + aH))); mDSPathBuilder->LineTo( mTarget->GetTransform().TransformPoint(Point(aX, aY + aH))); mDSPathBuilder->Close(); } } void CanvasRenderingContext2D::Ellipse(double aX, double aY, double aRadiusX, double aRadiusY, double aRotation, double aStartAngle, double aEndAngle, bool aAnticlockwise, ErrorResult& aError) { if (aRadiusX < 0.0 || aRadiusY < 0.0) { return aError.ThrowIndexSizeError("Negative radius"); } EnsureWritablePath(); ArcToBezier(this, Point(aX, aY), Size(aRadiusX, aRadiusY), aStartAngle, aEndAngle, aAnticlockwise, aRotation); } void CanvasRenderingContext2D::EnsureWritablePath() { EnsureTarget(); // NOTE: IsTargetValid() may be false here (mTarget == sErrorTarget) but we // go ahead and create a path anyway since callers depend on that. if (mDSPathBuilder) { return; } FillRule fillRule = CurrentState().fillRule; if (mPathBuilder) { if (mPathTransformWillUpdate) { mPath = mPathBuilder->Finish(); mDSPathBuilder = mPath->TransformedCopyToBuilder(mPathToDS, fillRule); mPath = nullptr; mPathBuilder = nullptr; mPathTransformWillUpdate = false; } return; } if (!mPath) { NS_ASSERTION( !mPathTransformWillUpdate, "mPathTransformWillUpdate should be false, if all paths are null"); mPathBuilder = mTarget->CreatePathBuilder(fillRule); } else if (!mPathTransformWillUpdate) { mPathBuilder = mPath->CopyToBuilder(fillRule); } else { mDSPathBuilder = mPath->TransformedCopyToBuilder(mPathToDS, fillRule); mPathTransformWillUpdate = false; mPath = nullptr; } } void CanvasRenderingContext2D::EnsureUserSpacePath( const CanvasWindingRule& aWinding) { FillRule fillRule = CurrentState().fillRule; if (aWinding == CanvasWindingRule::Evenodd) fillRule = FillRule::FILL_EVEN_ODD; EnsureTarget(); if (!IsTargetValid()) { return; } if (!mPath && !mPathBuilder && !mDSPathBuilder) { mPathBuilder = mTarget->CreatePathBuilder(fillRule); } if (mPathBuilder) { mPath = mPathBuilder->Finish(); mPathBuilder = nullptr; } if (mPath && mPathTransformWillUpdate) { mDSPathBuilder = mPath->TransformedCopyToBuilder(mPathToDS, fillRule); mPath = nullptr; mPathTransformWillUpdate = false; } if (mDSPathBuilder) { RefPtr dsPath; dsPath = mDSPathBuilder->Finish(); mDSPathBuilder = nullptr; Matrix inverse = mTarget->GetTransform(); if (!inverse.Invert()) { NS_WARNING("Could not invert transform"); return; } mPathBuilder = dsPath->TransformedCopyToBuilder(inverse, fillRule); 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::TransformWillUpdate() { EnsureTarget(); if (!IsTargetValid()) { return; } // Store the matrix that would transform the current path to device // space. if (mPath || mPathBuilder) { if (!mPathTransformWillUpdate) { // If the transform has already been updated, but a device space builder // has not been created yet mPathToDS contains the right transform to // transform the current mPath into device space. // We should leave it alone. mPathToDS = mTarget->GetTransform(); } mPathTransformWillUpdate = true; } } // // text // void CanvasRenderingContext2D::SetFont(const nsACString& aFont, ErrorResult& aError) { SetFontInternal(aFont, aError); } bool CanvasRenderingContext2D::SetFontInternal(const nsACString& aFont, ErrorResult& aError) { /* * If font is defined with relative units (e.g. ems) and the parent * ComputedStyle changes in between calls, setting the font to the * same value as previous could result in a different computed value, * so we cannot have the optimization where we check if the new font * string is equal to the old one. */ if (!mCanvasElement && !mDocShell) { NS_WARNING( "Canvas element must be non-null or a docshell must be provided"); aError.Throw(NS_ERROR_FAILURE); return false; } RefPtr presShell = GetPresShell(); if (NS_WARN_IF(!presShell)) { aError.Throw(NS_ERROR_FAILURE); return false; } nsCString usedFont; RefPtr sc = GetFontStyleForServo(mCanvasElement, aFont, presShell, usedFont, aError); if (!sc) { return false; } const nsStyleFont* fontStyle = sc->StyleFont(); nsPresContext* c = presShell->GetPresContext(); // 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); c->Document()->FlushUserFontSet(); nsFontMetrics::Params params; params.language = fontStyle->mLanguage; params.explicitLanguage = fontStyle->mExplicitLanguage; params.userFontSet = c->GetUserFontSet(); params.textPerf = c->GetTextPerfMetrics(); params.fontStats = c->GetFontMatchingStats(); RefPtr metrics = c->DeviceContext()->GetMetricsFor(resizedFont, params); gfxFontGroup* newFontGroup = metrics->GetThebesFontGroup(); CurrentState().fontGroup = newFontGroup; NS_ASSERTION(CurrentState().fontGroup, "Could not get font group"); CurrentState().font = usedFont; CurrentState().fontFont = fontStyle->mFont; CurrentState().fontFont.size = fontStyle->mSize; CurrentState().fontLanguage = fontStyle->mLanguage; CurrentState().fontExplicitLanguage = fontStyle->mExplicitLanguage; return true; } void CanvasRenderingContext2D::SetTextAlign(const nsAString& aTextAlign) { if (aTextAlign.EqualsLiteral("start")) CurrentState().textAlign = TextAlign::START; else if (aTextAlign.EqualsLiteral("end")) CurrentState().textAlign = TextAlign::END; else if (aTextAlign.EqualsLiteral("left")) CurrentState().textAlign = TextAlign::LEFT; else if (aTextAlign.EqualsLiteral("right")) CurrentState().textAlign = TextAlign::RIGHT; else if (aTextAlign.EqualsLiteral("center")) CurrentState().textAlign = TextAlign::CENTER; } void CanvasRenderingContext2D::GetTextAlign(nsAString& aTextAlign) { switch (CurrentState().textAlign) { case TextAlign::START: aTextAlign.AssignLiteral("start"); break; case TextAlign::END: aTextAlign.AssignLiteral("end"); break; case TextAlign::LEFT: aTextAlign.AssignLiteral("left"); break; case TextAlign::RIGHT: aTextAlign.AssignLiteral("right"); break; case TextAlign::CENTER: aTextAlign.AssignLiteral("center"); break; } } void CanvasRenderingContext2D::SetTextBaseline(const nsAString& aTextBaseline) { if (aTextBaseline.EqualsLiteral("top")) CurrentState().textBaseline = TextBaseline::TOP; else if (aTextBaseline.EqualsLiteral("hanging")) CurrentState().textBaseline = TextBaseline::HANGING; else if (aTextBaseline.EqualsLiteral("middle")) CurrentState().textBaseline = TextBaseline::MIDDLE; else if (aTextBaseline.EqualsLiteral("alphabetic")) CurrentState().textBaseline = TextBaseline::ALPHABETIC; else if (aTextBaseline.EqualsLiteral("ideographic")) CurrentState().textBaseline = TextBaseline::IDEOGRAPHIC; else if (aTextBaseline.EqualsLiteral("bottom")) CurrentState().textBaseline = TextBaseline::BOTTOM; } void CanvasRenderingContext2D::GetTextBaseline(nsAString& aTextBaseline) { switch (CurrentState().textBaseline) { case TextBaseline::TOP: aTextBaseline.AssignLiteral("top"); break; case TextBaseline::HANGING: aTextBaseline.AssignLiteral("hanging"); break; case TextBaseline::MIDDLE: aTextBaseline.AssignLiteral("middle"); break; case TextBaseline::ALPHABETIC: aTextBaseline.AssignLiteral("alphabetic"); break; case TextBaseline::IDEOGRAPHIC: aTextBaseline.AssignLiteral("ideographic"); break; case TextBaseline::BOTTOM: aTextBaseline.AssignLiteral("bottom"); break; } } /* * 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). * @param str The string whose whitespace characters to replace. */ static inline void TextReplaceWhitespaceCharacters(nsAutoString& aStr) { aStr.ReplaceChar("\x09\x0A\x0B\x0C\x0D", char16_t(' ')); } void CanvasRenderingContext2D::FillText(const nsAString& aText, double aX, double aY, const Optional& aMaxWidth, ErrorResult& aError) { DebugOnly metrics = DrawOrMeasureText( aText, aX, aY, aMaxWidth, TextDrawOperation::FILL, aError); MOZ_ASSERT(!metrics); // 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); // drawing operation never returns TextMetrics } TextMetrics* CanvasRenderingContext2D::MeasureText(const nsAString& aRawText, ErrorResult& aError) { Optional maxWidth; return DrawOrMeasureText(aRawText, 0, 0, maxWidth, TextDrawOperation::MEASURE, aError); } void CanvasRenderingContext2D::AddHitRegion(const HitRegionOptions& aOptions, ErrorResult& aError) { RefPtr path; if (aOptions.mPath) { EnsureTarget(); if (!IsTargetValid()) { return; } path = aOptions.mPath->GetPath(CanvasWindingRule::Nonzero, mTarget); } if (!path) { // check if the path is valid EnsureUserSpacePath(CanvasWindingRule::Nonzero); path = mPath; } if (!path) { return aError.ThrowNotSupportedError("Invalid path"); } // get the bounds of the current path. They are relative to the canvas gfx::Rect bounds(path->GetBounds(mTarget->GetTransform())); if ((bounds.width == 0) || (bounds.height == 0) || !bounds.IsFinite()) { return aError.ThrowNotSupportedError("The specified region has no pixels"); } // remove old hit region first RemoveHitRegion(aOptions.mId); if (aOptions.mControl) { // also remove regions with this control for (size_t x = 0; x < mHitRegionsOptions.Length(); x++) { RegionInfo& info = mHitRegionsOptions[x]; if (info.mElement == aOptions.mControl) { mHitRegionsOptions.RemoveElementAt(x); break; } } #ifdef ACCESSIBILITY aOptions.mControl->SetProperty(nsGkAtoms::hitregion, reinterpret_cast(true)); #endif } // finally, add the region to the list RegionInfo info; info.mId = aOptions.mId; info.mElement = aOptions.mControl; RefPtr pathBuilder = path->TransformedCopyToBuilder(mTarget->GetTransform()); info.mPath = pathBuilder->Finish(); mHitRegionsOptions.InsertElementAt(0, info); } void CanvasRenderingContext2D::RemoveHitRegion(const nsAString& aId) { if (aId.Length() == 0) { return; } for (size_t x = 0; x < mHitRegionsOptions.Length(); x++) { RegionInfo& info = mHitRegionsOptions[x]; if (info.mId == aId) { mHitRegionsOptions.RemoveElementAt(x); return; } } } void CanvasRenderingContext2D::ClearHitRegions() { mHitRegionsOptions.Clear(); } bool CanvasRenderingContext2D::GetHitRegionRect(Element* aElement, nsRect& aRect) { for (unsigned int x = 0; x < mHitRegionsOptions.Length(); x++) { RegionInfo& info = mHitRegionsOptions[x]; if (info.mElement == aElement) { gfx::Rect bounds(info.mPath->GetBounds()); gfxRect rect(bounds.x, bounds.y, bounds.width, bounds.height); aRect = nsLayoutUtils::RoundGfxRectToAppRect(rect, AppUnitsPerCSSPixel()); return true; } } return false; } /** * Used for nsBidiPresUtils::ProcessText */ struct MOZ_STACK_CLASS CanvasBidiProcessor : public nsBidiPresUtils::BidiProcessor { typedef CanvasRenderingContext2D::Style Style; CanvasBidiProcessor() : nsBidiPresUtils::BidiProcessor(), mCtx(nullptr), mFontgrp(nullptr), mAppUnitsPerDevPixel(0), mOp(CanvasRenderingContext2D::TextDrawOperation::FILL), mTextRunFlags(), mDoMeasureBoundingBox(false) { if (Preferences::GetBool(GFX_MISSING_FONTS_NOTIFY_PREF)) { mMissingFonts = MakeUnique(); } } ~CanvasBidiProcessor() { // notify front-end code if we encountered missing glyphs in any script if (mMissingFonts) { mMissingFonts->Flush(); } } typedef CanvasRenderingContext2D::ContextState ContextState; virtual void SetText(const char16_t* aText, int32_t aLength, nsBidiDirection aDirection) override { mFontgrp->UpdateUserFonts(); // ensure user font generation is current // adjust flags for current direction run gfx::ShapedTextFlags flags = mTextRunFlags; if (aDirection == NSBIDI_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()); } virtual nscoord GetWidth() override { gfxTextRun::Metrics textRunMetrics = mTextRun->MeasureText( mDoMeasureBoundingBox ? gfxFont::TIGHT_INK_EXTENTS : gfxFont::LOOSE_INK_EXTENTS, mDrawTarget); // 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(); } virtual void DrawText(nscoord aXOffset, nscoord aWidth) override { gfx::Point point = mPt; bool rtl = mTextRun->IsRightToLeft(); bool verticalRun = mTextRun->IsVertical(); RefPtr pattern; float& inlineCoord = verticalRun ? point.y : point.x; inlineCoord += aXOffset; // 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); 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; AdjustedTarget target(mCtx); if (!target) { return; } RefPtr thebes = gfxContext::CreatePreservingTransformOrNull(target); if (!thebes) { // If CreatePreservingTransformOrNull returns null, it will also have // issued a gfxCriticalNote already, so here we'll just bail out. return; } gfxTextRun::DrawParams params(thebes); const ContextState* state = &mCtx->CurrentState(); 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 = mCtx->UsedOperation(); if (!mCtx->IsTargetValid()) { return; } state = &mCtx->CurrentState(); params.drawOpts = &drawOpts; if (style == Style::STROKE) { strokeOpts.mLineWidth = state->lineWidth; strokeOpts.mLineJoin = state->lineJoin; strokeOpts.mLineCap = 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; // position of the left side of the string, alphabetic baseline gfx::Point mPt; // current font gfxFontGroup* mFontgrp; // 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; // operation (fill or stroke) CanvasRenderingContext2D::TextDrawOperation mOp; // 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; // true iff the bounding box should be measured bool mDoMeasureBoundingBox; }; TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText( const nsAString& aRawText, float aX, float aY, const Optional& aMaxWidth, TextDrawOperation aOp, ErrorResult& aError) { // Approximated baselines. In an ideal world, we'd read the baseline info // directly from the font (where available). Alas we currently lack // that functionality. These numbers are best guesses and should // suffice for now. Both are fractions of the em ascent/descent from the // alphabetic baseline. const double kHangingBaselineDefault = 0.8; // fraction of ascent const double kIdeographicBaselineDefault = 0.5; // fraction of descent if (!mCanvasElement && !mDocShell) { NS_WARNING( "Canvas element must be non-null or a docshell must be provided"); aError = NS_ERROR_FAILURE; return nullptr; } RefPtr presShell = GetPresShell(); if (NS_WARN_IF(!presShell)) { aError = NS_ERROR_FAILURE; return nullptr; } Document* document = presShell->GetDocument(); // replace all the whitespace characters with U+0020 SPACE nsAutoString textToDraw(aRawText); 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 || IsNaN(aMaxWidth.Value()))) { textToDraw.Truncate(); } // for now, default to ltr if not in doc bool isRTL = false; RefPtr canvasStyle; if (mCanvasElement && mCanvasElement->IsInComposedDoc()) { // try to find the closest context canvasStyle = nsComputedDOMStyle::GetComputedStyle(mCanvasElement, nullptr); if (!canvasStyle) { aError = NS_ERROR_FAILURE; return nullptr; } isRTL = canvasStyle->StyleVisibility()->mDirection == StyleDirection::Rtl; } else { isRTL = GET_BIDI_OPTION_DIRECTION(document->GetBidiOptions()) == IBMBIDI_TEXTDIRECTION_RTL; } // This is only needed to know if we can know the drawing bounding box easily. const bool doCalculateBounds = NeedToCalculateBounds(); if (presShell->IsDestroying()) { aError = NS_ERROR_FAILURE; return nullptr; } gfxFontGroup* currentFontStyle = GetCurrentFontStyle(); if (!currentFontStyle) { aError = NS_ERROR_FAILURE; return nullptr; } MOZ_ASSERT(!presShell->IsDestroying(), "GetCurrentFontStyle() should have returned null if the presshell " "is being destroyed"); nsPresContext* presContext = presShell->GetPresContext(); // 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 new TextMetrics(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 (!IsFinite(aX) || !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 new TextMetrics(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; // 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(); GetAppUnitsValues(&processor.mAppUnitsPerDevPixel, nullptr); processor.mPt = gfx::Point(aX, aY); processor.mDrawTarget = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget(); // 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; nscoord totalWidthCoord; // 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 ? NSBIDI_RTL : NSBIDI_LTR, presShell->GetPresContext(), processor, nsBidiPresUtils::MODE_MEASURE, nullptr, 0, &totalWidthCoord, &mBidiEngine); if (aError.Failed()) { return nullptr; } float totalWidth = float(totalWidthCoord) / processor.mAppUnitsPerDevPixel; // offset pt.x based on text align gfxFloat anchorX; const ContextState& state = CurrentState(); if (state.textAlign == TextAlign::CENTER) { anchorX = .5; } else if (state.textAlign == TextAlign::LEFT || (!isRTL && state.textAlign == TextAlign::START) || (isRTL && state.textAlign == TextAlign::END)) { anchorX = 0; } else { anchorX = 1; } float offsetX = anchorX * totalWidth; processor.mPt.x -= offsetX; // offset pt.y (or pt.x, for vertical text) based on text baseline processor.mFontgrp ->UpdateUserFonts(); // ensure user font generation is current const gfxFont::Metrics& fontMetrics = processor.mFontgrp->GetFirstValidFont()->GetMetrics( nsFontMetrics::eHorizontal); gfxFloat baselineAnchor; switch (state.textBaseline) { case TextBaseline::HANGING: baselineAnchor = fontMetrics.emAscent * kHangingBaselineDefault; break; case TextBaseline::TOP: baselineAnchor = fontMetrics.emAscent; break; case TextBaseline::MIDDLE: baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f; break; case TextBaseline::ALPHABETIC: baselineAnchor = 0; break; case TextBaseline::IDEOGRAPHIC: baselineAnchor = -fontMetrics.emDescent * kIdeographicBaselineDefault; break; case TextBaseline::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. gfx::ShapedTextFlags runOrientation = (processor.mTextRunFlags & gfx::ShapedTextFlags::TEXT_ORIENT_MASK); if (runOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL) { if (runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED || runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT) { // 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; double hangingBaseline = fontMetrics.emAscent * kHangingBaselineDefault - baselineAnchor; double ideographicBaseline = -fontMetrics.emDescent * kIdeographicBaselineDefault - baselineAnchor; return new TextMetrics( totalWidth, actualBoundingBoxLeft, actualBoundingBoxRight, fontMetrics.maxAscent - baselineAnchor, // fontBBAscent fontMetrics.maxDescent + baselineAnchor, // fontBBDescent actualBoundingBoxAscent, actualBoundingBoxDescent, fontMetrics.emAscent - baselineAnchor, // emHeightAscent -fontMetrics.emDescent - baselineAnchor, // emHeightDescent hangingBaseline, -baselineAnchor, // alphabeticBaseline ideographicBaseline); } // If we did not actually calculate bounds, set up a simple bounding box // based on the text position and advance. if (!doCalculateBounds) { processor.mBoundingBox.width = totalWidth; processor.mBoundingBox.MoveBy(gfxPoint(processor.mPt.x, processor.mPt.y)); } processor.mPt.x *= processor.mAppUnitsPerDevPixel; processor.mPt.y *= processor.mAppUnitsPerDevPixel; EnsureTarget(); if (!IsTargetValid()) { aError = NS_ERROR_FAILURE; return nullptr; } Matrix oldTransform = mTarget->GetTransform(); // 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); } // 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 ? NSBIDI_RTL : NSBIDI_LTR, presShell->GetPresContext(), processor, nsBidiPresUtils::MODE_DRAW, nullptr, 0, nullptr, &mBidiEngine); if (aError.Failed()) { return nullptr; } mTarget->SetTransform(oldTransform); if (aOp == CanvasRenderingContext2D::TextDrawOperation::FILL && !doCalculateBounds) { RedrawUser(boundingBox); } else { Redraw(); } aError = NS_OK; return nullptr; } gfxFontGroup* CanvasRenderingContext2D::GetCurrentFontStyle() { // use lazy initilization for the font group since it's rather expensive if (!CurrentState().fontGroup) { ErrorResult err; constexpr auto kDefaultFontStyle = "10px sans-serif"_ns; static float kDefaultFontSize = 10.0; RefPtr presShell = GetPresShell(); bool fontUpdated = SetFontInternal(kDefaultFontStyle, 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; gfxTextPerfMetrics* tp = nullptr; FontMatchingStats* fontStats = nullptr; if (presShell && !presShell->IsDestroying()) { tp = presShell->GetPresContext()->GetTextPerfMetrics(); fontStats = presShell->GetPresContext()->GetFontMatchingStats(); } int32_t perDevPixel, perCSSPixel; GetAppUnitsValues(&perDevPixel, &perCSSPixel); gfxFloat devToCssSize = gfxFloat(perDevPixel) / gfxFloat(perCSSPixel); CurrentState().fontGroup = gfxPlatform::GetPlatform()->CreateFontGroup( FontFamilyList(StyleGenericFontFamily::SansSerif), &style, language, explicitLanguage, tp, fontStats, nullptr, devToCssSize); if (CurrentState().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. CurrentState().fontGroup->CheckForUpdatedPlatformList(); } return CurrentState().fontGroup; } // // line caps/joins // void CanvasRenderingContext2D::SetLineCap(const nsAString& aLinecapStyle) { CapStyle cap; if (aLinecapStyle.EqualsLiteral("butt")) { cap = CapStyle::BUTT; } else if (aLinecapStyle.EqualsLiteral("round")) { cap = CapStyle::ROUND; } else if (aLinecapStyle.EqualsLiteral("square")) { cap = CapStyle::SQUARE; } else { // XXX ERRMSG we need to report an error to developers here! (bug 329026) return; } CurrentState().lineCap = cap; } void CanvasRenderingContext2D::GetLineCap(nsAString& aLinecapStyle) { switch (CurrentState().lineCap) { case CapStyle::BUTT: aLinecapStyle.AssignLiteral("butt"); break; case CapStyle::ROUND: aLinecapStyle.AssignLiteral("round"); break; case CapStyle::SQUARE: aLinecapStyle.AssignLiteral("square"); break; } } void CanvasRenderingContext2D::SetLineJoin(const nsAString& aLinejoinStyle) { JoinStyle j; if (aLinejoinStyle.EqualsLiteral("round")) { j = JoinStyle::ROUND; } else if (aLinejoinStyle.EqualsLiteral("bevel")) { j = JoinStyle::BEVEL; } else if (aLinejoinStyle.EqualsLiteral("miter")) { j = JoinStyle::MITER_OR_BEVEL; } else { // XXX ERRMSG we need to report an error to developers here! (bug 329026) return; } CurrentState().lineJoin = j; } void CanvasRenderingContext2D::GetLineJoin(nsAString& aLinejoinStyle, ErrorResult& aError) { switch (CurrentState().lineJoin) { case JoinStyle::ROUND: aLinejoinStyle.AssignLiteral("round"); break; case JoinStyle::BEVEL: aLinejoinStyle.AssignLiteral("bevel"); break; case JoinStyle::MITER_OR_BEVEL: aLinejoinStyle.AssignLiteral("miter"); break; default: aError.Throw(NS_ERROR_FAILURE); } } 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; } } EnsureUserSpacePath(aWinding); if (!mPath) { return false; } if (mPathTransformWillUpdate) { return mPath->ContainsPoint(Point(aX, aY), mPathToDS); } return mPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform()); } bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding, nsIPrincipal&) { 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; } } EnsureUserSpacePath(); if (!mPath) { return false; } const ContextState& state = CurrentState(); StrokeOptions strokeOptions(state.lineWidth, state.lineJoin, state.lineCap, state.miterLimit, state.dash.Length(), state.dash.Elements(), state.dashOffset); if (mPathTransformWillUpdate) { return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mPathToDS); } return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mTarget->GetTransform()); } bool CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx, const CanvasPath& aPath, double aX, double aY, nsIPrincipal&) { 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, state.lineJoin, 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(); } 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, int32_t aImageSize, double& aDestCoord, double& aDestSize) { double scale = aDestSize / aSourceSize; if (aSourceCoord < 0.0) { double destEnd = aDestCoord + aDestSize; aDestCoord -= aSourceCoord * scale; aDestSize = destEnd - aDestCoord; aSourceSize += aSourceCoord; aSourceCoord = 0.0; } double delta = aImageSize - (aSourceCoord + aSourceSize); if (delta < 0.0) { aDestSize += delta * scale; aSourceSize = aImageSize - aSourceCoord; } } // 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); if (!res.mSourceSurface) { return res; } int32_t corsmode = imgIRequest::CORS_NONE; if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) { res.mCORSUsed = corsmode != imgIRequest::CORS_NONE; } 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; } // 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; Element* element = nullptr; EnsureTarget(); if (!IsTargetValid()) { return; } if (aImage.IsHTMLCanvasElement()) { HTMLCanvasElement* canvas = &aImage.GetAsHTMLCanvasElement(); element = canvas; nsIntSize size = canvas->GetSize(); if (size.width == 0 || size.height == 0) { return aError.ThrowInvalidStateError("Passed-in canvas is empty"); } if (canvas->IsWriteOnly()) { SetWriteOnly(); } } else if (aImage.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.IsHTMLImageElement()) { HTMLImageElement* img = &aImage.GetAsHTMLImageElement(); element = img; } else if (aImage.IsSVGImageElement()) { SVGImageElement* img = &aImage.GetAsSVGImageElement(); element = img; } else { HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement(); video->MarkAsContentSource( mozilla::dom::HTMLVideoElement::CallerAPI::DRAW_IMAGE); element = video; } srcSurf = CanvasImageCache::LookupCanvas(element, mCanvasElement, &imgSize, &intrinsicImgSize); } 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; SurfaceFromElementResult res = CanvasRenderingContext2D::CachedSurfaceFromElement(element); if (!res.mSourceSurface) { res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget); } if (!res.mSourceSurface && !res.mDrawInfo.mImgContainer) { // 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) { aError.ThrowInvalidStateError("Passed-in image is \"broken\""); } return; } imgSize = res.mSize; intrinsicImgSize = res.mIntrinsicSize; if (mCanvasElement) { CanvasUtils::DoDrawImageSecurityCheck(mCanvasElement, res.mPrincipal, res.mIsWriteOnly, res.mCORSUsed); } if (res.mSourceSurface) { if (res.mImageRequest) { CanvasImageCache::NotifyDrawImage(element, mCanvasElement, res.mSourceSurface, imgSize, intrinsicImgSize); } srcSurf = res.mSourceSurface; } else { drawInfo = res.mDrawInfo; } } if (aOptional_argc == 0) { aSx = aSy = 0.0; aSw = (double)imgSize.width; aSh = (double)imgSize.height; aDw = (double)intrinsicImgSize.width; aDh = (double)intrinsicImgSize.height; } else if (aOptional_argc == 2) { aSx = aSy = 0.0; aSw = (double)imgSize.width; aSh = (double)imgSize.height; } if (aSw == 0.0 || aSh == 0.0) { return; } ClipImageDimension(aSx, aSw, imgSize.width, aDx, aDw); ClipImageDimension(aSy, aSh, imgSize.height, 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.width) < aSw && aDh * Abs(scale.height) < 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 == mCanvasElement) { // 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); if (!tempTarget) { gfxDevCrash(LogReason::InvalidDrawTarget) << "Invalid adjusted target in Canvas2D " << gfx::hexa((DrawTarget*)mTarget) << ", " << NeedToDrawShadow() << NeedToApplyFilter(); return; } auto op = UsedOperation(); if (!IsTargetValid() || !tempTarget) { return; } tempTarget->DrawSurface( srcSurf, gfx::Rect(aDx, aDy, aDw, aDh), sourceRect, DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED), DrawOptions(CurrentState().globalAlpha, op, antialiasMode)); } 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) { return; } // Get any existing transforms on the context, including transformations used // for context shadow. Matrix matrix = tempTarget->GetTransform(); gfxMatrix contextMatrix = ThebesMatrix(matrix); gfxSize contextScale(contextMatrix.ScaleFactors()); // Scale the dest rect to include the context scale. aDest.Scale(contextScale.width, contextScale.height); // Scale the image size to the dest rect, and adjust the source rect to match. gfxSize scale(aDest.width / aSrc.width, aDest.height / aSrc.height); IntSize scaledImageSize = IntSize::Ceil(aImgSize.width * scale.width, aImgSize.height * scale.height); aSrc.Scale(scale.width, scale.height); // 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); RefPtr context = gfxContext::CreateOrNull(tempTarget); if (!context) { gfxDevCrash(LogReason::InvalidContext) << "Canvas context problem"; return; } context->SetMatrixDouble( contextMatrix .PreScale(1.0 / contextScale.width, 1.0 / contextScale.height) .PreTranslate(aDest.x - aSrc.x, aDest.y - aSrc.y)); context->SetOp(UsedOperation()); // FLAG_CLAMP is added for increased performance, since we never tile here. uint32_t modifiedFlags = aImage.mDrawingFlags | imgIContainer::FLAG_CLAMP; CSSIntSize sz( scaledImageSize.width, scaledImageSize .height); // XXX hmm is scaledImageSize really in CSS pixels? 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, Some(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("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("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, 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; } // Flush layout updates if (!(aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DO_NOT_FLUSH)) { nsContentUtils::FlushLayoutForTree(aWindow.GetOuterWindow()); } CompositionOp op = UsedOperation(); bool discardContent = GlobalAlpha() == 1.0f && (op == CompositionOp::OP_OVER || op == CompositionOp::OP_SOURCE); const gfx::Rect drawRect(aX, aY, aW, aH); EnsureTarget(discardContent ? &drawRect : nullptr); if (!IsTargetValid()) { return; } RefPtr presContext; nsIDocShell* docshell = aWindow.GetDocShell(); if (docshell) { presContext = docshell->GetPresContext(); } if (!presContext) { aError.Throw(NS_ERROR_FAILURE); return; } nscolor backgroundColor; if (!ParseColor(aBgColor, &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; } RefPtr 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 = UsedOperation(); if (!IsTargetValid()) { aError.Throw(NS_ERROR_FAILURE); return; } } if (op == CompositionOp::OP_OVER && (!mBufferProvider || (mBufferProvider->GetType() != LayersBackend::LAYERS_CLIENT && mBufferProvider->GetType() != LayersBackend::LAYERS_WR))) { thebes = gfxContext::CreateOrNull(mTarget); MOZ_ASSERT(thebes); // already checked the draw target above // (in SupportsAzureContentForDrawTarget) thebes->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 = gfxContext::CreateOrNull(drawDT); MOZ_ASSERT(thebes); // alrady checked the draw target above thebes->SetMatrix(Matrix::Scaling(matrix._11, matrix._22)); } RefPtr presShell = presContext->PresShell(); Unused << presShell->RenderDocument(r, renderDocFlags, backgroundColor, thebes); // If this canvas was contained in the drawn window, the pre-transaction // callback may have returned its DT. If so, we must reacquire it here. EnsureTarget(discardContent ? &drawRect : nullptr); if (drawDT) { RefPtr snapshot = drawDT->Snapshot(); if (NS_WARN_IF(!snapshot)) { aError.Throw(NS_ERROR_FAILURE); return; } op = UsedOperation(); 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) { 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(aCx))) { // 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); } 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); CheckedInt len = CheckedInt(aWidth) * aHeight * 4; if (!len.isValid()) { 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 srcRect(0, 0, mWidth, mHeight); IntRect destRect(aX, aY, aWidth, aHeight); IntRect srcReadRect = srcRect.Intersect(destRect); if (srcReadRect.IsEmpty()) { *aRetval = darray; return NS_OK; } if (!mBufferProvider) { if (!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()); DataSourceSurface::MappedSurface rawData; if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) { return NS_ERROR_OUT_OF_MEMORY; } IntRect dstWriteRect = srcReadRect; dstWriteRect.MoveBy(-aX, -aY); // Check for site-specific permission. This check is not needed if the // canvas was created with a docshell (that is only done for special // internal uses). bool usePlaceholder = false; if (mCanvasElement) { nsCOMPtr ownerDoc = mCanvasElement->OwnerDoc(); usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx, aSubjectPrincipal); } do { uint8_t* randomData; if (usePlaceholder) { // 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(); } JS::AutoCheckCannotGC nogc; bool isShared; uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc); MOZ_ASSERT(!isShared); // Should not happen, data was created above if (usePlaceholder) { 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) { return; } RefPtr errorTarget = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget( IntSize(1, 1), SurfaceFormat::B8G8R8A8); MOZ_ASSERT(errorTarget, "Failed to allocate the error target!"); sErrorTarget = errorTarget; NS_ADDREF(sErrorTarget); } void CanvasRenderingContext2D::FillRuleChanged() { if (mPath) { mPathBuilder = mPath->CopyToBuilder(CurrentState().fillRule); mPath = nullptr; } } void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx, int32_t aDy, ErrorResult& aError) { RootedSpiderMonkeyInterface arr(RootingCx()); DebugOnly inited = arr.Init(aImageData.GetDataObject()); MOZ_ASSERT(inited); PutImageData_explicit(aDx, aDy, aImageData.Width(), aImageData.Height(), &arr, false, 0, 0, 0, 0, aError); } void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx, int32_t aDy, int32_t aDirtyX, int32_t aDirtyY, int32_t aDirtyWidth, int32_t aDirtyHeight, ErrorResult& aError) { RootedSpiderMonkeyInterface arr(RootingCx()); DebugOnly inited = arr.Init(aImageData.GetDataObject()); MOZ_ASSERT(inited); PutImageData_explicit(aDx, aDy, aImageData.Width(), aImageData.Height(), &arr, true, aDirtyX, aDirtyY, aDirtyWidth, aDirtyHeight, aError); } void CanvasRenderingContext2D::PutImageData_explicit( int32_t aX, int32_t aY, uint32_t aW, uint32_t aH, dom::Uint8ClampedArray* aArray, bool aHasDirtyRect, int32_t aDirtyX, int32_t aDirtyY, int32_t aDirtyWidth, int32_t aDirtyHeight, ErrorResult& aRv) { if (aW == 0 || aH == 0) { return aRv.ThrowInvalidStateError("Passed-in image is empty"); } IntRect dirtyRect; IntRect imageDataRect(0, 0, aW, aH); 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; } dirtyRect.MoveBy(IntPoint(aX, aY)); dirtyRect = IntRect(0, 0, mWidth, mHeight).Intersect(dirtyRect); if (dirtyRect.Width() <= 0 || dirtyRect.Height() <= 0) { return; } aArray->ComputeState(); uint32_t dataLen = aArray->Length(); uint32_t len = aW * aH * 4; if (dataLen != len) { return aRv.ThrowInvalidStateError("Invalid width or height"); } // The canvas spec says that the current path, transformation matrix, shadow // attributes, global alpha, the clipping region, and global composition // operator must not affect the getImageData() and putImageData() methods. const gfx::Rect putRect(dirtyRect); EnsureTarget(&putRect); if (!IsTargetValid()) { return aRv.Throw(NS_ERROR_FAILURE); } DataSourceSurface::MappedSurface map; RefPtr sourceSurface; uint8_t* lockedBits = nullptr; 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(); } IntRect srcRect = dirtyRect - IntPoint(aX, aY); uint8_t* srcData = aArray->Data() + srcRect.y * (aW * 4) + srcRect.x * 4; PremultiplyData( srcData, aW * 4, SurfaceFormat::R8G8B8A8, dstData, dstStride, mOpaque ? SurfaceFormat::X8R8G8B8_UINT32 : SurfaceFormat::A8R8G8B8_UINT32, dirtyRect.Size()); 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; CheckedInt len = CheckedInt(aW) * aH * 4; if (!len.isValid()) { 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()); if (!darray) { // TODO: Should use OOMReporter. aError.Throw(NS_ERROR_OUT_OF_MEMORY); 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::OnBeforePaintTransaction() { if (!mTarget) return; OnStableState(); } void CanvasRenderingContext2D::OnDidPaintTransaction() { MarkContextClean(); } already_AddRefed CanvasRenderingContext2D::GetCanvasLayer( nsDisplayListBuilder* aBuilder, Layer* aOldLayer, LayerManager* aManager) { 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(); return nullptr; } if (!mResetLayer && aOldLayer) { RefPtr ret = aOldLayer; return ret.forget(); } RefPtr canvasLayer = aManager->CreateCanvasLayer(); if (!canvasLayer) { NS_WARNING("CreateCanvasLayer returned null!"); // No DidTransactionCallback will be received, so mark the context clean // now so future invalidations will be dispatched. MarkContextClean(); return nullptr; } const auto canvasRenderer = canvasLayer->CreateOrGetCanvasRenderer(); InitializeCanvasRenderer(aBuilder, canvasRenderer); uint32_t flags = mOpaque ? Layer::CONTENT_OPAQUE : 0; canvasLayer->SetContentFlags(flags); mResetLayer = false; return canvasLayer.forget(); } 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 = mSharedPtrPtr; 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 = mSharedPtrPtr; 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::MarkContextCleanForFrameCapture() { mIsCapturedFrameInvalid = false; } bool CanvasRenderingContext2D::IsContextCleanForFrameCapture() { return !mIsCapturedFrameInvalid; } bool CanvasRenderingContext2D::ShouldForceInactiveLayer( LayerManager* aManager) { return !aManager->CanUseCanvasLayerForSize(GetSize()); } 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(); } } NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasPath, AddRef) NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasPath, Release) NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPath, mParent) CanvasPath::CanvasPath(nsISupports* aParent) : mParent(aParent) { mPathBuilder = gfxPlatform::GetPlatform() ->ScreenReferenceDrawTarget() ->CreatePathBuilder(); } CanvasPath::CanvasPath(nsISupports* aParent, already_AddRefed aPathBuilder) : mParent(aParent), mPathBuilder(aPathBuilder) { if (!mPathBuilder) { mPathBuilder = gfxPlatform::GetPlatform() ->ScreenReferenceDrawTarget() ->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 tempPath = aCanvasPath.GetPath( CanvasWindingRule::Nonzero, gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget().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(); } void CanvasPath::MoveTo(double aX, double aY) { EnsurePathBuilder(); mPathBuilder->MoveTo(Point(ToFloat(aX), ToFloat(aY))); } void CanvasPath::LineTo(double aX, double aY) { EnsurePathBuilder(); mPathBuilder->LineTo(Point(ToFloat(aX), ToFloat(aY))); } void CanvasPath::QuadraticCurveTo(double aCpx, double aCpy, double aX, double aY) { EnsurePathBuilder(); mPathBuilder->QuadraticBezierTo(gfx::Point(ToFloat(aCpx), ToFloat(aCpy)), gfx::Point(ToFloat(aX), ToFloat(aY))); } 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); // 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.x, p1.y); return; } // Check for colinearity dir = (p2.x - p1.x) * (p0.y - p1.y) + (p2.y - p1.y) * (p1.x - p0.x); if (dir == 0) { LineTo(p1.x, p1.y); 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) { MoveTo(aX, aY); LineTo(aX + aW, aY); LineTo(aX + aW, aY + aH); LineTo(aX, aY + aH); ClosePath(); } 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"); } EnsurePathBuilder(); ArcToBezier(this, Point(aX, aY), Size(aRadius, aRadius), aStartAngle, aEndAngle, aAnticlockwise); } 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); } void CanvasPath::LineTo(const gfx::Point& aPoint) { EnsurePathBuilder(); mPathBuilder->LineTo(aPoint); } void CanvasPath::BezierTo(const gfx::Point& aCP1, const gfx::Point& aCP2, const gfx::Point& aCP3) { EnsurePathBuilder(); mPathBuilder->BezierTo(aCP1, aCP2, aCP3); } void CanvasPath::AddPath(CanvasPath& aCanvasPath, const DOMMatrix2DInit& aInit, ErrorResult& aError) { RefPtr tempPath = aCanvasPath.GetPath( CanvasWindingRule::Nonzero, gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget().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 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); 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) { int32_t width = aContext->GetWidth(); int32_t height = aContext->GetHeight(); // 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(width) * height * 4; if (!bytes.isValid()) { return 0; } return bytes.value(); } } // namespace mozilla::dom