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_context && m_context->is2d()) {
252         CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
253         context2D->reset();
254     }
255
256     IntSize oldSize = size();
257     // If the size of an existing buffer matches, we can just clear it instead of reallocating.
258     // This optimization is only done for 2D canvases for now.
259     if (m_hasCreatedImageBuffer && oldSize == IntSize(w, h) && (!m_context || m_context->is2d())) {
260         if (!m_didClearImageBuffer)
261             clearImageBuffer();
262         return;
263     }
264     setSurfaceSize(IntSize(w, h));
265
266 #if ENABLE(WEBGL)
267     if (m_context && m_context->is3d() && oldSize != size())
268         static_cast<WebGLRenderingContext*>(m_context.get())->reshape(width(), height());
269 #endif
270
271     if (RenderObject* renderer = this->renderer()) {
272         if (m_rendererIsCanvas) {
273             if (oldSize != size()) {
274                 toRenderHTMLCanvas(renderer)->canvasSizeChanged();
275 #if USE(ACCELERATED_COMPOSITING)
276                 if (renderBox() && renderBox()->hasLayer() && renderBox()->layer()->hasAcceleratedCompositing())
277                     renderBox()->layer()->contentChanged(RenderLayer::CanvasChanged);
278 #endif
279             }
280             if (hadImageBuffer)
281                 renderer->repaint();
282         }
283     }
284
285     HashSet<CanvasObserver*>::iterator end = m_observers.end();
286     for (HashSet<CanvasObserver*>::iterator it = m_observers.begin(); it != end; ++it)
287         (*it)->canvasResized(this);
288 }
289
290 bool HTMLCanvasElement::paintsIntoCanvasBuffer() const
291 {
292     ASSERT(m_context);
293 #if USE(IOSURFACE_CANVAS_BACKING_STORE)
294     if (m_context->is2d())
295         return true;
296 #endif
297
298 #if USE(ACCELERATED_COMPOSITING)
299     if (!m_context->isAccelerated())
300         return true;
301
302     if (renderBox() && renderBox()->hasLayer() && renderBox()->layer()->hasAcceleratedCompositing())
303         return false;
304 #endif
305     return true;
306 }
307
308
309 void HTMLCanvasElement::paint(GraphicsContext* context, const LayoutRect& r, bool useLowQualityScale)
310 {
311     // Clear the dirty rect
312     m_dirtyRect = FloatRect();
313
314     if (context->paintingDisabled())
315         return;
316     
317     if (m_context) {
318         if (!paintsIntoCanvasBuffer() && !document()->printing())
319             return;
320         m_context->paintRenderingResultsToCanvas();
321     }
322
323     if (hasCreatedImageBuffer()) {
324         ImageBuffer* imageBuffer = buffer();
325         if (imageBuffer) {
326             if (m_presentedImage)
327                 context->drawImage(m_presentedImage.get(), ColorSpaceDeviceRGB, pixelSnappedIntRect(r), CompositeSourceOver, useLowQualityScale);
328             else
329                 context->drawImageBuffer(imageBuffer, ColorSpaceDeviceRGB, pixelSnappedIntRect(r), CompositeSourceOver, useLowQualityScale);
330         }
331     }
332
333 #if ENABLE(WEBGL)    
334     if (is3D())
335         static_cast<WebGLRenderingContext*>(m_context.get())->markLayerComposited();
336 #endif
337 }
338
339 #if ENABLE(WEBGL)
340 bool HTMLCanvasElement::is3D() const
341 {
342     return m_context && m_context->is3d();
343 }
344 #endif
345
346 void HTMLCanvasElement::makeRenderingResultsAvailable()
347 {
348     if (m_context)
349         m_context->paintRenderingResultsToCanvas();
350 }
351
352 void HTMLCanvasElement::makePresentationCopy()
353 {
354     if (!m_presentedImage) {
355         // The buffer contains the last presented data, so save a copy of it.
356         m_presentedImage = buffer()->copyImage(CopyBackingStore);
357     }
358 }
359
360 void HTMLCanvasElement::clearPresentationCopy()
361 {
362     m_presentedImage.clear();
363 }
364
365 void HTMLCanvasElement::setSurfaceSize(const IntSize& size)
366 {
367     m_size = size;
368     m_hasCreatedImageBuffer = false;
369     m_imageBuffer.clear();
370     clearCopiedImage();
371 }
372
373 String HTMLCanvasElement::toEncodingMimeType(const String& mimeType)
374 {
375     String lowercaseMimeType = mimeType.lower();
376
377     // FIXME: Make isSupportedImageMIMETypeForEncoding threadsafe (to allow this method to be used on a worker thread).
378     if (mimeType.isNull() || !MIMETypeRegistry::isSupportedImageMIMETypeForEncoding(lowercaseMimeType))
379         lowercaseMimeType = "image/png";
380
381     return lowercaseMimeType;
382 }
383
384 String HTMLCanvasElement::toDataURL(const String& mimeType, const double* quality, ExceptionCode& ec)
385 {
386     if (!m_originClean) {
387         ec = SECURITY_ERR;
388         return String();
389     }
390
391     if (m_size.isEmpty() || !buffer())
392         return String("data:,");
393
394     String encodingMimeType = toEncodingMimeType(mimeType);
395
396 #if USE(CG) || USE(SKIA)
397     // Try to get ImageData first, as that may avoid lossy conversions.
398     RefPtr<ImageData> imageData = getImageData();
399
400     if (imageData)
401         return ImageDataToDataURL(*imageData, encodingMimeType, quality);
402 #endif
403
404     makeRenderingResultsAvailable();
405
406     return buffer()->toDataURL(encodingMimeType, quality);
407 }
408
409 PassRefPtr<ImageData> HTMLCanvasElement::getImageData()
410 {
411     if (!m_context || !m_context->is3d())
412        return 0;
413
414 #if ENABLE(WEBGL)    
415     WebGLRenderingContext* ctx = static_cast<WebGLRenderingContext*>(m_context.get());
416
417     return ctx->paintRenderingResultsToImageData();
418 #else
419     return 0;
420 #endif
421 }
422
423 FloatRect HTMLCanvasElement::convertLogicalToDevice(const FloatRect& logicalRect) const
424 {
425     FloatRect deviceRect(logicalRect);
426     deviceRect.scale(m_deviceScaleFactor);
427
428     float x = floorf(deviceRect.x());
429     float y = floorf(deviceRect.y());
430     float w = ceilf(deviceRect.maxX() - x);
431     float h = ceilf(deviceRect.maxY() - y);
432     deviceRect.setX(x);
433     deviceRect.setY(y);
434     deviceRect.setWidth(w);
435     deviceRect.setHeight(h);
436
437     return deviceRect;
438 }
439
440 FloatSize HTMLCanvasElement::convertLogicalToDevice(const FloatSize& logicalSize) const
441 {
442     float width = ceilf(logicalSize.width() * m_deviceScaleFactor);
443     float height = ceilf(logicalSize.height() * m_deviceScaleFactor);
444     return FloatSize(width, height);
445 }
446
447 FloatSize HTMLCanvasElement::convertDeviceToLogical(const FloatSize& deviceSize) const
448 {
449     float width = ceilf(deviceSize.width() / m_deviceScaleFactor);
450     float height = ceilf(deviceSize.height() / m_deviceScaleFactor);
451     return FloatSize(width, height);
452 }
453
454 SecurityOrigin* HTMLCanvasElement::securityOrigin() const
455 {
456     return document()->securityOrigin();
457 }
458
459 CSSStyleSelector* HTMLCanvasElement::styleSelector()
460 {
461     return document()->styleSelector();
462 }
463
464 bool HTMLCanvasElement::shouldAccelerate(const IntSize& size) const
465 {
466 #if USE(IOSURFACE_CANVAS_BACKING_STORE)
467     UNUSED_PARAM(size);
468     return document()->settings() && document()->settings()->canvasUsesAcceleratedDrawing();
469 #elif ENABLE(ACCELERATED_2D_CANVAS)
470     if (m_context && !m_context->is2d())
471         return false;
472
473     Settings* settings = document()->settings();
474     if (!settings || !settings->accelerated2dCanvasEnabled())
475         return false;
476
477     // Do not use acceleration for small canvas.
478     if (size.width() * size.height() < settings->minimumAccelerated2dCanvasSize())
479         return false;
480
481 #if PLATFORM(CHROMIUM)
482     if (!PlatformSupport::canAccelerate2dCanvas())
483         return false;
484 #endif
485
486     return true;
487 #else
488     UNUSED_PARAM(size);
489     return false;
490 #endif
491 }
492
493 bool HTMLCanvasElement::shouldDefer() const
494 {
495 #if USE(SKIA)
496     if (m_context && !m_context->is2d())
497         return false;
498
499     Settings* settings = document()->settings();
500     if (!settings || !settings->deferred2dCanvasEnabled())
501         return false;
502
503     return true;
504 #else
505     return false;
506 #endif
507 }
508
509 void HTMLCanvasElement::createImageBuffer() const
510 {
511     ASSERT(!m_imageBuffer);
512
513     m_hasCreatedImageBuffer = true;
514     m_didClearImageBuffer = true;
515
516     FloatSize logicalSize = size();
517     FloatSize deviceSize = convertLogicalToDevice(logicalSize);
518     if (!deviceSize.isExpressibleAsIntSize())
519         return;
520
521     if (deviceSize.width() * deviceSize.height() > MaxCanvasArea)
522         return;
523 #if USE(SKIA)
524     if (deviceSize.width() > MaxSkiaDim || deviceSize.height() > MaxSkiaDim)
525         return;
526 #endif
527
528     IntSize bufferSize(deviceSize.width(), deviceSize.height());
529     if (!bufferSize.width() || !bufferSize.height())
530         return;
531
532     RenderingMode renderingMode = shouldAccelerate(bufferSize) ? Accelerated : 
533 #if USE(SKIA)
534         UnacceleratedNonPlatformBuffer;
535 #else
536         Unaccelerated;
537 #endif
538     DeferralMode deferralMode = shouldDefer() ? Deferred : NonDeferred;
539     m_imageBuffer = ImageBuffer::create(bufferSize, 1, ColorSpaceDeviceRGB, renderingMode, deferralMode);
540     if (!m_imageBuffer)
541         return;
542     m_imageBuffer->context()->scale(FloatSize(bufferSize.width() / logicalSize.width(), bufferSize.height() / logicalSize.height()));
543     m_imageBuffer->context()->setShadowsIgnoreTransforms(true);
544     m_imageBuffer->context()->setImageInterpolationQuality(DefaultInterpolationQuality);
545     m_imageBuffer->context()->setStrokeThickness(1);
546
547 #if USE(JSC)
548     JSC::JSLock lock(JSC::SilenceAssertionsOnly);
549     size_t numBytes = 4 * m_imageBuffer->internalSize().width() * m_imageBuffer->internalSize().height();
550     scriptExecutionContext()->globalData()->heap.reportExtraMemoryCost(numBytes);
551 #endif
552
553 #if USE(IOSURFACE_CANVAS_BACKING_STORE) || (ENABLE(ACCELERATED_2D_CANVAS) && USE(ACCELERATED_COMPOSITING))
554     if (m_context && m_context->is2d())
555         // Recalculate compositing requirements if acceleration state changed.
556         const_cast<HTMLCanvasElement*>(this)->setNeedsStyleRecalc(SyntheticStyleChange);
557 #endif
558 }
559
560 GraphicsContext* HTMLCanvasElement::drawingContext() const
561 {
562     return buffer() ? m_imageBuffer->context() : 0;
563 }
564
565 GraphicsContext* HTMLCanvasElement::existingDrawingContext() const
566 {
567     if (!m_hasCreatedImageBuffer)
568         return 0;
569
570     return drawingContext();
571 }
572
573 ImageBuffer* HTMLCanvasElement::buffer() const
574 {
575     if (!m_hasCreatedImageBuffer)
576         createImageBuffer();
577     return m_imageBuffer.get();
578 }
579
580 Image* HTMLCanvasElement::copiedImage() const
581 {
582     if (!m_copiedImage && buffer()) {
583         if (m_context)
584             m_context->paintRenderingResultsToCanvas();
585         m_copiedImage = buffer()->copyImage(CopyBackingStore);
586     }
587     return m_copiedImage.get();
588 }
589
590 void HTMLCanvasElement::clearImageBuffer() const
591 {
592     ASSERT(m_hasCreatedImageBuffer);
593     ASSERT(!m_didClearImageBuffer);
594
595     if (!m_context)
596         return;
597
598     m_didClearImageBuffer = true;
599
600     if (m_context->is2d()) {
601         CanvasRenderingContext2D* context2D = static_cast<CanvasRenderingContext2D*>(m_context.get());
602         // No need to undo transforms/clip/etc. because we are called right after the context is reset.
603         context2D->clearRect(0, 0, width(), height());
604     }
605 }
606
607 void HTMLCanvasElement::clearCopiedImage()
608 {
609     m_copiedImage.clear();
610     m_didClearImageBuffer = false;
611 }
612
613 AffineTransform HTMLCanvasElement::baseTransform() const
614 {
615     ASSERT(m_hasCreatedImageBuffer);
616     FloatSize unscaledSize = size();
617     FloatSize deviceSize = convertLogicalToDevice(unscaledSize);
618     IntSize size(deviceSize.width(), deviceSize.height());
619     AffineTransform transform;
620     if (size.width() && size.height())
621         transform.scaleNonUniform(size.width() / unscaledSize.width(), size.height() / unscaledSize.height());
622     return m_imageBuffer->baseTransform() * transform;
623 }
624
625 }