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 "core/html/HTMLCanvasElement.h"
30
31#include <math.h>
32#include "HTMLNames.h"
33#include "RuntimeEnabledFeatures.h"
34#include "bindings/v8/ExceptionMessages.h"
35#include "bindings/v8/ExceptionState.h"
36#include "bindings/v8/ScriptController.h"
37#include "core/dom/Document.h"
38#include "core/dom/ExceptionCode.h"
39#include "core/html/ImageData.h"
40#include "core/html/canvas/Canvas2DContextAttributes.h"
41#include "core/html/canvas/CanvasRenderingContext2D.h"
42#include "core/html/canvas/WebGLContextAttributes.h"
43#include "core/html/canvas/WebGLRenderingContext.h"
44#include "core/frame/Frame.h"
45#include "core/frame/Settings.h"
46#include "core/rendering/RenderHTMLCanvas.h"
47#include "platform/MIMETypeRegistry.h"
48#include "platform/graphics/Canvas2DImageBufferSurface.h"
49#include "platform/graphics/GraphicsContextStateSaver.h"
50#include "platform/graphics/ImageBuffer.h"
51#include "platform/graphics/UnacceleratedImageBufferSurface.h"
52#include "platform/graphics/gpu/WebGLImageBufferSurface.h"
53#include "public/platform/Platform.h"
54
55namespace WebCore {
56
57using namespace HTMLNames;
58
59// These values come from the WhatWG spec.
60static const int DefaultWidth = 300;
61static const int DefaultHeight = 150;
62
63// Firefox limits width/height to 32767 pixels, but slows down dramatically before it
64// reaches that limit. We limit by area instead, giving us larger maximum dimensions,
65// in exchange for a smaller maximum canvas size.
66static const int MaxCanvasArea = 32768 * 8192; // Maximum canvas area in CSS pixels
67
68//In Skia, we will also limit width/height to 32767.
69static const int MaxSkiaDim = 32767; // Maximum width/height in CSS pixels.
70
71HTMLCanvasElement::HTMLCanvasElement(Document& document)
72    : HTMLElement(canvasTag, document)
73    , m_size(DefaultWidth, DefaultHeight)
74    , m_rendererIsCanvas(false)
75    , m_ignoreReset(false)
76    , m_accelerationDisabled(false)
77    , m_externallyAllocatedMemory(0)
78    , m_originClean(true)
79    , m_didFailToCreateImageBuffer(false)
80    , m_didClearImageBuffer(false)
81{
82    ScriptWrappable::init(this);
83}
84
85PassRefPtr<HTMLCanvasElement> HTMLCanvasElement::create(Document& document)
86{
87    return adoptRef(new HTMLCanvasElement(document));
88}
89
90HTMLCanvasElement::~HTMLCanvasElement()
91{
92    setExternallyAllocatedMemory(0);
93    HashSet<CanvasObserver*>::iterator end = m_observers.end();
94    for (HashSet<CanvasObserver*>::iterator it = m_observers.begin(); it != end; ++it)
95        (*it)->canvasDestroyed(this);
96
97    m_context.clear(); // Ensure this goes away before the ImageBuffer.
98}
99
100void HTMLCanvasElement::parseAttribute(const QualifiedName& name, const AtomicString& value)
101{
102    if (name == widthAttr || name == heightAttr)
103        reset();
104    HTMLElement::parseAttribute(name, value);
105}
106
107RenderObject* HTMLCanvasElement::createRenderer(RenderStyle* style)
108{
109    Frame* frame = document().frame();
110    if (frame && frame->script().canExecuteScripts(NotAboutToExecuteScript)) {
111        m_rendererIsCanvas = true;
112        return new RenderHTMLCanvas(this);
113    }
114
115    m_rendererIsCanvas = false;
116    return HTMLElement::createRenderer(style);
117}
118
119Node::InsertionNotificationRequest HTMLCanvasElement::insertedInto(ContainerNode* node)
120{
121    setIsInCanvasSubtree(true);
122    return HTMLElement::insertedInto(node);
123}
124
125void HTMLCanvasElement::addObserver(CanvasObserver* observer)
126{
127    m_observers.add(observer);
128}
129
130void HTMLCanvasElement::removeObserver(CanvasObserver* observer)
131{
132    m_observers.remove(observer);
133}
134
135void HTMLCanvasElement::setHeight(int value)
136{
137    setIntegralAttribute(heightAttr, value);
138}
139
140void HTMLCanvasElement::setWidth(int value)
141{
142    setIntegralAttribute(widthAttr, value);
143}
144
145CanvasRenderingContext* HTMLCanvasElement::getContext(const String& type, CanvasContextAttributes* attrs)
146{
147    // A Canvas can either be "2D" or "webgl" but never both. If you request a 2D canvas and the existing
148    // context is already 2D, just return that. If the existing context is WebGL, then destroy it
149    // before creating a new 2D context. Vice versa when requesting a WebGL canvas. Requesting a
150    // context with any other type string will destroy any existing context.
151    enum ContextType {
152        Context2d,
153        ContextWebkit3d,
154        ContextExperimentalWebgl,
155        ContextWebgl,
156        // Only add new items to the end and keep the order of existing items.
157        ContextTypeCount,
158    };
159
160    // FIXME - The code depends on the context not going away once created, to prevent JS from
161    // seeing a dangling pointer. So for now we will disallow the context from being changed
162    // once it is created.
163    if (type == "2d") {
164        if (m_context && !m_context->is2d())
165            return 0;
166        if (!m_context) {
167            blink::Platform::current()->histogramEnumeration("Canvas.ContextType", Context2d, ContextTypeCount);
168            m_context = CanvasRenderingContext2D::create(this, static_cast<Canvas2DContextAttributes*>(attrs), document().inQuirksMode());
169            if (m_context)
170                scheduleLayerUpdate();
171        }
172        return m_context.get();
173    }
174
175    Settings* settings = document().settings();
176    if (settings && settings->webGLEnabled()) {
177        // Accept the legacy "webkit-3d" name as well as the provisional "experimental-webgl" name.
178        // Now that WebGL is ratified, we will also accept "webgl" as the context name in Chrome.
179        ContextType contextType;
180        bool is3dContext = true;
181        if (type == "webkit-3d")
182            contextType = ContextWebkit3d;
183        else if (type == "experimental-webgl")
184            contextType = ContextExperimentalWebgl;
185        else if (type == "webgl")
186            contextType = ContextWebgl;
187        else
188            is3dContext = false;
189
190        if (is3dContext) {
191            if (m_context && !m_context->is3d())
192                return 0;
193            if (!m_context) {
194                blink::Platform::current()->histogramEnumeration("Canvas.ContextType", contextType, ContextTypeCount);
195                m_context = WebGLRenderingContext::create(this, static_cast<WebGLContextAttributes*>(attrs));
196                if (m_context)
197                    scheduleLayerUpdate();
198            }
199            return m_context.get();
200        }
201    }
202    return 0;
203}
204
205void HTMLCanvasElement::didDraw(const FloatRect& rect)
206{
207    clearCopiedImage();
208
209    if (RenderBox* ro = renderBox()) {
210        FloatRect destRect = ro->contentBoxRect();
211        FloatRect r = mapRect(rect, FloatRect(0, 0, size().width(), size().height()), destRect);
212        r.intersect(destRect);
213        if (r.isEmpty() || m_dirtyRect.contains(r))
214            return;
215
216        m_dirtyRect.unite(r);
217        ro->repaintRectangle(enclosingIntRect(m_dirtyRect));
218    }
219
220    notifyObserversCanvasChanged(rect);
221}
222
223void HTMLCanvasElement::notifyObserversCanvasChanged(const FloatRect& rect)
224{
225    HashSet<CanvasObserver*>::iterator end = m_observers.end();
226    for (HashSet<CanvasObserver*>::iterator it = m_observers.begin(); it != end; ++it)
227        (*it)->canvasChanged(this, rect);
228}
229
230void HTMLCanvasElement::reset()
231{
232    if (m_ignoreReset)
233        return;
234
235    bool ok;
236    bool hadImageBuffer = hasImageBuffer();
237
238    int w = getAttribute(widthAttr).toInt(&ok);
239    if (!ok || w < 0)
240        w = DefaultWidth;
241
242    int h = getAttribute(heightAttr).toInt(&ok);
243    if (!ok || h < 0)
244        h = DefaultHeight;
245
246    if (m_contextStateSaver) {
247        // Reset to the initial graphics context state.
248        m_contextStateSaver->restore();
249        m_contextStateSaver->save();
250    }
251
252    if (m_context && m_context->is2d())
253        toCanvasRenderingContext2D(m_context.get())->reset();
254
255    IntSize oldSize = size();
256    IntSize newSize(w, h);
257
258    // If the size of an existing buffer matches, we can just clear it instead of reallocating.
259    // This optimization is only done for 2D canvases for now.
260    if (hadImageBuffer && oldSize == newSize && m_context && m_context->is2d()) {
261        if (!m_didClearImageBuffer)
262            clearImageBuffer();
263        return;
264    }
265
266    setSurfaceSize(newSize);
267
268    if (m_context && m_context->is3d() && oldSize != size())
269        toWebGLRenderingContext(m_context.get())->reshape(width(), height());
270
271    if (RenderObject* renderer = this->renderer()) {
272        if (m_rendererIsCanvas) {
273            if (oldSize != size()) {
274                toRenderHTMLCanvas(renderer)->canvasSizeChanged();
275                if (renderBox() && renderBox()->hasAcceleratedCompositing())
276                    renderBox()->contentChanged(CanvasChanged);
277            }
278            if (hadImageBuffer)
279                renderer->repaint();
280        }
281    }
282
283    HashSet<CanvasObserver*>::iterator end = m_observers.end();
284    for (HashSet<CanvasObserver*>::iterator it = m_observers.begin(); it != end; ++it)
285        (*it)->canvasResized(this);
286}
287
288bool HTMLCanvasElement::paintsIntoCanvasBuffer() const
289{
290    ASSERT(m_context);
291
292    if (!m_context->isAccelerated())
293        return true;
294
295    if (renderBox() && renderBox()->hasAcceleratedCompositing())
296        return false;
297
298    return true;
299}
300
301
302void HTMLCanvasElement::paint(GraphicsContext* context, const LayoutRect& r, bool useLowQualityScale)
303{
304    // Clear the dirty rect
305    m_dirtyRect = FloatRect();
306
307    if (context->paintingDisabled())
308        return;
309
310    if (m_context) {
311        if (!paintsIntoCanvasBuffer() && !document().printing())
312            return;
313        m_context->paintRenderingResultsToCanvas();
314    }
315
316    if (hasImageBuffer()) {
317        ImageBuffer* imageBuffer = buffer();
318        if (imageBuffer) {
319            CompositeOperator compositeOperator = !m_context || m_context->hasAlpha() ? CompositeSourceOver : CompositeCopy;
320            if (m_presentedImage)
321                context->drawImage(m_presentedImage.get(), pixelSnappedIntRect(r), compositeOperator, DoNotRespectImageOrientation, useLowQualityScale);
322            else
323                context->drawImageBuffer(imageBuffer, pixelSnappedIntRect(r), compositeOperator, blink::WebBlendModeNormal, useLowQualityScale);
324        }
325    }
326
327    if (is3D())
328        toWebGLRenderingContext(m_context.get())->markLayerComposited();
329}
330
331bool HTMLCanvasElement::is3D() const
332{
333    return m_context && m_context->is3d();
334}
335
336void HTMLCanvasElement::makePresentationCopy()
337{
338    if (!m_presentedImage) {
339        // The buffer contains the last presented data, so save a copy of it.
340        m_presentedImage = buffer()->copyImage(CopyBackingStore, Unscaled);
341    }
342}
343
344void HTMLCanvasElement::clearPresentationCopy()
345{
346    m_presentedImage.clear();
347}
348
349void HTMLCanvasElement::setSurfaceSize(const IntSize& size)
350{
351    m_size = size;
352    m_didFailToCreateImageBuffer = false;
353    m_contextStateSaver.clear();
354    m_imageBuffer.clear();
355    setExternallyAllocatedMemory(0);
356    clearCopiedImage();
357}
358
359String HTMLCanvasElement::toEncodingMimeType(const String& mimeType)
360{
361    String lowercaseMimeType = mimeType.lower();
362
363    // FIXME: Make isSupportedImageMIMETypeForEncoding threadsafe (to allow this method to be used on a worker thread).
364    if (mimeType.isNull() || !MIMETypeRegistry::isSupportedImageMIMETypeForEncoding(lowercaseMimeType))
365        lowercaseMimeType = "image/png";
366
367    return lowercaseMimeType;
368}
369
370String HTMLCanvasElement::toDataURL(const String& mimeType, const double* quality, ExceptionState& exceptionState)
371{
372    if (!m_originClean) {
373        exceptionState.throwSecurityError("Tainted canvases may not be exported.");
374        return String();
375    }
376
377    if (m_size.isEmpty() || !buffer())
378        return String("data:,");
379
380    String encodingMimeType = toEncodingMimeType(mimeType);
381
382    // Try to get ImageData first, as that may avoid lossy conversions.
383    RefPtr<ImageData> imageData = getImageData();
384
385    if (imageData)
386        return ImageDataToDataURL(ImageDataBuffer(imageData->size(), imageData->data()), encodingMimeType, quality);
387
388    if (m_context)
389        m_context->paintRenderingResultsToCanvas();
390
391    return buffer()->toDataURL(encodingMimeType, quality);
392}
393
394PassRefPtr<ImageData> HTMLCanvasElement::getImageData()
395{
396    if (!m_context || !m_context->is3d())
397       return 0;
398    return toWebGLRenderingContext(m_context.get())->paintRenderingResultsToImageData();
399}
400
401SecurityOrigin* HTMLCanvasElement::securityOrigin() const
402{
403    return document().securityOrigin();
404}
405
406bool HTMLCanvasElement::shouldAccelerate(const IntSize& size) const
407{
408    if (m_context && !m_context->is2d())
409        return false;
410
411    if (m_accelerationDisabled)
412        return false;
413
414    Settings* settings = document().settings();
415    if (!settings || !settings->accelerated2dCanvasEnabled())
416        return false;
417
418    // Do not use acceleration for small canvas.
419    if (size.width() * size.height() < settings->minimumAccelerated2dCanvasSize())
420        return false;
421
422    if (!blink::Platform::current()->canAccelerate2dCanvas())
423        return false;
424
425    return true;
426}
427
428PassOwnPtr<ImageBufferSurface> HTMLCanvasElement::createImageBufferSurface(const IntSize& deviceSize, int* msaaSampleCount)
429{
430    OpacityMode opacityMode = !m_context || m_context->hasAlpha() ? NonOpaque : Opaque;
431
432    *msaaSampleCount = 0;
433    if (is3D())
434        return adoptPtr(new WebGLImageBufferSurface(size(), opacityMode));
435
436    if (shouldAccelerate(deviceSize)) {
437        if (document().settings())
438            *msaaSampleCount = document().settings()->accelerated2dCanvasMSAASampleCount();
439        OwnPtr<ImageBufferSurface> surface = adoptPtr(new Canvas2DImageBufferSurface(size(), opacityMode, *msaaSampleCount));
440        if (surface->isValid())
441            return surface.release();
442    }
443
444    return adoptPtr(new UnacceleratedImageBufferSurface(size(), opacityMode));
445}
446
447void HTMLCanvasElement::createImageBuffer()
448{
449    ASSERT(!m_imageBuffer);
450
451    m_didFailToCreateImageBuffer = true;
452    m_didClearImageBuffer = true;
453
454    IntSize deviceSize = size();
455    if (deviceSize.width() * deviceSize.height() > MaxCanvasArea)
456        return;
457
458    if (deviceSize.width() > MaxSkiaDim || deviceSize.height() > MaxSkiaDim)
459        return;
460
461    if (!deviceSize.width() || !deviceSize.height())
462        return;
463
464    int msaaSampleCount;
465    OwnPtr<ImageBufferSurface> surface = createImageBufferSurface(deviceSize, &msaaSampleCount);
466    if (!surface->isValid())
467        return;
468    m_imageBuffer = ImageBuffer::create(surface.release());
469
470    m_didFailToCreateImageBuffer = false;
471
472    setExternallyAllocatedMemory(4 * width() * height());
473
474    if (is3D()) {
475        // Early out for WebGL canvases
476        m_contextStateSaver.clear();
477        return;
478    }
479
480    m_imageBuffer->context()->setShouldClampToSourceRect(false);
481    m_imageBuffer->context()->setImageInterpolationQuality(DefaultInterpolationQuality);
482    // Enabling MSAA overrides a request to disable antialiasing. This is true regardless of whether the
483    // rendering mode is accelerated or not. For consistency, we don't want to apply AA in accelerated
484    // canvases but not in unaccelerated canvases.
485    if (!msaaSampleCount && document().settings() && !document().settings()->antialiased2dCanvasEnabled())
486        m_imageBuffer->context()->setShouldAntialias(false);
487    // GraphicsContext's defaults don't always agree with the 2d canvas spec.
488    // See CanvasRenderingContext2D::State::State() for more information.
489    m_imageBuffer->context()->setMiterLimit(10);
490    m_imageBuffer->context()->setStrokeThickness(1);
491    m_contextStateSaver = adoptPtr(new GraphicsContextStateSaver(*m_imageBuffer->context()));
492
493    // Recalculate compositing requirements if acceleration state changed.
494    if (m_context)
495        scheduleLayerUpdate();
496}
497
498void HTMLCanvasElement::setExternallyAllocatedMemory(intptr_t externallyAllocatedMemory)
499{
500    v8::Isolate::GetCurrent()->AdjustAmountOfExternalAllocatedMemory(externallyAllocatedMemory - m_externallyAllocatedMemory);
501    m_externallyAllocatedMemory = externallyAllocatedMemory;
502}
503
504GraphicsContext* HTMLCanvasElement::drawingContext() const
505{
506    return buffer() ? m_imageBuffer->context() : 0;
507}
508
509GraphicsContext* HTMLCanvasElement::existingDrawingContext() const
510{
511    if (m_didFailToCreateImageBuffer) {
512        ASSERT(!hasImageBuffer());
513        return 0;
514    }
515
516    return drawingContext();
517}
518
519ImageBuffer* HTMLCanvasElement::buffer() const
520{
521    if (!hasImageBuffer() && !m_didFailToCreateImageBuffer)
522        const_cast<HTMLCanvasElement*>(this)->createImageBuffer();
523    return m_imageBuffer.get();
524}
525
526void HTMLCanvasElement::ensureUnacceleratedImageBuffer()
527{
528    if ((hasImageBuffer() && !m_imageBuffer->isAccelerated()) || m_didFailToCreateImageBuffer)
529        return;
530    m_imageBuffer.clear();
531    OpacityMode opacityMode = !m_context || m_context->hasAlpha() ? NonOpaque : Opaque;
532    m_imageBuffer = ImageBuffer::create(size(), opacityMode);
533    m_didFailToCreateImageBuffer = !m_imageBuffer;
534}
535
536Image* HTMLCanvasElement::copiedImage() const
537{
538    if (!m_copiedImage && buffer()) {
539        if (m_context)
540            m_context->paintRenderingResultsToCanvas();
541        m_copiedImage = buffer()->copyImage(CopyBackingStore, Unscaled);
542    }
543    return m_copiedImage.get();
544}
545
546void HTMLCanvasElement::clearImageBuffer()
547{
548    ASSERT(hasImageBuffer() && !m_didFailToCreateImageBuffer);
549    ASSERT(!m_didClearImageBuffer);
550    ASSERT(m_context);
551
552    m_didClearImageBuffer = true;
553
554    if (m_context->is2d()) {
555        // No need to undo transforms/clip/etc. because we are called right
556        // after the context is reset.
557        toCanvasRenderingContext2D(m_context.get())->clearRect(0, 0, width(), height());
558    }
559}
560
561void HTMLCanvasElement::clearCopiedImage()
562{
563    m_copiedImage.clear();
564    m_didClearImageBuffer = false;
565}
566
567AffineTransform HTMLCanvasElement::baseTransform() const
568{
569    ASSERT(hasImageBuffer() && !m_didFailToCreateImageBuffer);
570    return m_imageBuffer->baseTransform();
571}
572
573}
574