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