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