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/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 55 namespace WebCore { 56 57 using namespace HTMLNames; 58 59 // These values come from the WhatWG spec. 60 static const int DefaultWidth = 300; 61 static 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. 66 static const int MaxCanvasArea = 32768 * 8192; // Maximum canvas area in CSS pixels 67 68 //In Skia, we will also limit width/height to 32767. 69 static const int MaxSkiaDim = 32767; // Maximum width/height in CSS pixels. 70 71 HTMLCanvasElement::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 85 PassRefPtr<HTMLCanvasElement> HTMLCanvasElement::create(Document& document) 86 { 87 return adoptRef(new HTMLCanvasElement(document)); 88 } 89 90 HTMLCanvasElement::~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 100 void HTMLCanvasElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 101 { 102 if (name == widthAttr || name == heightAttr) 103 reset(); 104 HTMLElement::parseAttribute(name, value); 105 } 106 107 RenderObject* 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 119 Node::InsertionNotificationRequest HTMLCanvasElement::insertedInto(ContainerNode* node) 120 { 121 setIsInCanvasSubtree(true); 122 return HTMLElement::insertedInto(node); 123 } 124 125 void HTMLCanvasElement::addObserver(CanvasObserver* observer) 126 { 127 m_observers.add(observer); 128 } 129 130 void HTMLCanvasElement::removeObserver(CanvasObserver* observer) 131 { 132 m_observers.remove(observer); 133 } 134 135 void HTMLCanvasElement::setHeight(int value) 136 { 137 setIntegralAttribute(heightAttr, value); 138 } 139 140 void HTMLCanvasElement::setWidth(int value) 141 { 142 setIntegralAttribute(widthAttr, value); 143 } 144 145 CanvasRenderingContext* 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 205 void 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 223 void 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 230 void 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 288 bool 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 302 void 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 331 bool HTMLCanvasElement::is3D() const 332 { 333 return m_context && m_context->is3d(); 334 } 335 336 void 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 344 void HTMLCanvasElement::clearPresentationCopy() 345 { 346 m_presentedImage.clear(); 347 } 348 349 void 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 359 String 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 370 String 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 394 PassRefPtr<ImageData> HTMLCanvasElement::getImageData() 395 { 396 if (!m_context || !m_context->is3d()) 397 return 0; 398 return toWebGLRenderingContext(m_context.get())->paintRenderingResultsToImageData(); 399 } 400 401 SecurityOrigin* HTMLCanvasElement::securityOrigin() const 402 { 403 return document().securityOrigin(); 404 } 405 406 bool 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 428 PassOwnPtr<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 447 void 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 498 void HTMLCanvasElement::setExternallyAllocatedMemory(intptr_t externallyAllocatedMemory) 499 { 500 v8::Isolate::GetCurrent()->AdjustAmountOfExternalAllocatedMemory(externallyAllocatedMemory - m_externallyAllocatedMemory); 501 m_externallyAllocatedMemory = externallyAllocatedMemory; 502 } 503 504 GraphicsContext* HTMLCanvasElement::drawingContext() const 505 { 506 return buffer() ? m_imageBuffer->context() : 0; 507 } 508 509 GraphicsContext* HTMLCanvasElement::existingDrawingContext() const 510 { 511 if (m_didFailToCreateImageBuffer) { 512 ASSERT(!hasImageBuffer()); 513 return 0; 514 } 515 516 return drawingContext(); 517 } 518 519 ImageBuffer* HTMLCanvasElement::buffer() const 520 { 521 if (!hasImageBuffer() && !m_didFailToCreateImageBuffer) 522 const_cast<HTMLCanvasElement*>(this)->createImageBuffer(); 523 return m_imageBuffer.get(); 524 } 525 526 void 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 536 Image* 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 546 void 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 561 void HTMLCanvasElement::clearCopiedImage() 562 { 563 m_copiedImage.clear(); 564 m_didClearImageBuffer = false; 565 } 566 567 AffineTransform HTMLCanvasElement::baseTransform() const 568 { 569 ASSERT(hasImageBuffer() && !m_didFailToCreateImageBuffer); 570 return m_imageBuffer->baseTransform(); 571 } 572 573 } 574