| /* |
| * 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/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/OwnPtr.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) |
| , m_hasAlpha(attrs.alpha()) |
| , 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); |
| } |
| |
| void CanvasRenderingContext2D::setCanvasGetContextResult(RenderingContext& result) |
| { |
| result.setCanvasRenderingContext2D(this); |
| } |
| |
| void CanvasRenderingContext2D::unwindStateStack() |
| { |
| if (size_t stackSize = m_stateStack.size()) { |
| if (SkCanvas* skCanvas = canvas()->existingDrawingCanvas()) { |
| while (--stackSize) |
| skCanvas->restore(); |
| } |
| } |
| } |
| |
| CanvasRenderingContext2D::~CanvasRenderingContext2D() |
| { |
| } |
| |
| void CanvasRenderingContext2D::dispose() |
| { |
| if (m_pruneLocalFontCacheScheduled) |
| Platform::current()->currentThread()->removeTaskObserver(this); |
| } |
| |
| void CanvasRenderingContext2D::validateStateStack() |
| { |
| #if ENABLE(ASSERT) |
| SkCanvas* skCanvas = canvas()->existingDrawingCanvas(); |
| if (skCanvas && m_contextLostMode == NotLostContext) { |
| ASSERT(static_cast<size_t>(skCanvas->getSaveCount()) == m_stateStack.size()); |
| } |
| #endif |
| } |
| |
| 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()->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); |
| CanvasRenderingContext::trace(visitor); |
| BaseRenderingContext2D::trace(visitor); |
| SVGResourceClient::trace(visitor); |
| } |
| |
| void CanvasRenderingContext2D::dispatchContextLostEvent(Timer<CanvasRenderingContext2D>*) |
| { |
| if (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(Timer<CanvasRenderingContext2D>* timer) |
| { |
| if (m_contextLostMode == NotLostContext) { |
| // Canvas was already restored (possibly thanks to a resize), so stop trying. |
| m_tryRestoreContextEventTimer.stop(); |
| return; |
| } |
| |
| ASSERT(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(Timer<CanvasRenderingContext2D>*) |
| { |
| if (m_contextLostMode == NotLostContext) |
| return; |
| reset(); |
| m_contextLostMode = NotLostContext; |
| if (contextLostRestoredEventsEnabled()) { |
| Event* event(Event::create(EventTypeNames::contextrestored)); |
| canvas()->dispatchEvent(event); |
| } |
| } |
| |
| void CanvasRenderingContext2D::reset() |
| { |
| validateStateStack(); |
| unwindStateStack(); |
| m_stateStack.resize(1); |
| m_stateStack.first() = CanvasRenderingContext2DState::create(); |
| m_path.clear(); |
| SkCanvas* c = canvas()->existingDrawingCanvas(); |
| if (c) { |
| c->resetMatrix(); |
| c->clipRect(SkRect::MakeWH(canvas()->width(), canvas()->height()), SkRegion::kReplace_Op); |
| } |
| validateStateStack(); |
| } |
| |
| 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); |
| } |
| |
| 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.appendLiteral("italic "); |
| if (fontDescription.weight() == FontWeightBold) |
| serializedFont.appendLiteral("bold "); |
| if (fontDescription.variantCaps() == FontDescription::SmallCaps) |
| serializedFont.appendLiteral("small-caps "); |
| |
| serializedFont.appendNumber(fontDescription.computedPixelSize()); |
| serializedFont.appendLiteral("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 rendering text 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() |
| { |
| // 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; |
| Platform::current()->currentThread()->removeTaskObserver(this); |
| } |
| |
| 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); |
| } |
| |
| void CanvasRenderingContext2D::filterNeedsInvalidation() |
| { |
| 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(); |
| } |
| |
| bool CanvasRenderingContext2D::parseColorOrCurrentColor(Color& color, const String& colorString) const |
| { |
| return ::blink::parseColorOrCurrentColor(color, colorString, canvas()); |
| } |
| |
| std::pair<Element*, String> CanvasRenderingContext2D::getControlAndIdIfHitRegionExists(const LayoutPoint& location) |
| { |
| if (hitRegionsCount() <= 0) |
| return std::make_pair(nullptr, String()); |
| |
| 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 std::make_pair(hitRegion->control(), hitRegion->id()); |
| return std::make_pair(nullptr, hitRegion->id()); |
| } |
| return std::make_pair(nullptr, String()); |
| } |
| |
| 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) |
| { |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::FillPaintType); |
| } |
| |
| void CanvasRenderingContext2D::fillText(const String& text, double x, double y, double maxWidth) |
| { |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::FillPaintType, &maxWidth); |
| } |
| |
| void CanvasRenderingContext2D::strokeText(const String& text, double x, double y) |
| { |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::StrokePaintType); |
| } |
| |
| void CanvasRenderingContext2D::strokeText(const String& text, double x, double y, double maxWidth) |
| { |
| drawTextInternal(text, x, y, CanvasRenderingContext2DState::StrokePaintType, &maxWidth); |
| } |
| |
| TextMetrics* CanvasRenderingContext2D::measureText(const String& text) |
| { |
| TextMetrics* metrics = TextMetrics::create(); |
| |
| // The style resolution required for rendering text is not available in frame-less documents. |
| if (!canvas()->document().frame()) |
| return metrics; |
| |
| canvas()->document().updateStyleAndLayoutTreeForNode(canvas()); |
| const Font& font = accessFont(); |
| |
| 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 = font.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 rendering text 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 !m_hasAlpha, so we need to fall out of display list mode when |
| // drawing text to an opaque canvas |
| // crbug.com/583809 |
| if (!m_hasAlpha && !isAccelerated()) |
| canvas()->disableDeferral(DisableDeferralReasonSubPixelTextAntiAliasingSupport); |
| |
| const Font& font = accessFont(); |
| if (!font.primaryFont()) |
| return; |
| |
| const FontMetrics& fontMetrics = font.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(); |
| } |
| |
| int CanvasRenderingContext2D::getFontBaseline(const FontMetrics& fontMetrics) const |
| { |
| switch (state().getTextBaseline()) { |
| case TopTextBaseline: |
| return 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 (fontMetrics.ascent() * 4) / 5; |
| case BottomTextBaseline: |
| case IdeographicTextBaseline: |
| return -fontMetrics.descent(); |
| case MiddleTextBaseline: |
| return -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(m_hasAlpha); |
| } |
| |
| 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) |
| { |
| 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()); |
| |
| // Offset by the canvas rect, taking border and padding into account. |
| IntRect canvasRect = renderer->absoluteBoundingBoxRect(); |
| canvasRect.move(lbmo->borderLeft() + lbmo->paddingLeft(), lbmo->borderTop() + lbmo->paddingTop()); |
| LayoutRect elementRect = enclosingLayoutRect(transformedPath.boundingRect()); |
| elementRect.moveBy(canvasRect.location()); |
| axObjectCache->setCanvasObjectBounds(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; |
| } |
| |
| } // namespace blink |