blob: 82c81a723fde82cba8029ec9fa76c3db991d6e46 [file] [log] [blame]
/*
* Copyright (C) 2017-2025 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "OffscreenCanvas.h"
#if ENABLE(OFFSCREEN_CANVAS)
#include "BitmapImage.h"
#include "CSSValuePool.h"
#include "CanvasRenderingContext.h"
#include "Chrome.h"
#include "Document.h"
#include "EventDispatcher.h"
#include "GPU.h"
#include "GPUCanvasContext.h"
#include "HTMLCanvasElement.h"
#include "ImageBitmap.h"
#include "ImageBitmapRenderingContext.h"
#include "ImageData.h"
#include "JSBlob.h"
#include "JSDOMPromiseDeferred.h"
#include "MIMETypeRegistry.h"
#include "OffscreenCanvasRenderingContext2D.h"
#include "Page.h"
#include "PlaceholderRenderingContext.h"
#include "ScriptTrackingPrivacyCategory.h"
#include "WorkerClient.h"
#include "WorkerGlobalScope.h"
#include "WorkerNavigator.h"
#include <wtf/TZoneMallocInlines.h>
#if ENABLE(WEBGL)
#include "Settings.h"
#include "WebGLRenderingContext.h"
#include "WebGL2RenderingContext.h"
#endif // ENABLE(WEBGL)
#if HAVE(WEBGPU_IMPLEMENTATION)
#include "LocalDomWindow.h"
#include "Navigator.h"
#endif
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(DetachedOffscreenCanvas);
WTF_MAKE_TZONE_OR_ISO_ALLOCATED_IMPL(OffscreenCanvas);
DetachedOffscreenCanvas::DetachedOffscreenCanvas(const IntSize& size, bool originClean, RefPtr<PlaceholderRenderingContextSource>&& placeholderSource)
: m_placeholderSource(WTFMove(placeholderSource))
, m_size(size)
, m_originClean(originClean)
{
}
DetachedOffscreenCanvas::~DetachedOffscreenCanvas() = default;
RefPtr<PlaceholderRenderingContextSource> DetachedOffscreenCanvas::takePlaceholderSource()
{
return WTFMove(m_placeholderSource);
}
bool OffscreenCanvas::enabledForContext(ScriptExecutionContext& context)
{
UNUSED_PARAM(context);
#if ENABLE(OFFSCREEN_CANVAS_IN_WORKERS)
if (context.isWorkerGlobalScope())
return context.settingsValues().offscreenCanvasInWorkersEnabled;
#endif
ASSERT(context.isDocument());
return true;
}
Ref<OffscreenCanvas> OffscreenCanvas::create(ScriptExecutionContext& scriptExecutionContext, unsigned width, unsigned height)
{
auto canvas = adoptRef(*new OffscreenCanvas(scriptExecutionContext, { static_cast<int>(width), static_cast<int>(height) }, nullptr));
canvas->suspendIfNeeded();
return canvas;
}
Ref<OffscreenCanvas> OffscreenCanvas::create(ScriptExecutionContext& scriptExecutionContext, std::unique_ptr<DetachedOffscreenCanvas>&& detachedCanvas)
{
Ref<OffscreenCanvas> clone = adoptRef(*new OffscreenCanvas(scriptExecutionContext, detachedCanvas->size(), detachedCanvas->takePlaceholderSource()));
if (!detachedCanvas->originClean())
clone->setOriginTainted();
clone->suspendIfNeeded();
return clone;
}
Ref<OffscreenCanvas> OffscreenCanvas::create(ScriptExecutionContext& scriptExecutionContext, PlaceholderRenderingContext& placeholder)
{
auto offscreen = adoptRef(*new OffscreenCanvas(scriptExecutionContext, placeholder.size(), &placeholder.source()));
offscreen->suspendIfNeeded();
return offscreen;
}
OffscreenCanvas::OffscreenCanvas(ScriptExecutionContext& scriptExecutionContext, IntSize size, RefPtr<PlaceholderRenderingContextSource>&& placeholderSource)
: ActiveDOMObject(&scriptExecutionContext)
, CanvasBase(WTFMove(size), scriptExecutionContext)
, m_placeholderSource(WTFMove(placeholderSource))
{
}
OffscreenCanvas::~OffscreenCanvas()
{
notifyObserversCanvasDestroyed();
removeCanvasNeedingPreparationForDisplayOrFlush();
m_context = nullptr; // Ensure this goes away before the ImageBuffer.
setImageBuffer(nullptr);
}
void OffscreenCanvas::setWidth(unsigned newWidth)
{
if (m_detached)
return;
setSize(IntSize(newWidth, height()));
}
void OffscreenCanvas::setHeight(unsigned newHeight)
{
if (m_detached)
return;
setSize(IntSize(width(), newHeight));
}
void OffscreenCanvas::setSize(const IntSize& newSize)
{
CanvasBase::setSize(newSize);
reset();
if (RefPtr context = dynamicDowncast<GPUBasedCanvasRenderingContext>(m_context.get()))
context->reshape();
}
#if ENABLE(WEBGL)
static bool requiresAcceleratedCompositingForWebGL()
{
#if PLATFORM(GTK) || PLATFORM(WIN)
return false;
#else
return true;
#endif
}
static bool shouldEnableWebGL(const SettingsValues& settings, bool isWorker)
{
if (!settings.webGLEnabled)
return false;
if (!settings.allowWebGLInWorkers)
return false;
#if PLATFORM(IOS_FAMILY) || PLATFORM(MAC)
if (isWorker && !settings.useGPUProcessForWebGLEnabled)
return false;
#else
UNUSED_PARAM(isWorker);
#endif
if (!requiresAcceleratedCompositingForWebGL())
return true;
return settings.acceleratedCompositingEnabled;
}
#endif // ENABLE(WEBGL)
ExceptionOr<std::optional<OffscreenRenderingContext>> OffscreenCanvas::getContext(JSC::JSGlobalObject& state, RenderingContextType contextType, FixedVector<JSC::Strong<JSC::Unknown>>&& arguments)
{
if (m_detached)
return Exception { ExceptionCode::InvalidStateError };
if (contextType == RenderingContextType::_2d) {
if (!m_context) {
auto scope = DECLARE_THROW_SCOPE(state.vm());
auto settings = convert<IDLDictionary<CanvasRenderingContext2DSettings>>(state, arguments.isEmpty() ? JSC::jsUndefined() : (arguments[0].isObject() ? arguments[0].get() : JSC::jsNull()));
if (settings.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
m_context = OffscreenCanvasRenderingContext2D::create(*this, settings.releaseReturnValue());
}
if (RefPtr context = dynamicDowncast<OffscreenCanvasRenderingContext2D>(m_context.get()))
return { { WTFMove(context) } };
return { { std::nullopt } };
}
if (contextType == RenderingContextType::Bitmaprenderer) {
if (!m_context) {
auto scope = DECLARE_THROW_SCOPE(state.vm());
auto settings = convert<IDLDictionary<ImageBitmapRenderingContextSettings>>(state, arguments.isEmpty() ? JSC::jsUndefined() : (arguments[0].isObject() ? arguments[0].get() : JSC::jsNull()));
if (settings.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
m_context = ImageBitmapRenderingContext::create(*this, settings.releaseReturnValue());
downcast<ImageBitmapRenderingContext>(m_context.get())->transferFromImageBitmap(nullptr);
}
if (RefPtr context = dynamicDowncast<ImageBitmapRenderingContext>(m_context.get()))
return { { WTFMove(context) } };
return { { std::nullopt } };
}
if (contextType == RenderingContextType::Webgpu) {
#if HAVE(WEBGPU_IMPLEMENTATION)
if (!m_context) {
auto scope = DECLARE_THROW_SCOPE(state.vm());
RETURN_IF_EXCEPTION(scope, Exception { ExceptionCode::ExistingExceptionError });
Ref scriptExecutionContext = *this->scriptExecutionContext();
if (RefPtr globalScope = dynamicDowncast<WorkerGlobalScope>(scriptExecutionContext)) {
if (auto* gpu = globalScope->protectedNavigator()->gpu())
m_context = GPUCanvasContext::create(*this, *gpu, nullptr);
} else if (RefPtr document = dynamicDowncast<Document>(scriptExecutionContext)) {
if (RefPtr window = document->window()) {
if (auto* gpu = window->protectedNavigator()->gpu())
m_context = GPUCanvasContext::create(*this, *gpu, document.get());
}
}
}
if (RefPtr context = dynamicDowncast<GPUCanvasContext>(m_context.get()))
return { { WTFMove(context) } };
#endif
return { { std::nullopt } };
}
#if ENABLE(WEBGL)
if (contextType == RenderingContextType::Webgl || contextType == RenderingContextType::Webgl2) {
auto webGLVersion = contextType == RenderingContextType::Webgl ? WebGLVersion::WebGL1 : WebGLVersion::WebGL2;
if (!m_context) {
auto scope = DECLARE_THROW_SCOPE(state.vm());
auto attributes = convert<IDLDictionary<WebGLContextAttributes>>(state, arguments.isEmpty() ? JSC::jsUndefined() : (arguments[0].isObject() ? arguments[0].get() : JSC::jsNull()));
if (attributes.hasException(scope)) [[unlikely]]
return Exception { ExceptionCode::ExistingExceptionError };
auto* scriptExecutionContext = this->scriptExecutionContext();
if (shouldEnableWebGL(scriptExecutionContext->settingsValues(), is<WorkerGlobalScope>(scriptExecutionContext)))
m_context = WebGLRenderingContextBase::create(*this, attributes.releaseReturnValue(), webGLVersion);
}
if (webGLVersion == WebGLVersion::WebGL1) {
if (RefPtr context = dynamicDowncast<WebGLRenderingContext>(m_context.get()))
return { { WTFMove(context) } };
} else {
if (RefPtr context = dynamicDowncast<WebGL2RenderingContext>(m_context.get()))
return { { WTFMove(context) } };
}
return { { std::nullopt } };
}
#endif
return Exception { ExceptionCode::TypeError };
}
ExceptionOr<RefPtr<ImageBitmap>> OffscreenCanvas::transferToImageBitmap()
{
if (m_detached || !m_context)
return Exception { ExceptionCode::InvalidStateError };
if (size().isEmpty())
return { RefPtr<ImageBitmap> { nullptr } };
clearCopiedImage();
RefPtr buffer = m_context->transferToImageBuffer();
if (!buffer)
return Exception { ExceptionCode::UnknownError }; // UnknownError is used for DOM out-of-memory.
return { ImageBitmap::create(buffer.releaseNonNull(), originClean()) };
}
static String toEncodingMimeType(const String& mimeType)
{
if (!MIMETypeRegistry::isSupportedImageMIMETypeForEncoding(mimeType))
return "image/png"_s;
return mimeType.convertToASCIILowercase();
}
static std::optional<double> qualityFromDouble(double qualityNumber)
{
if (!(qualityNumber >= 0 && qualityNumber <= 1))
return std::nullopt;
return qualityNumber;
}
void OffscreenCanvas::convertToBlob(ImageEncodeOptions&& options, Ref<DeferredPromise>&& promise)
{
if (!originClean()) {
promise->reject(ExceptionCode::SecurityError);
return;
}
if (m_detached) {
promise->reject(ExceptionCode::InvalidStateError);
return;
}
if (size().isEmpty()) {
promise->reject(ExceptionCode::IndexSizeError);
return;
}
auto encodingMIMEType = toEncodingMimeType(options.type);
auto quality = qualityFromDouble(options.quality);
RefPtr context = canvasBaseScriptExecutionContext();
if (context && context->requiresScriptTrackingPrivacyProtection(ScriptTrackingPrivacyCategory::Canvas)) {
RefPtr buffer = createImageForNoiseInjection();
auto blobData = buffer->toData(encodingMIMEType, quality);
if (blobData.isEmpty())
promise->reject(ExceptionCode::EncodingError);
else
promise->resolveWithNewlyCreated<IDLInterface<Blob>>(Blob::create(context.get(), WTFMove(blobData), encodingMIMEType));
return;
}
RefPtr buffer = makeRenderingResultsAvailable();
if (!buffer) {
promise->reject(ExceptionCode::InvalidStateError);
return;
}
Vector<uint8_t> blobData = buffer->toData(encodingMIMEType, quality);
if (blobData.isEmpty()) {
promise->reject(ExceptionCode::EncodingError);
return;
}
Ref<Blob> blob = Blob::create(context.get(), WTFMove(blobData), encodingMIMEType);
promise->resolveWithNewlyCreated<IDLInterface<Blob>>(WTFMove(blob));
}
void OffscreenCanvas::didDraw(const std::optional<FloatRect>& rect, ShouldApplyPostProcessingToDirtyRect shouldApplyPostProcessingToDirtyRect)
{
clearCopiedImage();
scheduleCommitToPlaceholderCanvas();
CanvasBase::didDraw(rect, shouldApplyPostProcessingToDirtyRect);
}
Image* OffscreenCanvas::copiedImage() const
{
if (m_detached)
return nullptr;
if (!m_copiedImage) {
RefPtr buffer = const_cast<OffscreenCanvas*>(this)->makeRenderingResultsAvailable(ShouldApplyPostProcessingToDirtyRect::No);
if (buffer)
m_copiedImage = BitmapImage::create(buffer->copyNativeImage());
}
return m_copiedImage.get();
}
void OffscreenCanvas::clearCopiedImage() const
{
m_copiedImage = nullptr;
}
SecurityOrigin* OffscreenCanvas::securityOrigin() const
{
auto& scriptExecutionContext = *canvasBaseScriptExecutionContext();
if (auto* globalScope = dynamicDowncast<WorkerGlobalScope>(scriptExecutionContext))
return &globalScope->topOrigin();
return &downcast<Document>(scriptExecutionContext).securityOrigin();
}
bool OffscreenCanvas::canDetach() const
{
return !m_detached && !m_context;
}
std::unique_ptr<DetachedOffscreenCanvas> OffscreenCanvas::detach()
{
if (!canDetach())
return nullptr;
removeCanvasNeedingPreparationForDisplayOrFlush();
m_detached = true;
auto detached = makeUnique<DetachedOffscreenCanvas>(size(), originClean(), WTFMove(m_placeholderSource));
setSize(IntSize(0, 0));
return detached;
}
void OffscreenCanvas::commitToPlaceholderCanvas()
{
if (!m_placeholderSource)
return;
if (!m_context)
return;
if (m_context->compositingResultsNeedUpdating())
m_context->prepareForDisplay();
RefPtr imageBuffer = m_context->surfaceBufferToImageBuffer(CanvasRenderingContext::SurfaceBuffer::DisplayBuffer);
if (!imageBuffer)
return;
m_placeholderSource->setPlaceholderBuffer(*imageBuffer, m_context->canvasBase().originClean(), m_context->isOpaque());
}
void OffscreenCanvas::scheduleCommitToPlaceholderCanvas()
{
RefPtr scriptContext = scriptExecutionContext();
if (scriptContext && !m_hasScheduledCommit && m_placeholderSource) {
m_hasScheduledCommit = true;
scriptContext->postTask([protectedThis = Ref { *this }, this] (ScriptExecutionContext&) {
m_hasScheduledCommit = false;
commitToPlaceholderCanvas();
});
}
}
void OffscreenCanvas::createImageBuffer() const
{
const_cast<OffscreenCanvas*>(this)->setHasCreatedImageBuffer(true);
setImageBuffer(allocateImageBuffer());
}
void OffscreenCanvas::setImageBufferAndMarkDirty(RefPtr<ImageBuffer>&& buffer)
{
setHasCreatedImageBuffer(true);
setImageBuffer(WTFMove(buffer));
CanvasBase::didDraw(FloatRect(FloatPoint(), size()));
}
void OffscreenCanvas::reset()
{
resetGraphicsContextState();
if (RefPtr context = dynamicDowncast<OffscreenCanvasRenderingContext2D>(m_context.get()))
context->reset();
setHasCreatedImageBuffer(false);
setImageBuffer(nullptr);
clearCopiedImage();
notifyObserversCanvasResized();
scheduleCommitToPlaceholderCanvas();
}
void OffscreenCanvas::queueTaskKeepingObjectAlive(TaskSource source, Function<void(CanvasBase&)>&& task)
{
ActiveDOMObject::queueTaskKeepingObjectAlive(*this, source, [task = WTFMove(task)](auto& canvas) mutable {
task(canvas);
});
}
void OffscreenCanvas::dispatchEvent(Event& event)
{
EventDispatcher::dispatchEvent(std::initializer_list<EventTarget*>({ this }), event);
}
std::unique_ptr<CSSParserContext> OffscreenCanvas::createCSSParserContext() const
{
// FIXME: Rather than using a default CSSParserContext, there should be one exposed via ScriptExecutionContext.
return makeUnique<CSSParserContext>(HTMLStandardMode);
}
}
#endif