| /* |
| * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012 Apple Inc. |
| * All rights reserved. |
| * Copyright (C) 2008, 2010 Nokia Corporation and/or its subsidiary(-ies) |
| * Copyright (C) 2007 Alp Toker <alp@atoker.com> |
| * Copyright (C) 2008 Eric Seidel <eric@webkit.org> |
| * Copyright (C) 2008 Dirk Schulze <krit@webkit.org> |
| * Copyright (C) 2010 Torch Mobile (Beijing) Co. Ltd. All rights reserved. |
| * Copyright (C) 2012, 2013 Intel Corporation. All rights reserved. |
| * Copyright (C) 2013 Adobe Systems Incorporated. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "modules/canvas2d/CanvasRenderingContext2D.h" |
| |
| #include "bindings/core/v8/ExceptionMessages.h" |
| #include "bindings/core/v8/ExceptionState.h" |
| #include "bindings/core/v8/ExceptionStatePlaceholder.h" |
| #include "bindings/modules/v8/RenderingContext.h" |
| #include "core/CSSPropertyNames.h" |
| #include "core/css/StylePropertySet.h" |
| #include "core/css/resolver/StyleResolver.h" |
| #include "core/dom/AXObjectCache.h" |
| #include "core/dom/StyleEngine.h" |
| #include "core/events/Event.h" |
| #include "core/events/MouseEvent.h" |
| #include "core/frame/Settings.h" |
| #include "core/html/TextMetrics.h" |
| #include "core/html/canvas/CanvasFontCache.h" |
| #include "core/layout/HitTestCanvasResult.h" |
| #include "core/layout/LayoutBox.h" |
| #include "core/layout/LayoutTheme.h" |
| #include "modules/canvas2d/CanvasStyle.h" |
| #include "modules/canvas2d/HitRegion.h" |
| #include "modules/canvas2d/Path2D.h" |
| #include "platform/fonts/FontCache.h" |
| #include "platform/graphics/DrawLooperBuilder.h" |
| #include "platform/graphics/ExpensiveCanvasHeuristicParameters.h" |
| #include "platform/graphics/ImageBuffer.h" |
| #include "platform/graphics/StrokeData.h" |
| #include "platform/graphics/skia/SkiaUtils.h" |
| #include "platform/text/BidiTextRun.h" |
| #include "public/platform/Platform.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkImageFilter.h" |
| #include "wtf/MathExtras.h" |
| #include "wtf/text/StringBuilder.h" |
| #include "wtf/typed_arrays/ArrayBufferContents.h" |
| |
| namespace blink { |
| |
| static const char defaultFont[] = "10px sans-serif"; |
| static const char inherit[] = "inherit"; |
| static const char rtl[] = "rtl"; |
| static const char ltr[] = "ltr"; |
| static const double TryRestoreContextInterval = 0.5; |
| static const unsigned MaxTryRestoreContextAttempts = 4; |
| static const double cDeviceScaleFactor = 1.0; // Canvas is device independent |
| |
| static bool contextLostRestoredEventsEnabled() { |
| return RuntimeEnabledFeatures::experimentalCanvasFeaturesEnabled(); |
| } |
| |
| // Drawing methods need to use this instead of SkAutoCanvasRestore in case |
| // overdraw detection substitutes the recording canvas (to discard overdrawn |
| // draw calls). |
| class CanvasRenderingContext2DAutoRestoreSkCanvas { |
| STACK_ALLOCATED(); |
| |
| public: |
| explicit CanvasRenderingContext2DAutoRestoreSkCanvas( |
| CanvasRenderingContext2D* context) |
| : m_context(context), m_saveCount(0) { |
| ASSERT(m_context); |
| SkCanvas* c = m_context->drawingCanvas(); |
| if (c) { |
| m_saveCount = c->getSaveCount(); |
| } |
| } |
| |
| ~CanvasRenderingContext2DAutoRestoreSkCanvas() { |
| SkCanvas* c = m_context->drawingCanvas(); |
| if (c) |
| c->restoreToCount(m_saveCount); |
| m_context->validateStateStack(); |
| } |
| |
| private: |
| Member<CanvasRenderingContext2D> m_context; |
| int m_saveCount; |
| }; |
| |
| CanvasRenderingContext2D::CanvasRenderingContext2D( |
| HTMLCanvasElement* canvas, |
| const CanvasContextCreationAttributes& attrs, |
| Document& document) |
| : CanvasRenderingContext(canvas, nullptr, attrs), |
| m_contextLostMode(NotLostContext), |
| m_contextRestorable(true), |
| m_tryRestoreContextAttemptCount(0), |
| m_dispatchContextLostEventTimer( |
| this, |
| &CanvasRenderingContext2D::dispatchContextLostEvent), |
| m_dispatchContextRestoredEventTimer( |
| this, |
| &CanvasRenderingContext2D::dispatchContextRestoredEvent), |
| m_tryRestoreContextEventTimer( |
| this, |
| &CanvasRenderingContext2D::tryRestoreContextEvent), |
| m_pruneLocalFontCacheScheduled(false) { |
| if (document.settings() && |
| document.settings()->antialiasedClips2dCanvasEnabled()) |
| m_clipAntialiasing = AntiAliased; |
| setShouldAntialias(true); |
| ThreadState::current()->registerPreFinalizer(this); |
| validateStateStack(); |
| } |
| |
| void CanvasRenderingContext2D::setCanvasGetContextResult( |
| RenderingContext& result) { |
| result.setCanvasRenderingContext2D(this); |
| } |
| |
| CanvasRenderingContext2D::~CanvasRenderingContext2D() {} |
| |
| void CanvasRenderingContext2D::dispose() { |
| if (m_pruneLocalFontCacheScheduled) |
| Platform::current()->currentThread()->removeTaskObserver(this); |
| } |
| |
| void CanvasRenderingContext2D::validateStateStack() const { |
| #if DCHECK_IS_ON() |
| if (SkCanvas* skCanvas = canvas()->existingDrawingCanvas()) { |
| // The canvas should always have an initial save frame, to support |
| // resetting the top level matrix and clip. |
| DCHECK_GT(skCanvas->getSaveCount(), 1); |
| |
| if (m_contextLostMode == NotLostContext) { |
| DCHECK_EQ(static_cast<size_t>(skCanvas->getSaveCount()), |
| m_stateStack.size() + 1); |
| } |
| } |
| #endif |
| CHECK(m_stateStack.first() |
| .get()); // Temporary for investigating crbug.com/648510 |
| } |
| |
| bool CanvasRenderingContext2D::isAccelerated() const { |
| if (!canvas()->hasImageBuffer()) |
| return false; |
| return canvas()->buffer()->isAccelerated(); |
| } |
| |
| void CanvasRenderingContext2D::stop() { |
| if (!isContextLost()) { |
| // Never attempt to restore the context because the page is being torn down. |
| loseContext(SyntheticLostContext); |
| } |
| } |
| |
| bool CanvasRenderingContext2D::isContextLost() const { |
| return m_contextLostMode != NotLostContext; |
| } |
| |
| void CanvasRenderingContext2D::loseContext(LostContextMode lostMode) { |
| if (m_contextLostMode != NotLostContext) |
| return; |
| m_contextLostMode = lostMode; |
| if (m_contextLostMode == SyntheticLostContext && canvas()) { |
| canvas()->discardImageBuffer(); |
| } |
| m_dispatchContextLostEventTimer.startOneShot(0, BLINK_FROM_HERE); |
| } |
| |
| void CanvasRenderingContext2D::didSetSurfaceSize() { |
| if (!m_contextRestorable) |
| return; |
| // This code path is for restoring from an eviction |
| // Restoring from surface failure is handled internally |
| ASSERT(m_contextLostMode != NotLostContext && !canvas()->hasImageBuffer()); |
| |
| if (canvas()->buffer()) { |
| if (contextLostRestoredEventsEnabled()) { |
| m_dispatchContextRestoredEventTimer.startOneShot(0, BLINK_FROM_HERE); |
| } else { |
| // legacy synchronous context restoration. |
| reset(); |
| m_contextLostMode = NotLostContext; |
| } |
| } |
| } |
| |
| DEFINE_TRACE(CanvasRenderingContext2D) { |
| visitor->trace(m_hitRegionManager); |
| visitor->trace(m_filterOperations); |
| CanvasRenderingContext::trace(visitor); |
| BaseRenderingContext2D::trace(visitor); |
| SVGResourceClient::trace(visitor); |
| } |
| |
| void CanvasRenderingContext2D::dispatchContextLostEvent(TimerBase*) { |
| if (canvas() && contextLostRestoredEventsEnabled()) { |
| Event* event = Event::createCancelable(EventTypeNames::contextlost); |
| canvas()->dispatchEvent(event); |
| if (event->defaultPrevented()) { |
| m_contextRestorable = false; |
| } |
| } |
| |
| // If RealLostContext, it means the context was not lost due to surface |
| // failure but rather due to a an eviction, which means image buffer exists. |
| if (m_contextRestorable && m_contextLostMode == RealLostContext) { |
| m_tryRestoreContextAttemptCount = 0; |
| m_tryRestoreContextEventTimer.startRepeating(TryRestoreContextInterval, |
| BLINK_FROM_HERE); |
| } |
| } |
| |
| void CanvasRenderingContext2D::tryRestoreContextEvent(TimerBase* timer) { |
| if (m_contextLostMode == NotLostContext) { |
| // Canvas was already restored (possibly thanks to a resize), so stop |
| // trying. |
| m_tryRestoreContextEventTimer.stop(); |
| return; |
| } |
| |
| DCHECK(m_contextLostMode == RealLostContext); |
| if (canvas()->hasImageBuffer() && canvas()->buffer()->restoreSurface()) { |
| m_tryRestoreContextEventTimer.stop(); |
| dispatchContextRestoredEvent(nullptr); |
| } |
| |
| if (++m_tryRestoreContextAttemptCount > MaxTryRestoreContextAttempts) { |
| // final attempt: allocate a brand new image buffer instead of restoring |
| canvas()->discardImageBuffer(); |
| m_tryRestoreContextEventTimer.stop(); |
| if (canvas()->buffer()) |
| dispatchContextRestoredEvent(nullptr); |
| } |
| } |
| |
| void CanvasRenderingContext2D::dispatchContextRestoredEvent(TimerBase*) { |
| if (m_contextLostMode == NotLostContext) |
| return; |
| reset(); |
| m_contextLostMode = NotLostContext; |
| if (contextLostRestoredEventsEnabled()) { |
| Event* event(Event::create(EventTypeNames::contextrestored)); |
| canvas()->dispatchEvent(event); |
| } |
| } |
| |
| void CanvasRenderingContext2D::reset() { |
| // This is a multiple inherritance bootstrap |
| BaseRenderingContext2D::reset(); |
| } |
| |
| void CanvasRenderingContext2D::restoreCanvasMatrixClipStack(SkCanvas* c) const { |
| restoreMatrixClipStack(c); |
| } |
| |
| bool CanvasRenderingContext2D::shouldAntialias() const { |
| return state().shouldAntialias(); |
| } |
| |
| void CanvasRenderingContext2D::setShouldAntialias(bool doAA) { |
| modifiableState().setShouldAntialias(doAA); |
| } |
| |
| void CanvasRenderingContext2D::scrollPathIntoView() { |
| scrollPathIntoViewInternal(m_path); |
| } |
| |
| void CanvasRenderingContext2D::scrollPathIntoView(Path2D* path2d) { |
| scrollPathIntoViewInternal(path2d->path()); |
| } |
| |
| void CanvasRenderingContext2D::scrollPathIntoViewInternal(const Path& path) { |
| if (!state().isTransformInvertible() || path.isEmpty()) |
| return; |
| |
| canvas()->document().updateStyleAndLayoutIgnorePendingStylesheets(); |
| |
| LayoutObject* renderer = canvas()->layoutObject(); |
| LayoutBox* layoutBox = canvas()->layoutBox(); |
| if (!renderer || !layoutBox) |
| return; |
| |
| // Apply transformation and get the bounding rect |
| Path transformedPath = path; |
| transformedPath.transform(state().transform()); |
| FloatRect boundingRect = transformedPath.boundingRect(); |
| |
| // Offset by the canvas rect |
| LayoutRect pathRect(boundingRect); |
| IntRect canvasRect = layoutBox->absoluteContentBox(); |
| pathRect.moveBy(canvasRect.location()); |
| |
| renderer->scrollRectToVisible(pathRect, ScrollAlignment::alignCenterAlways, |
| ScrollAlignment::alignTopAlways); |
| |
| // TODO: should implement "inform the user" that the caret and/or |
| // selection the specified rectangle of the canvas. See |
| // http://crbug.com/357987 |
| } |
| |
| void CanvasRenderingContext2D::clearRect(double x, |
| double y, |
| double width, |
| double height) { |
| BaseRenderingContext2D::clearRect(x, y, width, height); |
| |
| if (m_hitRegionManager) { |
| FloatRect rect(x, y, width, height); |
| m_hitRegionManager->removeHitRegionsInRect(rect, state().transform()); |
| } |
| } |
| |
| void CanvasRenderingContext2D::didDraw(const SkIRect& dirtyRect) { |
| if (dirtyRect.isEmpty()) |
| return; |
| |
| if (ExpensiveCanvasHeuristicParameters::BlurredShadowsAreExpensive && |
| state().shouldDrawShadows() && state().shadowBlur() > 0) { |
| ImageBuffer* buffer = canvas()->buffer(); |
| if (buffer) |
| buffer->setHasExpensiveOp(); |
| } |
| |
| canvas()->didDraw(SkRect::Make(dirtyRect)); |
| } |
| |
| bool CanvasRenderingContext2D::stateHasFilter() { |
| return state().hasFilter(canvas(), canvas()->size(), this); |
| } |
| |
| sk_sp<SkImageFilter> CanvasRenderingContext2D::stateGetFilter() { |
| return state().getFilter(canvas(), canvas()->size(), this); |
| } |
| |
| void CanvasRenderingContext2D::snapshotStateForFilter() { |
| // The style resolution required for fonts is not available in frame-less |
| // documents. |
| if (!canvas()->document().frame()) |
| return; |
| |
| modifiableState().setFontForFilter(accessFont()); |
| } |
| |
| SkCanvas* CanvasRenderingContext2D::drawingCanvas() const { |
| if (isContextLost()) |
| return nullptr; |
| return canvas()->drawingCanvas(); |
| } |
| |
| SkCanvas* CanvasRenderingContext2D::existingDrawingCanvas() const { |
| return canvas()->existingDrawingCanvas(); |
| } |
| |
| void CanvasRenderingContext2D::disableDeferral(DisableDeferralReason reason) { |
| canvas()->disableDeferral(reason); |
| } |
| |
| AffineTransform CanvasRenderingContext2D::baseTransform() const { |
| return canvas()->baseTransform(); |
| } |
| |
| String CanvasRenderingContext2D::font() const { |
| if (!state().hasRealizedFont()) |
| return defaultFont; |
| |
| canvas()->document().canvasFontCache()->willUseCurrentFont(); |
| StringBuilder serializedFont; |
| const FontDescription& fontDescription = state().font().getFontDescription(); |
| |
| if (fontDescription.style() == FontStyleItalic) |
| serializedFont.append("italic "); |
| if (fontDescription.weight() == FontWeightBold) |
| serializedFont.append("bold "); |
| if (fontDescription.variantCaps() == FontDescription::SmallCaps) |
| serializedFont.append("small-caps "); |
| |
| serializedFont.appendNumber(fontDescription.computedPixelSize()); |
| serializedFont.append("px"); |
| |
| const FontFamily& firstFontFamily = fontDescription.family(); |
| for (const FontFamily* fontFamily = &firstFontFamily; fontFamily; |
| fontFamily = fontFamily->next()) { |
| if (fontFamily != &firstFontFamily) |
| serializedFont.append(','); |
| |
| // FIXME: We should append family directly to serializedFont rather than |
| // building a temporary string. |
| String family = fontFamily->family(); |
| if (family.startsWith("-webkit-")) |
| family = family.substring(8); |
| if (family.contains(' ')) |
| family = "\"" + family + "\""; |
| |
| serializedFont.append(' '); |
| serializedFont.append(family); |
| } |
| |
| return serializedFont.toString(); |
| } |
| |
| void CanvasRenderingContext2D::setFont(const String& newFont) { |
| // The style resolution required for fonts is not available in frame-less |
| // documents. |
| if (!canvas()->document().frame()) |
| return; |
| |
| canvas()->document().updateStyleAndLayoutTreeForNode(canvas()); |
| |
| // The following early exit is dependent on the cache not being empty |
| // because an empty cache may indicate that a style change has occured |
| // which would require that the font be re-resolved. This check has to |
| // come after the layout tree update to flush pending style changes. |
| if (newFont == state().unparsedFont() && state().hasRealizedFont() && |
| m_fontsResolvedUsingCurrentStyle.size() > 0) |
| return; |
| |
| CanvasFontCache* canvasFontCache = canvas()->document().canvasFontCache(); |
| |
| // Map the <canvas> font into the text style. If the font uses keywords like |
| // larger/smaller, these will work relative to the canvas. |
| RefPtr<ComputedStyle> fontStyle; |
| const ComputedStyle* computedStyle = canvas()->ensureComputedStyle(); |
| if (computedStyle) { |
| HashMap<String, Font>::iterator i = |
| m_fontsResolvedUsingCurrentStyle.find(newFont); |
| if (i != m_fontsResolvedUsingCurrentStyle.end()) { |
| ASSERT(m_fontLRUList.contains(newFont)); |
| m_fontLRUList.remove(newFont); |
| m_fontLRUList.add(newFont); |
| modifiableState().setFont( |
| i->value, canvas()->document().styleEngine().fontSelector()); |
| } else { |
| MutableStylePropertySet* parsedStyle = |
| canvasFontCache->parseFont(newFont); |
| if (!parsedStyle) |
| return; |
| fontStyle = ComputedStyle::create(); |
| FontDescription elementFontDescription( |
| computedStyle->getFontDescription()); |
| // Reset the computed size to avoid inheriting the zoom factor from the |
| // <canvas> element. |
| elementFontDescription.setComputedSize( |
| elementFontDescription.specifiedSize()); |
| fontStyle->setFontDescription(elementFontDescription); |
| fontStyle->font().update(fontStyle->font().getFontSelector()); |
| canvas()->document().ensureStyleResolver().computeFont(fontStyle.get(), |
| *parsedStyle); |
| m_fontsResolvedUsingCurrentStyle.add(newFont, fontStyle->font()); |
| ASSERT(!m_fontLRUList.contains(newFont)); |
| m_fontLRUList.add(newFont); |
| pruneLocalFontCache(canvasFontCache->hardMaxFonts()); // hard limit |
| schedulePruneLocalFontCacheIfNeeded(); // soft limit |
| modifiableState().setFont( |
| fontStyle->font(), canvas()->document().styleEngine().fontSelector()); |
| } |
| } else { |
| Font resolvedFont; |
| if (!canvasFontCache->getFontUsingDefaultStyle(newFont, resolvedFont)) |
| return; |
| modifiableState().setFont( |
| resolvedFont, canvas()->document().styleEngine().fontSelector()); |
| } |
| |
| // The parse succeeded. |
| String newFontSafeCopy(newFont); // Create a string copy since newFont can be |
| // deleted inside realizeSaves. |
| modifiableState().setUnparsedFont(newFontSafeCopy); |
| } |
| |
| void CanvasRenderingContext2D::schedulePruneLocalFontCacheIfNeeded() { |
| if (m_pruneLocalFontCacheScheduled) |
| return; |
| m_pruneLocalFontCacheScheduled = true; |
| Platform::current()->currentThread()->addTaskObserver(this); |
| } |
| |
| void CanvasRenderingContext2D::didProcessTask() { |
| Platform::current()->currentThread()->removeTaskObserver(this); |
| |
| // This should be the only place where canvas() needs to be checked for |
| // nullness because the circular refence with HTMLCanvasElement mean the |
| // canvas and the context keep each other alive as long as the pair is |
| // referenced the task observer is the only persisten refernce to this object |
| // that is not traced, so didProcessTask() may be call at a time when the |
| // canvas has been garbage collected but not the context. |
| if (!canvas()) |
| return; |
| |
| // The rendering surface needs to be prepared now because it will be too late |
| // to create a layer once we are in the paint invalidation phase. |
| canvas()->prepareSurfaceForPaintingIfNeeded(); |
| |
| pruneLocalFontCache(canvas()->document().canvasFontCache()->maxFonts()); |
| m_pruneLocalFontCacheScheduled = false; |
| } |
| |
| void CanvasRenderingContext2D::pruneLocalFontCache(size_t targetSize) { |
| if (targetSize == 0) { |
| // Short cut: LRU does not matter when evicting everything |
| m_fontLRUList.clear(); |
| m_fontsResolvedUsingCurrentStyle.clear(); |
| return; |
| } |
| while (m_fontLRUList.size() > targetSize) { |
| m_fontsResolvedUsingCurrentStyle.remove(m_fontLRUList.first()); |
| m_fontLRUList.removeFirst(); |
| } |
| } |
| |
| void CanvasRenderingContext2D::styleDidChange(const ComputedStyle* oldStyle, |
| const ComputedStyle& newStyle) { |
| if (oldStyle && oldStyle->font() == newStyle.font()) |
| return; |
| pruneLocalFontCache(0); |
| } |
| |
| TreeScope* CanvasRenderingContext2D::treeScope() { |
| return &canvas()->treeScope(); |
| } |
| |
| void CanvasRenderingContext2D::clearFilterReferences() { |
| m_filterOperations.removeClient(this); |
| m_filterOperations.clear(); |
| } |
| |
| void CanvasRenderingContext2D::updateFilterReferences( |
| const FilterOperations& filters) { |
| clearFilterReferences(); |
| filters.addClient(this); |
| m_filterOperations = filters; |
| } |
| |
| void CanvasRenderingContext2D::resourceContentChanged() { |
| resourceElementChanged(); |
| } |
| |
| void CanvasRenderingContext2D::resourceElementChanged() { |
| clearFilterReferences(); |
| state().clearResolvedFilter(); |
| } |
| |
| bool CanvasRenderingContext2D::originClean() const { |
| return canvas()->originClean(); |
| } |
| |
| void CanvasRenderingContext2D::setOriginTainted() { |
| return canvas()->setOriginTainted(); |
| } |
| |
| int CanvasRenderingContext2D::width() const { |
| return canvas()->width(); |
| } |
| |
| int CanvasRenderingContext2D::height() const { |
| return canvas()->height(); |
| } |
| |
| bool CanvasRenderingContext2D::hasImageBuffer() const { |
| return canvas()->hasImageBuffer(); |
| } |
| |
| ImageBuffer* CanvasRenderingContext2D::imageBuffer() const { |
| return canvas()->buffer(); |
| } |
| |
| PassRefPtr<Image> blink::CanvasRenderingContext2D::getImage( |
| AccelerationHint hint, |
| SnapshotReason reason) const { |
| if (!hasImageBuffer()) |
| return nullptr; |
| return canvas()->buffer()->newImageSnapshot(hint, reason); |
| } |
| |
| bool CanvasRenderingContext2D::parseColorOrCurrentColor( |
| Color& color, |
| const String& colorString) const { |
| return ::blink::parseColorOrCurrentColor(color, colorString, canvas()); |
| } |
| |
| HitTestCanvasResult* CanvasRenderingContext2D::getControlAndIdIfHitRegionExists( |
| const LayoutPoint& location) { |
| if (hitRegionsCount() <= 0) |
| return HitTestCanvasResult::create(String(), nullptr); |
| |
| LayoutBox* box = canvas()->layoutBox(); |
| FloatPoint localPos = |
| box->absoluteToLocal(FloatPoint(location), UseTransforms); |
| if (box->hasBorderOrPadding()) |
| localPos.move(-box->contentBoxOffset()); |
| localPos.scale(canvas()->width() / box->contentWidth(), |
| canvas()->height() / box->contentHeight()); |
| |
| HitRegion* hitRegion = hitRegionAtPoint(localPos); |
| if (hitRegion) { |
| Element* control = hitRegion->control(); |
| if (control && canvas()->isSupportedInteractiveCanvasFallback(*control)) |
| return HitTestCanvasResult::create(hitRegion->id(), hitRegion->control()); |
| return HitTestCanvasResult::create(hitRegion->id(), nullptr); |
| } |
| return HitTestCanvasResult::create(String(), nullptr); |
| } |
| |
| String CanvasRenderingContext2D::getIdFromControl(const Element* element) { |
| if (hitRegionsCount() <= 0) |
| return String(); |
| |
| if (HitRegion* hitRegion = m_hitRegionManager->getHitRegionByControl(element)) |
| return hitRegion->id(); |
| return String(); |
| } |
| |
| String CanvasRenderingContext2D::textAlign() const { |
| return textAlignName(state().getTextAlign()); |
| } |
| |
| void CanvasRenderingContext2D::setTextAlign(const String& s) { |
| TextAlign align; |
| if (!parseTextAlign(s, align)) |
| return; |
| if (state().getTextAlign() == align) |
| return; |
| modifiableState().setTextAlign(align); |
| } |
| |
| String CanvasRenderingContext2D::textBaseline() const { |
| return textBaselineName(state().getTextBaseline()); |
| } |
| |
| void CanvasRenderingContext2D::setTextBaseline(const String& s) { |
| TextBaseline baseline; |
| if (!parseTextBaseline(s, baseline)) |
| return; |
| if (state().getTextBaseline() == baseline) |
| return; |
| modifiableState().setTextBaseline(baseline); |
| } |
| |
| static inline TextDirection toTextDirection( |
| CanvasRenderingContext2DState::Direction direction, |
| HTMLCanvasElement* canvas, |
| const ComputedStyle** computedStyle = 0) { |
| const ComputedStyle* style = |
| (computedStyle || |
| direction == CanvasRenderingContext2DState::DirectionInherit) |
| ? canvas->ensureComputedStyle() |
| : nullptr; |
| if (computedStyle) |
| *computedStyle = style; |
| switch (direction) { |
| case CanvasRenderingContext2DState::DirectionInherit: |
| return style ? style->direction() : LTR; |
| case CanvasRenderingContext2DState::DirectionRTL: |
| return RTL; |
| case CanvasRenderingContext2DState::DirectionLTR: |
| return LTR; |
| } |
| ASSERT_NOT_REACHED(); |
| return LTR; |
| } |
| |
| String CanvasRenderingContext2D::direction() const { |
| if (state().getDirection() == CanvasRenderingContext2DState::DirectionInherit) |
| canvas()->document().updateStyleAndLayoutTreeForNode(canvas()); |
| return toTextDirection(state().getDirection(), canvas()) == RTL ? rtl : ltr; |
| } |
| |
| void CanvasRenderingContext2D::setDirection(const String& directionString) { |
| CanvasRenderingContext2DState::Direction direction; |
| if (directionString == inherit) |
| direction = CanvasRenderingContext2DState::DirectionInherit; |
| else if (directionString == rtl) |
| direction = CanvasRenderingContext2DState::DirectionRTL; |
| else if (directionString == ltr) |
| direction = CanvasRenderingContext2DState::DirectionLTR; |
| else |
| return; |
| |
| if (state().getDirection() == direction) |
| return; |
| |
| modifiableState().setDirection(direction); |
| } |
| |
| void CanvasRenderingContext2D::fillText(const String& text, |
| double x, |
| double y) { |
| trackDrawCall(FillText); |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::FillPaintType); |
| } |
| |
| void CanvasRenderingContext2D::fillText(const String& text, |
| double x, |
| double y, |
| double maxWidth) { |
| trackDrawCall(FillText); |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::FillPaintType, |
| &maxWidth); |
| } |
| |
| void CanvasRenderingContext2D::strokeText(const String& text, |
| double x, |
| double y) { |
| trackDrawCall(StrokeText); |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::StrokePaintType); |
| } |
| |
| void CanvasRenderingContext2D::strokeText(const String& text, |
| double x, |
| double y, |
| double maxWidth) { |
| trackDrawCall(StrokeText); |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::StrokePaintType, |
| &maxWidth); |
| } |
| |
| TextMetrics* CanvasRenderingContext2D::measureText(const String& text) { |
| TextMetrics* metrics = TextMetrics::create(); |
| |
| // The style resolution required for fonts is not available in frame-less |
| // documents. |
| if (!canvas()->document().frame()) |
| return metrics; |
| |
| canvas()->document().updateStyleAndLayoutTreeForNode(canvas()); |
| const Font& font = accessFont(); |
| const SimpleFontData* fontData = font.primaryFont(); |
| DCHECK(fontData); |
| if (!fontData) |
| return metrics; |
| |
| TextDirection direction; |
| if (state().getDirection() == CanvasRenderingContext2DState::DirectionInherit) |
| direction = determineDirectionality(text); |
| else |
| direction = toTextDirection(state().getDirection(), canvas()); |
| TextRun textRun(text, 0, 0, TextRun::AllowTrailingExpansion | |
| TextRun::ForbidLeadingExpansion, |
| direction, false); |
| textRun.setNormalizeSpace(true); |
| FloatRect textBounds = font.selectionRectForText( |
| textRun, FloatPoint(), font.getFontDescription().computedSize(), 0, -1, |
| true); |
| |
| // x direction |
| metrics->setWidth(font.width(textRun)); |
| metrics->setActualBoundingBoxLeft(-textBounds.x()); |
| metrics->setActualBoundingBoxRight(textBounds.maxX()); |
| |
| // y direction |
| const FontMetrics& fontMetrics = fontData->getFontMetrics(); |
| const float ascent = fontMetrics.floatAscent(); |
| const float descent = fontMetrics.floatDescent(); |
| const float baselineY = getFontBaseline(fontMetrics); |
| |
| metrics->setFontBoundingBoxAscent(ascent - baselineY); |
| metrics->setFontBoundingBoxDescent(descent + baselineY); |
| metrics->setActualBoundingBoxAscent(-textBounds.y() - baselineY); |
| metrics->setActualBoundingBoxDescent(textBounds.maxY() + baselineY); |
| |
| // Note : top/bottom and ascend/descend are currently the same, so there's no |
| // difference between the EM box's top and bottom and the font's ascend and |
| // descend |
| metrics->setEmHeightAscent(0); |
| metrics->setEmHeightDescent(0); |
| |
| metrics->setHangingBaseline(0.8f * ascent - baselineY); |
| metrics->setAlphabeticBaseline(-baselineY); |
| metrics->setIdeographicBaseline(-descent - baselineY); |
| return metrics; |
| } |
| |
| void CanvasRenderingContext2D::drawTextInternal( |
| const String& text, |
| double x, |
| double y, |
| CanvasRenderingContext2DState::PaintType paintType, |
| double* maxWidth) { |
| // The style resolution required for fonts is not available in frame-less |
| // documents. |
| if (!canvas()->document().frame()) |
| return; |
| |
| // accessFont needs the style to be up to date, but updating style can cause |
| // script to run, (e.g. due to autofocus) which can free the canvas (set size |
| // to 0, for example), so update style before grabbing the drawingCanvas. |
| canvas()->document().updateStyleAndLayoutTreeForNode(canvas()); |
| |
| SkCanvas* c = drawingCanvas(); |
| if (!c) |
| return; |
| |
| if (!std::isfinite(x) || !std::isfinite(y)) |
| return; |
| if (maxWidth && (!std::isfinite(*maxWidth) || *maxWidth <= 0)) |
| return; |
| |
| // Currently, SkPictureImageFilter does not support subpixel text |
| // anti-aliasing, which is expected when !creationAttributes().alpha(), so we |
| // need to fall out of display list mode when drawing text to an opaque |
| // canvas. crbug.com/583809 |
| if (!creationAttributes().alpha() && !isAccelerated()) |
| canvas()->disableDeferral( |
| DisableDeferralReasonSubPixelTextAntiAliasingSupport); |
| |
| const Font& font = accessFont(); |
| font.getFontDescription().setSubpixelAscentDescent(true); |
| const SimpleFontData* fontData = font.primaryFont(); |
| DCHECK(fontData); |
| if (!fontData) |
| return; |
| const FontMetrics& fontMetrics = fontData->getFontMetrics(); |
| |
| // FIXME: Need to turn off font smoothing. |
| |
| const ComputedStyle* computedStyle = 0; |
| TextDirection direction = |
| toTextDirection(state().getDirection(), canvas(), &computedStyle); |
| bool isRTL = direction == RTL; |
| bool override = |
| computedStyle ? isOverride(computedStyle->unicodeBidi()) : false; |
| |
| TextRun textRun(text, 0, 0, TextRun::AllowTrailingExpansion, direction, |
| override); |
| textRun.setNormalizeSpace(true); |
| // Draw the item text at the correct point. |
| FloatPoint location(x, y + getFontBaseline(fontMetrics)); |
| double fontWidth = font.width(textRun); |
| |
| bool useMaxWidth = (maxWidth && *maxWidth < fontWidth); |
| double width = useMaxWidth ? *maxWidth : fontWidth; |
| |
| TextAlign align = state().getTextAlign(); |
| if (align == StartTextAlign) |
| align = isRTL ? RightTextAlign : LeftTextAlign; |
| else if (align == EndTextAlign) |
| align = isRTL ? LeftTextAlign : RightTextAlign; |
| |
| switch (align) { |
| case CenterTextAlign: |
| location.setX(location.x() - width / 2); |
| break; |
| case RightTextAlign: |
| location.setX(location.x() - width); |
| break; |
| default: |
| break; |
| } |
| |
| // The slop built in to this mask rect matches the heuristic used in |
| // FontCGWin.cpp for GDI text. |
| TextRunPaintInfo textRunPaintInfo(textRun); |
| textRunPaintInfo.bounds = |
| FloatRect(location.x() - fontMetrics.height() / 2, |
| location.y() - fontMetrics.ascent() - fontMetrics.lineGap(), |
| width + fontMetrics.height(), fontMetrics.lineSpacing()); |
| if (paintType == CanvasRenderingContext2DState::StrokePaintType) |
| inflateStrokeRect(textRunPaintInfo.bounds); |
| |
| CanvasRenderingContext2DAutoRestoreSkCanvas stateRestorer(this); |
| if (useMaxWidth) { |
| drawingCanvas()->save(); |
| drawingCanvas()->translate(location.x(), location.y()); |
| // We draw when fontWidth is 0 so compositing operations (eg, a "copy" op) |
| // still work. |
| drawingCanvas()->scale((fontWidth > 0 ? (width / fontWidth) : 0), 1); |
| location = FloatPoint(); |
| } |
| |
| draw( |
| [&font, this, &textRunPaintInfo, &location]( |
| SkCanvas* c, const SkPaint* paint) // draw lambda |
| { |
| font.drawBidiText(c, textRunPaintInfo, location, |
| Font::UseFallbackIfFontNotReady, cDeviceScaleFactor, |
| *paint); |
| }, |
| [](const SkIRect& rect) // overdraw test lambda |
| { return false; }, |
| textRunPaintInfo.bounds, paintType); |
| } |
| |
| const Font& CanvasRenderingContext2D::accessFont() { |
| if (!state().hasRealizedFont()) |
| setFont(state().unparsedFont()); |
| canvas()->document().canvasFontCache()->willUseCurrentFont(); |
| return state().font(); |
| } |
| |
| float CanvasRenderingContext2D::getFontBaseline( |
| const FontMetrics& fontMetrics) const { |
| // If the font is so tiny that the lroundf operations result in two |
| // different types of text baselines to return the same baseline, use |
| // floating point metrics (crbug.com/338908). |
| // If you changed the heuristic here, for consistency please also change it |
| // in SimpleFontData::platformInit(). |
| bool useFloatAscentDescent = |
| fontMetrics.ascent() < 3 || fontMetrics.height() < 2; |
| switch (state().getTextBaseline()) { |
| case TopTextBaseline: |
| return useFloatAscentDescent ? fontMetrics.floatAscent() |
| : fontMetrics.ascent(); |
| case HangingTextBaseline: |
| // According to |
| // http://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling |
| // "FOP (Formatting Objects Processor) puts the hanging baseline at 80% of |
| // the ascender height" |
| return useFloatAscentDescent ? (fontMetrics.floatAscent() * 4.0) / 5.0 |
| : (fontMetrics.ascent() * 4) / 5; |
| case BottomTextBaseline: |
| case IdeographicTextBaseline: |
| return useFloatAscentDescent ? -fontMetrics.floatDescent() |
| : -fontMetrics.descent(); |
| case MiddleTextBaseline: |
| return useFloatAscentDescent |
| ? -fontMetrics.floatDescent() + fontMetrics.floatHeight() / 2.0 |
| : -fontMetrics.descent() + fontMetrics.height() / 2; |
| case AlphabeticTextBaseline: |
| default: |
| // Do nothing. |
| break; |
| } |
| return 0; |
| } |
| |
| void CanvasRenderingContext2D::setIsHidden(bool hidden) { |
| if (canvas()->hasImageBuffer()) |
| canvas()->buffer()->setIsHidden(hidden); |
| if (hidden) { |
| pruneLocalFontCache(0); |
| } |
| } |
| |
| bool CanvasRenderingContext2D::isTransformInvertible() const { |
| return state().isTransformInvertible(); |
| } |
| |
| WebLayer* CanvasRenderingContext2D::platformLayer() const { |
| return canvas()->buffer() ? canvas()->buffer()->platformLayer() : 0; |
| } |
| |
| void CanvasRenderingContext2D::getContextAttributes( |
| Canvas2DContextAttributes& attrs) const { |
| attrs.setAlpha(creationAttributes().alpha()); |
| attrs.setColorSpace(colorSpaceAsString()); |
| } |
| |
| void CanvasRenderingContext2D::drawFocusIfNeeded(Element* element) { |
| drawFocusIfNeededInternal(m_path, element); |
| } |
| |
| void CanvasRenderingContext2D::drawFocusIfNeeded(Path2D* path2d, |
| Element* element) { |
| drawFocusIfNeededInternal(path2d->path(), element); |
| } |
| |
| void CanvasRenderingContext2D::drawFocusIfNeededInternal(const Path& path, |
| Element* element) { |
| if (!focusRingCallIsValid(path, element)) |
| return; |
| |
| // Note: we need to check document->focusedElement() rather than just calling |
| // element->focused(), because element->focused() isn't updated until after |
| // focus events fire. |
| if (element->document().focusedElement() == element) { |
| scrollPathIntoViewInternal(path); |
| drawFocusRing(path); |
| } |
| |
| // Update its accessible bounds whether it's focused or not. |
| updateElementAccessibility(path, element); |
| } |
| |
| bool CanvasRenderingContext2D::focusRingCallIsValid(const Path& path, |
| Element* element) { |
| ASSERT(element); |
| if (!state().isTransformInvertible()) |
| return false; |
| if (path.isEmpty()) |
| return false; |
| if (!element->isDescendantOf(canvas())) |
| return false; |
| |
| return true; |
| } |
| |
| void CanvasRenderingContext2D::drawFocusRing(const Path& path) { |
| m_usageCounters.numDrawFocusCalls++; |
| if (!drawingCanvas()) |
| return; |
| |
| SkColor color = LayoutTheme::theme().focusRingColor().rgb(); |
| const int focusRingWidth = 5; |
| |
| drawPlatformFocusRing(path.getSkPath(), drawingCanvas(), color, |
| focusRingWidth); |
| |
| // We need to add focusRingWidth to dirtyRect. |
| StrokeData strokeData; |
| strokeData.setThickness(focusRingWidth); |
| |
| SkIRect dirtyRect; |
| if (!computeDirtyRect(path.strokeBoundingRect(strokeData), &dirtyRect)) |
| return; |
| |
| didDraw(dirtyRect); |
| } |
| |
| void CanvasRenderingContext2D::updateElementAccessibility(const Path& path, |
| Element* element) { |
| element->document().updateStyleAndLayoutIgnorePendingStylesheets(); |
| AXObjectCache* axObjectCache = element->document().existingAXObjectCache(); |
| LayoutBoxModelObject* lbmo = canvas()->layoutBoxModelObject(); |
| LayoutObject* renderer = canvas()->layoutObject(); |
| if (!axObjectCache || !lbmo || !renderer) |
| return; |
| |
| // Get the transformed path. |
| Path transformedPath = path; |
| transformedPath.transform(state().transform()); |
| |
| // Add border and padding to the bounding rect. |
| LayoutRect elementRect = enclosingLayoutRect(transformedPath.boundingRect()); |
| elementRect.move(lbmo->borderLeft() + lbmo->paddingLeft(), |
| lbmo->borderTop() + lbmo->paddingTop()); |
| |
| // Update the accessible object. |
| axObjectCache->setCanvasObjectBounds(canvas(), element, elementRect); |
| } |
| |
| void CanvasRenderingContext2D::addHitRegion(const HitRegionOptions& options, |
| ExceptionState& exceptionState) { |
| if (options.id().isEmpty() && !options.control()) { |
| exceptionState.throwDOMException(NotSupportedError, |
| "Both id and control are null."); |
| return; |
| } |
| |
| if (options.control() && |
| !canvas()->isSupportedInteractiveCanvasFallback(*options.control())) { |
| exceptionState.throwDOMException(NotSupportedError, |
| "The control is neither null nor a " |
| "supported interactive canvas fallback " |
| "element."); |
| return; |
| } |
| |
| Path hitRegionPath = options.hasPath() ? options.path()->path() : m_path; |
| |
| SkCanvas* c = drawingCanvas(); |
| |
| if (hitRegionPath.isEmpty() || !c || !state().isTransformInvertible() || |
| !c->getClipDeviceBounds(0)) { |
| exceptionState.throwDOMException(NotSupportedError, |
| "The specified path has no pixels."); |
| return; |
| } |
| |
| hitRegionPath.transform(state().transform()); |
| |
| if (state().hasClip()) { |
| hitRegionPath.intersectPath(state().getCurrentClipPath()); |
| if (hitRegionPath.isEmpty()) |
| exceptionState.throwDOMException(NotSupportedError, |
| "The specified path has no pixels."); |
| } |
| |
| if (!m_hitRegionManager) |
| m_hitRegionManager = HitRegionManager::create(); |
| |
| // Remove previous region (with id or control) |
| m_hitRegionManager->removeHitRegionById(options.id()); |
| m_hitRegionManager->removeHitRegionByControl(options.control()); |
| |
| HitRegion* hitRegion = HitRegion::create(hitRegionPath, options); |
| Element* element = hitRegion->control(); |
| if (element && element->isDescendantOf(canvas())) |
| updateElementAccessibility(hitRegion->path(), hitRegion->control()); |
| m_hitRegionManager->addHitRegion(hitRegion); |
| } |
| |
| void CanvasRenderingContext2D::removeHitRegion(const String& id) { |
| if (m_hitRegionManager) |
| m_hitRegionManager->removeHitRegionById(id); |
| } |
| |
| void CanvasRenderingContext2D::clearHitRegions() { |
| if (m_hitRegionManager) |
| m_hitRegionManager->removeAllHitRegions(); |
| } |
| |
| HitRegion* CanvasRenderingContext2D::hitRegionAtPoint(const FloatPoint& point) { |
| if (m_hitRegionManager) |
| return m_hitRegionManager->getHitRegionAtPoint(point); |
| |
| return nullptr; |
| } |
| |
| unsigned CanvasRenderingContext2D::hitRegionsCount() const { |
| if (m_hitRegionManager) |
| return m_hitRegionManager->getHitRegionsCount(); |
| |
| return 0; |
| } |
| |
| bool CanvasRenderingContext2D::isAccelerationOptimalForCanvasContent() const { |
| // Heuristic to determine if the GPU accelerated rendering pipeline is optimal |
| // for performance based on past usage. It has a bias towards suggesting that |
| // the accelerated pipeline is optimal. |
| |
| float acceleratedCost = estimateRenderingCost( |
| ExpensiveCanvasHeuristicParameters::AcceleratedModeIndex); |
| |
| float recordingCost = estimateRenderingCost( |
| ExpensiveCanvasHeuristicParameters::RecordingModeIndex); |
| |
| float costDifference = acceleratedCost - recordingCost; |
| float percentCostReduction = costDifference / acceleratedCost * 100.0; |
| float costDifferencePerFrame = |
| costDifference / m_usageCounters.numFramesSinceReset; |
| |
| if (percentCostReduction >= |
| ExpensiveCanvasHeuristicParameters:: |
| MinPercentageImprovementToSuggestDisableAcceleration && |
| costDifferencePerFrame >= |
| ExpensiveCanvasHeuristicParameters:: |
| MinCostPerFrameImprovementToSuggestDisableAcceleration) { |
| return false; |
| } |
| return true; |
| } |
| |
| void CanvasRenderingContext2D::resetUsageTracking() { |
| UsageCounters newCounters; |
| m_usageCounters = newCounters; |
| } |
| |
| } // namespace blink |