| /* |
| * Copyright (C) 2017 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. ``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 |
| * 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 "CanvasBase.h" |
| |
| #include "CanvasRenderingContext.h" |
| #include "Chrome.h" |
| #include "Document.h" |
| #include "Element.h" |
| #include "GraphicsClient.h" |
| #include "GraphicsContext.h" |
| #include "HTMLCanvasElement.h" |
| #include "HostWindow.h" |
| #include "ImageBuffer.h" |
| #include "InspectorInstrumentation.h" |
| #include "IntRect.h" |
| #include "NativeImage.h" |
| #include "NoiseInjectionPolicy.h" |
| #include "RenderElementInlines.h" |
| #include "ScriptTrackingPrivacyCategory.h" |
| #include "StyleCanvasImage.h" |
| #include "StyleComputedStyle+GettersInlines.h" |
| #include "TypedArrayPixelBuffer.h" |
| #include "WebCoreOpaqueRoot.h" |
| #include "WorkerClient.h" |
| #include "WorkerGlobalScope.h" |
| #include <JavaScriptCore/JSCInlines.h> |
| #include <JavaScriptCore/JSLock.h> |
| #include <atomic> |
| #include <wtf/Vector.h> |
| #include <wtf/WeakRandom.h> |
| #include <wtf/text/MakeString.h> |
| |
| namespace WebCore { |
| |
| static std::optional<size_t> maxCanvasAreaForTesting; |
| |
| static std::optional<uint64_t> canvasNoiseHashSaltIfNeeded(ScriptExecutionContext& context) |
| { |
| auto policies = context.noiseInjectionPolicies(); |
| if (policies.contains(NoiseInjectionPolicy::Minimal) |
| || (policies.contains(NoiseInjectionPolicy::Enhanced) && context.requiresScriptTrackingPrivacyProtection(ScriptTrackingPrivacyCategory::Canvas))) |
| return context.noiseInjectionHashSalt(); |
| return { }; |
| } |
| |
| CanvasDisplayBufferObserver::~CanvasDisplayBufferObserver() = default; |
| |
| CanvasBase::CanvasBase(IntSize size, ScriptExecutionContext& context) |
| : m_size { size } |
| , m_canvasNoiseHashSalt { canvasNoiseHashSaltIfNeeded(context) } |
| { |
| } |
| |
| CanvasBase::~CanvasBase() |
| { |
| ASSERT(m_didNotifyObserversCanvasDestroyed); |
| ASSERT(m_observers.isEmptyIgnoringNullReferences()); |
| m_canvasNoiseHashSalt = std::nullopt; |
| } |
| |
| RefPtr<ImageBuffer> CanvasBase::makeRenderingResultsAvailable(ShouldApplyPostProcessingToDirtyRect shouldApplyPostProcessingToDirtyRect) |
| { |
| if (RefPtr context = renderingContext()) { |
| RefPtr buffer = context->surfaceBufferToImageBuffer(CanvasRenderingContext::SurfaceBuffer::DrawingBuffer); |
| #if ASSERT_ENABLED && HAVE(IOSURFACE) |
| if (RefPtr scriptExecutionContext = canvasBaseScriptExecutionContext()) |
| ASSERT(!(scriptExecutionContext->isWorkerGlobalScope() && buffer && buffer->surface() && !buffer->isRemoteImageBufferProxy()), "Worker OffscreenCanvas is backed by a local IOSurface"); |
| #endif |
| if (m_canvasNoiseHashSalt && shouldApplyPostProcessingToDirtyRect == ShouldApplyPostProcessingToDirtyRect::Yes) |
| m_canvasNoiseInjection.postProcessDirtyCanvasBuffer(buffer.get(), *m_canvasNoiseHashSalt, context->is2d() ? CanvasNoiseInjectionPostProcessArea::DirtyRect : CanvasNoiseInjectionPostProcessArea::FullBuffer); |
| return buffer; |
| } |
| if (!validateArea()) |
| return nullptr; |
| // Transparent black bitmaps are not cached. |
| return createTransparentBlackImageBuffer(); |
| } |
| |
| RefPtr<ImageBuffer> CanvasBase::createTransparentBlackImageBuffer() const |
| { |
| if (!validateArea()) |
| return nullptr; |
| return ImageBuffer::create(size(), RenderingMode::Unaccelerated, RenderingPurpose::Unspecified, 1, ColorSpace::SRGB(), PixelFormat::BGRA8); |
| } |
| |
| RefPtr<NativeImage> CanvasBase::copyNativeImage() const |
| { |
| if (RefPtr context = renderingContext()) |
| return context->surfaceBufferToNativeImage(CanvasRenderingContext::SurfaceBuffer::DrawingBuffer); |
| if (!validateArea()) |
| return nullptr; |
| return ImageBuffer::sinkIntoNativeImage(ImageBuffer::create(size(), RenderingMode::Unaccelerated, RenderingPurpose::Unspecified, 1, ColorSpace::SRGB(), PixelFormat::BGRA8)); |
| } |
| |
| static inline size_t NODELETE maxCanvasArea() |
| { |
| if (maxCanvasAreaForTesting) |
| return *maxCanvasAreaForTesting; |
| |
| // Firefox limits width/height to 32767 pixels, but slows down dramatically before it |
| // reaches that limit. We limit by area instead, giving us larger maximum dimensions, |
| // in exchange for a smaller maximum canvas size. The maximum canvas size is in device pixels. |
| #if PLATFORM(IOS_FAMILY) |
| return 8192 * 8192; |
| #else |
| return 16384 * 16384; |
| #endif |
| } |
| |
| void CanvasBase::setMaxCanvasAreaForTesting(std::optional<size_t> size) |
| { |
| maxCanvasAreaForTesting = size; |
| } |
| |
| void CanvasBase::addObserver(CanvasObserver& observer) |
| { |
| m_observers.add(observer); |
| |
| if (is<Style::CanvasImage>(observer)) |
| InspectorInstrumentation::didChangeCSSCanvasClientNodes(*this); |
| } |
| |
| void CanvasBase::removeObserver(CanvasObserver& observer) |
| { |
| m_observers.remove(observer); |
| |
| if (is<Style::CanvasImage>(observer)) |
| InspectorInstrumentation::didChangeCSSCanvasClientNodes(*this); |
| } |
| |
| bool CanvasBase::hasObserver(CanvasObserver& observer) const |
| { |
| return m_observers.contains(observer); |
| } |
| |
| void CanvasBase::notifyObserversContentsWillChange(const FloatRect& rect) |
| { |
| for (CheckedRef observer : m_observers) |
| observer->canvasContentsWillChange(*this, rect); |
| } |
| |
| void CanvasBase::willUpdateContents(const std::optional<FloatRect>& rect, ShouldApplyPostProcessingToDirtyRect shouldApplyPostProcessingToDirtyRect) |
| { |
| addCanvasNeedingPreparationForDisplayOrFlush(); |
| IntRect dirtyRect { { }, size() }; |
| if (rect) |
| dirtyRect.intersect(enclosingIntRect(*rect)); |
| notifyObserversContentsWillChange(dirtyRect); |
| |
| // FIXME: We should exclude rects with ShouldApplyPostProcessingToDirtyRect::No |
| if (shouldInjectNoiseBeforeReadback()) { |
| if (shouldApplyPostProcessingToDirtyRect == ShouldApplyPostProcessingToDirtyRect::Yes) { |
| m_canvasNoiseInjection.updateDirtyRect(dirtyRect); |
| } else if (!rect) |
| m_canvasNoiseInjection.clearDirtyRect(); |
| } |
| } |
| |
| void CanvasBase::notifyObserversCanvasResized() |
| { |
| for (CheckedRef observer : m_observers) |
| observer->canvasResized(*this); |
| } |
| |
| void CanvasBase::notifyObserversCanvasDestroyed() |
| { |
| ASSERT(!m_didNotifyObserversCanvasDestroyed); |
| |
| for (CheckedRef observer : std::exchange(m_observers, WeakHashSet<CanvasObserver>())) |
| observer->canvasDestroyed(*this); |
| |
| #if ASSERT_ENABLED |
| m_didNotifyObserversCanvasDestroyed = true; |
| #endif |
| } |
| |
| void CanvasBase::addDisplayBufferObserver(CanvasDisplayBufferObserver& observer) |
| { |
| m_displayBufferObservers.add(observer); |
| } |
| |
| void CanvasBase::removeDisplayBufferObserver(CanvasDisplayBufferObserver& observer) |
| { |
| m_displayBufferObservers.remove(observer); |
| } |
| |
| void CanvasBase::notifyObserversCanvasDisplayBufferPrepared() |
| { |
| for (Ref observer : m_displayBufferObservers) |
| observer->canvasDisplayBufferPrepared(*this); |
| } |
| |
| HashSet<Ref<Element>> CanvasBase::cssCanvasClients() const |
| { |
| HashSet<Ref<Element>> cssCanvasClients; |
| for (CheckedRef observer : m_observers) { |
| RefPtr image = dynamicDowncast<Style::CanvasImage>(observer.get()); |
| if (!image) |
| continue; |
| |
| for (auto entry : image->clients()) { |
| CheckedRef client = entry.key; |
| if (RefPtr element = client->element()) |
| cssCanvasClients.add(element.releaseNonNull()); |
| } |
| } |
| return cssCanvasClients; |
| } |
| |
| bool CanvasBase::hasActiveInspectorCanvasCallTracer() const |
| { |
| RefPtr context = renderingContext(); |
| return context && context->hasActiveInspectorCanvasCallTracer(); |
| } |
| |
| void CanvasBase::setSize(const IntSize& size) |
| { |
| if (size == m_size) |
| return; |
| |
| m_size = size; |
| m_hasWarnedExceedsArea = false; |
| |
| if (RefPtr context = renderingContext()) |
| InspectorInstrumentation::didChangeCanvasSize(*context); |
| } |
| |
| bool CanvasBase::shouldAccelerate() const |
| { |
| size_t area = size().unclampedArea(); |
| RefPtr scriptExecutionContext = this->scriptExecutionContext(); |
| #if USE(CA) || USE(SKIA) |
| if (!scriptExecutionContext->settingsValues().canvasUsesAcceleratedDrawing) |
| return false; |
| if (area < scriptExecutionContext->settingsValues().minimumAccelerated2DContextArea) |
| return false; |
| #if PLATFORM(GTK) || ENABLE(WPE_PLATFORM) |
| if (!scriptExecutionContext->settingsValues().hardwareAccelerationEnabled) |
| return false; |
| #endif |
| return true; |
| #else |
| UNUSED_PARAM(area); |
| return false; |
| #endif |
| } |
| |
| bool CanvasBase::validateArea() const |
| { |
| size_t area = size().unclampedArea(); |
| if (!area) |
| return false; |
| if (area > maxCanvasArea()) { |
| if (!m_hasWarnedExceedsArea) { |
| if (RefPtr scriptExecutionContext = this->scriptExecutionContext()) { |
| auto message = makeString("Canvas area exceeds the maximum limit (width * height > "_s, maxCanvasArea(), ")."_s); |
| scriptExecutionContext->addConsoleMessage(MessageSource::JS, MessageLevel::Warning, message); |
| } |
| m_hasWarnedExceedsArea = true; |
| } |
| return false; |
| } |
| return true; |
| } |
| |
| bool CanvasBase::shouldInjectNoiseBeforeReadback() const |
| { |
| // Note, every early-return resulting from this check potentially leaks this state. This is a risk that we're accepting right now. |
| return !!m_canvasNoiseHashSalt; |
| } |
| |
| void CanvasBase::recordLastFillText(const String& text) |
| { |
| if (!shouldInjectNoiseBeforeReadback()) |
| return; |
| m_lastFillText = text; |
| } |
| |
| void CanvasBase::addCanvasNeedingPreparationForDisplayOrFlush() |
| { |
| RefPtr context = renderingContext(); |
| if (!context) |
| return; |
| if (context->isInPreparationForDisplayOrFlush()) |
| return; |
| if (RefPtr document = dynamicDowncast<Document>(scriptExecutionContext())) |
| document->addCanvasNeedingPreparationForDisplayOrFlush(*context); |
| // FIXME: WorkerGlobalContext does not have prepare phase yet. |
| } |
| |
| void CanvasBase::removeCanvasNeedingPreparationForDisplayOrFlush() |
| { |
| RefPtr context = renderingContext(); |
| if (!context) |
| return; |
| if (!context->isInPreparationForDisplayOrFlush()) |
| return; |
| if (RefPtr document = dynamicDowncast<Document>(scriptExecutionContext())) |
| document->removeCanvasNeedingPreparationForDisplayOrFlush(*context); |
| // FIXME: WorkerGlobalContext does not have prepare phase yet. |
| } |
| |
| bool CanvasBase::postProcessPixelBufferResults(PixelBuffer& pixelBuffer) const |
| { |
| if (m_canvasNoiseHashSalt) |
| return m_canvasNoiseInjection.postProcessPixelBufferResults(pixelBuffer, *m_canvasNoiseHashSalt); |
| return false; |
| } |
| |
| RefPtr<ImageBuffer> CanvasBase::createImageForNoiseInjection() const |
| { |
| RefPtr context = canvasBaseScriptExecutionContext(); |
| if (!context) |
| return { }; |
| |
| auto seed = static_cast<unsigned>(context->noiseInjectionHashSalt().value_or(0)); |
| auto buffer = ImageBuffer::create(size(), RenderingMode::Unaccelerated, RenderingPurpose::Unspecified, 1, ColorSpace::SRGB(), PixelFormat::BGRA8); |
| if (!buffer) |
| return { }; |
| |
| auto componentValues = WeakRandom { seed }.getUint32(); |
| auto fillColor = SRGBA<uint8_t> { |
| static_cast<uint8_t>((componentValues >> 24) & 0xFF), |
| static_cast<uint8_t>((componentValues >> 16) & 0xFF), |
| static_cast<uint8_t>((componentValues >> 8) & 0xFF), |
| static_cast<uint8_t>(componentValues & 0xFF), |
| }; |
| buffer->context().setFillColor(fillColor); |
| buffer->context().fillRect({ IntPoint { }, size() }); |
| return buffer; |
| } |
| |
| WebCoreOpaqueRoot root(CanvasBase* canvas) |
| { |
| return WebCoreOpaqueRoot { canvas }; |
| } |
| |
| } // namespace WebCore |