1 | /* |
2 | * Copyright (C) 2004-2017 Apple Inc. All rights reserved. |
3 | * Copyright (C) 2007 Alp Toker <alp@atoker.com> |
4 | * Copyright (C) 2010 Torch Mobile (Beijing) Co. Ltd. All rights reserved. |
5 | * |
6 | * Redistribution and use in source and binary forms, with or without |
7 | * modification, are permitted provided that the following conditions |
8 | * are met: |
9 | * 1. Redistributions of source code must retain the above copyright |
10 | * notice, this list of conditions and the following disclaimer. |
11 | * 2. Redistributions in binary form must reproduce the above copyright |
12 | * notice, this list of conditions and the following disclaimer in the |
13 | * documentation and/or other materials provided with the distribution. |
14 | * |
15 | * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
16 | * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
18 | * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
19 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
20 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
21 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
22 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
23 | * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
24 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
25 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
26 | */ |
27 | |
28 | #include "config.h" |
29 | #include "HTMLCanvasElement.h" |
30 | |
31 | #include "Blob.h" |
32 | #include "BlobCallback.h" |
33 | #include "CanvasGradient.h" |
34 | #include "CanvasPattern.h" |
35 | #include "CanvasRenderingContext2D.h" |
36 | #include "Document.h" |
37 | #include "Frame.h" |
38 | #include "FrameLoaderClient.h" |
39 | #include "GPUBasedCanvasRenderingContext.h" |
40 | #include "GeometryUtilities.h" |
41 | #include "GraphicsContext.h" |
42 | #include "HTMLNames.h" |
43 | #include "HTMLParserIdioms.h" |
44 | #include "ImageBitmapRenderingContext.h" |
45 | #include "ImageBuffer.h" |
46 | #include "ImageData.h" |
47 | #include "InspectorInstrumentation.h" |
48 | #include "JSDOMConvertDictionary.h" |
49 | #include "MIMETypeRegistry.h" |
50 | #include "RenderElement.h" |
51 | #include "RenderHTMLCanvas.h" |
52 | #include "ResourceLoadObserver.h" |
53 | #include "RuntimeEnabledFeatures.h" |
54 | #include "ScriptController.h" |
55 | #include "Settings.h" |
56 | #include "StringAdaptors.h" |
57 | #include <JavaScriptCore/JSCInlines.h> |
58 | #include <JavaScriptCore/JSLock.h> |
59 | #include <math.h> |
60 | #include <wtf/IsoMallocInlines.h> |
61 | #include <wtf/RAMSize.h> |
62 | #include <wtf/text/StringBuilder.h> |
63 | |
64 | #if ENABLE(MEDIA_STREAM) |
65 | #include "CanvasCaptureMediaStreamTrack.h" |
66 | #include "MediaStream.h" |
67 | #endif |
68 | |
69 | #if ENABLE(WEBGL) |
70 | #include "WebGLContextAttributes.h" |
71 | #include "WebGLRenderingContext.h" |
72 | #endif |
73 | |
74 | #if ENABLE(WEBGL2) |
75 | #include "WebGL2RenderingContext.h" |
76 | #endif |
77 | |
78 | #if ENABLE(WEBGPU) |
79 | #include "GPUCanvasContext.h" |
80 | #endif |
81 | |
82 | #if PLATFORM(COCOA) |
83 | #include "MediaSampleAVFObjC.h" |
84 | #include <pal/cf/CoreMediaSoftLink.h> |
85 | #endif |
86 | |
87 | namespace WebCore { |
88 | |
89 | WTF_MAKE_ISO_ALLOCATED_IMPL(HTMLCanvasElement); |
90 | |
91 | using namespace PAL; |
92 | using namespace HTMLNames; |
93 | |
94 | // These values come from the WhatWG/W3C HTML spec. |
95 | const int defaultWidth = 300; |
96 | const int defaultHeight = 150; |
97 | |
98 | // Firefox limits width/height to 32767 pixels, but slows down dramatically before it |
99 | // reaches that limit. We limit by area instead, giving us larger maximum dimensions, |
100 | // in exchange for a smaller maximum canvas size. The maximum canvas size is in device pixels. |
101 | #if PLATFORM(IOS_FAMILY) |
102 | const unsigned maxCanvasArea = 4096 * 4096; |
103 | #else |
104 | const unsigned maxCanvasArea = 16384 * 16384; |
105 | #endif |
106 | |
107 | #if USE(CG) |
108 | // FIXME: It seems strange that the default quality is not the one that is literally named "default". |
109 | // Should fix names to make this easier to understand, or write an excellent comment here explaining why not. |
110 | const InterpolationQuality defaultInterpolationQuality = InterpolationLow; |
111 | #else |
112 | const InterpolationQuality defaultInterpolationQuality = InterpolationDefault; |
113 | #endif |
114 | |
115 | static size_t activePixelMemory = 0; |
116 | |
117 | HTMLCanvasElement::HTMLCanvasElement(const QualifiedName& tagName, Document& document) |
118 | : HTMLElement(tagName, document) |
119 | , m_size(defaultWidth, defaultHeight) |
120 | { |
121 | ASSERT(hasTagName(canvasTag)); |
122 | } |
123 | |
124 | Ref<HTMLCanvasElement> HTMLCanvasElement::create(Document& document) |
125 | { |
126 | return adoptRef(*new HTMLCanvasElement(canvasTag, document)); |
127 | } |
128 | |
129 | Ref<HTMLCanvasElement> HTMLCanvasElement::create(const QualifiedName& tagName, Document& document) |
130 | { |
131 | return adoptRef(*new HTMLCanvasElement(tagName, document)); |
132 | } |
133 | |
134 | static void removeFromActivePixelMemory(size_t pixelsReleased) |
135 | { |
136 | if (!pixelsReleased) |
137 | return; |
138 | |
139 | if (pixelsReleased < activePixelMemory) |
140 | activePixelMemory -= pixelsReleased; |
141 | else |
142 | activePixelMemory = 0; |
143 | } |
144 | |
145 | HTMLCanvasElement::~HTMLCanvasElement() |
146 | { |
147 | notifyObserversCanvasDestroyed(); |
148 | |
149 | m_context = nullptr; // Ensure this goes away before the ImageBuffer. |
150 | |
151 | releaseImageBufferAndContext(); |
152 | } |
153 | |
154 | void HTMLCanvasElement::parseAttribute(const QualifiedName& name, const AtomicString& value) |
155 | { |
156 | if (name == widthAttr || name == heightAttr) |
157 | reset(); |
158 | HTMLElement::parseAttribute(name, value); |
159 | } |
160 | |
161 | RenderPtr<RenderElement> HTMLCanvasElement::createElementRenderer(RenderStyle&& style, const RenderTreePosition& insertionPosition) |
162 | { |
163 | RefPtr<Frame> frame = document().frame(); |
164 | if (frame && frame->script().canExecuteScripts(NotAboutToExecuteScript)) |
165 | return createRenderer<RenderHTMLCanvas>(*this, WTFMove(style)); |
166 | return HTMLElement::createElementRenderer(WTFMove(style), insertionPosition); |
167 | } |
168 | |
169 | bool HTMLCanvasElement::canContainRangeEndPoint() const |
170 | { |
171 | return false; |
172 | } |
173 | |
174 | bool HTMLCanvasElement::canStartSelection() const |
175 | { |
176 | return false; |
177 | } |
178 | |
179 | ExceptionOr<void> HTMLCanvasElement::setHeight(unsigned value) |
180 | { |
181 | if (m_context && m_context->isPlaceholder()) |
182 | return Exception { InvalidStateError }; |
183 | setAttributeWithoutSynchronization(heightAttr, AtomicString::number(limitToOnlyHTMLNonNegative(value, defaultHeight))); |
184 | return { }; |
185 | } |
186 | |
187 | ExceptionOr<void> HTMLCanvasElement::setWidth(unsigned value) |
188 | { |
189 | if (m_context && m_context->isPlaceholder()) |
190 | return Exception { InvalidStateError }; |
191 | setAttributeWithoutSynchronization(widthAttr, AtomicString::number(limitToOnlyHTMLNonNegative(value, defaultWidth))); |
192 | return { }; |
193 | } |
194 | |
195 | static inline size_t maxActivePixelMemory() |
196 | { |
197 | static size_t maxPixelMemory; |
198 | static std::once_flag onceFlag; |
199 | std::call_once(onceFlag, [] { |
200 | #if PLATFORM(IOS_FAMILY) |
201 | maxPixelMemory = ramSize() / 4; |
202 | #else |
203 | maxPixelMemory = std::max(ramSize() / 4, 2151 * MB); |
204 | #endif |
205 | }); |
206 | return maxPixelMemory; |
207 | } |
208 | |
209 | ExceptionOr<Optional<RenderingContext>> HTMLCanvasElement::getContext(JSC::ExecState& state, const String& contextId, Vector<JSC::Strong<JSC::Unknown>>&& arguments) |
210 | { |
211 | if (m_context) { |
212 | if (m_context->isPlaceholder()) |
213 | return Exception { InvalidStateError }; |
214 | |
215 | if (m_context->is2d()) { |
216 | if (!is2dType(contextId)) |
217 | return Optional<RenderingContext> { WTF::nullopt }; |
218 | return Optional<RenderingContext> { RefPtr<CanvasRenderingContext2D> { &downcast<CanvasRenderingContext2D>(*m_context) } }; |
219 | } |
220 | |
221 | if (m_context->isBitmapRenderer()) { |
222 | if (!isBitmapRendererType(contextId)) |
223 | return Optional<RenderingContext> { WTF::nullopt }; |
224 | return Optional<RenderingContext> { RefPtr<ImageBitmapRenderingContext> { &downcast<ImageBitmapRenderingContext>(*m_context) } }; |
225 | } |
226 | |
227 | #if ENABLE(WEBGL) |
228 | if (m_context->isWebGL()) { |
229 | if (!isWebGLType(contextId)) |
230 | return Optional<RenderingContext> { WTF::nullopt }; |
231 | if (is<WebGLRenderingContext>(*m_context)) |
232 | return Optional<RenderingContext> { RefPtr<WebGLRenderingContext> { &downcast<WebGLRenderingContext>(*m_context) } }; |
233 | #if ENABLE(WEBGL2) |
234 | ASSERT(is<WebGL2RenderingContext>(*m_context)); |
235 | return Optional<RenderingContext> { RefPtr<WebGL2RenderingContext> { &downcast<WebGL2RenderingContext>(*m_context) } }; |
236 | #endif |
237 | } |
238 | #endif |
239 | |
240 | #if ENABLE(WEBGPU) |
241 | if (m_context->isWebGPU()) { |
242 | if (!isWebGPUType(contextId)) |
243 | return Optional<RenderingContext> { WTF::nullopt }; |
244 | return Optional<RenderingContext> { RefPtr<GPUCanvasContext> { &downcast<GPUCanvasContext>(*m_context) } }; |
245 | } |
246 | #endif |
247 | |
248 | ASSERT_NOT_REACHED(); |
249 | return Optional<RenderingContext> { WTF::nullopt }; |
250 | } |
251 | |
252 | if (is2dType(contextId)) { |
253 | auto context = createContext2d(contextId); |
254 | if (!context) |
255 | return Optional<RenderingContext> { WTF::nullopt }; |
256 | return Optional<RenderingContext> { RefPtr<CanvasRenderingContext2D> { context } }; |
257 | } |
258 | |
259 | if (isBitmapRendererType(contextId)) { |
260 | auto scope = DECLARE_THROW_SCOPE(state.vm()); |
261 | auto attributes = convert<IDLDictionary<ImageBitmapRenderingContextSettings>>(state, !arguments.isEmpty() ? arguments[0].get() : JSC::jsUndefined()); |
262 | RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError }); |
263 | |
264 | auto context = createContextBitmapRenderer(contextId, WTFMove(attributes)); |
265 | if (!context) |
266 | return Optional<RenderingContext> { WTF::nullopt }; |
267 | return Optional<RenderingContext> { RefPtr<ImageBitmapRenderingContext> { context } }; |
268 | } |
269 | |
270 | #if ENABLE(WEBGL) |
271 | if (isWebGLType(contextId)) { |
272 | auto scope = DECLARE_THROW_SCOPE(state.vm()); |
273 | auto attributes = convert<IDLDictionary<WebGLContextAttributes>>(state, !arguments.isEmpty() ? arguments[0].get() : JSC::jsUndefined()); |
274 | RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError }); |
275 | |
276 | auto context = createContextWebGL(contextId, WTFMove(attributes)); |
277 | if (!context) |
278 | return Optional<RenderingContext> { WTF::nullopt }; |
279 | |
280 | if (is<WebGLRenderingContext>(*context)) |
281 | return Optional<RenderingContext> { RefPtr<WebGLRenderingContext> { &downcast<WebGLRenderingContext>(*context) } }; |
282 | #if ENABLE(WEBGL2) |
283 | ASSERT(is<WebGL2RenderingContext>(*context)); |
284 | return Optional<RenderingContext> { RefPtr<WebGL2RenderingContext> { &downcast<WebGL2RenderingContext>(*context) } }; |
285 | #endif |
286 | } |
287 | #endif |
288 | |
289 | #if ENABLE(WEBGPU) |
290 | if (isWebGPUType(contextId)) { |
291 | auto context = createContextWebGPU(contextId); |
292 | if (!context) |
293 | return Optional<RenderingContext> { WTF::nullopt }; |
294 | return Optional<RenderingContext> { RefPtr<GPUCanvasContext> { context } }; |
295 | } |
296 | #endif |
297 | |
298 | return Optional<RenderingContext> { WTF::nullopt }; |
299 | } |
300 | |
301 | CanvasRenderingContext* HTMLCanvasElement::getContext(const String& type) |
302 | { |
303 | if (HTMLCanvasElement::is2dType(type)) |
304 | return getContext2d(type); |
305 | |
306 | if (HTMLCanvasElement::isBitmapRendererType(type)) |
307 | return getContextBitmapRenderer(type); |
308 | |
309 | #if ENABLE(WEBGL) |
310 | if (HTMLCanvasElement::isWebGLType(type)) |
311 | return getContextWebGL(type); |
312 | #endif |
313 | |
314 | #if ENABLE(WEBGPU) |
315 | if (HTMLCanvasElement::isWebGPUType(type)) |
316 | return getContextWebGPU(type); |
317 | #endif |
318 | |
319 | return nullptr; |
320 | } |
321 | |
322 | bool HTMLCanvasElement::is2dType(const String& type) |
323 | { |
324 | return type == "2d" ; |
325 | } |
326 | |
327 | CanvasRenderingContext2D* HTMLCanvasElement::createContext2d(const String& type) |
328 | { |
329 | ASSERT_UNUSED(HTMLCanvasElement::is2dType(type), type); |
330 | ASSERT(!m_context); |
331 | |
332 | bool usesDashboardCompatibilityMode = false; |
333 | #if ENABLE(DASHBOARD_SUPPORT) |
334 | usesDashboardCompatibilityMode = document().settings().usesDashboardBackwardCompatibilityMode(); |
335 | #endif |
336 | |
337 | // Make sure we don't use more pixel memory than the system can support. |
338 | size_t requestedPixelMemory = 4 * width() * height(); |
339 | if (activePixelMemory + requestedPixelMemory > maxActivePixelMemory()) { |
340 | StringBuilder stringBuilder; |
341 | stringBuilder.appendLiteral("Total canvas memory use exceeds the maximum limit (" ); |
342 | stringBuilder.appendNumber(maxActivePixelMemory() / 1024 / 1024); |
343 | stringBuilder.appendLiteral(" MB)." ); |
344 | document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, stringBuilder.toString()); |
345 | return nullptr; |
346 | } |
347 | |
348 | m_context = CanvasRenderingContext2D::create(*this, document().inQuirksMode(), usesDashboardCompatibilityMode); |
349 | |
350 | downcast<CanvasRenderingContext2D>(*m_context).setUsesDisplayListDrawing(m_usesDisplayListDrawing); |
351 | downcast<CanvasRenderingContext2D>(*m_context).setTracksDisplayListReplay(m_tracksDisplayListReplay); |
352 | |
353 | #if USE(IOSURFACE_CANVAS_BACKING_STORE) || ENABLE(ACCELERATED_2D_CANVAS) |
354 | // Need to make sure a RenderLayer and compositing layer get created for the Canvas. |
355 | invalidateStyleAndLayerComposition(); |
356 | #endif |
357 | |
358 | return static_cast<CanvasRenderingContext2D*>(m_context.get()); |
359 | } |
360 | |
361 | CanvasRenderingContext2D* HTMLCanvasElement::getContext2d(const String& type) |
362 | { |
363 | ASSERT_UNUSED(HTMLCanvasElement::is2dType(type), type); |
364 | |
365 | if (m_context && !m_context->is2d()) |
366 | return nullptr; |
367 | |
368 | if (!m_context) |
369 | return createContext2d(type); |
370 | return static_cast<CanvasRenderingContext2D*>(m_context.get()); |
371 | } |
372 | |
373 | #if ENABLE(WEBGL) |
374 | |
375 | static bool requiresAcceleratedCompositingForWebGL() |
376 | { |
377 | #if PLATFORM(GTK) || PLATFORM(WIN_CAIRO) |
378 | return false; |
379 | #else |
380 | return true; |
381 | #endif |
382 | |
383 | } |
384 | static bool shouldEnableWebGL(const Settings& settings) |
385 | { |
386 | if (!settings.webGLEnabled()) |
387 | return false; |
388 | |
389 | if (!requiresAcceleratedCompositingForWebGL()) |
390 | return true; |
391 | |
392 | return settings.acceleratedCompositingEnabled(); |
393 | } |
394 | |
395 | bool HTMLCanvasElement::isWebGLType(const String& type) |
396 | { |
397 | // Retain support for the legacy "webkit-3d" name. |
398 | return type == "webgl" || type == "experimental-webgl" |
399 | #if ENABLE(WEBGL2) |
400 | || type == "webgl2" |
401 | #endif |
402 | || type == "webkit-3d" ; |
403 | } |
404 | |
405 | WebGLRenderingContextBase* HTMLCanvasElement::createContextWebGL(const String& type, WebGLContextAttributes&& attrs) |
406 | { |
407 | ASSERT(HTMLCanvasElement::isWebGLType(type)); |
408 | ASSERT(!m_context); |
409 | |
410 | if (!shouldEnableWebGL(document().settings())) |
411 | return nullptr; |
412 | |
413 | m_context = WebGLRenderingContextBase::create(*this, attrs, type); |
414 | if (m_context) { |
415 | // Need to make sure a RenderLayer and compositing layer get created for the Canvas. |
416 | invalidateStyleAndLayerComposition(); |
417 | } |
418 | |
419 | return downcast<WebGLRenderingContextBase>(m_context.get()); |
420 | } |
421 | |
422 | WebGLRenderingContextBase* HTMLCanvasElement::getContextWebGL(const String& type, WebGLContextAttributes&& attrs) |
423 | { |
424 | ASSERT(HTMLCanvasElement::isWebGLType(type)); |
425 | |
426 | if (!shouldEnableWebGL(document().settings())) |
427 | return nullptr; |
428 | |
429 | if (m_context && !m_context->isWebGL()) |
430 | return nullptr; |
431 | |
432 | if (!m_context) |
433 | return createContextWebGL(type, WTFMove(attrs)); |
434 | return &downcast<WebGLRenderingContextBase>(*m_context); |
435 | } |
436 | |
437 | #endif // ENABLE(WEBGL) |
438 | |
439 | #if ENABLE(WEBGPU) |
440 | |
441 | bool HTMLCanvasElement::isWebGPUType(const String& type) |
442 | { |
443 | return type == "gpu" ; |
444 | } |
445 | |
446 | GPUCanvasContext* HTMLCanvasElement::createContextWebGPU(const String& type) |
447 | { |
448 | ASSERT_UNUSED(type, HTMLCanvasElement::isWebGPUType(type)); |
449 | ASSERT(!m_context); |
450 | |
451 | if (!RuntimeEnabledFeatures::sharedFeatures().webGPUEnabled()) |
452 | return nullptr; |
453 | |
454 | m_context = GPUCanvasContext::create(*this); |
455 | if (m_context) { |
456 | // Need to make sure a RenderLayer and compositing layer get created for the Canvas. |
457 | invalidateStyleAndLayerComposition(); |
458 | } |
459 | |
460 | return static_cast<GPUCanvasContext*>(m_context.get()); |
461 | } |
462 | |
463 | GPUCanvasContext* HTMLCanvasElement::getContextWebGPU(const String& type) |
464 | { |
465 | ASSERT_UNUSED(type, HTMLCanvasElement::isWebGPUType(type)); |
466 | |
467 | if (!RuntimeEnabledFeatures::sharedFeatures().webGPUEnabled()) |
468 | return nullptr; |
469 | |
470 | if (m_context && !m_context->isWebGPU()) |
471 | return nullptr; |
472 | |
473 | if (!m_context) |
474 | return createContextWebGPU(type); |
475 | return static_cast<GPUCanvasContext*>(m_context.get()); |
476 | } |
477 | |
478 | #endif // ENABLE(WEBGPU) |
479 | |
480 | bool HTMLCanvasElement::isBitmapRendererType(const String& type) |
481 | { |
482 | return type == "bitmaprenderer" ; |
483 | } |
484 | |
485 | ImageBitmapRenderingContext* HTMLCanvasElement::createContextBitmapRenderer(const String& type, ImageBitmapRenderingContextSettings&& settings) |
486 | { |
487 | ASSERT_UNUSED(type, HTMLCanvasElement::isBitmapRendererType(type)); |
488 | ASSERT(!m_context); |
489 | |
490 | m_context = ImageBitmapRenderingContext::create(*this, WTFMove(settings)); |
491 | |
492 | #if USE(IOSURFACE_CANVAS_BACKING_STORE) || ENABLE(ACCELERATED_2D_CANVAS) |
493 | // Need to make sure a RenderLayer and compositing layer get created for the Canvas. |
494 | invalidateStyleAndLayerComposition(); |
495 | #endif |
496 | |
497 | return static_cast<ImageBitmapRenderingContext*>(m_context.get()); |
498 | } |
499 | |
500 | ImageBitmapRenderingContext* HTMLCanvasElement::getContextBitmapRenderer(const String& type, ImageBitmapRenderingContextSettings&& settings) |
501 | { |
502 | ASSERT_UNUSED(type, HTMLCanvasElement::isBitmapRendererType(type)); |
503 | if (!m_context) |
504 | return createContextBitmapRenderer(type, WTFMove(settings)); |
505 | return static_cast<ImageBitmapRenderingContext*>(m_context.get()); |
506 | } |
507 | |
508 | void HTMLCanvasElement::didDraw(const FloatRect& rect) |
509 | { |
510 | clearCopiedImage(); |
511 | |
512 | FloatRect dirtyRect = rect; |
513 | if (auto* renderer = renderBox()) { |
514 | FloatRect destRect; |
515 | if (is<RenderReplaced>(renderer)) |
516 | destRect = downcast<RenderReplaced>(renderer)->replacedContentRect(); |
517 | else |
518 | destRect = renderer->contentBoxRect(); |
519 | |
520 | // Inflate dirty rect to cover antialiasing on image buffers. |
521 | if (drawingContext() && drawingContext()->shouldAntialias()) |
522 | dirtyRect.inflate(1); |
523 | |
524 | FloatRect r = mapRect(dirtyRect, FloatRect(0, 0, size().width(), size().height()), destRect); |
525 | r.intersect(destRect); |
526 | |
527 | if (!r.isEmpty() && !m_dirtyRect.contains(r)) { |
528 | m_dirtyRect.unite(r); |
529 | renderer->repaintRectangle(enclosingIntRect(m_dirtyRect)); |
530 | } |
531 | } |
532 | notifyObserversCanvasChanged(dirtyRect); |
533 | } |
534 | |
535 | void HTMLCanvasElement::reset() |
536 | { |
537 | if (m_ignoreReset) |
538 | return; |
539 | |
540 | bool hadImageBuffer = hasCreatedImageBuffer(); |
541 | |
542 | int w = limitToOnlyHTMLNonNegative(attributeWithoutSynchronization(widthAttr), defaultWidth); |
543 | int h = limitToOnlyHTMLNonNegative(attributeWithoutSynchronization(heightAttr), defaultHeight); |
544 | |
545 | if (m_contextStateSaver) { |
546 | // Reset to the initial graphics context state. |
547 | m_contextStateSaver->restore(); |
548 | m_contextStateSaver->save(); |
549 | } |
550 | |
551 | if (is<CanvasRenderingContext2D>(m_context.get())) |
552 | downcast<CanvasRenderingContext2D>(*m_context).reset(); |
553 | |
554 | IntSize oldSize = size(); |
555 | IntSize newSize(w, h); |
556 | // If the size of an existing buffer matches, we can just clear it instead of reallocating. |
557 | // This optimization is only done for 2D canvases for now. |
558 | if (m_hasCreatedImageBuffer && oldSize == newSize && m_context && m_context->is2d()) { |
559 | if (!m_didClearImageBuffer) |
560 | clearImageBuffer(); |
561 | return; |
562 | } |
563 | |
564 | setSurfaceSize(newSize); |
565 | |
566 | if (isGPUBased() && oldSize != size()) |
567 | downcast<GPUBasedCanvasRenderingContext>(*m_context).reshape(width(), height()); |
568 | |
569 | auto renderer = this->renderer(); |
570 | if (is<RenderHTMLCanvas>(renderer)) { |
571 | auto& canvasRenderer = downcast<RenderHTMLCanvas>(*renderer); |
572 | if (oldSize != size()) { |
573 | canvasRenderer.canvasSizeChanged(); |
574 | if (canvasRenderer.hasAcceleratedCompositing()) |
575 | canvasRenderer.contentChanged(CanvasChanged); |
576 | } |
577 | if (hadImageBuffer) |
578 | canvasRenderer.repaint(); |
579 | } |
580 | |
581 | notifyObserversCanvasResized(); |
582 | } |
583 | |
584 | bool HTMLCanvasElement::paintsIntoCanvasBuffer() const |
585 | { |
586 | ASSERT(m_context); |
587 | #if USE(IOSURFACE_CANVAS_BACKING_STORE) |
588 | if (m_context->is2d() || m_context->isBitmapRenderer()) |
589 | return true; |
590 | #endif |
591 | |
592 | if (!m_context->isAccelerated()) |
593 | return true; |
594 | |
595 | if (renderBox() && renderBox()->hasAcceleratedCompositing()) |
596 | return false; |
597 | |
598 | return true; |
599 | } |
600 | |
601 | |
602 | void HTMLCanvasElement::paint(GraphicsContext& context, const LayoutRect& r) |
603 | { |
604 | // Clear the dirty rect |
605 | m_dirtyRect = FloatRect(); |
606 | |
607 | if (!context.paintingDisabled()) { |
608 | bool shouldPaint = true; |
609 | |
610 | if (m_context) { |
611 | shouldPaint = paintsIntoCanvasBuffer() || document().printing(); |
612 | if (shouldPaint) |
613 | m_context->paintRenderingResultsToCanvas(); |
614 | } |
615 | |
616 | if (shouldPaint) { |
617 | if (hasCreatedImageBuffer()) { |
618 | ImageBuffer* imageBuffer = buffer(); |
619 | if (imageBuffer) { |
620 | if (m_presentedImage) { |
621 | ImageOrientationDescription orientationDescription; |
622 | #if ENABLE(CSS_IMAGE_ORIENTATION) |
623 | orientationDescription.setImageOrientationEnum(renderer()->style().imageOrientation()); |
624 | #endif |
625 | context.drawImage(*m_presentedImage, snappedIntRect(r), ImagePaintingOptions(orientationDescription)); |
626 | } else |
627 | context.drawImageBuffer(*imageBuffer, snappedIntRect(r)); |
628 | } |
629 | } |
630 | |
631 | if (isGPUBased()) |
632 | downcast<GPUBasedCanvasRenderingContext>(*m_context).markLayerComposited(); |
633 | } |
634 | } |
635 | |
636 | if (UNLIKELY(m_context && m_context->callTracingActive())) |
637 | InspectorInstrumentation::didFinishRecordingCanvasFrame(*m_context); |
638 | } |
639 | |
640 | bool HTMLCanvasElement::isGPUBased() const |
641 | { |
642 | return m_context && m_context->isGPUBased(); |
643 | } |
644 | |
645 | void HTMLCanvasElement::makeRenderingResultsAvailable() |
646 | { |
647 | if (m_context) |
648 | m_context->paintRenderingResultsToCanvas(); |
649 | } |
650 | |
651 | void HTMLCanvasElement::makePresentationCopy() |
652 | { |
653 | if (!m_presentedImage) { |
654 | // The buffer contains the last presented data, so save a copy of it. |
655 | m_presentedImage = buffer()->copyImage(CopyBackingStore, PreserveResolution::Yes); |
656 | } |
657 | } |
658 | |
659 | void HTMLCanvasElement::clearPresentationCopy() |
660 | { |
661 | m_presentedImage = nullptr; |
662 | } |
663 | |
664 | void HTMLCanvasElement::releaseImageBufferAndContext() |
665 | { |
666 | m_contextStateSaver = nullptr; |
667 | setImageBuffer(nullptr); |
668 | } |
669 | |
670 | void HTMLCanvasElement::setSurfaceSize(const IntSize& size) |
671 | { |
672 | m_size = size; |
673 | m_hasCreatedImageBuffer = false; |
674 | releaseImageBufferAndContext(); |
675 | clearCopiedImage(); |
676 | } |
677 | |
678 | static String toEncodingMimeType(const String& mimeType) |
679 | { |
680 | if (!MIMETypeRegistry::isSupportedImageMIMETypeForEncoding(mimeType)) |
681 | return "image/png"_s ; |
682 | return mimeType.convertToASCIILowercase(); |
683 | } |
684 | |
685 | // https://html.spec.whatwg.org/multipage/canvas.html#a-serialisation-of-the-bitmap-as-a-file |
686 | static Optional<double> qualityFromJSValue(JSC::JSValue qualityValue) |
687 | { |
688 | if (!qualityValue.isNumber()) |
689 | return WTF::nullopt; |
690 | |
691 | double qualityNumber = qualityValue.asNumber(); |
692 | if (qualityNumber < 0 || qualityNumber > 1) |
693 | return WTF::nullopt; |
694 | |
695 | return qualityNumber; |
696 | } |
697 | |
698 | ExceptionOr<UncachedString> HTMLCanvasElement::toDataURL(const String& mimeType, JSC::JSValue qualityValue) |
699 | { |
700 | if (!originClean()) |
701 | return Exception { SecurityError }; |
702 | |
703 | if (m_size.isEmpty() || !buffer()) |
704 | return UncachedString { "data:,"_s }; |
705 | if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) |
706 | ResourceLoadObserver::shared().logCanvasRead(document()); |
707 | |
708 | auto encodingMIMEType = toEncodingMimeType(mimeType); |
709 | auto quality = qualityFromJSValue(qualityValue); |
710 | |
711 | #if USE(CG) |
712 | // Try to get ImageData first, as that may avoid lossy conversions. |
713 | if (auto imageData = getImageData()) |
714 | return UncachedString { dataURL(*imageData, encodingMIMEType, quality) }; |
715 | #endif |
716 | |
717 | makeRenderingResultsAvailable(); |
718 | |
719 | return UncachedString { buffer()->toDataURL(encodingMIMEType, quality) }; |
720 | } |
721 | |
722 | ExceptionOr<UncachedString> HTMLCanvasElement::toDataURL(const String& mimeType) |
723 | { |
724 | return toDataURL(mimeType, { }); |
725 | } |
726 | |
727 | ExceptionOr<void> HTMLCanvasElement::toBlob(ScriptExecutionContext& context, Ref<BlobCallback>&& callback, const String& mimeType, JSC::JSValue qualityValue) |
728 | { |
729 | if (!originClean()) |
730 | return Exception { SecurityError }; |
731 | |
732 | if (m_size.isEmpty() || !buffer()) { |
733 | callback->scheduleCallback(context, nullptr); |
734 | return { }; |
735 | } |
736 | if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) |
737 | ResourceLoadObserver::shared().logCanvasRead(document()); |
738 | |
739 | auto encodingMIMEType = toEncodingMimeType(mimeType); |
740 | auto quality = qualityFromJSValue(qualityValue); |
741 | |
742 | #if USE(CG) |
743 | if (auto imageData = getImageData()) { |
744 | RefPtr<Blob> blob; |
745 | Vector<uint8_t> blobData = data(*imageData, encodingMIMEType, quality); |
746 | if (!blobData.isEmpty()) |
747 | blob = Blob::create(WTFMove(blobData), encodingMIMEType); |
748 | callback->scheduleCallback(context, WTFMove(blob)); |
749 | return { }; |
750 | } |
751 | #endif |
752 | |
753 | makeRenderingResultsAvailable(); |
754 | |
755 | RefPtr<Blob> blob; |
756 | Vector<uint8_t> blobData = buffer()->toData(encodingMIMEType, quality); |
757 | if (!blobData.isEmpty()) |
758 | blob = Blob::create(WTFMove(blobData), encodingMIMEType); |
759 | callback->scheduleCallback(context, WTFMove(blob)); |
760 | return { }; |
761 | } |
762 | |
763 | RefPtr<ImageData> HTMLCanvasElement::getImageData() |
764 | { |
765 | #if ENABLE(WEBGL) |
766 | if (is<WebGLRenderingContextBase>(m_context.get())) { |
767 | if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) |
768 | ResourceLoadObserver::shared().logCanvasRead(document()); |
769 | return downcast<WebGLRenderingContextBase>(*m_context).paintRenderingResultsToImageData(); |
770 | } |
771 | #endif |
772 | return nullptr; |
773 | } |
774 | |
775 | #if ENABLE(MEDIA_STREAM) |
776 | |
777 | RefPtr<MediaSample> HTMLCanvasElement::toMediaSample() |
778 | { |
779 | auto* imageBuffer = buffer(); |
780 | if (!imageBuffer) |
781 | return nullptr; |
782 | if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) |
783 | ResourceLoadObserver::shared().logCanvasRead(document()); |
784 | |
785 | #if PLATFORM(COCOA) |
786 | makeRenderingResultsAvailable(); |
787 | return MediaSampleAVFObjC::createImageSample(imageBuffer->toBGRAData(), width(), height()); |
788 | #else |
789 | return nullptr; |
790 | #endif |
791 | } |
792 | |
793 | ExceptionOr<Ref<MediaStream>> HTMLCanvasElement::captureStream(ScriptExecutionContext& context, Optional<double>&& frameRequestRate) |
794 | { |
795 | if (!originClean()) |
796 | return Exception(SecurityError, "Canvas is tainted"_s ); |
797 | if (RuntimeEnabledFeatures::sharedFeatures().webAPIStatisticsEnabled()) |
798 | ResourceLoadObserver::shared().logCanvasRead(document()); |
799 | |
800 | if (frameRequestRate && frameRequestRate.value() < 0) |
801 | return Exception(NotSupportedError, "frameRequestRate is negative"_s ); |
802 | |
803 | auto track = CanvasCaptureMediaStreamTrack::create(context, *this, WTFMove(frameRequestRate)); |
804 | auto stream = MediaStream::create(context); |
805 | stream->addTrack(track); |
806 | return stream; |
807 | } |
808 | #endif |
809 | |
810 | SecurityOrigin* HTMLCanvasElement::securityOrigin() const |
811 | { |
812 | return &document().securityOrigin(); |
813 | } |
814 | |
815 | bool HTMLCanvasElement::shouldAccelerate(const IntSize& size) const |
816 | { |
817 | auto& settings = document().settings(); |
818 | |
819 | auto area = size.area<RecordOverflow>(); |
820 | if (area.hasOverflowed()) |
821 | return false; |
822 | |
823 | if (area > settings.maximumAccelerated2dCanvasSize()) |
824 | return false; |
825 | |
826 | #if USE(IOSURFACE_CANVAS_BACKING_STORE) |
827 | return settings.canvasUsesAcceleratedDrawing(); |
828 | #elif ENABLE(ACCELERATED_2D_CANVAS) |
829 | if (m_context && !m_context->is2d()) |
830 | return false; |
831 | |
832 | if (!settings.accelerated2dCanvasEnabled()) |
833 | return false; |
834 | |
835 | if (area < settings.minimumAccelerated2dCanvasSize()) |
836 | return false; |
837 | |
838 | return true; |
839 | #else |
840 | UNUSED_PARAM(size); |
841 | return false; |
842 | #endif |
843 | } |
844 | |
845 | size_t HTMLCanvasElement::memoryCost() const |
846 | { |
847 | // memoryCost() may be invoked concurrently from a GC thread, and we need to be careful |
848 | // about what data we access here and how. We need to hold a lock to prevent m_imageBuffer |
849 | // from being changed while we access it. |
850 | auto locker = holdLock(m_imageBufferAssignmentLock); |
851 | if (!m_imageBuffer) |
852 | return 0; |
853 | return m_imageBuffer->memoryCost(); |
854 | } |
855 | |
856 | size_t HTMLCanvasElement::externalMemoryCost() const |
857 | { |
858 | // externalMemoryCost() may be invoked concurrently from a GC thread, and we need to be careful |
859 | // about what data we access here and how. We need to hold a lock to prevent m_imageBuffer |
860 | // from being changed while we access it. |
861 | auto locker = holdLock(m_imageBufferAssignmentLock); |
862 | if (!m_imageBuffer) |
863 | return 0; |
864 | return m_imageBuffer->externalMemoryCost(); |
865 | } |
866 | |
867 | void HTMLCanvasElement::setUsesDisplayListDrawing(bool usesDisplayListDrawing) |
868 | { |
869 | if (usesDisplayListDrawing == m_usesDisplayListDrawing) |
870 | return; |
871 | |
872 | m_usesDisplayListDrawing = usesDisplayListDrawing; |
873 | |
874 | if (is<CanvasRenderingContext2D>(m_context.get())) |
875 | downcast<CanvasRenderingContext2D>(*m_context).setUsesDisplayListDrawing(m_usesDisplayListDrawing); |
876 | } |
877 | |
878 | void HTMLCanvasElement::setTracksDisplayListReplay(bool tracksDisplayListReplay) |
879 | { |
880 | if (tracksDisplayListReplay == m_tracksDisplayListReplay) |
881 | return; |
882 | |
883 | m_tracksDisplayListReplay = tracksDisplayListReplay; |
884 | |
885 | if (is<CanvasRenderingContext2D>(m_context.get())) |
886 | downcast<CanvasRenderingContext2D>(*m_context).setTracksDisplayListReplay(m_tracksDisplayListReplay); |
887 | } |
888 | |
889 | String HTMLCanvasElement::displayListAsText(DisplayList::AsTextFlags flags) const |
890 | { |
891 | if (is<CanvasRenderingContext2D>(m_context.get())) |
892 | return downcast<CanvasRenderingContext2D>(*m_context).displayListAsText(flags); |
893 | |
894 | return String(); |
895 | } |
896 | |
897 | String HTMLCanvasElement::replayDisplayListAsText(DisplayList::AsTextFlags flags) const |
898 | { |
899 | if (is<CanvasRenderingContext2D>(m_context.get())) |
900 | return downcast<CanvasRenderingContext2D>(*m_context).replayDisplayListAsText(flags); |
901 | |
902 | return String(); |
903 | } |
904 | |
905 | void HTMLCanvasElement::createImageBuffer() const |
906 | { |
907 | ASSERT(!m_imageBuffer); |
908 | |
909 | m_hasCreatedImageBuffer = true; |
910 | m_didClearImageBuffer = true; |
911 | |
912 | // Perform multiplication as floating point to avoid overflow |
913 | if (float(width()) * height() > maxCanvasArea) { |
914 | StringBuilder stringBuilder; |
915 | stringBuilder.appendLiteral("Canvas area exceeds the maximum limit (width * height > " ); |
916 | stringBuilder.appendNumber(maxCanvasArea); |
917 | stringBuilder.appendLiteral(")." ); |
918 | document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, stringBuilder.toString()); |
919 | return; |
920 | } |
921 | |
922 | // Make sure we don't use more pixel memory than the system can support. |
923 | size_t requestedPixelMemory = 4 * width() * height(); |
924 | if (activePixelMemory + requestedPixelMemory > maxActivePixelMemory()) { |
925 | StringBuilder stringBuilder; |
926 | stringBuilder.appendLiteral("Total canvas memory use exceeds the maximum limit (" ); |
927 | stringBuilder.appendNumber(maxActivePixelMemory() / 1024 / 1024); |
928 | stringBuilder.appendLiteral(" MB)." ); |
929 | document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, stringBuilder.toString()); |
930 | return; |
931 | } |
932 | |
933 | if (!width() || !height()) |
934 | return; |
935 | |
936 | RenderingMode renderingMode = shouldAccelerate(size()) ? Accelerated : Unaccelerated; |
937 | |
938 | auto hostWindow = (document().view() && document().view()->root()) ? document().view()->root()->hostWindow() : nullptr; |
939 | setImageBuffer(ImageBuffer::create(size(), renderingMode, 1, ColorSpaceSRGB, hostWindow)); |
940 | } |
941 | |
942 | void HTMLCanvasElement::setImageBuffer(std::unique_ptr<ImageBuffer>&& buffer) const |
943 | { |
944 | size_t previousMemoryCost = memoryCost(); |
945 | removeFromActivePixelMemory(previousMemoryCost); |
946 | |
947 | { |
948 | auto locker = holdLock(m_imageBufferAssignmentLock); |
949 | m_contextStateSaver = nullptr; |
950 | m_imageBuffer = WTFMove(buffer); |
951 | } |
952 | |
953 | if (m_imageBuffer && m_size != m_imageBuffer->internalSize()) |
954 | m_size = m_imageBuffer->internalSize(); |
955 | |
956 | size_t currentMemoryCost = memoryCost(); |
957 | activePixelMemory += currentMemoryCost; |
958 | |
959 | if (m_context && m_imageBuffer && previousMemoryCost != currentMemoryCost) |
960 | InspectorInstrumentation::didChangeCanvasMemory(*m_context); |
961 | |
962 | if (!m_imageBuffer) |
963 | return; |
964 | m_imageBuffer->context().setShadowsIgnoreTransforms(true); |
965 | m_imageBuffer->context().setImageInterpolationQuality(defaultInterpolationQuality); |
966 | m_imageBuffer->context().setStrokeThickness(1); |
967 | m_contextStateSaver = std::make_unique<GraphicsContextStateSaver>(m_imageBuffer->context()); |
968 | |
969 | JSC::JSLockHolder lock(HTMLElement::scriptExecutionContext()->vm()); |
970 | HTMLElement::scriptExecutionContext()->vm().heap.reportExtraMemoryAllocated(memoryCost()); |
971 | |
972 | #if USE(IOSURFACE_CANVAS_BACKING_STORE) || ENABLE(ACCELERATED_2D_CANVAS) |
973 | if (m_context && m_context->is2d()) { |
974 | // Recalculate compositing requirements if acceleration state changed. |
975 | const_cast<HTMLCanvasElement*>(this)->invalidateStyleAndLayerComposition(); |
976 | } |
977 | #endif |
978 | } |
979 | |
980 | void HTMLCanvasElement::setImageBufferAndMarkDirty(std::unique_ptr<ImageBuffer>&& buffer) |
981 | { |
982 | m_hasCreatedImageBuffer = true; |
983 | setImageBuffer(WTFMove(buffer)); |
984 | didDraw(FloatRect(FloatPoint(), m_size)); |
985 | } |
986 | |
987 | GraphicsContext* HTMLCanvasElement::drawingContext() const |
988 | { |
989 | if (m_context && !m_context->is2d()) |
990 | return nullptr; |
991 | |
992 | return buffer() ? &m_imageBuffer->context() : nullptr; |
993 | } |
994 | |
995 | GraphicsContext* HTMLCanvasElement::existingDrawingContext() const |
996 | { |
997 | if (!m_hasCreatedImageBuffer) |
998 | return nullptr; |
999 | |
1000 | return drawingContext(); |
1001 | } |
1002 | |
1003 | ImageBuffer* HTMLCanvasElement::buffer() const |
1004 | { |
1005 | if (!m_hasCreatedImageBuffer) |
1006 | createImageBuffer(); |
1007 | return m_imageBuffer.get(); |
1008 | } |
1009 | |
1010 | Image* HTMLCanvasElement::copiedImage() const |
1011 | { |
1012 | if (!m_copiedImage && buffer()) { |
1013 | if (m_context) |
1014 | m_context->paintRenderingResultsToCanvas(); |
1015 | m_copiedImage = buffer()->copyImage(CopyBackingStore, PreserveResolution::Yes); |
1016 | } |
1017 | return m_copiedImage.get(); |
1018 | } |
1019 | |
1020 | void HTMLCanvasElement::clearImageBuffer() const |
1021 | { |
1022 | ASSERT(m_hasCreatedImageBuffer); |
1023 | ASSERT(!m_didClearImageBuffer); |
1024 | ASSERT(m_context); |
1025 | |
1026 | m_didClearImageBuffer = true; |
1027 | |
1028 | if (is<CanvasRenderingContext2D>(*m_context)) { |
1029 | // No need to undo transforms/clip/etc. because we are called right after the context is reset. |
1030 | downcast<CanvasRenderingContext2D>(*m_context).clearRect(0, 0, width(), height()); |
1031 | } |
1032 | } |
1033 | |
1034 | void HTMLCanvasElement::clearCopiedImage() |
1035 | { |
1036 | m_copiedImage = nullptr; |
1037 | m_didClearImageBuffer = false; |
1038 | } |
1039 | |
1040 | AffineTransform HTMLCanvasElement::baseTransform() const |
1041 | { |
1042 | ASSERT(m_hasCreatedImageBuffer); |
1043 | return m_imageBuffer->baseTransform(); |
1044 | } |
1045 | |
1046 | } |
1047 | |