6c372255639b0d7c8a5bf99a27a996fdb4fd5eff
[WebKit-https.git] / Source / WebCore / html / HTMLCanvasElement.cpp
1 /*
2  * Copyright (C) 2004, 2006, 2007 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 "Attribute.h"
32 #include "CanvasGradient.h"
33 #include "CanvasPattern.h"
34 #include "CanvasRenderingContext2D.h"
35 #include "Chrome.h"
36 #include "ChromeClient.h"
37 #include "Document.h"
38 #include "ExceptionCode.h"
39 #include "Frame.h"
40 #include "FrameLoaderClient.h"
41 #include "GeometryUtilities.h"
42 #include "GraphicsContext.h"
43 #include "HTMLNames.h"
44 #include "ImageData.h"
45 #include "MIMETypeRegistry.h"
46 #include "Page.h"
47 #include "RenderHTMLCanvas.h"
48 #include "ScriptController.h"
49 #include "Settings.h"
50 #include <math.h>
51
52 #include <runtime/JSCInlines.h>
53 #include <runtime/JSLock.h>
54
55 #if ENABLE(WEBGL)    
56 #include "WebGLContextAttributes.h"
57 #include "WebGLRenderingContextBase.h"
58 #endif
59
60 namespace WebCore {
61
62 using namespace HTMLNames;
63
64 // These values come from the WhatWG spec.
65 static const int DefaultWidth = 300;
66 static const int DefaultHeight = 150;
67
68 // Firefox limits width/height to 32767 pixels, but slows down dramatically before it
69 // reaches that limit. We limit by area instead, giving us larger maximum dimensions,
70 // in exchange for a smaller maximum canvas size. The maximum canvas size is in CSS pixels.
71 #if PLATFORM(IOS)
72 static const float MaxCanvasArea = 4580 * 1145; // 20 MB assuming 4 bytes per pixel
73 #else
74 static const float MaxCanvasArea = 32768 * 8192;
75 #endif
76
77 HTMLCanvasElement::HTMLCanvasElement(const QualifiedName& tagName, Document& document)
78     : HTMLElement(tagName, document)
79     , m_size(DefaultWidth, DefaultHeight)
80     , m_rendererIsCanvas(false)
81     , m_ignoreReset(false)
82     , m_originClean(true)
83     , m_hasCreatedImageBuffer(false)
84     , m_didClearImageBuffer(false)
85 {
86     ASSERT(hasTagName(canvasTag));
87 }
88
89 Ref<HTMLCanvasElement> HTMLCanvasElement::create(Document& document)
90 {
91     return adoptRef(*new HTMLCanvasElement(canvasTag, document));
92 }
93
94 Ref<HTMLCanvasElement> HTMLCanvasElement::create(const QualifiedName& tagName, Document& document)
95 {
96     return adoptRef(*new HTMLCanvasElement(tagName, document));
97 }
98
99 HTMLCanvasElement::~HTMLCanvasElement()
100 {
101     for (auto it = m_observers.begin(), end = m_observers.end(); it != end; ++it)
102         (*it)->canvasDestroyed(*this);
103
104     m_context = nullptr; // Ensure this goes away before the ImageBuffer.
105 }
106
107 void HTMLCanvasElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
108 {
109     if (name == widthAttr || name == heightAttr)
110         reset();
111     HTMLElement::parseAttribute(name, value);
112 }
113
114 RenderPtr<RenderElement> HTMLCanvasElement::createElementRenderer(Ref<RenderStyle>&& style)
115 {
116     Frame* frame = document().frame();
117     if (frame && frame->script().canExecuteScripts(NotAboutToExecuteScript)) {
118         m_rendererIsCanvas = true;
119         return createRenderer<RenderHTMLCanvas>(*this, WTF::move(style));
120     }
121
122     m_rendererIsCanvas = false;
123     return HTMLElement::createElementRenderer(WTF::move(style));
124 }
125
126 bool HTMLCanvasElement::canContainRangeEndPoint() const
127 {
128     return false;
129 }
130
131 bool HTMLCanvasElement::canStartSelection() const
132 {
133     return false;
134 }
135
136 void HTMLCanvasElement::addObserver(CanvasObserver& observer)
137 {
138     m_observers.add(&observer);
139 }
140
141 void HTMLCanvasElement::removeObserver(CanvasObserver& observer)
142 {
143     m_observers.remove(&observer);
144 }
145
146 void HTMLCanvasElement::setHeight(int value)
147 {
148     setIntegralAttribute(heightAttr, value);
149 }
150
151 void HTMLCanvasElement::setWidth(int value)
152 {
153     setIntegralAttribute(widthAttr, value);
154 }
155
156 #if ENABLE(WEBGL)
157 static bool requiresAcceleratedCompositingForWebGL()
158 {
159 #if PLATFORM(GTK) || PLATFORM(EFL)
160     return false;
161 #else
162     return true;
163 #endif
164
165 }
166 static bool shouldEnableWebGL(Settings* settings)
167 {
168     if (!settings)
169         return false;
170
171     if (!settings->webGLEnabled())
172         return false;
173
174     if (!requiresAcceleratedCompositingForWebGL())
175         return true;
176
177     return settings->acceleratedCompositingEnabled();
178 }
179 #endif
180
181 CanvasRenderingContext* HTMLCanvasElement::getContext(const String& type, CanvasContextAttributes* attrs)
182 {
183     // A Canvas can either be "2D" or "webgl" but never both. If you request a 2D canvas and the existing
184     // context is already 2D, just return that. If the existing context is WebGL, then destroy it
185     // before creating a new 2D context. Vice versa when requesting a WebGL canvas. Requesting a
186     // context with any other type string will destroy any existing context.
187     
188     // FIXME: The code depends on the context not going away once created, to prevent JS from
189     // seeing a dangling pointer. So for now we will disallow the context from being changed
190     // once it is created. https://bugs.webkit.org/show_bug.cgi?id=117095
191     if (is2dType(type)) {
192         if (m_context && !m_context->is2d())
193             return nullptr;
194         if (!m_context) {
195             bool usesDashbardCompatibilityMode = false;
196 #if ENABLE(DASHBOARD_SUPPORT)
197             if (Settings* settings = document().settings())
198                 usesDashbardCompatibilityMode = settings->usesDashboardBackwardCompatibilityMode();
199 #endif
200             m_context = std::make_unique<CanvasRenderingContext2D>(this, document().inQuirksMode(), usesDashbardCompatibilityMode);
201 #if USE(IOSURFACE_CANVAS_BACKING_STORE) || ENABLE(ACCELERATED_2D_CANVAS)
202             // Need to make sure a RenderLayer and compositing layer get created for the Canvas
203             setNeedsStyleRecalc(SyntheticStyleChange);
204 #endif
205         }
206         return m_context.get();
207     }
208 #if ENABLE(WEBGL)
209     if (shouldEnableWebGL(document().settings())) {
210
211         if (is3dType(type)) {
212             if (m_context && !m_context->is3d())
213                 return nullptr;
214             if (!m_context) {
215                 m_context = WebGLRenderingContextBase::create(this, static_cast<WebGLContextAttributes*>(attrs), type);
216                 if (m_context) {
217                     // Need to make sure a RenderLayer and compositing layer get created for the Canvas
218                     setNeedsStyleRecalc(SyntheticStyleChange);
219                 }
220             }
221             return m_context.get();
222         }
223     }
224 #else
225     UNUSED_PARAM(attrs);
226 #endif
227     return nullptr;
228 }
229     
230 bool HTMLCanvasElement::probablySupportsContext(const String& type, CanvasContextAttributes*)
231 {
232     // FIXME: Provide implementation that accounts for attributes. Bugzilla bug 117093
233     // https://bugs.webkit.org/show_bug.cgi?id=117093
234
235     // FIXME: The code depends on the context not going away once created (as getContext
236     // is implemented under this assumption) https://bugs.webkit.org/show_bug.cgi?id=117095
237     if (is2dType(type))
238         return !m_context || m_context->is2d();
239
240 #if ENABLE(WEBGL)
241     if (shouldEnableWebGL(document().settings())) {
242         if (is3dType(type))
243             return !m_context || m_context->is3d();
244     }
245 #endif
246     return false;
247 }
248
249 bool HTMLCanvasElement::is2dType(const String& type)
250 {
251     return type == "2d";
252 }
253
254 #if ENABLE(WEBGL)
255 bool HTMLCanvasElement::is3dType(const String& type)
256 {
257     // Retain support for the legacy "webkit-3d" name.
258     return type == "webgl" || type == "experimental-webgl" || type == "webkit-3d" || type == "experimental-webgl2";
259 }
260 #endif
261
262 void HTMLCanvasElement::didDraw(const FloatRect& rect)
263 {
264     clearCopiedImage();
265
266     if (RenderBox* ro = renderBox()) {
267         FloatRect destRect = ro->contentBoxRect();
268         FloatRect r = mapRect(rect, FloatRect(0, 0, size().width(), size().height()), destRect);
269         r.intersect(destRect);
270         if (r.isEmpty() || m_dirtyRect.contains(r))
271             return;
272
273         m_dirtyRect.unite(r);
274         ro->repaintRectangle(enclosingIntRect(m_dirtyRect));
275     }
276
277     notifyObserversCanvasChanged(rect);
278 }
279
280 void HTMLCanvasElement::notifyObserversCanvasChanged(const FloatRect& rect)
281 {
282     // Choke point for canvas drawing; notify DOMTimer of the event.
283     DOMTimer::scriptDidCauseElementRepaint(*this);
284
285     for (auto it = m_observers.begin(), end = m_observers.end(); it != end; ++it)
286         (*it)->canvasChanged(*this, rect);
287 }
288
289 void HTMLCanvasElement::reset()
290 {
291     if (m_ignoreReset)
292         return;
293
294     bool ok;
295     bool hadImageBuffer = hasCreatedImageBuffer();
296
297     int w = getAttribute(widthAttr).toInt(&ok);
298     if (!ok || w < 0)
299         w = DefaultWidth;
300
301     int h = getAttribute(heightAttr).toInt(&ok);
302     if (!ok || h < 0)
303         h = DefaultHeight;
304
305     if (m_contextStateSaver) {
306         // Reset to the initial graphics context state.
307         m_contextStateSaver->restore();
308         m_contextStateSaver->save();
309     }
310
311     if (m_context && m_context->is2d()) {
312         CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
313         context2D->reset();
314     }
315
316     IntSize oldSize = size();
317     IntSize newSize(w, h);
318     // If the size of an existing buffer matches, we can just clear it instead of reallocating.
319     // This optimization is only done for 2D canvases for now.
320     if (m_hasCreatedImageBuffer && oldSize == newSize && m_context && m_context->is2d()) {
321         if (!m_didClearImageBuffer)
322             clearImageBuffer();
323         return;
324     }
325
326     setSurfaceSize(newSize);
327
328 #if ENABLE(WEBGL)
329     if (is3D() && oldSize != size())
330         static_cast<WebGLRenderingContextBase*>(m_context.get())->reshape(width(), height());
331 #endif
332
333     if (auto renderer = this->renderer()) {
334         if (m_rendererIsCanvas) {
335             if (oldSize != size()) {
336                 downcast<RenderHTMLCanvas>(*renderer).canvasSizeChanged();
337                 if (renderBox() && renderBox()->hasAcceleratedCompositing())
338                     renderBox()->contentChanged(CanvasChanged);
339             }
340             if (hadImageBuffer)
341                 renderer->repaint();
342         }
343     }
344
345     for (auto it = m_observers.begin(), end = m_observers.end(); it != end; ++it)
346         (*it)->canvasResized(*this);
347 }
348
349 bool HTMLCanvasElement::paintsIntoCanvasBuffer() const
350 {
351     ASSERT(m_context);
352 #if USE(IOSURFACE_CANVAS_BACKING_STORE)
353     if (m_context->is2d())
354         return true;
355 #endif
356
357     if (!m_context->isAccelerated())
358         return true;
359
360     if (renderBox() && renderBox()->hasAcceleratedCompositing())
361         return false;
362
363     return true;
364 }
365
366
367 void HTMLCanvasElement::paint(GraphicsContext* context, const LayoutRect& r, bool useLowQualityScale)
368 {
369     // Clear the dirty rect
370     m_dirtyRect = FloatRect();
371
372     if (context->paintingDisabled())
373         return;
374     
375     if (m_context) {
376         if (!paintsIntoCanvasBuffer() && !document().printing())
377             return;
378         m_context->paintRenderingResultsToCanvas();
379     }
380
381     if (hasCreatedImageBuffer()) {
382         ImageBuffer* imageBuffer = buffer();
383         if (imageBuffer) {
384             if (m_presentedImage) {
385                 ImageOrientationDescription orientationDescription;
386 #if ENABLE(CSS_IMAGE_ORIENTATION)
387                 orientationDescription.setImageOrientationEnum(renderer()->style().imageOrientation());
388 #endif 
389                 context->drawImage(m_presentedImage.get(), ColorSpaceDeviceRGB, snappedIntRect(r), ImagePaintingOptions(orientationDescription, useLowQualityScale));
390             } else
391                 context->drawImageBuffer(imageBuffer, ColorSpaceDeviceRGB, snappedIntRect(r), useLowQualityScale);
392         }
393     }
394
395 #if ENABLE(WEBGL)    
396     if (is3D())
397         static_cast<WebGLRenderingContextBase*>(m_context.get())->markLayerComposited();
398 #endif
399 }
400
401 #if ENABLE(WEBGL)
402 bool HTMLCanvasElement::is3D() const
403 {
404     return m_context && m_context->is3d();
405 }
406 #endif
407
408 void HTMLCanvasElement::makeRenderingResultsAvailable()
409 {
410     if (m_context)
411         m_context->paintRenderingResultsToCanvas();
412 }
413
414 void HTMLCanvasElement::makePresentationCopy()
415 {
416     if (!m_presentedImage) {
417         // The buffer contains the last presented data, so save a copy of it.
418         m_presentedImage = buffer()->copyImage(CopyBackingStore, Unscaled);
419     }
420 }
421
422 void HTMLCanvasElement::clearPresentationCopy()
423 {
424     m_presentedImage.clear();
425 }
426
427 void HTMLCanvasElement::setSurfaceSize(const IntSize& size)
428 {
429     m_size = size;
430     m_hasCreatedImageBuffer = false;
431     m_contextStateSaver = nullptr;
432     m_imageBuffer.reset();
433     clearCopiedImage();
434 }
435
436 String HTMLCanvasElement::toEncodingMimeType(const String& mimeType)
437 {
438     String lowercaseMimeType = mimeType.lower();
439
440     // FIXME: Make isSupportedImageMIMETypeForEncoding threadsafe (to allow this method to be used on a worker thread).
441     if (mimeType.isNull() || !MIMETypeRegistry::isSupportedImageMIMETypeForEncoding(lowercaseMimeType))
442         lowercaseMimeType = "image/png";
443
444     return lowercaseMimeType;
445 }
446
447 String HTMLCanvasElement::toDataURL(const String& mimeType, const double* quality, ExceptionCode& ec)
448 {
449     if (!m_originClean) {
450         ec = SECURITY_ERR;
451         return String();
452     }
453
454     if (m_size.isEmpty() || !buffer())
455         return String("data:,");
456
457     String encodingMimeType = toEncodingMimeType(mimeType);
458
459 #if USE(CG)
460     // Try to get ImageData first, as that may avoid lossy conversions.
461     RefPtr<ImageData> imageData = getImageData();
462
463     if (imageData)
464         return ImageDataToDataURL(*imageData, encodingMimeType, quality);
465 #endif
466
467     makeRenderingResultsAvailable();
468
469     return buffer()->toDataURL(encodingMimeType, quality);
470 }
471
472 RefPtr<ImageData> HTMLCanvasElement::getImageData()
473 {
474 #if ENABLE(WEBGL)
475     if (!is3D())
476         return nullptr;
477
478     WebGLRenderingContextBase* ctx = static_cast<WebGLRenderingContextBase*>(m_context.get());
479
480     return ctx->paintRenderingResultsToImageData();
481 #else
482     return nullptr;
483 #endif
484 }
485
486 FloatRect HTMLCanvasElement::convertLogicalToDevice(const FloatRect& logicalRect) const
487 {
488     FloatRect deviceRect(logicalRect);
489
490     float x = floorf(deviceRect.x());
491     float y = floorf(deviceRect.y());
492     float w = ceilf(deviceRect.maxX() - x);
493     float h = ceilf(deviceRect.maxY() - y);
494     deviceRect.setX(x);
495     deviceRect.setY(y);
496     deviceRect.setWidth(w);
497     deviceRect.setHeight(h);
498
499     return deviceRect;
500 }
501
502 FloatSize HTMLCanvasElement::convertLogicalToDevice(const FloatSize& logicalSize) const
503 {
504     float width = ceilf(logicalSize.width());
505     float height = ceilf(logicalSize.height());
506     return FloatSize(width, height);
507 }
508
509 FloatSize HTMLCanvasElement::convertDeviceToLogical(const FloatSize& deviceSize) const
510 {
511     float width = ceilf(deviceSize.width());
512     float height = ceilf(deviceSize.height());
513     return FloatSize(width, height);
514 }
515
516 SecurityOrigin* HTMLCanvasElement::securityOrigin() const
517 {
518     return document().securityOrigin();
519 }
520
521 bool HTMLCanvasElement::shouldAccelerate(const IntSize& size) const
522 {
523 #if USE(IOSURFACE_CANVAS_BACKING_STORE)
524     UNUSED_PARAM(size);
525     return document().settings() && document().settings()->canvasUsesAcceleratedDrawing();
526 #elif ENABLE(ACCELERATED_2D_CANVAS)
527     if (m_context && !m_context->is2d())
528         return false;
529
530     Settings* settings = document().settings();
531     if (!settings || !settings->accelerated2dCanvasEnabled())
532         return false;
533
534     // Do not use acceleration for small canvas.
535     if (size.width() * size.height() < settings->minimumAccelerated2dCanvasSize())
536         return false;
537
538     return true;
539 #else
540     UNUSED_PARAM(size);
541     return false;
542 #endif
543 }
544
545 void HTMLCanvasElement::createImageBuffer() const
546 {
547     ASSERT(!m_imageBuffer);
548
549     m_hasCreatedImageBuffer = true;
550     m_didClearImageBuffer = true;
551
552     FloatSize logicalSize = size();
553     FloatSize deviceSize = convertLogicalToDevice(logicalSize);
554     if (!deviceSize.isExpressibleAsIntSize())
555         return;
556
557     if (deviceSize.width() * deviceSize.height() > MaxCanvasArea) {
558         StringBuilder stringBuilder;
559         stringBuilder.append("Canvas area exceeds the maximum limit (width * height > ");
560         stringBuilder.appendNumber(MaxCanvasArea);
561         stringBuilder.append(").");
562         document().addConsoleMessage(MessageSource::JS, MessageLevel::Warning, stringBuilder.toString());
563         return;
564     }
565
566     IntSize bufferSize(deviceSize.width(), deviceSize.height());
567     if (!bufferSize.width() || !bufferSize.height())
568         return;
569
570     RenderingMode renderingMode = shouldAccelerate(bufferSize) ? Accelerated : Unaccelerated;
571     m_imageBuffer = ImageBuffer::create(size(), 1, ColorSpaceDeviceRGB, renderingMode);
572     if (!m_imageBuffer)
573         return;
574     m_imageBuffer->context()->setShadowsIgnoreTransforms(true);
575     m_imageBuffer->context()->setImageInterpolationQuality(DefaultInterpolationQuality);
576     if (document().settings() && !document().settings()->antialiased2dCanvasEnabled())
577         m_imageBuffer->context()->setShouldAntialias(false);
578     m_imageBuffer->context()->setStrokeThickness(1);
579     m_contextStateSaver = std::make_unique<GraphicsContextStateSaver>(*m_imageBuffer->context());
580
581     JSC::JSLockHolder lock(scriptExecutionContext()->vm());
582     size_t numBytes = 4 * m_imageBuffer->internalSize().width() * m_imageBuffer->internalSize().height();
583     scriptExecutionContext()->vm().heap.reportExtraMemoryCost(numBytes);
584
585 #if USE(IOSURFACE_CANVAS_BACKING_STORE) || ENABLE(ACCELERATED_2D_CANVAS)
586     if (m_context && m_context->is2d())
587         // Recalculate compositing requirements if acceleration state changed.
588         const_cast<HTMLCanvasElement*>(this)->setNeedsStyleRecalc(SyntheticStyleChange);
589 #endif
590 }
591
592 GraphicsContext* HTMLCanvasElement::drawingContext() const
593 {
594     return buffer() ? m_imageBuffer->context() : nullptr;
595 }
596
597 GraphicsContext* HTMLCanvasElement::existingDrawingContext() const
598 {
599     if (!m_hasCreatedImageBuffer)
600         return nullptr;
601
602     return drawingContext();
603 }
604
605 ImageBuffer* HTMLCanvasElement::buffer() const
606 {
607     if (!m_hasCreatedImageBuffer)
608         createImageBuffer();
609     return m_imageBuffer.get();
610 }
611
612 Image* HTMLCanvasElement::copiedImage() const
613 {
614     if (!m_copiedImage && buffer()) {
615         if (m_context)
616             m_context->paintRenderingResultsToCanvas();
617         m_copiedImage = buffer()->copyImage(CopyBackingStore, Unscaled);
618     }
619     return m_copiedImage.get();
620 }
621
622 void HTMLCanvasElement::clearImageBuffer() const
623 {
624     ASSERT(m_hasCreatedImageBuffer);
625     ASSERT(!m_didClearImageBuffer);
626     ASSERT(m_context);
627
628     m_didClearImageBuffer = true;
629
630     if (m_context->is2d()) {
631         CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
632         // No need to undo transforms/clip/etc. because we are called right after the context is reset.
633         context2D->clearRect(0, 0, width(), height());
634     }
635 }
636
637 void HTMLCanvasElement::clearCopiedImage()
638 {
639     m_copiedImage.clear();
640     m_didClearImageBuffer = false;
641 }
642
643 AffineTransform HTMLCanvasElement::baseTransform() const
644 {
645     ASSERT(m_hasCreatedImageBuffer);
646     FloatSize unscaledSize = size();
647     FloatSize deviceSize = convertLogicalToDevice(unscaledSize);
648     IntSize size(deviceSize.width(), deviceSize.height());
649     AffineTransform transform;
650     if (size.width() && size.height())
651         transform.scaleNonUniform(size.width() / unscaledSize.width(), size.height() / unscaledSize.height());
652     return m_imageBuffer->baseTransform() * transform;
653 }
654
655 }