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