/* -*- 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 "CanvasRenderingContextHelper.h" #include "GLContext.h" #include "ImageBitmapRenderingContext.h" #include "ImageEncoder.h" #include "mozilla/dom/BlobImpl.h" #include "mozilla/dom/CanvasRenderingContext2D.h" #include "mozilla/dom/OffscreenCanvasRenderingContext2D.h" #include "mozilla/GfxMessageUtils.h" #include "mozilla/Telemetry.h" #include "mozilla/UniquePtr.h" #include "mozilla/webgpu/CanvasContext.h" #include "MozFramebuffer.h" #include "nsContentUtils.h" #include "nsDOMJSUtils.h" #include "nsIScriptContext.h" #include "nsJSUtils.h" #include "ClientWebGLContext.h" namespace mozilla::dom { CanvasRenderingContextHelper::CanvasRenderingContextHelper() : mCurrentContextType(CanvasContextType::NoContext) {} void CanvasRenderingContextHelper::ToBlob( JSContext* aCx, nsIGlobalObject* aGlobal, BlobCallback& aCallback, const nsAString& aType, JS::Handle aParams, bool aUsePlaceholder, ErrorResult& aRv) { // Encoder callback when encoding is complete. class EncodeCallback : public EncodeCompleteCallback { public: EncodeCallback(nsIGlobalObject* aGlobal, BlobCallback* aCallback) : mGlobal(aGlobal), mBlobCallback(aCallback) {} // This is called on main thread. MOZ_CAN_RUN_SCRIPT nsresult ReceiveBlobImpl(already_AddRefed aBlobImpl) override { MOZ_ASSERT(NS_IsMainThread()); RefPtr blobImpl = aBlobImpl; RefPtr blob; if (blobImpl) { blob = Blob::Create(mGlobal, blobImpl); } RefPtr callback(std::move(mBlobCallback)); ErrorResult rv; callback->Call(blob, rv); mGlobal = nullptr; MOZ_ASSERT(!mBlobCallback); return rv.StealNSResult(); } bool CanBeDeletedOnAnyThread() override { // EncodeCallback is used from the main thread only. return false; } nsCOMPtr mGlobal; RefPtr mBlobCallback; }; RefPtr callback = new EncodeCallback(aGlobal, &aCallback); ToBlob(aCx, callback, aType, aParams, aUsePlaceholder, aRv); } void CanvasRenderingContextHelper::ToBlob( JSContext* aCx, EncodeCompleteCallback* aCallback, const nsAString& aType, JS::Handle aParams, bool aUsePlaceholder, ErrorResult& aRv) { nsAutoString type; nsContentUtils::ASCIIToLower(aType, type); nsAutoString params; bool usingCustomParseOptions; aRv = ParseParams(aCx, type, aParams, params, &usingCustomParseOptions); if (aRv.Failed()) { return; } ToBlob(aCallback, type, params, usingCustomParseOptions, aUsePlaceholder, aRv); } void CanvasRenderingContextHelper::ToBlob(EncodeCompleteCallback* aCallback, nsAString& aType, const nsAString& aEncodeOptions, bool aUsingCustomOptions, bool aUsePlaceholder, ErrorResult& aRv) { const nsIntSize elementSize = GetWidthHeight(); if (mCurrentContext) { // We disallow canvases of width or height zero, and set them to 1, so // we will have a discrepancy with the sizes of the canvas and the context. // That discrepancy is OK, the rest are not. if ((elementSize.width != mCurrentContext->GetWidth() && (elementSize.width != 0 || mCurrentContext->GetWidth() != 1)) || (elementSize.height != mCurrentContext->GetHeight() && (elementSize.height != 0 || mCurrentContext->GetHeight() != 1))) { aRv.Throw(NS_ERROR_FAILURE); return; } } UniquePtr imageBuffer; int32_t format = 0; auto imageSize = gfx::IntSize{elementSize.width, elementSize.height}; if (mCurrentContext) { imageBuffer = mCurrentContext->GetImageBuffer(&format, &imageSize); } RefPtr callback = aCallback; aRv = ImageEncoder::ExtractDataAsync( aType, aEncodeOptions, aUsingCustomOptions, std::move(imageBuffer), format, {imageSize.width, imageSize.height}, aUsePlaceholder, callback); } already_AddRefed CanvasRenderingContextHelper::CreateContext(CanvasContextType aContextType) { return CreateContextHelper(aContextType, layers::LayersBackend::LAYERS_NONE); } already_AddRefed CanvasRenderingContextHelper::CreateContextHelper( CanvasContextType aContextType, layers::LayersBackend aCompositorBackend) { MOZ_ASSERT(aContextType != CanvasContextType::NoContext); RefPtr ret; switch (aContextType) { case CanvasContextType::NoContext: break; case CanvasContextType::Canvas2D: Telemetry::Accumulate(Telemetry::CANVAS_2D_USED, 1); ret = new CanvasRenderingContext2D(aCompositorBackend); break; case CanvasContextType::OffscreenCanvas2D: Telemetry::Accumulate(Telemetry::CANVAS_2D_USED, 1); ret = new OffscreenCanvasRenderingContext2D(aCompositorBackend); break; case CanvasContextType::WebGL1: Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_USED, 1); ret = new ClientWebGLContext(/*webgl2:*/ false); break; case CanvasContextType::WebGL2: Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_USED, 1); ret = new ClientWebGLContext(/*webgl2:*/ true); break; case CanvasContextType::WebGPU: // TODO // Telemetry::Accumulate(Telemetry::CANVAS_WEBGPU_USED, 1); ret = new webgpu::CanvasContext(); break; case CanvasContextType::ImageBitmap: ret = new ImageBitmapRenderingContext(); break; } MOZ_ASSERT(ret); ret->Initialize(); return ret.forget(); } already_AddRefed CanvasRenderingContextHelper::GetOrCreateContext( JSContext* aCx, const nsAString& aContextId, JS::Handle aContextOptions, ErrorResult& aRv) { CanvasContextType contextType; if (!CanvasUtils::GetCanvasContextType(aContextId, &contextType)) return nullptr; return GetOrCreateContext(aCx, contextType, aContextOptions, aRv); } already_AddRefed CanvasRenderingContextHelper::GetOrCreateContext( JSContext* aCx, CanvasContextType aContextType, JS::Handle aContextOptions, ErrorResult& aRv) { if (!mCurrentContext) { // This canvas doesn't have a context yet. RefPtr context; context = CreateContext(aContextType); if (!context) { return nullptr; } // Ensure that the context participates in CC. Note that returning a // CC participant from QI doesn't addref. nsXPCOMCycleCollectionParticipant* cp = nullptr; CallQueryInterface(context, &cp); if (!cp) { aRv.Throw(NS_ERROR_FAILURE); return nullptr; } mCurrentContext = std::move(context); mCurrentContextType = aContextType; // https://html.spec.whatwg.org/multipage/canvas.html#dom-canvas-getcontext-dev // Step 1. If options is not an object, then set options to null. JS::Rooted options(RootingCx(), aContextOptions); if (!options.isObject()) { options.setNull(); } nsresult rv = UpdateContext(aCx, options, aRv); if (NS_FAILED(rv)) { // See bug 645792 and bug 1215072. // We want to throw only if dictionary initialization fails, // so only in case aRv has been set to some error value. if (aContextType == CanvasContextType::WebGL1) { Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_SUCCESS, 0); } else if (aContextType == CanvasContextType::WebGL2) { Telemetry::Accumulate(Telemetry::CANVAS_WEBGL2_SUCCESS, 0); } else if (aContextType == CanvasContextType::WebGPU) { // Telemetry::Accumulate(Telemetry::CANVAS_WEBGPU_SUCCESS, 0); } return nullptr; } if (aContextType == CanvasContextType::WebGL1) { Telemetry::Accumulate(Telemetry::CANVAS_WEBGL_SUCCESS, 1); } else if (aContextType == CanvasContextType::WebGL2) { Telemetry::Accumulate(Telemetry::CANVAS_WEBGL2_SUCCESS, 1); } else if (aContextType == CanvasContextType::WebGPU) { // Telemetry::Accumulate(Telemetry::CANVAS_WEBGPU_SUCCESS, 1); } } else { // We already have a context of some type. if (aContextType != mCurrentContextType) return nullptr; } nsCOMPtr context = mCurrentContext; return context.forget(); } nsresult CanvasRenderingContextHelper::UpdateContext( JSContext* aCx, JS::Handle aNewContextOptions, ErrorResult& aRvForDictionaryInit) { if (!mCurrentContext) return NS_OK; nsIntSize sz = GetWidthHeight(); nsCOMPtr currentContext = mCurrentContext; currentContext->SetOpaqueValueFromOpaqueAttr(GetOpaqueAttr()); nsresult rv = currentContext->SetContextOptions(aCx, aNewContextOptions, aRvForDictionaryInit); if (NS_FAILED(rv)) { mCurrentContext = nullptr; return rv; } rv = currentContext->SetDimensions(sz.width, sz.height); if (NS_FAILED(rv)) { mCurrentContext = nullptr; } return rv; } nsresult CanvasRenderingContextHelper::ParseParams( JSContext* aCx, const nsAString& aType, const JS::Value& aEncoderOptions, nsAString& outParams, bool* const outUsingCustomParseOptions) { // Quality parameter is only valid for the image/jpeg and image/webp MIME // types. if (aType.EqualsLiteral("image/jpeg") || aType.EqualsLiteral("image/webp")) { if (aEncoderOptions.isNumber()) { double quality = aEncoderOptions.toNumber(); // Quality must be between 0.0 and 1.0, inclusive if (quality >= 0.0 && quality <= 1.0) { outParams.AppendLiteral("quality="); outParams.AppendInt(NS_lround(quality * 100.0)); } } } // If we haven't parsed the aParams check for proprietary options. // The proprietary option -moz-parse-options will take a image lib encoder // parse options string as is and pass it to the encoder. *outUsingCustomParseOptions = false; if (outParams.Length() == 0 && aEncoderOptions.isString()) { constexpr auto mozParseOptions = u"-moz-parse-options:"_ns; nsAutoJSString paramString; if (!paramString.init(aCx, aEncoderOptions.toString())) { return NS_ERROR_FAILURE; } if (StringBeginsWith(paramString, mozParseOptions)) { nsDependentSubstring parseOptions = Substring(paramString, mozParseOptions.Length(), paramString.Length() - mozParseOptions.Length()); outParams.Append(parseOptions); *outUsingCustomParseOptions = true; } } return NS_OK; } } // namespace mozilla::dom