mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-30 19:28:40 +09:00
We already had part of the plumbing for other canvas manipulations, so this is somewhat simplified. This excludes the interface as that is breaking the build at this stage. Some more research is required.
6691 lines
197 KiB
C++
6691 lines
197 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "CanvasRenderingContext2D.h"
|
|
|
|
#include "mozilla/gfx/Helpers.h"
|
|
#include "nsXULElement.h"
|
|
|
|
#include "nsAutoPtr.h"
|
|
#include "nsIServiceManager.h"
|
|
#include "nsMathUtils.h"
|
|
#include "SVGImageContext.h"
|
|
|
|
#include "nsContentUtils.h"
|
|
|
|
#include "nsIDocument.h"
|
|
#include "mozilla/dom/HTMLCanvasElement.h"
|
|
#include "nsSVGEffects.h"
|
|
#include "nsPresContext.h"
|
|
#include "nsIPresShell.h"
|
|
|
|
#include "nsIInterfaceRequestorUtils.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsError.h"
|
|
|
|
#include "nsCSSParser.h"
|
|
#include "mozilla/css/StyleRule.h"
|
|
#include "mozilla/css/Declaration.h"
|
|
#include "nsComputedDOMStyle.h"
|
|
#include "nsStyleSet.h"
|
|
|
|
#include "nsPrintfCString.h"
|
|
|
|
#include "nsReadableUtils.h"
|
|
|
|
#include "nsColor.h"
|
|
#include "nsGfxCIID.h"
|
|
#include "nsIDocShell.h"
|
|
#include "nsIDOMWindow.h"
|
|
#include "nsPIDOMWindow.h"
|
|
#include "nsDisplayList.h"
|
|
#include "nsFocusManager.h"
|
|
#include "nsContentUtils.h"
|
|
|
|
#include "nsTArray.h"
|
|
|
|
#include "ImageEncoder.h"
|
|
#include "ImageRegion.h"
|
|
|
|
#include "gfxContext.h"
|
|
#include "gfxImageSurface.h"
|
|
#include "gfxPlatform.h"
|
|
#include "gfxFont.h"
|
|
#include "gfxBlur.h"
|
|
#include "gfxPrefs.h"
|
|
#include "gfxUtils.h"
|
|
|
|
#include "nsFrameLoader.h"
|
|
#include "nsBidi.h"
|
|
#include "nsBidiPresUtils.h"
|
|
#include "Layers.h"
|
|
#include "LayerUserData.h"
|
|
#include "CanvasUtils.h"
|
|
#include "nsIMemoryReporter.h"
|
|
#include "nsStyleUtil.h"
|
|
#include "CanvasImageCache.h"
|
|
|
|
#include <algorithm>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
|
|
#include "jsapi.h"
|
|
#include "jsfriendapi.h"
|
|
#include "js/Conversions.h"
|
|
|
|
#include "mozilla/Alignment.h"
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/CheckedInt.h"
|
|
#include "mozilla/DebugOnly.h"
|
|
#include "mozilla/dom/ContentParent.h"
|
|
#include "mozilla/dom/DOMMatrix.h"
|
|
#include "mozilla/dom/ImageBitmap.h"
|
|
#include "mozilla/dom/ImageData.h"
|
|
#include "mozilla/dom/PBrowserParent.h"
|
|
#include "mozilla/dom/ToJSValue.h"
|
|
#include "mozilla/dom/TypedArray.h"
|
|
#include "mozilla/EndianUtils.h"
|
|
#include "mozilla/gfx/2D.h"
|
|
#include "mozilla/gfx/Helpers.h"
|
|
#include "mozilla/gfx/Tools.h"
|
|
#include "mozilla/gfx/PathHelpers.h"
|
|
#include "mozilla/gfx/DataSurfaceHelpers.h"
|
|
#include "mozilla/gfx/PatternHelpers.h"
|
|
#include "mozilla/layers/PersistentBufferProvider.h"
|
|
#include "mozilla/MathAlgorithms.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/TimeStamp.h"
|
|
#include "mozilla/UniquePtr.h"
|
|
#include "mozilla/Unused.h"
|
|
#include "nsCCUncollectableMarker.h"
|
|
#include "nsWrapperCacheInlines.h"
|
|
#include "mozilla/dom/CanvasRenderingContext2DBinding.h"
|
|
#include "mozilla/dom/CanvasPath.h"
|
|
#include "mozilla/dom/HTMLImageElement.h"
|
|
#include "mozilla/dom/HTMLVideoElement.h"
|
|
#include "mozilla/dom/SVGImageElement.h"
|
|
#include "mozilla/dom/SVGMatrix.h"
|
|
#include "mozilla/dom/TextMetrics.h"
|
|
#include "mozilla/dom/SVGMatrix.h"
|
|
#include "mozilla/FloatingPoint.h"
|
|
#include "nsGlobalWindow.h"
|
|
#include "GLContext.h"
|
|
#include "GLContextProvider.h"
|
|
#include "SVGContentUtils.h"
|
|
#include "nsIScreenManager.h"
|
|
#include "nsFilterInstance.h"
|
|
#include "nsSVGLength2.h"
|
|
#include "nsDeviceContext.h"
|
|
#include "nsFontMetrics.h"
|
|
#include "Units.h"
|
|
#include "CanvasUtils.h"
|
|
#include "nsStyleSet.h"
|
|
#include "mozilla/layers/CanvasClient.h"
|
|
|
|
#undef free // apparently defined by some windows header, clashing with a free()
|
|
// method in SkTypes.h
|
|
#include "SkiaGLGlue.h"
|
|
#ifdef USE_SKIA
|
|
#include "SurfaceTypes.h"
|
|
#include "GLBlitHelper.h"
|
|
#endif
|
|
|
|
using mozilla::gl::GLContext;
|
|
using mozilla::gl::SkiaGLGlue;
|
|
using mozilla::gl::GLContextProvider;
|
|
|
|
#ifdef XP_WIN
|
|
#include "gfxWindowsPlatform.h"
|
|
#endif
|
|
|
|
// windows.h (included by chromium code) defines this, in its infinite wisdom
|
|
#undef DrawText
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::CanvasUtils;
|
|
using namespace mozilla::css;
|
|
using namespace mozilla::gfx;
|
|
using namespace mozilla::image;
|
|
using namespace mozilla::ipc;
|
|
using namespace mozilla::layers;
|
|
|
|
namespace mozilla {
|
|
namespace dom {
|
|
|
|
// Cap sigma to avoid overly large temp surfaces.
|
|
const Float SIGMA_MAX = 100;
|
|
|
|
const size_t MAX_STYLE_STACK_SIZE = 1024;
|
|
|
|
/* Memory reporter stuff */
|
|
static int64_t gCanvasAzureMemoryUsed = 0;
|
|
|
|
// Adds Save() / Restore() calls to the scope.
|
|
class MOZ_RAII AutoSaveRestore
|
|
{
|
|
public:
|
|
explicit AutoSaveRestore(CanvasRenderingContext2D* aCtx
|
|
MOZ_GUARD_OBJECT_NOTIFIER_PARAM)
|
|
: mCtx(aCtx)
|
|
{
|
|
MOZ_GUARD_OBJECT_NOTIFIER_INIT;
|
|
mCtx->Save();
|
|
}
|
|
~AutoSaveRestore() { mCtx->Restore(); }
|
|
private:
|
|
RefPtr<CanvasRenderingContext2D> mCtx;
|
|
MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER
|
|
};
|
|
|
|
// This is KIND_OTHER because it's not always clear where in memory the pixels
|
|
// of a canvas are stored. Furthermore, this memory will be tracked by the
|
|
// underlying surface implementations. See bug 655638 for details.
|
|
class Canvas2dPixelsReporter final : public nsIMemoryReporter
|
|
{
|
|
~Canvas2dPixelsReporter() {}
|
|
public:
|
|
NS_DECL_ISUPPORTS
|
|
|
|
NS_IMETHOD CollectReports(nsIHandleReportCallback* aHandleReport,
|
|
nsISupports* aData, bool aAnonymize) override
|
|
{
|
|
MOZ_COLLECT_REPORT(
|
|
"canvas-2d-pixels", KIND_OTHER, UNITS_BYTES, gCanvasAzureMemoryUsed,
|
|
"Memory used by 2D canvases. Each canvas requires "
|
|
"(width * height * 4) bytes.");
|
|
|
|
return NS_OK;
|
|
}
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(Canvas2dPixelsReporter, nsIMemoryReporter)
|
|
|
|
class CanvasRadialGradient : public CanvasGradient
|
|
{
|
|
public:
|
|
CanvasRadialGradient(CanvasRenderingContext2D* aContext,
|
|
const Point& aBeginOrigin, Float aBeginRadius,
|
|
const Point& aEndOrigin, Float aEndRadius)
|
|
: CanvasGradient(aContext, Type::RADIAL)
|
|
, mCenter1(aBeginOrigin)
|
|
, mCenter2(aEndOrigin)
|
|
, mRadius1(aBeginRadius)
|
|
, mRadius2(aEndRadius)
|
|
{
|
|
}
|
|
|
|
Point mCenter1;
|
|
Point mCenter2;
|
|
Float mRadius1;
|
|
Float mRadius2;
|
|
};
|
|
|
|
class CanvasLinearGradient : public CanvasGradient
|
|
{
|
|
public:
|
|
CanvasLinearGradient(CanvasRenderingContext2D* aContext,
|
|
const Point& aBegin, const Point& aEnd)
|
|
: CanvasGradient(aContext, Type::LINEAR)
|
|
, mBegin(aBegin)
|
|
, mEnd(aEnd)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
friend struct CanvasBidiProcessor;
|
|
friend class CanvasGeneralPattern;
|
|
|
|
// Beginning of linear gradient.
|
|
Point mBegin;
|
|
// End of linear gradient.
|
|
Point mEnd;
|
|
};
|
|
|
|
bool
|
|
CanvasRenderingContext2D::PatternIsOpaque(CanvasRenderingContext2D::Style aStyle) const
|
|
{
|
|
const ContextState& state = CurrentState();
|
|
if (state.globalAlpha < 1.0) {
|
|
return false;
|
|
}
|
|
|
|
if (state.patternStyles[aStyle] && state.patternStyles[aStyle]->mSurface) {
|
|
return IsOpaqueFormat(state.patternStyles[aStyle]->mSurface->GetFormat());
|
|
}
|
|
|
|
// TODO: for gradient patterns we could check that all stops are opaque
|
|
// colors.
|
|
|
|
if (!state.gradientStyles[aStyle]) {
|
|
// it's a color pattern.
|
|
return Color::FromABGR(state.colorStyles[aStyle]).a >= 1.0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// This class is named 'GeneralCanvasPattern' instead of just
|
|
// 'GeneralPattern' to keep Windows PGO builds from confusing the
|
|
// GeneralPattern class in gfxContext.cpp with this one.
|
|
class CanvasGeneralPattern
|
|
{
|
|
public:
|
|
typedef CanvasRenderingContext2D::Style Style;
|
|
typedef CanvasRenderingContext2D::ContextState ContextState;
|
|
|
|
Pattern& ForStyle(CanvasRenderingContext2D* aCtx,
|
|
Style aStyle,
|
|
DrawTarget* aRT)
|
|
{
|
|
// This should only be called once or the mPattern destructor will
|
|
// not be executed.
|
|
NS_ASSERTION(!mPattern.GetPattern(), "ForStyle() should only be called once on CanvasGeneralPattern!");
|
|
|
|
const ContextState& state = aCtx->CurrentState();
|
|
|
|
if (state.StyleIsColor(aStyle)) {
|
|
mPattern.InitColorPattern(ToDeviceColor(state.colorStyles[aStyle]));
|
|
} else if (state.gradientStyles[aStyle] &&
|
|
state.gradientStyles[aStyle]->GetType() == CanvasGradient::Type::LINEAR) {
|
|
auto gradient =
|
|
static_cast<CanvasLinearGradient*>(state.gradientStyles[aStyle].get());
|
|
|
|
mPattern.InitLinearGradientPattern(gradient->mBegin, gradient->mEnd,
|
|
gradient->GetGradientStopsForTarget(aRT));
|
|
} else if (state.gradientStyles[aStyle] &&
|
|
state.gradientStyles[aStyle]->GetType() == CanvasGradient::Type::RADIAL) {
|
|
auto gradient =
|
|
static_cast<CanvasRadialGradient*>(state.gradientStyles[aStyle].get());
|
|
|
|
mPattern.InitRadialGradientPattern(gradient->mCenter1, gradient->mCenter2,
|
|
gradient->mRadius1, gradient->mRadius2,
|
|
gradient->GetGradientStopsForTarget(aRT));
|
|
} else if (state.patternStyles[aStyle]) {
|
|
if (aCtx->mCanvasElement) {
|
|
CanvasUtils::DoDrawImageSecurityCheck(aCtx->mCanvasElement,
|
|
state.patternStyles[aStyle]->mPrincipal,
|
|
state.patternStyles[aStyle]->mForceWriteOnly,
|
|
state.patternStyles[aStyle]->mCORSUsed);
|
|
}
|
|
|
|
ExtendMode mode;
|
|
if (state.patternStyles[aStyle]->mRepeat == CanvasPattern::RepeatMode::NOREPEAT) {
|
|
mode = ExtendMode::CLAMP;
|
|
} else {
|
|
mode = ExtendMode::REPEAT;
|
|
}
|
|
|
|
SamplingFilter samplingFilter;
|
|
if (state.imageSmoothingEnabled) {
|
|
samplingFilter = SamplingFilter::GOOD;
|
|
} else {
|
|
samplingFilter = SamplingFilter::POINT;
|
|
}
|
|
|
|
mPattern.InitSurfacePattern(state.patternStyles[aStyle]->mSurface, mode,
|
|
state.patternStyles[aStyle]->mTransform,
|
|
samplingFilter);
|
|
}
|
|
|
|
return *mPattern.GetPattern();
|
|
}
|
|
|
|
GeneralPattern mPattern;
|
|
};
|
|
|
|
/* This is an RAII based class that can be used as a drawtarget for
|
|
* operations that need to have a filter applied to their results.
|
|
* All coordinates passed to the constructor are in device space.
|
|
*/
|
|
class AdjustedTargetForFilter
|
|
{
|
|
public:
|
|
typedef CanvasRenderingContext2D::ContextState ContextState;
|
|
|
|
AdjustedTargetForFilter(CanvasRenderingContext2D* aCtx,
|
|
DrawTarget* aFinalTarget,
|
|
const gfx::IntPoint& aFilterSpaceToTargetOffset,
|
|
const gfx::IntRect& aPreFilterBounds,
|
|
const gfx::IntRect& aPostFilterBounds,
|
|
gfx::CompositionOp aCompositionOp)
|
|
: mFinalTarget(aFinalTarget)
|
|
, mCtx(aCtx)
|
|
, mPostFilterBounds(aPostFilterBounds)
|
|
, mOffset(aFilterSpaceToTargetOffset)
|
|
, mCompositionOp(aCompositionOp)
|
|
{
|
|
nsIntRegion sourceGraphicNeededRegion;
|
|
nsIntRegion fillPaintNeededRegion;
|
|
nsIntRegion strokePaintNeededRegion;
|
|
|
|
FilterSupport::ComputeSourceNeededRegions(
|
|
aCtx->CurrentState().filter, mPostFilterBounds,
|
|
sourceGraphicNeededRegion, fillPaintNeededRegion,
|
|
strokePaintNeededRegion);
|
|
|
|
mSourceGraphicRect = sourceGraphicNeededRegion.GetBounds();
|
|
mFillPaintRect = fillPaintNeededRegion.GetBounds();
|
|
mStrokePaintRect = strokePaintNeededRegion.GetBounds();
|
|
|
|
mSourceGraphicRect = mSourceGraphicRect.Intersect(aPreFilterBounds);
|
|
|
|
if (mSourceGraphicRect.IsEmpty()) {
|
|
// The filter might not make any use of the source graphic. We need to
|
|
// create a DrawTarget that we can return from DT() anyway, so we'll
|
|
// just use a 1x1-sized one.
|
|
mSourceGraphicRect.SizeTo(1, 1);
|
|
}
|
|
|
|
mTarget = mFinalTarget->CreateSimilarDrawTarget(mSourceGraphicRect.Size(),
|
|
SurfaceFormat::B8G8R8A8);
|
|
|
|
if (!mTarget) {
|
|
// XXX - Deal with the situation where our temp size is too big to
|
|
// fit in a texture (bug 1066622).
|
|
mTarget = mFinalTarget;
|
|
mCtx = nullptr;
|
|
mFinalTarget = nullptr;
|
|
return;
|
|
}
|
|
|
|
mTarget->SetTransform(
|
|
mFinalTarget->GetTransform().PostTranslate(-mSourceGraphicRect.TopLeft() + mOffset));
|
|
}
|
|
|
|
// Return a SourceSurface that contains the FillPaint or StrokePaint source.
|
|
already_AddRefed<SourceSurface>
|
|
DoSourcePaint(gfx::IntRect& aRect, CanvasRenderingContext2D::Style aStyle)
|
|
{
|
|
if (aRect.IsEmpty()) {
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<DrawTarget> dt =
|
|
mFinalTarget->CreateSimilarDrawTarget(aRect.Size(), SurfaceFormat::B8G8R8A8);
|
|
if (!dt) {
|
|
aRect.SetEmpty();
|
|
return nullptr;
|
|
}
|
|
|
|
Matrix transform =
|
|
mFinalTarget->GetTransform().PostTranslate(-aRect.TopLeft() + mOffset);
|
|
|
|
dt->SetTransform(transform);
|
|
|
|
if (transform.Invert()) {
|
|
gfx::Rect dtBounds(0, 0, aRect.width, aRect.height);
|
|
gfx::Rect fillRect = transform.TransformBounds(dtBounds);
|
|
dt->FillRect(fillRect, CanvasGeneralPattern().ForStyle(mCtx, aStyle, dt));
|
|
}
|
|
return dt->Snapshot();
|
|
}
|
|
|
|
~AdjustedTargetForFilter()
|
|
{
|
|
if (!mCtx) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<SourceSurface> snapshot = mTarget->Snapshot();
|
|
|
|
RefPtr<SourceSurface> fillPaint =
|
|
DoSourcePaint(mFillPaintRect, CanvasRenderingContext2D::Style::FILL);
|
|
RefPtr<SourceSurface> strokePaint =
|
|
DoSourcePaint(mStrokePaintRect, CanvasRenderingContext2D::Style::STROKE);
|
|
|
|
AutoRestoreTransform autoRestoreTransform(mFinalTarget);
|
|
mFinalTarget->SetTransform(Matrix());
|
|
|
|
MOZ_RELEASE_ASSERT(!mCtx->CurrentState().filter.mPrimitives.IsEmpty());
|
|
gfx::FilterSupport::RenderFilterDescription(
|
|
mFinalTarget, mCtx->CurrentState().filter,
|
|
gfx::Rect(mPostFilterBounds),
|
|
snapshot, mSourceGraphicRect,
|
|
fillPaint, mFillPaintRect,
|
|
strokePaint, mStrokePaintRect,
|
|
mCtx->CurrentState().filterAdditionalImages,
|
|
mPostFilterBounds.TopLeft() - mOffset,
|
|
DrawOptions(1.0f, mCompositionOp));
|
|
|
|
const gfx::FilterDescription& filter = mCtx->CurrentState().filter;
|
|
MOZ_RELEASE_ASSERT(!filter.mPrimitives.IsEmpty());
|
|
if (filter.mPrimitives.LastElement().IsTainted() && mCtx->mCanvasElement) {
|
|
mCtx->mCanvasElement->SetWriteOnly();
|
|
}
|
|
}
|
|
|
|
DrawTarget* DT()
|
|
{
|
|
return mTarget;
|
|
}
|
|
|
|
private:
|
|
RefPtr<DrawTarget> mTarget;
|
|
RefPtr<DrawTarget> mFinalTarget;
|
|
CanvasRenderingContext2D* mCtx;
|
|
gfx::IntRect mSourceGraphicRect;
|
|
gfx::IntRect mFillPaintRect;
|
|
gfx::IntRect mStrokePaintRect;
|
|
gfx::IntRect mPostFilterBounds;
|
|
gfx::IntPoint mOffset;
|
|
gfx::CompositionOp mCompositionOp;
|
|
};
|
|
|
|
/* This is an RAII based class that can be used as a drawtarget for
|
|
* operations that need to have a shadow applied to their results.
|
|
* All coordinates passed to the constructor are in device space.
|
|
*/
|
|
class AdjustedTargetForShadow
|
|
{
|
|
public:
|
|
typedef CanvasRenderingContext2D::ContextState ContextState;
|
|
|
|
AdjustedTargetForShadow(CanvasRenderingContext2D* aCtx,
|
|
DrawTarget* aFinalTarget,
|
|
const gfx::Rect& aBounds,
|
|
gfx::CompositionOp aCompositionOp)
|
|
: mFinalTarget(aFinalTarget)
|
|
, mCtx(aCtx)
|
|
, mCompositionOp(aCompositionOp)
|
|
{
|
|
const ContextState& state = mCtx->CurrentState();
|
|
mSigma = state.ShadowBlurSigma();
|
|
|
|
// We actually include the bounds of the shadow blur, this makes it
|
|
// easier to execute the actual blur on hardware, and shouldn't affect
|
|
// the amount of pixels that need to be touched.
|
|
gfx::Rect bounds = aBounds;
|
|
int32_t blurRadius = state.ShadowBlurRadius();
|
|
bounds.Inflate(blurRadius);
|
|
bounds.RoundOut();
|
|
bounds.ToIntRect(&mTempRect);
|
|
|
|
mTarget =
|
|
mFinalTarget->CreateShadowDrawTarget(mTempRect.Size(),
|
|
SurfaceFormat::B8G8R8A8, mSigma);
|
|
|
|
if (!mTarget) {
|
|
// XXX - Deal with the situation where our temp size is too big to
|
|
// fit in a texture (bug 1066622).
|
|
mTarget = mFinalTarget;
|
|
mCtx = nullptr;
|
|
mFinalTarget = nullptr;
|
|
} else {
|
|
mTarget->SetTransform(
|
|
mFinalTarget->GetTransform().PostTranslate(-mTempRect.TopLeft()));
|
|
}
|
|
}
|
|
|
|
~AdjustedTargetForShadow()
|
|
{
|
|
if (!mCtx) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<SourceSurface> snapshot = mTarget->Snapshot();
|
|
|
|
mFinalTarget->DrawSurfaceWithShadow(snapshot, mTempRect.TopLeft(),
|
|
Color::FromABGR(mCtx->CurrentState().shadowColor),
|
|
mCtx->CurrentState().shadowOffset, mSigma,
|
|
mCompositionOp);
|
|
}
|
|
|
|
DrawTarget* DT()
|
|
{
|
|
return mTarget;
|
|
}
|
|
|
|
gfx::IntPoint OffsetToFinalDT()
|
|
{
|
|
return mTempRect.TopLeft();
|
|
}
|
|
|
|
private:
|
|
RefPtr<DrawTarget> mTarget;
|
|
RefPtr<DrawTarget> mFinalTarget;
|
|
CanvasRenderingContext2D* mCtx;
|
|
Float mSigma;
|
|
gfx::IntRect mTempRect;
|
|
gfx::CompositionOp mCompositionOp;
|
|
};
|
|
|
|
/*
|
|
* This is an RAII based class that can be used as a drawtarget for
|
|
* operations that need a shadow or a filter drawn. It will automatically
|
|
* provide a temporary target when needed, and if so blend it back with a
|
|
* shadow, filter, or both.
|
|
* If both a shadow and a filter are needed, the filter is applied first,
|
|
* and the shadow is applied to the filtered results.
|
|
*
|
|
* aBounds specifies the bounds of the drawing operation that will be
|
|
* drawn to the target, it is given in device space! If this is nullptr the
|
|
* drawing operation will be assumed to cover the whole canvas.
|
|
*/
|
|
class AdjustedTarget
|
|
{
|
|
public:
|
|
typedef CanvasRenderingContext2D::ContextState ContextState;
|
|
|
|
explicit AdjustedTarget(CanvasRenderingContext2D* aCtx,
|
|
const gfx::Rect* aBounds = nullptr)
|
|
{
|
|
// All rects in this function are in the device space of ctx->mTarget.
|
|
|
|
// In order to keep our temporary surfaces as small as possible, we first
|
|
// calculate what their maximum required bounds would need to be if we
|
|
// were to fill the whole canvas. Everything outside those bounds we don't
|
|
// need to render.
|
|
gfx::Rect r(0, 0, aCtx->mWidth, aCtx->mHeight);
|
|
gfx::Rect maxSourceNeededBoundsForShadow =
|
|
MaxSourceNeededBoundsForShadow(r, aCtx);
|
|
gfx::Rect maxSourceNeededBoundsForFilter =
|
|
MaxSourceNeededBoundsForFilter(maxSourceNeededBoundsForShadow, aCtx);
|
|
if (!aCtx->IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
gfx::Rect bounds = maxSourceNeededBoundsForFilter;
|
|
if (aBounds) {
|
|
bounds = bounds.Intersect(*aBounds);
|
|
}
|
|
gfx::Rect boundsAfterFilter = BoundsAfterFilter(bounds, aCtx);
|
|
if (!aCtx->IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
mozilla::gfx::CompositionOp op = aCtx->CurrentState().op;
|
|
|
|
gfx::IntPoint offsetToFinalDT;
|
|
|
|
// First set up the shadow draw target, because the shadow goes outside.
|
|
// It applies to the post-filter results, if both a filter and a shadow
|
|
// are used.
|
|
if (aCtx->NeedToDrawShadow()) {
|
|
mShadowTarget = MakeUnique<AdjustedTargetForShadow>(
|
|
aCtx, aCtx->mTarget, boundsAfterFilter, op);
|
|
mTarget = mShadowTarget->DT();
|
|
offsetToFinalDT = mShadowTarget->OffsetToFinalDT();
|
|
|
|
// If we also have a filter, the filter needs to be drawn with OP_OVER
|
|
// because shadow drawing already applies op on the result.
|
|
op = gfx::CompositionOp::OP_OVER;
|
|
}
|
|
|
|
// Now set up the filter draw target.
|
|
const bool applyFilter = aCtx->NeedToApplyFilter();
|
|
if (!aCtx->IsTargetValid()) {
|
|
return;
|
|
}
|
|
if (applyFilter) {
|
|
bounds.RoundOut();
|
|
|
|
if (!mTarget) {
|
|
mTarget = aCtx->mTarget;
|
|
}
|
|
gfx::IntRect intBounds;
|
|
if (!bounds.ToIntRect(&intBounds)) {
|
|
return;
|
|
}
|
|
mFilterTarget = MakeUnique<AdjustedTargetForFilter>(
|
|
aCtx, mTarget, offsetToFinalDT, intBounds,
|
|
gfx::RoundedToInt(boundsAfterFilter), op);
|
|
mTarget = mFilterTarget->DT();
|
|
}
|
|
if (!mTarget) {
|
|
mTarget = aCtx->mTarget;
|
|
}
|
|
}
|
|
|
|
~AdjustedTarget()
|
|
{
|
|
// The order in which the targets are finalized is important.
|
|
// Filters are inside, any shadow applies to the post-filter results.
|
|
mFilterTarget.reset();
|
|
mShadowTarget.reset();
|
|
}
|
|
|
|
operator DrawTarget*()
|
|
{
|
|
return mTarget;
|
|
}
|
|
|
|
DrawTarget* operator->() MOZ_NO_ADDREF_RELEASE_ON_RETURN
|
|
{
|
|
return mTarget;
|
|
}
|
|
|
|
private:
|
|
|
|
gfx::Rect
|
|
MaxSourceNeededBoundsForFilter(const gfx::Rect& aDestBounds, CanvasRenderingContext2D* aCtx)
|
|
{
|
|
const bool applyFilter = aCtx->NeedToApplyFilter();
|
|
if (!aCtx->IsTargetValid()) {
|
|
return aDestBounds;
|
|
}
|
|
if (!applyFilter) {
|
|
return aDestBounds;
|
|
}
|
|
|
|
nsIntRegion sourceGraphicNeededRegion;
|
|
nsIntRegion fillPaintNeededRegion;
|
|
nsIntRegion strokePaintNeededRegion;
|
|
|
|
FilterSupport::ComputeSourceNeededRegions(
|
|
aCtx->CurrentState().filter, gfx::RoundedToInt(aDestBounds),
|
|
sourceGraphicNeededRegion, fillPaintNeededRegion, strokePaintNeededRegion);
|
|
|
|
return gfx::Rect(sourceGraphicNeededRegion.GetBounds());
|
|
}
|
|
|
|
gfx::Rect
|
|
MaxSourceNeededBoundsForShadow(const gfx::Rect& aDestBounds, CanvasRenderingContext2D* aCtx)
|
|
{
|
|
if (!aCtx->NeedToDrawShadow()) {
|
|
return aDestBounds;
|
|
}
|
|
|
|
const ContextState& state = aCtx->CurrentState();
|
|
gfx::Rect sourceBounds = aDestBounds - state.shadowOffset;
|
|
sourceBounds.Inflate(state.ShadowBlurRadius());
|
|
|
|
// Union the shadow source with the original rect because we're going to
|
|
// draw both.
|
|
return sourceBounds.Union(aDestBounds);
|
|
}
|
|
|
|
gfx::Rect
|
|
BoundsAfterFilter(const gfx::Rect& aBounds, CanvasRenderingContext2D* aCtx)
|
|
{
|
|
const bool applyFilter = aCtx->NeedToApplyFilter();
|
|
if (!aCtx->IsTargetValid()) {
|
|
return aBounds;
|
|
}
|
|
if (!applyFilter) {
|
|
return aBounds;
|
|
}
|
|
|
|
gfx::Rect bounds(aBounds);
|
|
bounds.RoundOut();
|
|
|
|
gfx::IntRect intBounds;
|
|
if (!bounds.ToIntRect(&intBounds)) {
|
|
return gfx::Rect();
|
|
}
|
|
|
|
nsIntRegion extents =
|
|
gfx::FilterSupport::ComputePostFilterExtents(aCtx->CurrentState().filter,
|
|
intBounds);
|
|
return gfx::Rect(extents.GetBounds());
|
|
}
|
|
|
|
RefPtr<DrawTarget> mTarget;
|
|
UniquePtr<AdjustedTargetForShadow> mShadowTarget;
|
|
UniquePtr<AdjustedTargetForFilter> mFilterTarget;
|
|
};
|
|
|
|
void
|
|
CanvasPattern::SetTransform(SVGMatrix& aMatrix)
|
|
{
|
|
mTransform = ToMatrix(aMatrix.GetMatrix());
|
|
}
|
|
|
|
void
|
|
CanvasGradient::AddColorStop(float aOffset, const nsAString& aColorstr, ErrorResult& aRv)
|
|
{
|
|
if (aOffset < 0.0 || aOffset > 1.0) {
|
|
aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
nsCSSValue value;
|
|
nsCSSParser parser;
|
|
if (!parser.ParseColorString(aColorstr, nullptr, 0, value)) {
|
|
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
|
|
return;
|
|
}
|
|
|
|
nscolor color;
|
|
nsCOMPtr<nsIPresShell> presShell = mContext ? mContext->GetPresShell() : nullptr;
|
|
if (!nsRuleNode::ComputeColor(value, presShell ? presShell->GetPresContext() : nullptr,
|
|
nullptr, color)) {
|
|
aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
|
|
return;
|
|
}
|
|
|
|
mStops = nullptr;
|
|
|
|
GradientStop newStop;
|
|
|
|
newStop.offset = aOffset;
|
|
newStop.color = Color::FromABGR(color);
|
|
|
|
mRawStops.AppendElement(newStop);
|
|
}
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasGradient, AddRef)
|
|
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasGradient, Release)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasGradient, mContext)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasPattern, AddRef)
|
|
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasPattern, Release)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPattern, mContext)
|
|
|
|
class CanvasShutdownObserver final : public nsIObserver
|
|
{
|
|
public:
|
|
explicit CanvasShutdownObserver(CanvasRenderingContext2D* aCanvas)
|
|
: mCanvas(aCanvas)
|
|
{}
|
|
|
|
void OnShutdown() {
|
|
if(!mCanvas) {
|
|
return;
|
|
}
|
|
|
|
mCanvas = nullptr;
|
|
nsContentUtils::UnregisterShutdownObserver(this);
|
|
}
|
|
|
|
NS_DECL_ISUPPORTS
|
|
NS_DECL_NSIOBSERVER
|
|
private:
|
|
~CanvasShutdownObserver() {}
|
|
|
|
CanvasRenderingContext2D* mCanvas;
|
|
};
|
|
|
|
NS_IMPL_ISUPPORTS(CanvasShutdownObserver, nsIObserver)
|
|
|
|
NS_IMETHODIMP
|
|
CanvasShutdownObserver::Observe(nsISupports* aSubject,
|
|
const char* aTopic,
|
|
const char16_t* aData)
|
|
{
|
|
if (mCanvas && strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
|
|
mCanvas->OnShutdown();
|
|
OnShutdown();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
class CanvasDrawObserver
|
|
{
|
|
public:
|
|
explicit CanvasDrawObserver(CanvasRenderingContext2D* aCanvasContext);
|
|
|
|
// Only enumerate draw calls that could affect the heuristic
|
|
enum DrawCallType {
|
|
PutImageData,
|
|
GetImageData,
|
|
DrawImage
|
|
};
|
|
|
|
// This is the one that we call on relevant draw calls and count
|
|
// GPU vs. CPU preferrable calls...
|
|
void DidDrawCall(DrawCallType aType);
|
|
|
|
// When this returns true, the observer is done making the decisions.
|
|
// Right now, we expect to get rid of the observer after the FrameEnd
|
|
// returns true, though the decision could eventually change if the
|
|
// function calls shift. If we change to monitor the functions called
|
|
// and make decisions to change more than once, we would probably want
|
|
// FrameEnd to reset the timer and counters as it returns true.
|
|
bool FrameEnd();
|
|
|
|
private:
|
|
// These values will be picked up from preferences:
|
|
int32_t mMinFramesBeforeDecision;
|
|
float mMinSecondsBeforeDecision;
|
|
int32_t mMinCallsBeforeDecision;
|
|
|
|
CanvasRenderingContext2D* mCanvasContext;
|
|
int32_t mSoftwarePreferredCalls;
|
|
int32_t mGPUPreferredCalls;
|
|
int32_t mFramesRendered;
|
|
TimeStamp mCreationTime;
|
|
};
|
|
|
|
// We are not checking for the validity of the preference values. For example,
|
|
// negative values will have an effect of a quick exit, so no harm done.
|
|
CanvasDrawObserver::CanvasDrawObserver(CanvasRenderingContext2D* aCanvasContext)
|
|
: mMinFramesBeforeDecision(gfxPrefs::CanvasAutoAccelerateMinFrames())
|
|
, mMinSecondsBeforeDecision(gfxPrefs::CanvasAutoAccelerateMinSeconds())
|
|
, mMinCallsBeforeDecision(gfxPrefs::CanvasAutoAccelerateMinCalls())
|
|
, mCanvasContext(aCanvasContext)
|
|
, mSoftwarePreferredCalls(0)
|
|
, mGPUPreferredCalls(0)
|
|
, mFramesRendered(0)
|
|
, mCreationTime(TimeStamp::NowLoRes())
|
|
{}
|
|
|
|
void
|
|
CanvasDrawObserver::DidDrawCall(DrawCallType aType)
|
|
{
|
|
switch (aType) {
|
|
case PutImageData:
|
|
case GetImageData:
|
|
if (mGPUPreferredCalls == 0 && mSoftwarePreferredCalls == 0) {
|
|
mCreationTime = TimeStamp::NowLoRes();
|
|
}
|
|
mSoftwarePreferredCalls++;
|
|
break;
|
|
case DrawImage:
|
|
if (mGPUPreferredCalls == 0 && mSoftwarePreferredCalls == 0) {
|
|
mCreationTime = TimeStamp::NowLoRes();
|
|
}
|
|
mGPUPreferredCalls++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we return true, the observer is done making the decisions...
|
|
bool
|
|
CanvasDrawObserver::FrameEnd()
|
|
{
|
|
mFramesRendered++;
|
|
|
|
// We log the first mMinFramesBeforeDecision frames of any
|
|
// canvas object then make a call to determine whether it should
|
|
// be GPU or CPU backed
|
|
if ((mFramesRendered >= mMinFramesBeforeDecision) ||
|
|
((TimeStamp::NowLoRes() - mCreationTime).ToSeconds()) > mMinSecondsBeforeDecision) {
|
|
|
|
// If we don't have enough data, don't bother changing...
|
|
if (mGPUPreferredCalls > mMinCallsBeforeDecision ||
|
|
mSoftwarePreferredCalls > mMinCallsBeforeDecision) {
|
|
CanvasRenderingContext2D::RenderingMode switchToMode;
|
|
if (mGPUPreferredCalls >= mSoftwarePreferredCalls) {
|
|
switchToMode = CanvasRenderingContext2D::RenderingMode::OpenGLBackendMode;
|
|
} else {
|
|
switchToMode = CanvasRenderingContext2D::RenderingMode::SoftwareBackendMode;
|
|
}
|
|
if (switchToMode != mCanvasContext->mRenderingMode) {
|
|
if (!mCanvasContext->SwitchRenderingMode(switchToMode)) {
|
|
gfxDebug() << "Canvas acceleration failed mode switch to " << switchToMode;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we ever redesign this class to constantly monitor the functions
|
|
// and keep making decisions, we would probably want to reset the counters
|
|
// and the timers here...
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class CanvasRenderingContext2DUserData : public LayerUserData {
|
|
public:
|
|
explicit CanvasRenderingContext2DUserData(CanvasRenderingContext2D* aContext)
|
|
: mContext(aContext)
|
|
{
|
|
aContext->mUserDatas.AppendElement(this);
|
|
}
|
|
~CanvasRenderingContext2DUserData()
|
|
{
|
|
if (mContext) {
|
|
mContext->mUserDatas.RemoveElement(this);
|
|
}
|
|
}
|
|
|
|
static void PreTransactionCallback(void* aData)
|
|
{
|
|
auto self = static_cast<CanvasRenderingContext2DUserData*>(aData);
|
|
CanvasRenderingContext2D* context = self->mContext;
|
|
if (!context || !context->mTarget)
|
|
return;
|
|
|
|
context->OnStableState();
|
|
}
|
|
|
|
static void DidTransactionCallback(void* aData)
|
|
{
|
|
auto self = static_cast<CanvasRenderingContext2DUserData*>(aData);
|
|
if (self->mContext) {
|
|
self->mContext->MarkContextClean();
|
|
if (self->mContext->mDrawObserver) {
|
|
if (self->mContext->mDrawObserver->FrameEnd()) {
|
|
// Note that this call deletes and nulls out mDrawObserver:
|
|
self->mContext->RemoveDrawObserver();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
bool IsForContext(CanvasRenderingContext2D* aContext)
|
|
{
|
|
return mContext == aContext;
|
|
}
|
|
void Forget()
|
|
{
|
|
mContext = nullptr;
|
|
}
|
|
|
|
private:
|
|
CanvasRenderingContext2D* mContext;
|
|
};
|
|
|
|
class CanvasFilterChainObserver : public nsSVGFilterChainObserver
|
|
{
|
|
public:
|
|
CanvasFilterChainObserver(nsTArray<nsStyleFilter>& aFilters,
|
|
Element* aCanvasElement,
|
|
CanvasRenderingContext2D* aContext)
|
|
: nsSVGFilterChainObserver(aFilters, aCanvasElement)
|
|
, mContext(aContext)
|
|
{
|
|
}
|
|
|
|
virtual void DoUpdate() override
|
|
{
|
|
if (!mContext) {
|
|
MOZ_CRASH("GFX: This should never be called without a context");
|
|
}
|
|
// Refresh the cached FilterDescription in mContext->CurrentState().filter.
|
|
// If this filter is not at the top of the state stack, we'll refresh the
|
|
// wrong filter, but that's ok, because we'll refresh the right filter
|
|
// when we pop the state stack in CanvasRenderingContext2D::Restore().
|
|
RefPtr<CanvasRenderingContext2D> kungFuDeathGrip(mContext);
|
|
kungFuDeathGrip->UpdateFilter();
|
|
}
|
|
|
|
void DetachFromContext() { mContext = nullptr; }
|
|
|
|
private:
|
|
CanvasRenderingContext2D* mContext;
|
|
};
|
|
|
|
NS_IMPL_CYCLE_COLLECTING_ADDREF(CanvasRenderingContext2D)
|
|
NS_IMPL_CYCLE_COLLECTING_RELEASE(CanvasRenderingContext2D)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(CanvasRenderingContext2D)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CanvasRenderingContext2D)
|
|
// Make sure we remove ourselves from the list of demotable contexts (raw pointers),
|
|
// since we're logically destructed at this point.
|
|
CanvasRenderingContext2D::RemoveDemotableContext(tmp);
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCanvasElement)
|
|
for (uint32_t i = 0; i < tmp->mStyleStack.Length(); i++) {
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].patternStyles[Style::STROKE]);
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].patternStyles[Style::FILL]);
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].gradientStyles[Style::STROKE]);
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].gradientStyles[Style::FILL]);
|
|
auto filterChainObserver =
|
|
static_cast<CanvasFilterChainObserver*>(tmp->mStyleStack[i].filterChainObserver.get());
|
|
if (filterChainObserver) {
|
|
filterChainObserver->DetachFromContext();
|
|
}
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].filterChainObserver);
|
|
}
|
|
for (size_t x = 0 ; x < tmp->mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = tmp->mHitRegionsOptions[x];
|
|
if (info.mElement) {
|
|
ImplCycleCollectionUnlink(info.mElement);
|
|
}
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CanvasRenderingContext2D)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCanvasElement)
|
|
for (uint32_t i = 0; i < tmp->mStyleStack.Length(); i++) {
|
|
ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].patternStyles[Style::STROKE], "Stroke CanvasPattern");
|
|
ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].patternStyles[Style::FILL], "Fill CanvasPattern");
|
|
ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].gradientStyles[Style::STROKE], "Stroke CanvasGradient");
|
|
ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].gradientStyles[Style::FILL], "Fill CanvasGradient");
|
|
ImplCycleCollectionTraverse(cb, tmp->mStyleStack[i].filterChainObserver, "Filter Chain Observer");
|
|
}
|
|
for (size_t x = 0 ; x < tmp->mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = tmp->mHitRegionsOptions[x];
|
|
if (info.mElement) {
|
|
ImplCycleCollectionTraverse(cb, info.mElement, "Hit region fallback element");
|
|
}
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(CanvasRenderingContext2D)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(CanvasRenderingContext2D)
|
|
if (nsCCUncollectableMarker::sGeneration && tmp->IsBlack()) {
|
|
dom::Element* canvasElement = tmp->mCanvasElement;
|
|
if (canvasElement) {
|
|
if (canvasElement->IsPurple()) {
|
|
canvasElement->RemovePurple();
|
|
}
|
|
dom::Element::MarkNodeChildren(canvasElement);
|
|
}
|
|
return true;
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(CanvasRenderingContext2D)
|
|
return nsCCUncollectableMarker::sGeneration && tmp->IsBlack();
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(CanvasRenderingContext2D)
|
|
return nsCCUncollectableMarker::sGeneration && tmp->IsBlack();
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasRenderingContext2D)
|
|
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
|
|
NS_INTERFACE_MAP_ENTRY(nsICanvasRenderingContextInternal)
|
|
NS_INTERFACE_MAP_ENTRY(nsISupports)
|
|
NS_INTERFACE_MAP_END
|
|
|
|
/**
|
|
** CanvasRenderingContext2D impl
|
|
**/
|
|
|
|
|
|
// Initialize our static variables.
|
|
uintptr_t CanvasRenderingContext2D::sNumLivingContexts = 0;
|
|
DrawTarget* CanvasRenderingContext2D::sErrorTarget = nullptr;
|
|
|
|
|
|
|
|
CanvasRenderingContext2D::CanvasRenderingContext2D(layers::LayersBackend aCompositorBackend)
|
|
: mRenderingMode(RenderingMode::OpenGLBackendMode)
|
|
, mCompositorBackend(aCompositorBackend)
|
|
// these are the default values from the Canvas spec
|
|
, mWidth(0), mHeight(0)
|
|
, mZero(false), mOpaque(false)
|
|
, mResetLayer(true)
|
|
, mIPC(false)
|
|
, mIsSkiaGL(false)
|
|
, mHasPendingStableStateCallback(false)
|
|
, mDrawObserver(nullptr)
|
|
, mIsEntireFrameInvalid(false)
|
|
, mPredictManyRedrawCalls(false)
|
|
, mIsCapturedFrameInvalid(false)
|
|
, mPathTransformWillUpdate(false)
|
|
, mInvalidateCount(0)
|
|
, mWriteOnly(false) // == !origin-clean
|
|
{
|
|
sNumLivingContexts++;
|
|
|
|
mShutdownObserver = new CanvasShutdownObserver(this);
|
|
nsContentUtils::RegisterShutdownObserver(mShutdownObserver);
|
|
|
|
// The default is to use OpenGL mode
|
|
if (AllowOpenGLCanvas()) {
|
|
mDrawObserver = new CanvasDrawObserver(this);
|
|
} else {
|
|
mRenderingMode = RenderingMode::SoftwareBackendMode;
|
|
}
|
|
}
|
|
|
|
CanvasRenderingContext2D::~CanvasRenderingContext2D()
|
|
{
|
|
RemoveDrawObserver();
|
|
RemovePostRefreshObserver();
|
|
RemoveShutdownObserver();
|
|
Reset();
|
|
// Drop references from all CanvasRenderingContext2DUserData to this context
|
|
for (uint32_t i = 0; i < mUserDatas.Length(); ++i) {
|
|
mUserDatas[i]->Forget();
|
|
}
|
|
sNumLivingContexts--;
|
|
if (!sNumLivingContexts) {
|
|
NS_IF_RELEASE(sErrorTarget);
|
|
}
|
|
RemoveDemotableContext(this);
|
|
}
|
|
|
|
JSObject*
|
|
CanvasRenderingContext2D::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
|
{
|
|
return CanvasRenderingContext2DBinding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::ParseColor(const nsAString& aString,
|
|
nscolor* aColor)
|
|
{
|
|
nsIDocument* document = mCanvasElement
|
|
? mCanvasElement->OwnerDoc()
|
|
: nullptr;
|
|
|
|
// Pass the CSS Loader object to the parser, to allow parser error
|
|
// reports to include the outer window ID.
|
|
nsCSSParser parser(document ? document->CSSLoader() : nullptr);
|
|
nsCSSValue value;
|
|
if (!parser.ParseColorString(aString, nullptr, 0, value)) {
|
|
return false;
|
|
}
|
|
|
|
if (value.IsNumericColorUnit()) {
|
|
// if we already have a color we can just use it directly
|
|
*aColor = value.GetColorValue();
|
|
} else {
|
|
// otherwise resolve it
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
RefPtr<nsStyleContext> parentContext;
|
|
if (mCanvasElement && mCanvasElement->IsInUncomposedDoc()) {
|
|
// Inherit from the canvas element.
|
|
parentContext = nsComputedDOMStyle::GetStyleContextForElement(
|
|
mCanvasElement, nullptr, presShell);
|
|
}
|
|
|
|
Unused << nsRuleNode::ComputeColor(
|
|
value, presShell ? presShell->GetPresContext() : nullptr, parentContext,
|
|
*aColor);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
nsresult
|
|
CanvasRenderingContext2D::Reset()
|
|
{
|
|
if (mCanvasElement) {
|
|
mCanvasElement->InvalidateCanvas();
|
|
}
|
|
|
|
// only do this for non-docshell created contexts,
|
|
// since those are the ones that we created a surface for
|
|
if (mTarget && IsTargetValid() && !mDocShell) {
|
|
gCanvasAzureMemoryUsed -= mWidth * mHeight * 4;
|
|
}
|
|
|
|
bool forceReset = true;
|
|
ReturnTarget(forceReset);
|
|
mTarget = nullptr;
|
|
mBufferProvider = nullptr;
|
|
|
|
// reset hit regions
|
|
mHitRegionsOptions.ClearAndRetainStorage();
|
|
|
|
// Since the target changes the backing texture will change, and this will
|
|
// no longer be valid.
|
|
mIsEntireFrameInvalid = false;
|
|
mPredictManyRedrawCalls = false;
|
|
mIsCapturedFrameInvalid = false;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::OnShutdown()
|
|
{
|
|
mShutdownObserver = nullptr;
|
|
|
|
RefPtr<PersistentBufferProvider> provider = mBufferProvider;
|
|
|
|
Reset();
|
|
|
|
if (provider) {
|
|
provider->OnShutdown();
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RemoveShutdownObserver()
|
|
{
|
|
if (mShutdownObserver) {
|
|
mShutdownObserver->OnShutdown();
|
|
mShutdownObserver = nullptr;
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetStyleFromString(const nsAString& aStr,
|
|
Style aWhichStyle)
|
|
{
|
|
MOZ_ASSERT(!aStr.IsVoid());
|
|
|
|
nscolor color;
|
|
if (!ParseColor(aStr, &color)) {
|
|
return;
|
|
}
|
|
|
|
CurrentState().SetColorStyle(aWhichStyle, color);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetStyleAsUnion(OwningStringOrCanvasGradientOrCanvasPattern& aValue,
|
|
Style aWhichStyle)
|
|
{
|
|
const ContextState& state = CurrentState();
|
|
if (state.patternStyles[aWhichStyle]) {
|
|
aValue.SetAsCanvasPattern() = state.patternStyles[aWhichStyle];
|
|
} else if (state.gradientStyles[aWhichStyle]) {
|
|
aValue.SetAsCanvasGradient() = state.gradientStyles[aWhichStyle];
|
|
} else {
|
|
StyleColorToString(state.colorStyles[aWhichStyle], aValue.SetAsString());
|
|
}
|
|
}
|
|
|
|
// static
|
|
void
|
|
CanvasRenderingContext2D::StyleColorToString(const nscolor& aColor, nsAString& aStr)
|
|
{
|
|
// We can't reuse the normal CSS color stringification code,
|
|
// because the spec calls for a different algorithm for canvas.
|
|
if (NS_GET_A(aColor) == 255) {
|
|
CopyUTF8toUTF16(nsPrintfCString("#%02x%02x%02x",
|
|
NS_GET_R(aColor),
|
|
NS_GET_G(aColor),
|
|
NS_GET_B(aColor)),
|
|
aStr);
|
|
} else {
|
|
CopyUTF8toUTF16(nsPrintfCString("rgba(%d, %d, %d, ",
|
|
NS_GET_R(aColor),
|
|
NS_GET_G(aColor),
|
|
NS_GET_B(aColor)),
|
|
aStr);
|
|
aStr.AppendFloat(nsStyleUtil::ColorComponentToFloat(NS_GET_A(aColor)));
|
|
aStr.Append(')');
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
CanvasRenderingContext2D::Redraw()
|
|
{
|
|
mIsCapturedFrameInvalid = true;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mIsEntireFrameInvalid = true;
|
|
|
|
if (!mCanvasElement) {
|
|
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
|
|
return NS_OK;
|
|
}
|
|
|
|
nsSVGEffects::InvalidateDirectRenderingObservers(mCanvasElement);
|
|
|
|
mCanvasElement->InvalidateCanvasContent(nullptr);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Redraw(const gfx::Rect& aR)
|
|
{
|
|
mIsCapturedFrameInvalid = true;
|
|
|
|
++mInvalidateCount;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
return;
|
|
}
|
|
|
|
if (mPredictManyRedrawCalls ||
|
|
mInvalidateCount > kCanvasMaxInvalidateCount) {
|
|
Redraw();
|
|
return;
|
|
}
|
|
|
|
if (!mCanvasElement) {
|
|
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
|
|
return;
|
|
}
|
|
|
|
nsSVGEffects::InvalidateDirectRenderingObservers(mCanvasElement);
|
|
|
|
mCanvasElement->InvalidateCanvasContent(&aR);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::DidRefresh()
|
|
{
|
|
if (IsTargetValid() && mIsSkiaGL) {
|
|
SkiaGLGlue* glue = gfxPlatform::GetPlatform()->GetSkiaGLGlue();
|
|
MOZ_ASSERT(glue);
|
|
|
|
auto gl = glue->GetGLContext();
|
|
gl->FlushIfHeavyGLCallsSinceLastFlush();
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RedrawUser(const gfxRect& aR)
|
|
{
|
|
mIsCapturedFrameInvalid = true;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
++mInvalidateCount;
|
|
return;
|
|
}
|
|
|
|
gfx::Rect newr = mTarget->GetTransform().TransformBounds(ToRect(aR));
|
|
Redraw(newr);
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::AllowOpenGLCanvas() const
|
|
{
|
|
// If we somehow didn't have the correct compositor in the constructor,
|
|
// we could do something like this to get it:
|
|
//
|
|
// HTMLCanvasElement* el = GetCanvas();
|
|
// if (el) {
|
|
// mCompositorBackend = el->GetCompositorBackendType();
|
|
// }
|
|
//
|
|
// We could have LAYERS_NONE if there was no widget at the time of
|
|
// canvas creation, but in that case the
|
|
// HTMLCanvasElement::GetCompositorBackendType would return LAYERS_NONE
|
|
// as well, so it wouldn't help much.
|
|
|
|
return (mCompositorBackend == LayersBackend::LAYERS_OPENGL) &&
|
|
gfxPlatform::GetPlatform()->AllowOpenGLCanvas();
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::SwitchRenderingMode(RenderingMode aRenderingMode)
|
|
{
|
|
if (!IsTargetValid() || mRenderingMode == aRenderingMode) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(mBufferProvider);
|
|
|
|
#ifdef USE_SKIA_GPU
|
|
// Do not attempt to switch into GL mode if the platform doesn't allow it.
|
|
if ((aRenderingMode == RenderingMode::OpenGLBackendMode) &&
|
|
!AllowOpenGLCanvas()) {
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
RefPtr<PersistentBufferProvider> oldBufferProvider = mBufferProvider;
|
|
|
|
// Return the old target to the buffer provider.
|
|
// We need to do this before calling EnsureTarget.
|
|
ReturnTarget();
|
|
mTarget = nullptr;
|
|
mBufferProvider = nullptr;
|
|
mResetLayer = true;
|
|
|
|
// Borrowing the snapshot must be done after ReturnTarget.
|
|
RefPtr<SourceSurface> snapshot = oldBufferProvider->BorrowSnapshot();
|
|
|
|
// Recreate mTarget using the new rendering mode
|
|
RenderingMode attemptedMode = EnsureTarget(nullptr, aRenderingMode);
|
|
if (!IsTargetValid()) {
|
|
oldBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
return false;
|
|
}
|
|
|
|
// We succeeded, so update mRenderingMode to reflect reality
|
|
mRenderingMode = attemptedMode;
|
|
|
|
// Restore the content from the old DrawTarget
|
|
// Clips and transform were already restored in EnsureTarget.
|
|
mTarget->CopySurface(snapshot, IntRect(0, 0, mWidth, mHeight), IntPoint());
|
|
oldBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
return true;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::Demote()
|
|
{
|
|
if (SwitchRenderingMode(RenderingMode::SoftwareBackendMode)) {
|
|
RemoveDemotableContext(this);
|
|
}
|
|
}
|
|
|
|
std::vector<CanvasRenderingContext2D*>&
|
|
CanvasRenderingContext2D::DemotableContexts()
|
|
{
|
|
// This is a list of raw pointers to cycle-collected objects. We need to ensure
|
|
// that we remove elements from it during UNLINK (which can happen considerably before
|
|
// the actual destructor) since the object is logically destroyed at that point
|
|
// and will be in an inconsistant state.
|
|
static std::vector<CanvasRenderingContext2D*> contexts;
|
|
return contexts;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::DemoteOldestContextIfNecessary()
|
|
{
|
|
const size_t kMaxContexts = 64;
|
|
|
|
std::vector<CanvasRenderingContext2D*>& contexts = DemotableContexts();
|
|
if (contexts.size() < kMaxContexts)
|
|
return;
|
|
|
|
CanvasRenderingContext2D* oldest = contexts.front();
|
|
if (oldest->SwitchRenderingMode(RenderingMode::SoftwareBackendMode)) {
|
|
RemoveDemotableContext(oldest);
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::AddDemotableContext(CanvasRenderingContext2D* aContext)
|
|
{
|
|
std::vector<CanvasRenderingContext2D*>::iterator iter = std::find(DemotableContexts().begin(), DemotableContexts().end(), aContext);
|
|
if (iter != DemotableContexts().end())
|
|
return;
|
|
|
|
DemotableContexts().push_back(aContext);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RemoveDemotableContext(CanvasRenderingContext2D* aContext)
|
|
{
|
|
std::vector<CanvasRenderingContext2D*>::iterator iter = std::find(DemotableContexts().begin(), DemotableContexts().end(), aContext);
|
|
if (iter != DemotableContexts().end())
|
|
DemotableContexts().erase(iter);
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::CheckSizeForSkiaGL(IntSize aSize) {
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
int minsize = Preferences::GetInt("gfx.canvas.min-size-for-skia-gl", 128);
|
|
if (aSize.width < minsize || aSize.height < minsize) {
|
|
return false;
|
|
}
|
|
|
|
// Maximum pref allows 3 different options:
|
|
// 0 means unlimited size
|
|
// > 0 means use value as an absolute threshold
|
|
// < 0 means use the number of screen pixels as a threshold
|
|
int maxsize = Preferences::GetInt("gfx.canvas.max-size-for-skia-gl", 0);
|
|
|
|
// unlimited max size
|
|
if (!maxsize) {
|
|
return true;
|
|
}
|
|
|
|
// absolute max size threshold
|
|
if (maxsize > 0) {
|
|
return aSize.width <= maxsize && aSize.height <= maxsize;
|
|
}
|
|
|
|
// Cache the number of pixels on the primary screen
|
|
static int32_t gScreenPixels = -1;
|
|
if (gScreenPixels < 0) {
|
|
// Default to historical mobile screen size of 980x480, like FishIEtank.
|
|
// In addition, allow skia use up to this size even if the screen is smaller.
|
|
// A lot content expects this size to work well.
|
|
// See Bug 999841
|
|
if (gfxPlatform::GetPlatform()->HasEnoughTotalSystemMemoryForSkiaGL()) {
|
|
gScreenPixels = 980 * 480;
|
|
}
|
|
|
|
nsCOMPtr<nsIScreenManager> screenManager =
|
|
do_GetService("@mozilla.org/gfx/screenmanager;1");
|
|
if (screenManager) {
|
|
nsCOMPtr<nsIScreen> primaryScreen;
|
|
screenManager->GetPrimaryScreen(getter_AddRefs(primaryScreen));
|
|
if (primaryScreen) {
|
|
int32_t x, y, width, height;
|
|
primaryScreen->GetRect(&x, &y, &width, &height);
|
|
|
|
gScreenPixels = std::max(gScreenPixels, width * height);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Just always use a scale of 1.0. It can be changed if a lot of contents need it.
|
|
static double gDefaultScale = 1.0;
|
|
|
|
double scale = gDefaultScale > 0 ? gDefaultScale : 1.0;
|
|
int32_t threshold = ceil(scale * scale * gScreenPixels);
|
|
|
|
// screen size acts as max threshold
|
|
return threshold < 0 || (aSize.width * aSize.height) <= threshold;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ScheduleStableStateCallback()
|
|
{
|
|
if (mHasPendingStableStateCallback) {
|
|
return;
|
|
}
|
|
mHasPendingStableStateCallback = true;
|
|
|
|
nsContentUtils::RunInStableState(
|
|
NewRunnableMethod(this, &CanvasRenderingContext2D::OnStableState)
|
|
);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::OnStableState()
|
|
{
|
|
if (!mHasPendingStableStateCallback) {
|
|
return;
|
|
}
|
|
|
|
ReturnTarget();
|
|
|
|
mHasPendingStableStateCallback = false;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RestoreClipsAndTransformToTarget()
|
|
{
|
|
// Restore clips and transform.
|
|
mTarget->SetTransform(Matrix());
|
|
|
|
if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) {
|
|
// Cairo doesn't play well with huge clips. When given a very big clip it
|
|
// will try to allocate big mask surface without taking the target
|
|
// size into account which can cause OOM. See bug 1034593.
|
|
// This limits the clip extents to the size of the canvas.
|
|
// A fix in Cairo would probably be preferable, but requires somewhat
|
|
// invasive changes.
|
|
mTarget->PushClipRect(gfx::Rect(0, 0, mWidth, mHeight));
|
|
}
|
|
|
|
for (const auto& style : mStyleStack) {
|
|
for (const auto& clipOrTransform : style.clipsAndTransforms) {
|
|
if (clipOrTransform.IsClip()) {
|
|
mTarget->PushClip(clipOrTransform.clip);
|
|
} else {
|
|
mTarget->SetTransform(clipOrTransform.transform);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CanvasRenderingContext2D::RenderingMode
|
|
CanvasRenderingContext2D::EnsureTarget(const gfx::Rect* aCoveredRect,
|
|
RenderingMode aRenderingMode)
|
|
{
|
|
if (AlreadyShutDown()) {
|
|
gfxCriticalError() << "Attempt to render into a Canvas2d after shutdown.";
|
|
SetErrorState();
|
|
return aRenderingMode;
|
|
}
|
|
|
|
// This would make no sense, so make sure we don't get ourselves in a mess
|
|
MOZ_ASSERT(mRenderingMode != RenderingMode::DefaultBackendMode);
|
|
|
|
RenderingMode mode = (aRenderingMode == RenderingMode::DefaultBackendMode) ? mRenderingMode : aRenderingMode;
|
|
|
|
if (mTarget && mode == mRenderingMode) {
|
|
return mRenderingMode;
|
|
}
|
|
|
|
// Check that the dimensions are sane
|
|
if (mWidth > gfxPrefs::MaxCanvasSize() ||
|
|
mHeight > gfxPrefs::MaxCanvasSize() ||
|
|
mWidth < 0 || mHeight < 0) {
|
|
|
|
SetErrorState();
|
|
return aRenderingMode;
|
|
}
|
|
|
|
// If the next drawing command covers the entire canvas, we can skip copying
|
|
// from the previous frame and/or clearing the canvas.
|
|
gfx::Rect canvasRect(0, 0, mWidth, mHeight);
|
|
bool canDiscardContent = aCoveredRect &&
|
|
CurrentState().transform.TransformBounds(*aCoveredRect).Contains(canvasRect);
|
|
|
|
// If a clip is active we don't know for sure that the next drawing command
|
|
// will really cover the entire canvas.
|
|
for (const auto& style : mStyleStack) {
|
|
if (!canDiscardContent) {
|
|
break;
|
|
}
|
|
for (const auto& clipOrTransform : style.clipsAndTransforms) {
|
|
if (clipOrTransform.IsClip()) {
|
|
canDiscardContent = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
ScheduleStableStateCallback();
|
|
|
|
IntRect persistedRect = canDiscardContent ? IntRect()
|
|
: IntRect(0, 0, mWidth, mHeight);
|
|
|
|
if (mBufferProvider && mode == mRenderingMode) {
|
|
mTarget = mBufferProvider->BorrowDrawTarget(persistedRect);
|
|
|
|
if (mTarget && !mBufferProvider->PreservesDrawingState()) {
|
|
RestoreClipsAndTransformToTarget();
|
|
}
|
|
|
|
if (mTarget) {
|
|
return mode;
|
|
}
|
|
}
|
|
|
|
RefPtr<DrawTarget> newTarget;
|
|
RefPtr<PersistentBufferProvider> newProvider;
|
|
|
|
if (mode == RenderingMode::OpenGLBackendMode &&
|
|
!TrySkiaGLTarget(newTarget, newProvider)) {
|
|
// Fall back to software.
|
|
mode = RenderingMode::SoftwareBackendMode;
|
|
}
|
|
|
|
if (mode == RenderingMode::SoftwareBackendMode &&
|
|
!TrySharedTarget(newTarget, newProvider) &&
|
|
!TryBasicTarget(newTarget, newProvider)) {
|
|
SetErrorState();
|
|
return mode;
|
|
}
|
|
|
|
MOZ_ASSERT(newTarget);
|
|
MOZ_ASSERT(newProvider);
|
|
|
|
mTarget = newTarget.forget();
|
|
mBufferProvider = newProvider.forget();
|
|
|
|
RegisterAllocation();
|
|
|
|
// Skia expects the unused X channel to contains 0 even for opaque operations
|
|
// so we can't skip clearing in that case, even if we are going to cover the
|
|
// entire canvas in the next drawing operation.
|
|
if (!canDiscardContent || mTarget->GetBackendType() == gfx::BackendType::SKIA) {
|
|
mTarget->ClearRect(canvasRect);
|
|
}
|
|
|
|
RestoreClipsAndTransformToTarget();
|
|
|
|
// Force a full layer transaction since we didn't have a layer before
|
|
// and now we might need one.
|
|
if (mCanvasElement) {
|
|
mCanvasElement->InvalidateCanvas();
|
|
}
|
|
// Calling Redraw() tells our invalidation machinery that the entire
|
|
// canvas is already invalid, which can speed up future drawing.
|
|
Redraw();
|
|
|
|
return mode;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetInitialState()
|
|
{
|
|
// Set up the initial canvas defaults
|
|
mPathBuilder = nullptr;
|
|
mPath = nullptr;
|
|
mDSPathBuilder = nullptr;
|
|
mPathTransformWillUpdate = false;
|
|
|
|
mStyleStack.Clear();
|
|
ContextState* state = mStyleStack.AppendElement();
|
|
state->globalAlpha = 1.0;
|
|
|
|
state->colorStyles[Style::FILL] = NS_RGB(0,0,0);
|
|
state->colorStyles[Style::STROKE] = NS_RGB(0,0,0);
|
|
state->shadowColor = NS_RGBA(0,0,0,0);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetErrorState()
|
|
{
|
|
EnsureErrorTarget();
|
|
|
|
if (mTarget && mTarget != sErrorTarget) {
|
|
gCanvasAzureMemoryUsed -= mWidth * mHeight * 4;
|
|
}
|
|
|
|
mTarget = sErrorTarget;
|
|
mBufferProvider = nullptr;
|
|
|
|
// clear transforms, clips, etc.
|
|
SetInitialState();
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RegisterAllocation()
|
|
{
|
|
// XXX - It would make more sense to track the allocation in
|
|
// PeristentBufferProvider, rather than here.
|
|
static bool registered = false;
|
|
// FIXME: Disable the reporter for now, see bug 1241865
|
|
if (!registered && false) {
|
|
registered = true;
|
|
RegisterStrongMemoryReporter(new Canvas2dPixelsReporter());
|
|
}
|
|
|
|
gCanvasAzureMemoryUsed += mWidth * mHeight * 4;
|
|
JSContext* context = nsContentUtils::GetCurrentJSContext();
|
|
if (context) {
|
|
JS_updateMallocCounter(context, mWidth * mHeight * 4);
|
|
}
|
|
}
|
|
|
|
static already_AddRefed<LayerManager>
|
|
LayerManagerFromCanvasElement(nsINode* aCanvasElement)
|
|
{
|
|
if (!aCanvasElement || !aCanvasElement->OwnerDoc()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return nsContentUtils::PersistentLayerManagerForDocument(aCanvasElement->OwnerDoc());
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::TrySkiaGLTarget(RefPtr<gfx::DrawTarget>& aOutDT,
|
|
RefPtr<layers::PersistentBufferProvider>& aOutProvider)
|
|
{
|
|
aOutDT = nullptr;
|
|
aOutProvider = nullptr;
|
|
|
|
mIsSkiaGL = false;
|
|
|
|
IntSize size(mWidth, mHeight);
|
|
if (!AllowOpenGLCanvas() || !CheckSizeForSkiaGL(size)) {
|
|
return false;
|
|
}
|
|
|
|
|
|
RefPtr<LayerManager> layerManager = LayerManagerFromCanvasElement(mCanvasElement);
|
|
|
|
if (!layerManager) {
|
|
return false;
|
|
}
|
|
|
|
DemoteOldestContextIfNecessary();
|
|
mBufferProvider = nullptr;
|
|
|
|
#ifdef USE_SKIA_GPU
|
|
SkiaGLGlue* glue = gfxPlatform::GetPlatform()->GetSkiaGLGlue();
|
|
if (!glue || !glue->GetGrContext() || !glue->GetGLContext()) {
|
|
return false;
|
|
}
|
|
|
|
SurfaceFormat format = GetSurfaceFormat();
|
|
aOutDT = Factory::CreateDrawTargetSkiaWithGrContext(glue->GetGrContext(),
|
|
size, format);
|
|
if (!aOutDT) {
|
|
gfxCriticalNote << "Failed to create a SkiaGL DrawTarget, falling back to software\n";
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(aOutDT->GetType() == DrawTargetType::HARDWARE_RASTER);
|
|
|
|
AddDemotableContext(this);
|
|
aOutProvider = new PersistentBufferProviderBasic(aOutDT);
|
|
mIsSkiaGL = true;
|
|
// Drop a note in the debug builds if we ever use accelerated Skia canvas.
|
|
gfxWarningOnce() << "Using SkiaGL canvas.";
|
|
#endif
|
|
|
|
// could still be null if USE_SKIA_GPU is not #defined.
|
|
return !!aOutDT;
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::TrySharedTarget(RefPtr<gfx::DrawTarget>& aOutDT,
|
|
RefPtr<layers::PersistentBufferProvider>& aOutProvider)
|
|
{
|
|
aOutDT = nullptr;
|
|
aOutProvider = nullptr;
|
|
|
|
if (!mCanvasElement || !mCanvasElement->OwnerDoc()) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<LayerManager> layerManager = LayerManagerFromCanvasElement(mCanvasElement);
|
|
|
|
if (!layerManager) {
|
|
return false;
|
|
}
|
|
|
|
aOutProvider = layerManager->CreatePersistentBufferProvider(GetSize(), GetSurfaceFormat());
|
|
|
|
if (!aOutProvider) {
|
|
return false;
|
|
}
|
|
|
|
// We can pass an empty persisted rect since we just created the buffer
|
|
// provider (nothing to restore).
|
|
aOutDT = aOutProvider->BorrowDrawTarget(IntRect());
|
|
MOZ_ASSERT(aOutDT);
|
|
|
|
return !!aOutDT;
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::TryBasicTarget(RefPtr<gfx::DrawTarget>& aOutDT,
|
|
RefPtr<layers::PersistentBufferProvider>& aOutProvider)
|
|
{
|
|
aOutDT = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget(GetSize(),
|
|
GetSurfaceFormat());
|
|
if (!aOutDT) {
|
|
return false;
|
|
}
|
|
|
|
aOutProvider = new PersistentBufferProviderBasic(aOutDT);
|
|
return true;
|
|
}
|
|
|
|
int32_t
|
|
CanvasRenderingContext2D::GetWidth() const
|
|
{
|
|
return mWidth;
|
|
}
|
|
|
|
int32_t
|
|
CanvasRenderingContext2D::GetHeight() const
|
|
{
|
|
return mHeight;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::SetDimensions(int32_t aWidth, int32_t aHeight)
|
|
{
|
|
// Zero sized surfaces can cause problems.
|
|
mZero = false;
|
|
if (aHeight == 0) {
|
|
aHeight = 1;
|
|
mZero = true;
|
|
}
|
|
if (aWidth == 0) {
|
|
aWidth = 1;
|
|
mZero = true;
|
|
}
|
|
|
|
ClearTarget(aWidth, aHeight);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ClearTarget(int32_t aWidth, int32_t aHeight)
|
|
{
|
|
Reset();
|
|
|
|
mResetLayer = true;
|
|
|
|
SetInitialState();
|
|
|
|
// Update dimensions only if new (strictly positive) values were passed.
|
|
if (aWidth > 0 && aHeight > 0) {
|
|
mWidth = aWidth;
|
|
mHeight = aHeight;
|
|
}
|
|
|
|
// For vertical writing-mode, unless text-orientation is sideways,
|
|
// we'll modify the initial value of textBaseline to 'middle'.
|
|
RefPtr<nsStyleContext> canvasStyle;
|
|
if (mCanvasElement && mCanvasElement->IsInUncomposedDoc()) {
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
if (presShell) {
|
|
canvasStyle =
|
|
nsComputedDOMStyle::GetStyleContextForElement(mCanvasElement,
|
|
nullptr,
|
|
presShell);
|
|
if (canvasStyle) {
|
|
WritingMode wm(canvasStyle);
|
|
if (wm.IsVertical() && !wm.IsSideways()) {
|
|
CurrentState().textBaseline = TextBaseline::MIDDLE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ReturnTarget(bool aForceReset)
|
|
{
|
|
if (mTarget && mBufferProvider && mTarget != sErrorTarget) {
|
|
CurrentState().transform = mTarget->GetTransform();
|
|
if (aForceReset || !mBufferProvider->PreservesDrawingState()) {
|
|
for (const auto& style : mStyleStack) {
|
|
for (const auto& clipOrTransform : style.clipsAndTransforms) {
|
|
if (clipOrTransform.IsClip()) {
|
|
mTarget->PopClip();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) {
|
|
// With the cairo backend we pushed an extra clip rect which we have to
|
|
// balance out here. See the comment in RestoreClipsAndTransformToTarget.
|
|
mTarget->PopClip();
|
|
}
|
|
|
|
mTarget->SetTransform(Matrix());
|
|
}
|
|
|
|
mBufferProvider->ReturnDrawTarget(mTarget.forget());
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::InitializeWithDrawTarget(nsIDocShell* aShell,
|
|
NotNull<gfx::DrawTarget*> aTarget)
|
|
{
|
|
RemovePostRefreshObserver();
|
|
mDocShell = aShell;
|
|
AddPostRefreshObserverIfNecessary();
|
|
|
|
IntSize size = aTarget->GetSize();
|
|
SetDimensions(size.width, size.height);
|
|
|
|
mTarget = aTarget;
|
|
mBufferProvider = new PersistentBufferProviderBasic(aTarget);
|
|
|
|
if (mTarget->GetBackendType() == gfx::BackendType::CAIRO) {
|
|
// Cf comment in EnsureTarget
|
|
mTarget->PushClipRect(gfx::Rect(Point(0, 0), Size(mWidth, mHeight)));
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::SetIsOpaque(bool aIsOpaque)
|
|
{
|
|
if (aIsOpaque != mOpaque) {
|
|
mOpaque = aIsOpaque;
|
|
ClearTarget();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::SetIsIPC(bool aIsIPC)
|
|
{
|
|
if (aIsIPC != mIPC) {
|
|
mIPC = aIsIPC;
|
|
ClearTarget();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::SetContextOptions(JSContext* aCx,
|
|
JS::Handle<JS::Value> aOptions,
|
|
ErrorResult& aRvForDictionaryInit)
|
|
{
|
|
if (aOptions.isNullOrUndefined()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// This shouldn't be called before drawing starts, so there should be no drawtarget yet
|
|
MOZ_ASSERT(!mTarget);
|
|
|
|
ContextAttributes2D attributes;
|
|
if (!attributes.Init(aCx, aOptions)) {
|
|
aRvForDictionaryInit.Throw(NS_ERROR_UNEXPECTED);
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
if (Preferences::GetBool("gfx.canvas.willReadFrequently.enable", false)) {
|
|
// Use software when there is going to be a lot of readback
|
|
if (attributes.mWillReadFrequently) {
|
|
|
|
// We want to lock into software, so remove the observer that
|
|
// may potentially change that...
|
|
RemoveDrawObserver();
|
|
mRenderingMode = RenderingMode::SoftwareBackendMode;
|
|
}
|
|
}
|
|
|
|
if (!attributes.mAlpha) {
|
|
SetIsOpaque(true);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
UniquePtr<uint8_t[]>
|
|
CanvasRenderingContext2D::GetImageBuffer(int32_t* aFormat)
|
|
{
|
|
UniquePtr<uint8_t[]> ret;
|
|
|
|
*aFormat = 0;
|
|
|
|
RefPtr<SourceSurface> snapshot;
|
|
if (mTarget) {
|
|
snapshot = mTarget->Snapshot();
|
|
} else if (mBufferProvider) {
|
|
snapshot = mBufferProvider->BorrowSnapshot();
|
|
} else {
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return nullptr;
|
|
}
|
|
snapshot = mTarget->Snapshot();
|
|
}
|
|
|
|
if (snapshot) {
|
|
RefPtr<DataSourceSurface> data = snapshot->GetDataSurface();
|
|
if (data && data->GetSize() == GetSize()) {
|
|
*aFormat = imgIEncoder::INPUT_FORMAT_HOSTARGB;
|
|
ret = SurfaceToPackedBGRA(data);
|
|
}
|
|
}
|
|
|
|
if (!mTarget && mBufferProvider) {
|
|
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
nsString CanvasRenderingContext2D::GetHitRegion(const mozilla::gfx::Point& aPoint)
|
|
{
|
|
for (size_t x = 0 ; x < mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = mHitRegionsOptions[x];
|
|
if (info.mPath->ContainsPoint(aPoint, Matrix())) {
|
|
return info.mId;
|
|
}
|
|
}
|
|
return nsString();
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::GetInputStream(const char* aMimeType,
|
|
const char16_t* aEncoderOptions,
|
|
nsIInputStream** aStream)
|
|
{
|
|
nsCString enccid("@mozilla.org/image/encoder;2?type=");
|
|
enccid += aMimeType;
|
|
nsCOMPtr<imgIEncoder> encoder = do_CreateInstance(enccid.get());
|
|
if (!encoder) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
int32_t format = 0;
|
|
UniquePtr<uint8_t[]> imageBuffer = GetImageBuffer(&format);
|
|
if (!imageBuffer) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
bool PoisonData = Preferences::GetBool("canvas.poisondata",false);
|
|
if (PoisonData) {
|
|
int PoisonInterval = Preferences::GetInt("canvas.poisondata.interval", 300);
|
|
PoisonInterval = (PoisonInterval < 1) ? 1 : (PoisonInterval > 28800) ? 28800 : PoisonInterval;
|
|
unsigned int epoch = time(nullptr);
|
|
srand(epoch / PoisonInterval);
|
|
// Image buffer is always a packed BGRA array (BGRX -> BGR[FF])
|
|
// so always 4-byte pixels.
|
|
// GetImageBuffer => SurfaceToPackedBGRA [=> ConvertBGRXToBGRA]
|
|
for (int32_t j = 0; j < mWidth * mHeight * 4; ++j) {
|
|
if (imageBuffer[j] !=0 && imageBuffer[j] != 255)
|
|
imageBuffer[j] += rand() % 3 - 1;
|
|
}
|
|
}
|
|
|
|
return ImageEncoder::GetInputStream(mWidth, mHeight, imageBuffer.get(),
|
|
format, encoder, aEncoderOptions,
|
|
aStream);
|
|
}
|
|
|
|
SurfaceFormat
|
|
CanvasRenderingContext2D::GetSurfaceFormat() const
|
|
{
|
|
return mOpaque ? SurfaceFormat::B8G8R8X8 : SurfaceFormat::B8G8R8A8;
|
|
}
|
|
|
|
//
|
|
// state
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::Save()
|
|
{
|
|
EnsureTarget();
|
|
if (MOZ_UNLIKELY(!mTarget || mStyleStack.IsEmpty())) {
|
|
SetErrorState();
|
|
return;
|
|
}
|
|
mStyleStack[mStyleStack.Length() - 1].transform = mTarget->GetTransform();
|
|
mStyleStack.SetCapacity(mStyleStack.Length() + 1);
|
|
mStyleStack.AppendElement(CurrentState());
|
|
|
|
if (mStyleStack.Length() > MAX_STYLE_STACK_SIZE) {
|
|
// This is not fast, but is better than OOMing and shouldn't be hit by
|
|
// reasonable code.
|
|
mStyleStack.RemoveElementAt(0);
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Restore()
|
|
{
|
|
if (MOZ_UNLIKELY(mStyleStack.Length() < 2)) {
|
|
return;
|
|
}
|
|
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
for (const auto& clipOrTransform : CurrentState().clipsAndTransforms) {
|
|
if (clipOrTransform.IsClip()) {
|
|
mTarget->PopClip();
|
|
}
|
|
}
|
|
|
|
mStyleStack.RemoveElementAt(mStyleStack.Length() - 1);
|
|
|
|
mTarget->SetTransform(CurrentState().transform);
|
|
}
|
|
|
|
//
|
|
// transformations
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::Scale(double aX, double aY, ErrorResult& aError)
|
|
{
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newMatrix = mTarget->GetTransform();
|
|
newMatrix.PreScale(aX, aY);
|
|
|
|
SetTransformInternal(newMatrix);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Rotate(double aAngle, ErrorResult& aError)
|
|
{
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newMatrix = Matrix::Rotation(aAngle) * mTarget->GetTransform();
|
|
|
|
SetTransformInternal(newMatrix);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Translate(double aX, double aY, ErrorResult& aError)
|
|
{
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newMatrix = mTarget->GetTransform();
|
|
newMatrix.PreTranslate(aX, aY);
|
|
|
|
SetTransformInternal(newMatrix);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Transform(double aM11, double aM12, double aM21,
|
|
double aM22, double aDx, double aDy,
|
|
ErrorResult& aError)
|
|
{
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newMatrix(aM11, aM12, aM21, aM22, aDx, aDy);
|
|
newMatrix *= mTarget->GetTransform();
|
|
|
|
SetTransformInternal(newMatrix);
|
|
}
|
|
|
|
already_AddRefed<DOMMatrix>
|
|
CanvasRenderingContext2D::GetTransform(ErrorResult& aError) {
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
RefPtr<DOMMatrix> matrix =
|
|
new DOMMatrix(GetParentObject(), mTarget->GetTransform());
|
|
return matrix.forget();
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetTransform(double aM11, double aM12,
|
|
double aM21, double aM22,
|
|
double aDx, double aDy,
|
|
ErrorResult& aError)
|
|
{
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
SetTransformInternal(Matrix(aM11, aM12, aM21, aM22, aDx, aDy));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetTransform(const DOMMatrix2DInit& aInit,
|
|
ErrorResult& aError) {
|
|
TransformWillUpdate();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
RefPtr<DOMMatrixReadOnly> matrix =
|
|
DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError);
|
|
if (!aError.Failed()) {
|
|
SetTransformInternal(Matrix(*(matrix->GetInternal2D())));
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetTransformInternal(const Matrix& aTransform)
|
|
{
|
|
if (!aTransform.IsFinite()) {
|
|
return;
|
|
}
|
|
|
|
// Save the transform in the clip stack to be able to replay clips properly.
|
|
auto& clipsAndTransforms = CurrentState().clipsAndTransforms;
|
|
if (clipsAndTransforms.IsEmpty() || clipsAndTransforms.LastElement().IsClip()) {
|
|
clipsAndTransforms.AppendElement(ClipState(aTransform));
|
|
} else {
|
|
// If the last item is a transform we can replace it instead of appending
|
|
// a new item.
|
|
clipsAndTransforms.LastElement().transform = aTransform;
|
|
}
|
|
mTarget->SetTransform(aTransform);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ResetTransform(ErrorResult& aError)
|
|
{
|
|
SetTransform(1.0, 0.0, 0.0, 1.0, 0.0, 0.0, aError);
|
|
}
|
|
|
|
static void
|
|
MatrixToJSObject(JSContext* aCx, const Matrix& aMatrix,
|
|
JS::MutableHandle<JSObject*> aResult, ErrorResult& aError)
|
|
{
|
|
double elts[6] = { aMatrix._11, aMatrix._12,
|
|
aMatrix._21, aMatrix._22,
|
|
aMatrix._31, aMatrix._32 };
|
|
|
|
// XXX Should we enter GetWrapper()'s compartment?
|
|
JS::Rooted<JS::Value> val(aCx);
|
|
if (!ToJSValue(aCx, elts, &val)) {
|
|
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
} else {
|
|
aResult.set(&val.toObject());
|
|
}
|
|
}
|
|
|
|
static bool
|
|
ObjectToMatrix(JSContext* aCx, JS::Handle<JSObject*> aObj, Matrix& aMatrix,
|
|
ErrorResult& aError)
|
|
{
|
|
uint32_t length;
|
|
if (!JS_GetArrayLength(aCx, aObj, &length) || length != 6) {
|
|
// Not an array-like thing or wrong size
|
|
aError.Throw(NS_ERROR_INVALID_ARG);
|
|
return false;
|
|
}
|
|
|
|
Float* elts[] = { &aMatrix._11, &aMatrix._12, &aMatrix._21, &aMatrix._22,
|
|
&aMatrix._31, &aMatrix._32 };
|
|
for (uint32_t i = 0; i < 6; ++i) {
|
|
JS::Rooted<JS::Value> elt(aCx);
|
|
double d;
|
|
if (!JS_GetElement(aCx, aObj, i, &elt)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
if (!CoerceDouble(elt, &d)) {
|
|
aError.Throw(NS_ERROR_INVALID_ARG);
|
|
return false;
|
|
}
|
|
if (!FloatValidate(d)) {
|
|
// This is weird, but it's the behavior of SetTransform()
|
|
return false;
|
|
}
|
|
*elts[i] = Float(d);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetMozCurrentTransform(JSContext* aCx,
|
|
JS::Handle<JSObject*> aCurrentTransform,
|
|
ErrorResult& aError)
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newCTM;
|
|
if (ObjectToMatrix(aCx, aCurrentTransform, newCTM, aError) && newCTM.IsFinite()) {
|
|
mTarget->SetTransform(newCTM);
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetMozCurrentTransform(JSContext* aCx,
|
|
JS::MutableHandle<JSObject*> aResult,
|
|
ErrorResult& aError)
|
|
{
|
|
EnsureTarget();
|
|
|
|
MatrixToJSObject(aCx, mTarget ? mTarget->GetTransform() : Matrix(),
|
|
aResult, aError);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetMozCurrentTransformInverse(JSContext* aCx,
|
|
JS::Handle<JSObject*> aCurrentTransform,
|
|
ErrorResult& aError)
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
Matrix newCTMInverse;
|
|
if (ObjectToMatrix(aCx, aCurrentTransform, newCTMInverse, aError)) {
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
if (newCTMInverse.Invert() && newCTMInverse.IsFinite()) {
|
|
mTarget->SetTransform(newCTMInverse);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetMozCurrentTransformInverse(JSContext* aCx,
|
|
JS::MutableHandle<JSObject*> aResult,
|
|
ErrorResult& aError)
|
|
{
|
|
EnsureTarget();
|
|
|
|
if (!mTarget) {
|
|
MatrixToJSObject(aCx, Matrix(), aResult, aError);
|
|
return;
|
|
}
|
|
|
|
Matrix ctm = mTarget->GetTransform();
|
|
|
|
if (!ctm.Invert()) {
|
|
double NaN = JS_GetNaNValue(aCx).toDouble();
|
|
ctm = Matrix(NaN, NaN, NaN, NaN, NaN, NaN);
|
|
}
|
|
|
|
MatrixToJSObject(aCx, ctm, aResult, aError);
|
|
}
|
|
|
|
//
|
|
// colors
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetStyleFromUnion(const StringOrCanvasGradientOrCanvasPattern& aValue,
|
|
Style aWhichStyle)
|
|
{
|
|
if (aValue.IsString()) {
|
|
SetStyleFromString(aValue.GetAsString(), aWhichStyle);
|
|
return;
|
|
}
|
|
|
|
if (aValue.IsCanvasGradient()) {
|
|
SetStyleFromGradient(aValue.GetAsCanvasGradient(), aWhichStyle);
|
|
return;
|
|
}
|
|
|
|
if (aValue.IsCanvasPattern()) {
|
|
SetStyleFromPattern(aValue.GetAsCanvasPattern(), aWhichStyle);
|
|
return;
|
|
}
|
|
|
|
MOZ_ASSERT_UNREACHABLE("Invalid union value");
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetFillRule(const nsAString& aString)
|
|
{
|
|
FillRule rule;
|
|
|
|
if (aString.EqualsLiteral("evenodd"))
|
|
rule = FillRule::FILL_EVEN_ODD;
|
|
else if (aString.EqualsLiteral("nonzero"))
|
|
rule = FillRule::FILL_WINDING;
|
|
else
|
|
return;
|
|
|
|
CurrentState().fillRule = rule;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetFillRule(nsAString& aString)
|
|
{
|
|
switch (CurrentState().fillRule) {
|
|
case FillRule::FILL_WINDING:
|
|
aString.AssignLiteral("nonzero"); break;
|
|
case FillRule::FILL_EVEN_ODD:
|
|
aString.AssignLiteral("evenodd"); break;
|
|
}
|
|
}
|
|
//
|
|
// gradients and patterns
|
|
//
|
|
already_AddRefed<CanvasGradient>
|
|
CanvasRenderingContext2D::CreateLinearGradient(double aX0, double aY0, double aX1, double aY1)
|
|
{
|
|
RefPtr<CanvasGradient> grad =
|
|
new CanvasLinearGradient(this, Point(aX0, aY0), Point(aX1, aY1));
|
|
|
|
return grad.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasGradient>
|
|
CanvasRenderingContext2D::CreateRadialGradient(double aX0, double aY0, double aR0,
|
|
double aX1, double aY1, double aR1,
|
|
ErrorResult& aError)
|
|
{
|
|
if (aR0 < 0.0 || aR1 < 0.0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasGradient> grad =
|
|
new CanvasRadialGradient(this, Point(aX0, aY0), aR0, Point(aX1, aY1), aR1);
|
|
|
|
return grad.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasPattern>
|
|
CanvasRenderingContext2D::CreatePattern(const CanvasImageSource& aSource,
|
|
const nsAString& aRepeat,
|
|
ErrorResult& aError)
|
|
{
|
|
CanvasPattern::RepeatMode repeatMode =
|
|
CanvasPattern::RepeatMode::NOREPEAT;
|
|
|
|
if (aRepeat.IsEmpty() || aRepeat.EqualsLiteral("repeat")) {
|
|
repeatMode = CanvasPattern::RepeatMode::REPEAT;
|
|
} else if (aRepeat.EqualsLiteral("repeat-x")) {
|
|
repeatMode = CanvasPattern::RepeatMode::REPEATX;
|
|
} else if (aRepeat.EqualsLiteral("repeat-y")) {
|
|
repeatMode = CanvasPattern::RepeatMode::REPEATY;
|
|
} else if (aRepeat.EqualsLiteral("no-repeat")) {
|
|
repeatMode = CanvasPattern::RepeatMode::NOREPEAT;
|
|
} else {
|
|
aError.Throw(NS_ERROR_DOM_SYNTAX_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
Element* element;
|
|
if (aSource.IsHTMLCanvasElement()) {
|
|
HTMLCanvasElement* canvas = &aSource.GetAsHTMLCanvasElement();
|
|
element = canvas;
|
|
|
|
nsIntSize size = canvas->GetSize();
|
|
if (size.width == 0 || size.height == 0) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// Special case for Canvas, which could be an Azure canvas!
|
|
nsICanvasRenderingContextInternal* srcCanvas = canvas->GetContextAtIndex(0);
|
|
if (srcCanvas) {
|
|
// This might not be an Azure canvas!
|
|
RefPtr<SourceSurface> srcSurf = srcCanvas->GetSurfaceSnapshot();
|
|
if (!srcSurf) {
|
|
JSContext* context = nsContentUtils::GetCurrentJSContext();
|
|
if (context) {
|
|
JS_ReportWarningASCII(context,
|
|
"CanvasRenderingContext2D.createPattern()"
|
|
" failed to snapshot source canvas.");
|
|
}
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasPattern> pat =
|
|
new CanvasPattern(this, srcSurf, repeatMode, element->NodePrincipal(), canvas->IsWriteOnly(), false);
|
|
|
|
return pat.forget();
|
|
}
|
|
} else if (aSource.IsHTMLImageElement()) {
|
|
HTMLImageElement* img = &aSource.GetAsHTMLImageElement();
|
|
if (img->IntrinsicState().HasState(NS_EVENT_STATE_BROKEN)) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
element = img;
|
|
} else if (aSource.IsSVGImageElement()) {
|
|
SVGImageElement* img = &aSource.GetAsSVGImageElement();
|
|
if (img->IntrinsicState().HasState(NS_EVENT_STATE_BROKEN)) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
element = img;
|
|
} else if (aSource.IsHTMLVideoElement()) {
|
|
auto& video = aSource.GetAsHTMLVideoElement();
|
|
video.MarkAsContentSource(mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_PATTERN);
|
|
element = &video;
|
|
} else {
|
|
// Special case for ImageBitmap
|
|
ImageBitmap& imgBitmap = aSource.GetAsImageBitmap();
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
RefPtr<SourceSurface> srcSurf = imgBitmap.PrepareForDrawTarget(mTarget);
|
|
if (!srcSurf) {
|
|
JSContext* context = nsContentUtils::GetCurrentJSContext();
|
|
if (context) {
|
|
JS_ReportWarningASCII(context,
|
|
"CanvasRenderingContext2D.createPattern()"
|
|
" failed to prepare source ImageBitmap.");
|
|
}
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// An ImageBitmap never taints others so we set principalForSecurityCheck to
|
|
// nullptr and set CORSUsed to true for passing the security check in
|
|
// CanvasUtils::DoDrawImageSecurityCheck().
|
|
RefPtr<CanvasPattern> pat =
|
|
new CanvasPattern(this, srcSurf, repeatMode, nullptr,
|
|
imgBitmap.IsWriteOnly(), true);
|
|
|
|
return pat.forget();
|
|
}
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// The canvas spec says that createPattern should use the first frame
|
|
// of animated images
|
|
nsLayoutUtils::SurfaceFromElementResult res =
|
|
nsLayoutUtils::SurfaceFromElement(element,
|
|
nsLayoutUtils::SFE_WANT_FIRST_FRAME, mTarget);
|
|
|
|
if (!res.GetSourceSurface()) {
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasPattern> pat = new CanvasPattern(this, res.GetSourceSurface(), repeatMode,
|
|
res.mPrincipal, res.mIsWriteOnly,
|
|
res.mCORSUsed);
|
|
return pat.forget();
|
|
}
|
|
|
|
//
|
|
// shadows
|
|
//
|
|
void
|
|
CanvasRenderingContext2D::SetShadowColor(const nsAString& aShadowColor)
|
|
{
|
|
nscolor color;
|
|
if (!ParseColor(aShadowColor, &color)) {
|
|
return;
|
|
}
|
|
|
|
CurrentState().shadowColor = color;
|
|
}
|
|
|
|
//
|
|
// filters
|
|
//
|
|
|
|
static already_AddRefed<Declaration>
|
|
CreateDeclaration(nsINode* aNode,
|
|
const nsCSSPropertyID aProp1, const nsAString& aValue1, bool* aChanged1,
|
|
const nsCSSPropertyID aProp2, const nsAString& aValue2, bool* aChanged2)
|
|
{
|
|
nsIPrincipal* principal = aNode->NodePrincipal();
|
|
nsIDocument* document = aNode->OwnerDoc();
|
|
|
|
nsIURI* docURL = document->GetDocumentURI();
|
|
nsIURI* baseURL = document->GetDocBaseURI();
|
|
|
|
// Pass the CSS Loader object to the parser, to allow parser error reports
|
|
// to include the outer window ID.
|
|
nsCSSParser parser(document->CSSLoader());
|
|
|
|
RefPtr<Declaration> declaration =
|
|
parser.ParseStyleAttribute(EmptyString(), docURL, baseURL, principal);
|
|
|
|
if (aProp1 != eCSSProperty_UNKNOWN) {
|
|
parser.ParseProperty(aProp1, aValue1, docURL, baseURL, principal,
|
|
declaration, aChanged1, false);
|
|
}
|
|
|
|
if (aProp2 != eCSSProperty_UNKNOWN) {
|
|
parser.ParseProperty(aProp2, aValue2, docURL, baseURL, principal,
|
|
declaration, aChanged2, false);
|
|
}
|
|
|
|
declaration->SetImmutable();
|
|
return declaration.forget();
|
|
}
|
|
|
|
static already_AddRefed<Declaration>
|
|
CreateFontDeclaration(const nsAString& aFont,
|
|
nsINode* aNode,
|
|
bool* aOutFontChanged)
|
|
{
|
|
bool lineHeightChanged;
|
|
return CreateDeclaration(aNode,
|
|
eCSSProperty_font, aFont, aOutFontChanged,
|
|
eCSSProperty_line_height, NS_LITERAL_STRING("normal"), &lineHeightChanged);
|
|
}
|
|
|
|
static already_AddRefed<nsStyleContext>
|
|
GetFontParentStyleContext(Element* aElement, nsIPresShell* aPresShell,
|
|
ErrorResult& aError)
|
|
{
|
|
if (aElement && aElement->IsInUncomposedDoc()) {
|
|
// Inherit from the canvas element.
|
|
RefPtr<nsStyleContext> result =
|
|
nsComputedDOMStyle::GetStyleContextForElement(aElement, nullptr,
|
|
aPresShell);
|
|
if (!result) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
return result.forget();
|
|
}
|
|
|
|
// otherwise inherit from default (10px sans-serif)
|
|
|
|
nsStyleSet* styleSet = aPresShell->StyleSet();
|
|
if (!styleSet) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
bool changed;
|
|
RefPtr<css::Declaration> parentRule =
|
|
CreateFontDeclaration(NS_LITERAL_STRING("10px sans-serif"),
|
|
aPresShell->GetDocument(), &changed);
|
|
|
|
nsTArray<nsCOMPtr<nsIStyleRule>> parentRules;
|
|
parentRules.AppendElement(parentRule);
|
|
RefPtr<nsStyleContext> result =
|
|
styleSet->ResolveStyleForRules(nullptr, parentRules);
|
|
|
|
if (!result) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
return result.forget();
|
|
}
|
|
|
|
static bool
|
|
PropertyIsInheritOrInitial(Declaration* aDeclaration, const nsCSSPropertyID aProperty)
|
|
{
|
|
// We know the declaration is not !important, so we can use
|
|
// GetNormalBlock().
|
|
const nsCSSValue* filterVal =
|
|
aDeclaration->GetNormalBlock()->ValueFor(aProperty);
|
|
return (!filterVal || (filterVal->GetUnit() == eCSSUnit_Unset ||
|
|
filterVal->GetUnit() == eCSSUnit_Inherit ||
|
|
filterVal->GetUnit() == eCSSUnit_Initial));
|
|
}
|
|
|
|
static already_AddRefed<nsStyleContext>
|
|
GetFontStyleContext(Element* aElement, const nsAString& aFont,
|
|
nsIPresShell* aPresShell,
|
|
nsAString& aOutUsedFont,
|
|
ErrorResult& aError)
|
|
{
|
|
nsStyleSet* styleSet = aPresShell->StyleSet();
|
|
if (!styleSet) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
bool fontParsedSuccessfully = false;
|
|
RefPtr<css::Declaration> decl =
|
|
CreateFontDeclaration(aFont, aPresShell->GetDocument(),
|
|
&fontParsedSuccessfully);
|
|
|
|
if (!fontParsedSuccessfully) {
|
|
// We got a syntax error. The spec says this value must be ignored.
|
|
return nullptr;
|
|
}
|
|
|
|
// In addition to unparseable values, the spec says we need to reject
|
|
// 'inherit' and 'initial'. The easiest way to check for this is to look
|
|
// at font-size-adjust, which the font shorthand resets to either 'none' or
|
|
// '-moz-system-font'.
|
|
if (PropertyIsInheritOrInitial(decl, eCSSProperty_font_size_adjust)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// have to get a parent style context for inherit-like relative
|
|
// values (2em, bolder, etc.)
|
|
RefPtr<nsStyleContext> parentContext =
|
|
GetFontParentStyleContext(aElement, aPresShell, aError);
|
|
|
|
if (aError.Failed()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_RELEASE_ASSERT(parentContext,
|
|
"GFX: GetFontParentStyleContext should have returned an error if it couldn't get a parent context.");
|
|
|
|
MOZ_ASSERT(!aPresShell->IsDestroying(),
|
|
"GetFontParentStyleContext should have returned an error if the presshell is being destroyed.");
|
|
|
|
nsTArray<nsCOMPtr<nsIStyleRule>> rules;
|
|
rules.AppendElement(decl);
|
|
// add a rule to prevent text zoom from affecting the style
|
|
rules.AppendElement(new nsDisableTextZoomStyleRule);
|
|
|
|
RefPtr<nsStyleContext> sc =
|
|
styleSet->ResolveStyleForRules(parentContext, rules);
|
|
|
|
// The font getter is required to be reserialized based on what we
|
|
// parsed (including having line-height removed). (Older drafts of
|
|
// the spec required font sizes be converted to pixels, but that no
|
|
// longer seems to be required.)
|
|
decl->GetPropertyValueByID(eCSSProperty_font, aOutUsedFont);
|
|
|
|
return sc.forget();
|
|
}
|
|
|
|
static already_AddRefed<Declaration>
|
|
CreateFilterDeclaration(const nsAString& aFilter,
|
|
nsINode* aNode,
|
|
bool* aOutFilterChanged)
|
|
{
|
|
bool dummy;
|
|
return CreateDeclaration(aNode,
|
|
eCSSProperty_filter, aFilter, aOutFilterChanged,
|
|
eCSSProperty_UNKNOWN, EmptyString(), &dummy);
|
|
}
|
|
|
|
static already_AddRefed<nsStyleContext>
|
|
ResolveStyleForFilter(const nsAString& aFilterString,
|
|
nsIPresShell* aPresShell,
|
|
nsStyleContext* aParentContext,
|
|
ErrorResult& aError)
|
|
{
|
|
nsStyleSet* styleSet = aPresShell->StyleSet();
|
|
if (!styleSet) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
nsIDocument* document = aPresShell->GetDocument();
|
|
bool filterChanged = false;
|
|
RefPtr<css::Declaration> decl =
|
|
CreateFilterDeclaration(aFilterString, document, &filterChanged);
|
|
|
|
if (!filterChanged) {
|
|
// Refuse to accept the filter, but do not throw an error.
|
|
return nullptr;
|
|
}
|
|
|
|
// In addition to unparseable values, the spec says we need to reject
|
|
// 'inherit' and 'initial'.
|
|
if (PropertyIsInheritOrInitial(decl, eCSSProperty_filter)) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsTArray<nsCOMPtr<nsIStyleRule>> rules;
|
|
rules.AppendElement(decl);
|
|
|
|
RefPtr<nsStyleContext> sc =
|
|
styleSet->ResolveStyleForRules(aParentContext, rules);
|
|
|
|
return sc.forget();
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::ParseFilter(const nsAString& aString,
|
|
nsTArray<nsStyleFilter>& aFilterChain,
|
|
ErrorResult& aError)
|
|
{
|
|
if (!mCanvasElement && !mDocShell) {
|
|
NS_WARNING("Canvas element must be non-null or a docshell must be provided");
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
if (!presShell) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
nsString usedFont;
|
|
RefPtr<nsStyleContext> parentContext =
|
|
GetFontStyleContext(mCanvasElement, GetFont(),
|
|
presShell, usedFont, aError);
|
|
if (!parentContext) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
RefPtr<nsStyleContext> sc =
|
|
ResolveStyleForFilter(aString, presShell, parentContext, aError);
|
|
|
|
if (!sc) {
|
|
return false;
|
|
}
|
|
|
|
aFilterChain = sc->StyleEffects()->mFilters;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetFilter(const nsAString& aFilter, ErrorResult& aError)
|
|
{
|
|
nsTArray<nsStyleFilter> filterChain;
|
|
if (ParseFilter(aFilter, filterChain, aError)) {
|
|
CurrentState().filterString = aFilter;
|
|
filterChain.SwapElements(CurrentState().filterChain);
|
|
if (mCanvasElement) {
|
|
CurrentState().filterChainObserver =
|
|
new CanvasFilterChainObserver(CurrentState().filterChain,
|
|
mCanvasElement, this);
|
|
UpdateFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
class CanvasUserSpaceMetrics : public UserSpaceMetricsWithSize
|
|
{
|
|
public:
|
|
CanvasUserSpaceMetrics(const gfx::IntSize& aSize, const nsFont& aFont,
|
|
nsIAtom* aFontLanguage, bool aExplicitLanguage,
|
|
nsPresContext* aPresContext)
|
|
: mSize(aSize)
|
|
, mFont(aFont)
|
|
, mFontLanguage(aFontLanguage)
|
|
, mExplicitLanguage(aExplicitLanguage)
|
|
, mPresContext(aPresContext)
|
|
{
|
|
}
|
|
|
|
virtual float GetEmLength() const override
|
|
{
|
|
return NSAppUnitsToFloatPixels(mFont.size,
|
|
nsPresContext::AppUnitsPerCSSPixel());
|
|
}
|
|
|
|
virtual float GetExLength() const override
|
|
{
|
|
nsDeviceContext* dc = mPresContext->DeviceContext();
|
|
nsFontMetrics::Params params;
|
|
params.language = mFontLanguage;
|
|
params.explicitLanguage = mExplicitLanguage;
|
|
params.textPerf = mPresContext->GetTextPerfMetrics();
|
|
RefPtr<nsFontMetrics> fontMetrics = dc->GetMetricsFor(mFont, params);
|
|
return NSAppUnitsToFloatPixels(fontMetrics->XHeight(),
|
|
nsPresContext::AppUnitsPerCSSPixel());
|
|
}
|
|
|
|
virtual gfx::Size GetSize() const override
|
|
{ return Size(mSize); }
|
|
|
|
private:
|
|
gfx::IntSize mSize;
|
|
const nsFont& mFont;
|
|
nsIAtom* mFontLanguage;
|
|
bool mExplicitLanguage;
|
|
nsPresContext* mPresContext;
|
|
};
|
|
|
|
void
|
|
CanvasRenderingContext2D::UpdateFilter()
|
|
{
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
if (!presShell || presShell->IsDestroying()) {
|
|
// Ensure we set an empty filter and update the state to
|
|
// reflect the current "taint" status of the canvas
|
|
CurrentState().filter = FilterDescription();
|
|
CurrentState().filterSourceGraphicTainted =
|
|
(mCanvasElement && mCanvasElement->IsWriteOnly());
|
|
return;
|
|
}
|
|
|
|
// The filter might reference an SVG filter that is declared inside this
|
|
// document. Flush frames so that we'll have an nsSVGFilterFrame to work
|
|
// with.
|
|
presShell->FlushPendingNotifications(Flush_Frames);
|
|
MOZ_RELEASE_ASSERT(!mStyleStack.IsEmpty());
|
|
if (MOZ_UNLIKELY(presShell->IsDestroying())) {
|
|
return;
|
|
}
|
|
|
|
bool sourceGraphicIsTainted =
|
|
(mCanvasElement && mCanvasElement->IsWriteOnly());
|
|
|
|
CurrentState().filter =
|
|
nsFilterInstance::GetFilterDescription(mCanvasElement,
|
|
CurrentState().filterChain,
|
|
sourceGraphicIsTainted,
|
|
CanvasUserSpaceMetrics(GetSize(),
|
|
CurrentState().fontFont,
|
|
CurrentState().fontLanguage,
|
|
CurrentState().fontExplicitLanguage,
|
|
presShell->GetPresContext()),
|
|
gfxRect(0, 0, mWidth, mHeight),
|
|
CurrentState().filterAdditionalImages);
|
|
CurrentState().filterSourceGraphicTainted = sourceGraphicIsTainted;
|
|
}
|
|
|
|
//
|
|
// rects
|
|
//
|
|
|
|
static bool
|
|
ValidateRect(double& aX, double& aY, double& aWidth, double& aHeight, bool aIsZeroSizeValid)
|
|
{
|
|
if (!aIsZeroSizeValid && (aWidth == 0.0 || aHeight == 0.0)) {
|
|
return false;
|
|
}
|
|
|
|
// bug 1018527
|
|
// The values of canvas API input are in double precision, but Moz2D APIs are
|
|
// using float precision. Bypass canvas API calls when the input is out of
|
|
// float precision to avoid precision problem
|
|
if (!std::isfinite((float)aX) || !std::isfinite((float)aY) ||
|
|
!std::isfinite((float)aWidth) || !std::isfinite((float)aHeight)) {
|
|
return false;
|
|
}
|
|
|
|
// bug 1074733
|
|
// The canvas spec does not forbid rects with negative w or h, so given
|
|
// corners (x, y), (x+w, y), (x+w, y+h), and (x, y+h) we must generate
|
|
// the appropriate rect by flipping negative dimensions. This prevents
|
|
// draw targets from receiving "empty" rects later on.
|
|
if (aWidth < 0) {
|
|
aWidth = -aWidth;
|
|
aX -= aWidth;
|
|
}
|
|
if (aHeight < 0) {
|
|
aHeight = -aHeight;
|
|
aY -= aHeight;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ClearRect(double aX, double aY, double aW,
|
|
double aH)
|
|
{
|
|
// Do not allow zeros - it's a no-op at that point per spec.
|
|
if (!ValidateRect(aX, aY, aW, aH, false)) {
|
|
return;
|
|
}
|
|
|
|
gfx::Rect clearRect(aX, aY, aW, aH);
|
|
|
|
EnsureTarget(&clearRect);
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
mTarget->ClearRect(clearRect);
|
|
|
|
RedrawUser(gfxRect(aX, aY, aW, aH));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::FillRect(double aX, double aY, double aW, double aH)
|
|
{
|
|
if (!ValidateRect(aX, aY, aW, aH, true)) {
|
|
return;
|
|
}
|
|
|
|
const ContextState* state = &CurrentState();
|
|
if (state->patternStyles[Style::FILL]) {
|
|
CanvasPattern::RepeatMode repeat =
|
|
state->patternStyles[Style::FILL]->mRepeat;
|
|
// In the FillRect case repeat modes are easy to deal with.
|
|
bool limitx = repeat == CanvasPattern::RepeatMode::NOREPEAT ||
|
|
repeat == CanvasPattern::RepeatMode::REPEATY;
|
|
bool limity = repeat == CanvasPattern::RepeatMode::NOREPEAT ||
|
|
repeat == CanvasPattern::RepeatMode::REPEATX;
|
|
|
|
IntSize patternSize =
|
|
state->patternStyles[Style::FILL]->mSurface->GetSize();
|
|
|
|
// We always need to execute painting for non-over operators, even if
|
|
// we end up with w/h = 0.
|
|
if (limitx) {
|
|
if (aX < 0) {
|
|
aW += aX;
|
|
if (aW < 0) {
|
|
aW = 0;
|
|
}
|
|
|
|
aX = 0;
|
|
}
|
|
if (aX + aW > patternSize.width) {
|
|
aW = patternSize.width - aX;
|
|
if (aW < 0) {
|
|
aW = 0;
|
|
}
|
|
}
|
|
}
|
|
if (limity) {
|
|
if (aY < 0) {
|
|
aH += aY;
|
|
if (aH < 0) {
|
|
aH = 0;
|
|
}
|
|
|
|
aY = 0;
|
|
}
|
|
if (aY + aH > patternSize.height) {
|
|
aH = patternSize.height - aY;
|
|
if (aH < 0) {
|
|
aH = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
state = nullptr;
|
|
|
|
CompositionOp op = UsedOperation();
|
|
bool discardContent = PatternIsOpaque(Style::FILL)
|
|
&& (op == CompositionOp::OP_OVER || op == CompositionOp::OP_SOURCE);
|
|
const gfx::Rect fillRect(aX, aY, aW, aH);
|
|
EnsureTarget(discardContent ? &fillRect : nullptr);
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
gfx::Rect bounds;
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
if (needBounds) {
|
|
bounds = mTarget->GetTransform().TransformBounds(fillRect);
|
|
}
|
|
|
|
AntialiasMode antialiasMode = CurrentState().imageSmoothingEnabled ?
|
|
AntialiasMode::DEFAULT : AntialiasMode::NONE;
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
target->FillRect(gfx::Rect(aX, aY, aW, aH),
|
|
CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget),
|
|
DrawOptions(CurrentState().globalAlpha, op, antialiasMode));
|
|
|
|
RedrawUser(gfxRect(aX, aY, aW, aH));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::StrokeRect(double aX, double aY, double aW, double aH)
|
|
{
|
|
if (!aW && !aH) {
|
|
return;
|
|
}
|
|
|
|
if (!ValidateRect(aX, aY, aW, aH, true)) {
|
|
return;
|
|
}
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
const ContextState& state = CurrentState();
|
|
bounds = gfx::Rect(aX - state.lineWidth / 2.0f, aY - state.lineWidth / 2.0f,
|
|
aW + state.lineWidth, aH + state.lineWidth);
|
|
bounds = mTarget->GetTransform().TransformBounds(bounds);
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
if (!aH) {
|
|
CapStyle cap = CapStyle::BUTT;
|
|
if (CurrentState().lineJoin == JoinStyle::ROUND) {
|
|
cap = CapStyle::ROUND;
|
|
}
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
const ContextState& state = CurrentState();
|
|
target->
|
|
StrokeLine(Point(aX, aY), Point(aX + aW, aY),
|
|
CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget),
|
|
StrokeOptions(state.lineWidth, state.lineJoin,
|
|
cap, state.miterLimit,
|
|
state.dash.Length(),
|
|
state.dash.Elements(),
|
|
state.dashOffset),
|
|
DrawOptions(state.globalAlpha, op));
|
|
return;
|
|
}
|
|
|
|
if (!aW) {
|
|
CapStyle cap = CapStyle::BUTT;
|
|
if (CurrentState().lineJoin == JoinStyle::ROUND) {
|
|
cap = CapStyle::ROUND;
|
|
}
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
const ContextState& state = CurrentState();
|
|
target->
|
|
StrokeLine(Point(aX, aY), Point(aX, aY + aH),
|
|
CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget),
|
|
StrokeOptions(state.lineWidth, state.lineJoin,
|
|
cap, state.miterLimit,
|
|
state.dash.Length(),
|
|
state.dash.Elements(),
|
|
state.dashOffset),
|
|
DrawOptions(state.globalAlpha, op));
|
|
return;
|
|
}
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
const ContextState& state = CurrentState();
|
|
target->
|
|
StrokeRect(gfx::Rect(aX, aY, aW, aH),
|
|
CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget),
|
|
StrokeOptions(state.lineWidth, state.lineJoin,
|
|
state.lineCap, state.miterLimit,
|
|
state.dash.Length(),
|
|
state.dash.Elements(),
|
|
state.dashOffset),
|
|
DrawOptions(state.globalAlpha, op));
|
|
|
|
Redraw();
|
|
}
|
|
|
|
//
|
|
// path bits
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::BeginPath()
|
|
{
|
|
mPath = nullptr;
|
|
mPathBuilder = nullptr;
|
|
mDSPathBuilder = nullptr;
|
|
mPathTransformWillUpdate = false;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Fill(const CanvasWindingRule& aWinding)
|
|
{
|
|
EnsureUserSpacePath(aWinding);
|
|
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
bounds = mPath->GetBounds(mTarget->GetTransform());
|
|
}
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid() || !target) {
|
|
return;
|
|
}
|
|
target->Fill(mPath,
|
|
CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget),
|
|
DrawOptions(CurrentState().globalAlpha, op));
|
|
Redraw();
|
|
}
|
|
|
|
void CanvasRenderingContext2D::Fill(const CanvasPath& aPath, const CanvasWindingRule& aWinding)
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<gfx::Path> gfxpath = aPath.GetPath(aWinding, mTarget);
|
|
if (!gfxpath) {
|
|
return;
|
|
}
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
bounds = gfxpath->GetBounds(mTarget->GetTransform());
|
|
}
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid() || !target) {
|
|
return;
|
|
}
|
|
target->Fill(gfxpath,
|
|
CanvasGeneralPattern().ForStyle(this, Style::FILL, mTarget),
|
|
DrawOptions(CurrentState().globalAlpha, op));
|
|
Redraw();
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Stroke()
|
|
{
|
|
EnsureUserSpacePath();
|
|
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
|
|
const ContextState* state = &CurrentState();
|
|
StrokeOptions strokeOptions(state->lineWidth, state->lineJoin,
|
|
state->lineCap, state->miterLimit,
|
|
state->dash.Length(), state->dash.Elements(),
|
|
state->dashOffset);
|
|
state = nullptr;
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
bounds =
|
|
mPath->GetStrokedBounds(strokeOptions, mTarget->GetTransform());
|
|
}
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid() || !target) {
|
|
return;
|
|
}
|
|
target->Stroke(mPath,
|
|
CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget),
|
|
strokeOptions, DrawOptions(CurrentState().globalAlpha, op));
|
|
Redraw();
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Stroke(const CanvasPath& aPath)
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<gfx::Path> gfxpath = aPath.GetPath(CanvasWindingRule::Nonzero, mTarget);
|
|
|
|
if (!gfxpath) {
|
|
return;
|
|
}
|
|
|
|
const ContextState* state = &CurrentState();
|
|
StrokeOptions strokeOptions(state->lineWidth, state->lineJoin,
|
|
state->lineCap, state->miterLimit,
|
|
state->dash.Length(), state->dash.Elements(),
|
|
state->dashOffset);
|
|
state = nullptr;
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
bounds =
|
|
gfxpath->GetStrokedBounds(strokeOptions, mTarget->GetTransform());
|
|
}
|
|
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid() || !target) {
|
|
return;
|
|
}
|
|
target->Stroke(gfxpath,
|
|
CanvasGeneralPattern().ForStyle(this, Style::STROKE, mTarget),
|
|
strokeOptions, DrawOptions(CurrentState().globalAlpha, op));
|
|
Redraw();
|
|
}
|
|
|
|
void CanvasRenderingContext2D::DrawFocusIfNeeded(mozilla::dom::Element& aElement,
|
|
ErrorResult& aRv)
|
|
{
|
|
EnsureUserSpacePath();
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
|
|
if (DrawCustomFocusRing(aElement)) {
|
|
AutoSaveRestore asr(this);
|
|
|
|
// set state to conforming focus state
|
|
ContextState* state = &CurrentState();
|
|
state->globalAlpha = 1.0;
|
|
state->shadowBlur = 0;
|
|
state->shadowOffset.x = 0;
|
|
state->shadowOffset.y = 0;
|
|
state->op = mozilla::gfx::CompositionOp::OP_OVER;
|
|
|
|
state->lineCap = CapStyle::BUTT;
|
|
state->lineJoin = mozilla::gfx::JoinStyle::MITER_OR_BEVEL;
|
|
state->lineWidth = 1;
|
|
state->dash.Clear();
|
|
|
|
// color and style of the rings is the same as for image maps
|
|
// set the background focus color
|
|
state->SetColorStyle(Style::STROKE, NS_RGBA(255, 255, 255, 255));
|
|
state = nullptr;
|
|
|
|
// draw the focus ring
|
|
Stroke();
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
|
|
// set dashing for foreground
|
|
nsTArray<mozilla::gfx::Float>& dash = CurrentState().dash;
|
|
for (uint32_t i = 0; i < 2; ++i) {
|
|
if (!dash.AppendElement(1, fallible)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// set the foreground focus color
|
|
CurrentState().SetColorStyle(Style::STROKE, NS_RGBA(0,0,0, 255));
|
|
// draw the focus ring
|
|
Stroke();
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::DrawCustomFocusRing(mozilla::dom::Element& aElement)
|
|
{
|
|
EnsureUserSpacePath();
|
|
|
|
HTMLCanvasElement* canvas = GetCanvas();
|
|
|
|
if (!canvas|| !nsContentUtils::ContentIsDescendantOf(&aElement, canvas)) {
|
|
return false;
|
|
}
|
|
|
|
nsIFocusManager* fm = nsFocusManager::GetFocusManager();
|
|
if (fm) {
|
|
// check that the element i focused
|
|
nsCOMPtr<nsIDOMElement> focusedElement;
|
|
fm->GetFocusedElement(getter_AddRefs(focusedElement));
|
|
if (SameCOMIdentity(aElement.AsDOMNode(), focusedElement)) {
|
|
if (nsPIDOMWindowOuter* window = aElement.OwnerDoc()->GetWindow()) {
|
|
return window->ShouldShowFocusRing();
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Clip(const CanvasWindingRule& aWinding)
|
|
{
|
|
EnsureUserSpacePath(aWinding);
|
|
|
|
if (!mPath) {
|
|
return;
|
|
}
|
|
|
|
mTarget->PushClip(mPath);
|
|
CurrentState().clipsAndTransforms.AppendElement(ClipState(mPath));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Clip(const CanvasPath& aPath, const CanvasWindingRule& aWinding)
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<gfx::Path> gfxpath = aPath.GetPath(aWinding, mTarget);
|
|
|
|
if (!gfxpath) {
|
|
return;
|
|
}
|
|
|
|
mTarget->PushClip(gfxpath);
|
|
CurrentState().clipsAndTransforms.AppendElement(ClipState(gfxpath));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ArcTo(double aX1, double aY1, double aX2,
|
|
double aY2, double aRadius,
|
|
ErrorResult& aError)
|
|
{
|
|
if (aRadius < 0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsureWritablePath();
|
|
|
|
// Current point in user space!
|
|
Point p0;
|
|
if (mPathBuilder) {
|
|
p0 = mPathBuilder->CurrentPoint();
|
|
} else {
|
|
Matrix invTransform = mTarget->GetTransform();
|
|
if (!invTransform.Invert()) {
|
|
return;
|
|
}
|
|
|
|
p0 = invTransform.TransformPoint(mDSPathBuilder->CurrentPoint());
|
|
}
|
|
|
|
Point p1(aX1, aY1);
|
|
Point p2(aX2, aY2);
|
|
|
|
// Execute these calculations in double precision to avoid cumulative
|
|
// rounding errors.
|
|
double dir, a2, b2, c2, cosx, sinx, d, anx, any,
|
|
bnx, bny, x3, y3, x4, y4, cx, cy, angle0, angle1;
|
|
bool anticlockwise;
|
|
|
|
if (p0 == p1 || p1 == p2 || aRadius == 0) {
|
|
LineTo(p1.x, p1.y);
|
|
return;
|
|
}
|
|
|
|
// Check for colinearity
|
|
dir = (p2.x - p1.x) * (p0.y - p1.y) + (p2.y - p1.y) * (p1.x - p0.x);
|
|
if (dir == 0) {
|
|
LineTo(p1.x, p1.y);
|
|
return;
|
|
}
|
|
|
|
|
|
// XXX - Math for this code was already available from the non-azure code
|
|
// and would be well tested. Perhaps converting to bezier directly might
|
|
// be more efficient longer run.
|
|
a2 = (p0.x-aX1)*(p0.x-aX1) + (p0.y-aY1)*(p0.y-aY1);
|
|
b2 = (aX1-aX2)*(aX1-aX2) + (aY1-aY2)*(aY1-aY2);
|
|
c2 = (p0.x-aX2)*(p0.x-aX2) + (p0.y-aY2)*(p0.y-aY2);
|
|
cosx = (a2+b2-c2)/(2*sqrt(a2*b2));
|
|
|
|
sinx = sqrt(1 - cosx*cosx);
|
|
d = aRadius / ((1 - cosx) / sinx);
|
|
|
|
anx = (aX1-p0.x) / sqrt(a2);
|
|
any = (aY1-p0.y) / sqrt(a2);
|
|
bnx = (aX1-aX2) / sqrt(b2);
|
|
bny = (aY1-aY2) / sqrt(b2);
|
|
x3 = aX1 - anx*d;
|
|
y3 = aY1 - any*d;
|
|
x4 = aX1 - bnx*d;
|
|
y4 = aY1 - bny*d;
|
|
anticlockwise = (dir < 0);
|
|
cx = x3 + any*aRadius*(anticlockwise ? 1 : -1);
|
|
cy = y3 - anx*aRadius*(anticlockwise ? 1 : -1);
|
|
angle0 = atan2((y3-cy), (x3-cx));
|
|
angle1 = atan2((y4-cy), (x4-cx));
|
|
|
|
|
|
LineTo(x3, y3);
|
|
|
|
Arc(cx, cy, aRadius, angle0, angle1, anticlockwise, aError);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Arc(double aX, double aY, double aR,
|
|
double aStartAngle, double aEndAngle,
|
|
bool aAnticlockwise, ErrorResult& aError)
|
|
{
|
|
if (aR < 0.0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsureWritablePath();
|
|
|
|
ArcToBezier(this, Point(aX, aY), Size(aR, aR), aStartAngle, aEndAngle, aAnticlockwise);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Rect(double aX, double aY, double aW, double aH)
|
|
{
|
|
EnsureWritablePath();
|
|
|
|
if (mPathBuilder) {
|
|
mPathBuilder->MoveTo(Point(aX, aY));
|
|
mPathBuilder->LineTo(Point(aX + aW, aY));
|
|
mPathBuilder->LineTo(Point(aX + aW, aY + aH));
|
|
mPathBuilder->LineTo(Point(aX, aY + aH));
|
|
mPathBuilder->Close();
|
|
} else {
|
|
mDSPathBuilder->MoveTo(mTarget->GetTransform().TransformPoint(Point(aX, aY)));
|
|
mDSPathBuilder->LineTo(mTarget->GetTransform().TransformPoint(Point(aX + aW, aY)));
|
|
mDSPathBuilder->LineTo(mTarget->GetTransform().TransformPoint(Point(aX + aW, aY + aH)));
|
|
mDSPathBuilder->LineTo(mTarget->GetTransform().TransformPoint(Point(aX, aY + aH)));
|
|
mDSPathBuilder->Close();
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::Ellipse(double aX, double aY, double aRadiusX, double aRadiusY,
|
|
double aRotation, double aStartAngle, double aEndAngle,
|
|
bool aAnticlockwise, ErrorResult& aError)
|
|
{
|
|
if (aRadiusX < 0.0 || aRadiusY < 0.0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsureWritablePath();
|
|
|
|
ArcToBezier(this, Point(aX, aY), Size(aRadiusX, aRadiusY), aStartAngle, aEndAngle,
|
|
aAnticlockwise, aRotation);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::EnsureWritablePath()
|
|
{
|
|
EnsureTarget();
|
|
// NOTE: IsTargetValid() may be false here (mTarget == sErrorTarget) but we
|
|
// go ahead and create a path anyway since callers depend on that.
|
|
|
|
if (mDSPathBuilder) {
|
|
return;
|
|
}
|
|
|
|
FillRule fillRule = CurrentState().fillRule;
|
|
|
|
if (mPathBuilder) {
|
|
if (mPathTransformWillUpdate) {
|
|
mPath = mPathBuilder->Finish();
|
|
mDSPathBuilder =
|
|
mPath->TransformedCopyToBuilder(mPathToDS, fillRule);
|
|
mPath = nullptr;
|
|
mPathBuilder = nullptr;
|
|
mPathTransformWillUpdate = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!mPath) {
|
|
NS_ASSERTION(!mPathTransformWillUpdate, "mPathTransformWillUpdate should be false, if all paths are null");
|
|
mPathBuilder = mTarget->CreatePathBuilder(fillRule);
|
|
} else if (!mPathTransformWillUpdate) {
|
|
mPathBuilder = mPath->CopyToBuilder(fillRule);
|
|
} else {
|
|
mDSPathBuilder =
|
|
mPath->TransformedCopyToBuilder(mPathToDS, fillRule);
|
|
mPathTransformWillUpdate = false;
|
|
mPath = nullptr;
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::EnsureUserSpacePath(const CanvasWindingRule& aWinding)
|
|
{
|
|
FillRule fillRule = CurrentState().fillRule;
|
|
if (aWinding == CanvasWindingRule::Evenodd)
|
|
fillRule = FillRule::FILL_EVEN_ODD;
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
if (!mPath && !mPathBuilder && !mDSPathBuilder) {
|
|
mPathBuilder = mTarget->CreatePathBuilder(fillRule);
|
|
}
|
|
|
|
if (mPathBuilder) {
|
|
mPath = mPathBuilder->Finish();
|
|
mPathBuilder = nullptr;
|
|
}
|
|
|
|
if (mPath &&
|
|
mPathTransformWillUpdate) {
|
|
mDSPathBuilder =
|
|
mPath->TransformedCopyToBuilder(mPathToDS, fillRule);
|
|
mPath = nullptr;
|
|
mPathTransformWillUpdate = false;
|
|
}
|
|
|
|
if (mDSPathBuilder) {
|
|
RefPtr<Path> dsPath;
|
|
dsPath = mDSPathBuilder->Finish();
|
|
mDSPathBuilder = nullptr;
|
|
|
|
Matrix inverse = mTarget->GetTransform();
|
|
if (!inverse.Invert()) {
|
|
NS_WARNING("Could not invert transform");
|
|
return;
|
|
}
|
|
|
|
mPathBuilder =
|
|
dsPath->TransformedCopyToBuilder(inverse, fillRule);
|
|
mPath = mPathBuilder->Finish();
|
|
mPathBuilder = nullptr;
|
|
}
|
|
|
|
if (mPath && mPath->GetFillRule() != fillRule) {
|
|
mPathBuilder = mPath->CopyToBuilder(fillRule);
|
|
mPath = mPathBuilder->Finish();
|
|
mPathBuilder = nullptr;
|
|
}
|
|
|
|
NS_ASSERTION(mPath, "mPath should exist");
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::TransformWillUpdate()
|
|
{
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
// Store the matrix that would transform the current path to device
|
|
// space.
|
|
if (mPath || mPathBuilder) {
|
|
if (!mPathTransformWillUpdate) {
|
|
// If the transform has already been updated, but a device space builder
|
|
// has not been created yet mPathToDS contains the right transform to
|
|
// transform the current mPath into device space.
|
|
// We should leave it alone.
|
|
mPathToDS = mTarget->GetTransform();
|
|
}
|
|
mPathTransformWillUpdate = true;
|
|
}
|
|
}
|
|
|
|
//
|
|
// text
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetFont(const nsAString& aFont,
|
|
ErrorResult& aError)
|
|
{
|
|
SetFontInternal(aFont, aError);
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::SetFontInternal(const nsAString& aFont,
|
|
ErrorResult& aError)
|
|
{
|
|
/*
|
|
* If font is defined with relative units (e.g. ems) and the parent
|
|
* style context changes in between calls, setting the font to the
|
|
* same value as previous could result in a different computed value,
|
|
* so we cannot have the optimization where we check if the new font
|
|
* string is equal to the old one.
|
|
*/
|
|
|
|
if (!mCanvasElement && !mDocShell) {
|
|
NS_WARNING("Canvas element must be non-null or a docshell must be provided");
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
if (!presShell) {
|
|
// Do not throw here. We may be in a situation where we're loading in an iframe
|
|
// that is sandboxed, and/or initially hidden with display:none, in which case
|
|
// we don't want to throw an error but silently fail.
|
|
// If we don't do this, JS trying to set context.font to something will abort,
|
|
// breaking e.g. third party serviced graphs.
|
|
return false;
|
|
}
|
|
|
|
nsString usedFont;
|
|
RefPtr<nsStyleContext> sc =
|
|
GetFontStyleContext(mCanvasElement, aFont, presShell, usedFont, aError);
|
|
if (!sc) {
|
|
return false;
|
|
}
|
|
|
|
const nsStyleFont* fontStyle = sc->StyleFont();
|
|
|
|
nsPresContext* c = presShell->GetPresContext();
|
|
|
|
// Purposely ignore the font size that respects the user's minimum
|
|
// font preference (fontStyle->mFont.size) in favor of the computed
|
|
// size (fontStyle->mSize). See
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=698652.
|
|
MOZ_ASSERT(!fontStyle->mAllowZoom,
|
|
"expected text zoom to be disabled on this nsStyleFont");
|
|
nsFont resizedFont(fontStyle->mFont);
|
|
// Create a font group working in units of CSS pixels instead of the usual
|
|
// device pixels, to avoid being affected by page zoom. nsFontMetrics will
|
|
// convert nsFont size in app units to device pixels for the font group, so
|
|
// here we first apply to the size the equivalent of a conversion from device
|
|
// pixels to CSS pixels, to adjust for the difference in expectations from
|
|
// other nsFontMetrics clients.
|
|
resizedFont.size =
|
|
(fontStyle->mSize * c->AppUnitsPerDevPixel()) / c->AppUnitsPerCSSPixel();
|
|
|
|
nsFontMetrics::Params params;
|
|
params.language = fontStyle->mLanguage;
|
|
params.explicitLanguage = fontStyle->mExplicitLanguage;
|
|
params.userFontSet = c->GetUserFontSet();
|
|
params.textPerf = c->GetTextPerfMetrics();
|
|
RefPtr<nsFontMetrics> metrics =
|
|
c->DeviceContext()->GetMetricsFor(resizedFont, params);
|
|
|
|
gfxFontGroup* newFontGroup = metrics->GetThebesFontGroup();
|
|
CurrentState().fontGroup = newFontGroup;
|
|
NS_ASSERTION(CurrentState().fontGroup, "Could not get font group");
|
|
CurrentState().font = usedFont;
|
|
CurrentState().fontFont = fontStyle->mFont;
|
|
CurrentState().fontFont.size = fontStyle->mSize;
|
|
CurrentState().fontLanguage = fontStyle->mLanguage;
|
|
CurrentState().fontExplicitLanguage = fontStyle->mExplicitLanguage;
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetTextAlign(const nsAString& aTextAlign)
|
|
{
|
|
if (aTextAlign.EqualsLiteral("start"))
|
|
CurrentState().textAlign = TextAlign::START;
|
|
else if (aTextAlign.EqualsLiteral("end"))
|
|
CurrentState().textAlign = TextAlign::END;
|
|
else if (aTextAlign.EqualsLiteral("left"))
|
|
CurrentState().textAlign = TextAlign::LEFT;
|
|
else if (aTextAlign.EqualsLiteral("right"))
|
|
CurrentState().textAlign = TextAlign::RIGHT;
|
|
else if (aTextAlign.EqualsLiteral("center"))
|
|
CurrentState().textAlign = TextAlign::CENTER;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetTextAlign(nsAString& aTextAlign)
|
|
{
|
|
switch (CurrentState().textAlign)
|
|
{
|
|
case TextAlign::START:
|
|
aTextAlign.AssignLiteral("start");
|
|
break;
|
|
case TextAlign::END:
|
|
aTextAlign.AssignLiteral("end");
|
|
break;
|
|
case TextAlign::LEFT:
|
|
aTextAlign.AssignLiteral("left");
|
|
break;
|
|
case TextAlign::RIGHT:
|
|
aTextAlign.AssignLiteral("right");
|
|
break;
|
|
case TextAlign::CENTER:
|
|
aTextAlign.AssignLiteral("center");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetTextBaseline(const nsAString& aTextBaseline)
|
|
{
|
|
if (aTextBaseline.EqualsLiteral("top"))
|
|
CurrentState().textBaseline = TextBaseline::TOP;
|
|
else if (aTextBaseline.EqualsLiteral("hanging"))
|
|
CurrentState().textBaseline = TextBaseline::HANGING;
|
|
else if (aTextBaseline.EqualsLiteral("middle"))
|
|
CurrentState().textBaseline = TextBaseline::MIDDLE;
|
|
else if (aTextBaseline.EqualsLiteral("alphabetic"))
|
|
CurrentState().textBaseline = TextBaseline::ALPHABETIC;
|
|
else if (aTextBaseline.EqualsLiteral("ideographic"))
|
|
CurrentState().textBaseline = TextBaseline::IDEOGRAPHIC;
|
|
else if (aTextBaseline.EqualsLiteral("bottom"))
|
|
CurrentState().textBaseline = TextBaseline::BOTTOM;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetTextBaseline(nsAString& aTextBaseline)
|
|
{
|
|
switch (CurrentState().textBaseline)
|
|
{
|
|
case TextBaseline::TOP:
|
|
aTextBaseline.AssignLiteral("top");
|
|
break;
|
|
case TextBaseline::HANGING:
|
|
aTextBaseline.AssignLiteral("hanging");
|
|
break;
|
|
case TextBaseline::MIDDLE:
|
|
aTextBaseline.AssignLiteral("middle");
|
|
break;
|
|
case TextBaseline::ALPHABETIC:
|
|
aTextBaseline.AssignLiteral("alphabetic");
|
|
break;
|
|
case TextBaseline::IDEOGRAPHIC:
|
|
aTextBaseline.AssignLiteral("ideographic");
|
|
break;
|
|
case TextBaseline::BOTTOM:
|
|
aTextBaseline.AssignLiteral("bottom");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Helper function that replaces the whitespace characters in a string
|
|
* with U+0020 SPACE. The whitespace characters are defined as U+0020 SPACE,
|
|
* U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF), U+000B LINE
|
|
* TABULATION, U+000C FORM FEED (FF), and U+000D CARRIAGE RETURN (CR).
|
|
* @param str The string whose whitespace characters to replace.
|
|
*/
|
|
static inline void
|
|
TextReplaceWhitespaceCharacters(nsAutoString& aStr)
|
|
{
|
|
aStr.ReplaceChar("\x09\x0A\x0B\x0C\x0D", char16_t(' '));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::FillText(const nsAString& aText, double aX,
|
|
double aY,
|
|
const Optional<double>& aMaxWidth,
|
|
ErrorResult& aError)
|
|
{
|
|
aError = DrawOrMeasureText(aText, aX, aY, aMaxWidth, TextDrawOperation::FILL, nullptr);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::StrokeText(const nsAString& aText, double aX,
|
|
double aY,
|
|
const Optional<double>& aMaxWidth,
|
|
ErrorResult& aError)
|
|
{
|
|
aError = DrawOrMeasureText(aText, aX, aY, aMaxWidth, TextDrawOperation::STROKE, nullptr);
|
|
}
|
|
|
|
TextMetrics*
|
|
CanvasRenderingContext2D::MeasureText(const nsAString& aRawText,
|
|
ErrorResult& aError)
|
|
{
|
|
float width;
|
|
Optional<double> maxWidth;
|
|
aError = DrawOrMeasureText(aRawText, 0, 0, maxWidth, TextDrawOperation::MEASURE, &width);
|
|
if (aError.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return new TextMetrics(width);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::AddHitRegion(const HitRegionOptions& aOptions, ErrorResult& aError)
|
|
{
|
|
RefPtr<gfx::Path> path;
|
|
if (aOptions.mPath) {
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
path = aOptions.mPath->GetPath(CanvasWindingRule::Nonzero, mTarget);
|
|
}
|
|
|
|
if (!path) {
|
|
// check if the path is valid
|
|
EnsureUserSpacePath(CanvasWindingRule::Nonzero);
|
|
path = mPath;
|
|
}
|
|
|
|
if (!path) {
|
|
aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
|
return;
|
|
}
|
|
|
|
// get the bounds of the current path. They are relative to the canvas
|
|
gfx::Rect bounds(path->GetBounds(mTarget->GetTransform()));
|
|
if ((bounds.width == 0) || (bounds.height == 0) || !bounds.IsFinite()) {
|
|
// The specified region has no pixels.
|
|
aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
|
return;
|
|
}
|
|
|
|
// remove old hit region first
|
|
RemoveHitRegion(aOptions.mId);
|
|
|
|
if (aOptions.mControl) {
|
|
// also remove regions with this control
|
|
for (size_t x = 0; x < mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = mHitRegionsOptions[x];
|
|
if (info.mElement == aOptions.mControl) {
|
|
mHitRegionsOptions.RemoveElementAt(x);
|
|
break;
|
|
}
|
|
}
|
|
#ifdef ACCESSIBILITY
|
|
aOptions.mControl->SetProperty(nsGkAtoms::hitregion, new bool(true),
|
|
nsINode::DeleteProperty<bool>);
|
|
#endif
|
|
}
|
|
|
|
// finally, add the region to the list
|
|
RegionInfo info;
|
|
info.mId = aOptions.mId;
|
|
info.mElement = aOptions.mControl;
|
|
RefPtr<PathBuilder> pathBuilder = path->TransformedCopyToBuilder(mTarget->GetTransform());
|
|
info.mPath = pathBuilder->Finish();
|
|
|
|
mHitRegionsOptions.InsertElementAt(0, info);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::RemoveHitRegion(const nsAString& aId)
|
|
{
|
|
if (aId.Length() == 0) {
|
|
return;
|
|
}
|
|
|
|
for (size_t x = 0; x < mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = mHitRegionsOptions[x];
|
|
if (info.mId == aId) {
|
|
mHitRegionsOptions.RemoveElementAt(x);
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::ClearHitRegions()
|
|
{
|
|
mHitRegionsOptions.Clear();
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::GetHitRegionRect(Element* aElement, nsRect& aRect)
|
|
{
|
|
for (unsigned int x = 0; x < mHitRegionsOptions.Length(); x++) {
|
|
RegionInfo& info = mHitRegionsOptions[x];
|
|
if (info.mElement == aElement) {
|
|
gfx::Rect bounds(info.mPath->GetBounds());
|
|
gfxRect rect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
aRect = nsLayoutUtils::RoundGfxRectToAppRect(rect, AppUnitsPerCSSPixel());
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Used for nsBidiPresUtils::ProcessText
|
|
*/
|
|
struct MOZ_STACK_CLASS CanvasBidiProcessor : public nsBidiPresUtils::BidiProcessor
|
|
{
|
|
typedef CanvasRenderingContext2D::Style Style;
|
|
|
|
CanvasBidiProcessor()
|
|
: nsBidiPresUtils::BidiProcessor()
|
|
{
|
|
if (Preferences::GetBool(GFX_MISSING_FONTS_NOTIFY_PREF)) {
|
|
mMissingFonts = new gfxMissingFontRecorder();
|
|
}
|
|
}
|
|
|
|
~CanvasBidiProcessor()
|
|
{
|
|
// notify front-end code if we encountered missing glyphs in any script
|
|
if (mMissingFonts) {
|
|
mMissingFonts->Flush();
|
|
}
|
|
}
|
|
|
|
typedef CanvasRenderingContext2D::ContextState ContextState;
|
|
|
|
virtual void SetText(const char16_t* aText, int32_t aLength, nsBidiDirection aDirection)
|
|
{
|
|
mFontgrp->UpdateUserFonts(); // ensure user font generation is current
|
|
// adjust flags for current direction run
|
|
uint32_t flags = mTextRunFlags;
|
|
if (aDirection == NSBIDI_RTL) {
|
|
flags |= gfxTextRunFactory::TEXT_IS_RTL;
|
|
} else {
|
|
flags &= ~gfxTextRunFactory::TEXT_IS_RTL;
|
|
}
|
|
mTextRun = mFontgrp->MakeTextRun(aText,
|
|
aLength,
|
|
mDrawTarget,
|
|
mAppUnitsPerDevPixel,
|
|
flags,
|
|
mMissingFonts);
|
|
}
|
|
|
|
virtual nscoord GetWidth()
|
|
{
|
|
gfxTextRun::Metrics textRunMetrics = mTextRun->MeasureText(
|
|
mDoMeasureBoundingBox ? gfxFont::TIGHT_INK_EXTENTS
|
|
: gfxFont::LOOSE_INK_EXTENTS, mDrawTarget);
|
|
|
|
// this only measures the height; the total width is gotten from the
|
|
// the return value of ProcessText.
|
|
if (mDoMeasureBoundingBox) {
|
|
textRunMetrics.mBoundingBox.Scale(1.0 / mAppUnitsPerDevPixel);
|
|
mBoundingBox = mBoundingBox.Union(textRunMetrics.mBoundingBox);
|
|
}
|
|
|
|
return NSToCoordRound(textRunMetrics.mAdvanceWidth);
|
|
}
|
|
|
|
already_AddRefed<gfxPattern> GetGradientFor(Style aStyle)
|
|
{
|
|
RefPtr<gfxPattern> pattern;
|
|
CanvasGradient* gradient = mCtx->CurrentState().gradientStyles[aStyle];
|
|
CanvasGradient::Type type = gradient->GetType();
|
|
|
|
switch (type) {
|
|
case CanvasGradient::Type::RADIAL: {
|
|
auto radial = static_cast<CanvasRadialGradient*>(gradient);
|
|
pattern = new gfxPattern(radial->mCenter1.x, radial->mCenter1.y,
|
|
radial->mRadius1, radial->mCenter2.x,
|
|
radial->mCenter2.y, radial->mRadius2);
|
|
break;
|
|
}
|
|
case CanvasGradient::Type::LINEAR: {
|
|
auto linear = static_cast<CanvasLinearGradient*>(gradient);
|
|
pattern = new gfxPattern(linear->mBegin.x, linear->mBegin.y,
|
|
linear->mEnd.x, linear->mEnd.y);
|
|
break;
|
|
}
|
|
default:
|
|
MOZ_ASSERT(false, "Should be linear or radial gradient.");
|
|
return nullptr;
|
|
}
|
|
|
|
for (auto stop : gradient->mRawStops) {
|
|
pattern->AddColorStop(stop.offset, stop.color);
|
|
}
|
|
|
|
return pattern.forget();
|
|
}
|
|
|
|
gfx::ExtendMode CvtCanvasRepeatToGfxRepeat(
|
|
CanvasPattern::RepeatMode aRepeatMode)
|
|
{
|
|
switch (aRepeatMode) {
|
|
case CanvasPattern::RepeatMode::REPEAT:
|
|
return gfx::ExtendMode::REPEAT;
|
|
case CanvasPattern::RepeatMode::REPEATX:
|
|
return gfx::ExtendMode::REPEAT_X;
|
|
case CanvasPattern::RepeatMode::REPEATY:
|
|
return gfx::ExtendMode::REPEAT_Y;
|
|
case CanvasPattern::RepeatMode::NOREPEAT:
|
|
return gfx::ExtendMode::CLAMP;
|
|
default:
|
|
return gfx::ExtendMode::CLAMP;
|
|
}
|
|
}
|
|
|
|
already_AddRefed<gfxPattern> GetPatternFor(Style aStyle)
|
|
{
|
|
const CanvasPattern* pat = mCtx->CurrentState().patternStyles[aStyle];
|
|
RefPtr<gfxPattern> pattern = new gfxPattern(pat->mSurface, Matrix());
|
|
pattern->SetExtend(CvtCanvasRepeatToGfxRepeat(pat->mRepeat));
|
|
return pattern.forget();
|
|
}
|
|
|
|
virtual void DrawText(nscoord aXOffset, nscoord aWidth)
|
|
{
|
|
gfxPoint point = mPt;
|
|
bool rtl = mTextRun->IsRightToLeft();
|
|
bool verticalRun = mTextRun->IsVertical();
|
|
RefPtr<gfxPattern> pattern;
|
|
|
|
gfxFloat& inlineCoord = verticalRun ? point.y : point.x;
|
|
inlineCoord += aXOffset;
|
|
|
|
// offset is given in terms of left side of string
|
|
if (rtl) {
|
|
// Bug 581092 - don't use rounded pixel width to advance to
|
|
// right-hand end of run, because this will cause different
|
|
// glyph positioning for LTR vs RTL drawing of the same
|
|
// glyph string on OS X and DWrite where textrun widths may
|
|
// involve fractional pixels.
|
|
gfxTextRun::Metrics textRunMetrics =
|
|
mTextRun->MeasureText(mDoMeasureBoundingBox ?
|
|
gfxFont::TIGHT_INK_EXTENTS :
|
|
gfxFont::LOOSE_INK_EXTENTS,
|
|
mDrawTarget);
|
|
inlineCoord += textRunMetrics.mAdvanceWidth;
|
|
// old code was:
|
|
// point.x += width * mAppUnitsPerDevPixel;
|
|
// TODO: restore this if/when we move to fractional coords
|
|
// throughout the text layout process
|
|
}
|
|
|
|
mCtx->EnsureTarget();
|
|
if (!mCtx->IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
// Defer the tasks to gfxTextRun which will handle color/svg-in-ot fonts
|
|
// appropriately.
|
|
StrokeOptions strokeOpts;
|
|
DrawOptions drawOpts;
|
|
Style style = (mOp == CanvasRenderingContext2D::TextDrawOperation::FILL)
|
|
? Style::FILL
|
|
: Style::STROKE;
|
|
|
|
AdjustedTarget target(mCtx);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<gfxContext> thebes =
|
|
gfxContext::CreatePreservingTransformOrNull(target);
|
|
if (!thebes) {
|
|
// If CreatePreservingTransformOrNull returns null, it will also have
|
|
// issued a gfxCriticalNote already, so here we'll just bail out.
|
|
return;
|
|
}
|
|
gfxTextRun::DrawParams params(thebes);
|
|
|
|
const ContextState* state = &mCtx->CurrentState();
|
|
if (state->StyleIsColor(style)) { // Color
|
|
nscolor fontColor = state->colorStyles[style];
|
|
if (style == Style::FILL) {
|
|
params.context->SetColor(Color::FromABGR(fontColor));
|
|
} else {
|
|
params.textStrokeColor = fontColor;
|
|
}
|
|
} else {
|
|
if (state->gradientStyles[style]) { // Gradient
|
|
pattern = GetGradientFor(style);
|
|
} else if (state->patternStyles[style]) { // Pattern
|
|
if (mCtx->mCanvasElement) {
|
|
CanvasUtils::DoDrawImageSecurityCheck(
|
|
mCtx->mCanvasElement, state->patternStyles[style]->mPrincipal,
|
|
state->patternStyles[style]->mForceWriteOnly,
|
|
state->patternStyles[style]->mCORSUsed);
|
|
}
|
|
pattern = GetPatternFor(style);
|
|
} else {
|
|
MOZ_ASSERT(false, "Should never reach here.");
|
|
return;
|
|
}
|
|
MOZ_ASSERT(pattern, "No valid pattern.");
|
|
|
|
if (style == Style::FILL) {
|
|
params.context->SetPattern(pattern);
|
|
} else {
|
|
params.textStrokePattern = pattern;
|
|
}
|
|
}
|
|
|
|
drawOpts.mAlpha = state->globalAlpha;
|
|
drawOpts.mCompositionOp = mCtx->UsedOperation();
|
|
if (!mCtx->IsTargetValid()) {
|
|
return;
|
|
}
|
|
state = &mCtx->CurrentState();
|
|
params.drawOpts = &drawOpts;
|
|
|
|
if (style == Style::STROKE) {
|
|
strokeOpts.mLineWidth = state->lineWidth;
|
|
strokeOpts.mLineJoin = state->lineJoin;
|
|
strokeOpts.mLineCap = state->lineCap;
|
|
strokeOpts.mMiterLimit = state->miterLimit;
|
|
strokeOpts.mDashLength = state->dash.Length();
|
|
strokeOpts.mDashPattern =
|
|
(strokeOpts.mDashLength > 0) ? state->dash.Elements() : 0;
|
|
strokeOpts.mDashOffset = state->dashOffset;
|
|
|
|
params.drawMode = DrawMode::GLYPH_STROKE;
|
|
params.strokeOpts = &strokeOpts;
|
|
}
|
|
|
|
mTextRun->Draw(gfxTextRun::Range(mTextRun.get()), point, params);
|
|
}
|
|
|
|
// current text run
|
|
RefPtr<gfxTextRun> mTextRun;
|
|
|
|
// pointer to a screen reference context used to measure text and such
|
|
RefPtr<DrawTarget> mDrawTarget;
|
|
|
|
// Pointer to the draw target we should fill our text to
|
|
CanvasRenderingContext2D* mCtx;
|
|
|
|
// position of the left side of the string, alphabetic baseline
|
|
gfxPoint mPt;
|
|
|
|
// current font
|
|
gfxFontGroup* mFontgrp;
|
|
|
|
// to record any unsupported characters found in the text,
|
|
// and notify front-end if it is interested
|
|
nsAutoPtr<gfxMissingFontRecorder> mMissingFonts;
|
|
|
|
// dev pixel conversion factor
|
|
int32_t mAppUnitsPerDevPixel;
|
|
|
|
// operation (fill or stroke)
|
|
CanvasRenderingContext2D::TextDrawOperation mOp;
|
|
|
|
// union of bounding boxes of all runs, needed for shadows
|
|
gfxRect mBoundingBox;
|
|
|
|
// flags to use when creating textrun, based on CSS style
|
|
uint32_t mTextRunFlags;
|
|
|
|
// true iff the bounding box should be measured
|
|
bool mDoMeasureBoundingBox;
|
|
};
|
|
|
|
nsresult
|
|
CanvasRenderingContext2D::DrawOrMeasureText(const nsAString& aRawText,
|
|
float aX,
|
|
float aY,
|
|
const Optional<double>& aMaxWidth,
|
|
TextDrawOperation aOp,
|
|
float* aWidth)
|
|
{
|
|
nsresult rv;
|
|
|
|
if (!mCanvasElement && !mDocShell) {
|
|
NS_WARNING("Canvas element must be non-null or a docshell must be provided");
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
if (!presShell)
|
|
return NS_ERROR_FAILURE;
|
|
|
|
nsIDocument* document = presShell->GetDocument();
|
|
|
|
// replace all the whitespace characters with U+0020 SPACE
|
|
nsAutoString textToDraw(aRawText);
|
|
TextReplaceWhitespaceCharacters(textToDraw);
|
|
|
|
// According to spec, the API should return an empty array if maxWidth was provided
|
|
// but is less than or equal to zero or equal to NaN.
|
|
if (aMaxWidth.WasPassed() && (aMaxWidth.Value() <= 0 || IsNaN(aMaxWidth.Value()))) {
|
|
textToDraw.Truncate();
|
|
}
|
|
|
|
// for now, default to ltr if not in doc
|
|
bool isRTL = false;
|
|
|
|
RefPtr<nsStyleContext> canvasStyle;
|
|
if (mCanvasElement && mCanvasElement->IsInUncomposedDoc()) {
|
|
// try to find the closest context
|
|
canvasStyle =
|
|
nsComputedDOMStyle::GetStyleContextForElement(mCanvasElement,
|
|
nullptr,
|
|
presShell);
|
|
if (!canvasStyle) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
isRTL = canvasStyle->StyleVisibility()->mDirection ==
|
|
NS_STYLE_DIRECTION_RTL;
|
|
} else {
|
|
isRTL = GET_BIDI_OPTION_DIRECTION(document->GetBidiOptions()) == IBMBIDI_TEXTDIRECTION_RTL;
|
|
}
|
|
|
|
// This is only needed to know if we can know the drawing bounding box easily.
|
|
const bool doCalculateBounds = NeedToCalculateBounds();
|
|
if (presShell->IsDestroying()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
gfxFontGroup* currentFontStyle = GetCurrentFontStyle();
|
|
if (!currentFontStyle) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_ASSERT(!presShell->IsDestroying(),
|
|
"GetCurrentFontStyle() should have returned null if the presshell is being destroyed");
|
|
|
|
// ensure user font set is up to date
|
|
currentFontStyle->
|
|
SetUserFontSet(presShell->GetPresContext()->GetUserFontSet());
|
|
|
|
if (currentFontStyle->GetStyle()->size == 0.0F) {
|
|
if (aWidth) {
|
|
*aWidth = 0;
|
|
}
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!IsFinite(aX) || !IsFinite(aY)) {
|
|
return NS_OK;
|
|
}
|
|
|
|
CanvasBidiProcessor processor;
|
|
|
|
// If we don't have a style context, we can't set up vertical-text flags
|
|
// (for now, at least; perhaps we need new Canvas API to control this).
|
|
processor.mTextRunFlags = canvasStyle ?
|
|
nsLayoutUtils::GetTextRunFlagsForStyle(canvasStyle,
|
|
canvasStyle->StyleFont(),
|
|
canvasStyle->StyleText(),
|
|
0) : 0;
|
|
|
|
GetAppUnitsValues(&processor.mAppUnitsPerDevPixel, nullptr);
|
|
processor.mPt = gfxPoint(aX, aY);
|
|
processor.mDrawTarget =
|
|
gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
|
|
|
|
// If we don't have a target then we don't have a transform. A target won't
|
|
// be needed in the case where we're measuring the text size. This allows
|
|
// to avoid creating a target if it's only being used to measure text sizes.
|
|
if (mTarget) {
|
|
processor.mDrawTarget->SetTransform(mTarget->GetTransform());
|
|
}
|
|
processor.mCtx = this;
|
|
processor.mOp = aOp;
|
|
processor.mBoundingBox = gfxRect(0, 0, 0, 0);
|
|
processor.mDoMeasureBoundingBox = doCalculateBounds || !mIsEntireFrameInvalid;
|
|
processor.mFontgrp = currentFontStyle;
|
|
|
|
nscoord totalWidthCoord;
|
|
|
|
// calls bidi algo twice since it needs the full text width and the
|
|
// bounding boxes before rendering anything
|
|
nsBidi bidiEngine;
|
|
rv = nsBidiPresUtils::ProcessText(textToDraw.get(),
|
|
textToDraw.Length(),
|
|
isRTL ? NSBIDI_RTL : NSBIDI_LTR,
|
|
presShell->GetPresContext(),
|
|
processor,
|
|
nsBidiPresUtils::MODE_MEASURE,
|
|
nullptr,
|
|
0,
|
|
&totalWidthCoord,
|
|
&bidiEngine);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
float totalWidth = float(totalWidthCoord) / processor.mAppUnitsPerDevPixel;
|
|
if (aWidth) {
|
|
*aWidth = totalWidth;
|
|
}
|
|
|
|
// if only measuring, don't need to do any more work
|
|
if (aOp==TextDrawOperation::MEASURE) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// offset pt.x based on text align
|
|
gfxFloat anchorX;
|
|
|
|
const ContextState& state = CurrentState();
|
|
if (state.textAlign == TextAlign::CENTER) {
|
|
anchorX = .5;
|
|
} else if (state.textAlign == TextAlign::LEFT ||
|
|
(!isRTL && state.textAlign == TextAlign::START) ||
|
|
(isRTL && state.textAlign == TextAlign::END)) {
|
|
anchorX = 0;
|
|
} else {
|
|
anchorX = 1;
|
|
}
|
|
|
|
processor.mPt.x -= anchorX * totalWidth;
|
|
|
|
// offset pt.y (or pt.x, for vertical text) based on text baseline
|
|
processor.mFontgrp->UpdateUserFonts(); // ensure user font generation is current
|
|
const gfxFont::Metrics& fontMetrics =
|
|
processor.mFontgrp->GetFirstValidFont()->GetMetrics(gfxFont::eHorizontal);
|
|
|
|
gfxFloat baselineAnchor;
|
|
|
|
switch (state.textBaseline)
|
|
{
|
|
case TextBaseline::HANGING:
|
|
// fall through; best we can do with the information available
|
|
case TextBaseline::TOP:
|
|
baselineAnchor = fontMetrics.emAscent;
|
|
break;
|
|
case TextBaseline::MIDDLE:
|
|
baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f;
|
|
break;
|
|
case TextBaseline::IDEOGRAPHIC:
|
|
// fall through; best we can do with the information available
|
|
case TextBaseline::ALPHABETIC:
|
|
baselineAnchor = 0;
|
|
break;
|
|
case TextBaseline::BOTTOM:
|
|
baselineAnchor = -fontMetrics.emDescent;
|
|
break;
|
|
default:
|
|
MOZ_CRASH("GFX: unexpected TextBaseline");
|
|
}
|
|
|
|
// We can't query the textRun directly, as it may not have been created yet;
|
|
// so instead we check the flags that will be used to initialize it.
|
|
uint16_t runOrientation =
|
|
(processor.mTextRunFlags & gfxTextRunFactory::TEXT_ORIENT_MASK);
|
|
if (runOrientation != gfxTextRunFactory::TEXT_ORIENT_HORIZONTAL) {
|
|
if (runOrientation == gfxTextRunFactory::TEXT_ORIENT_VERTICAL_MIXED ||
|
|
runOrientation == gfxTextRunFactory::TEXT_ORIENT_VERTICAL_UPRIGHT) {
|
|
// Adjust to account for mTextRun being shaped using center baseline
|
|
// rather than alphabetic.
|
|
baselineAnchor -= (fontMetrics.emAscent - fontMetrics.emDescent) * .5f;
|
|
}
|
|
processor.mPt.x -= baselineAnchor;
|
|
} else {
|
|
processor.mPt.y += baselineAnchor;
|
|
}
|
|
|
|
// correct bounding box to get it to be the correct size/position
|
|
processor.mBoundingBox.width = totalWidth;
|
|
processor.mBoundingBox.MoveBy(processor.mPt);
|
|
|
|
processor.mPt.x *= processor.mAppUnitsPerDevPixel;
|
|
processor.mPt.y *= processor.mAppUnitsPerDevPixel;
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
Matrix oldTransform = mTarget->GetTransform();
|
|
// if text is over aMaxWidth, then scale the text horizontally such that its
|
|
// width is precisely aMaxWidth
|
|
if (aMaxWidth.WasPassed() && aMaxWidth.Value() > 0 &&
|
|
totalWidth > aMaxWidth.Value()) {
|
|
Matrix newTransform = oldTransform;
|
|
|
|
// Translate so that the anchor point is at 0,0, then scale and then
|
|
// translate back.
|
|
newTransform.PreTranslate(aX, 0);
|
|
newTransform.PreScale(aMaxWidth.Value() / totalWidth, 1);
|
|
newTransform.PreTranslate(-aX, 0);
|
|
/* we do this to avoid an ICE in the android compiler */
|
|
Matrix androidCompilerBug = newTransform;
|
|
mTarget->SetTransform(androidCompilerBug);
|
|
}
|
|
|
|
// save the previous bounding box
|
|
gfxRect boundingBox = processor.mBoundingBox;
|
|
|
|
// don't ever need to measure the bounding box twice
|
|
processor.mDoMeasureBoundingBox = false;
|
|
|
|
rv = nsBidiPresUtils::ProcessText(textToDraw.get(),
|
|
textToDraw.Length(),
|
|
isRTL ? NSBIDI_RTL : NSBIDI_LTR,
|
|
presShell->GetPresContext(),
|
|
processor,
|
|
nsBidiPresUtils::MODE_DRAW,
|
|
nullptr,
|
|
0,
|
|
nullptr,
|
|
&bidiEngine);
|
|
|
|
|
|
mTarget->SetTransform(oldTransform);
|
|
|
|
if (aOp == CanvasRenderingContext2D::TextDrawOperation::FILL &&
|
|
!doCalculateBounds) {
|
|
RedrawUser(boundingBox);
|
|
return NS_OK;
|
|
}
|
|
|
|
Redraw();
|
|
return NS_OK;
|
|
}
|
|
|
|
gfxFontGroup*
|
|
CanvasRenderingContext2D::GetCurrentFontStyle()
|
|
{
|
|
// use lazy initilization for the font group since it's rather expensive
|
|
if (!CurrentState().fontGroup) {
|
|
ErrorResult err;
|
|
NS_NAMED_LITERAL_STRING(kDefaultFontStyle, "10px sans-serif");
|
|
static float kDefaultFontSize = 10.0;
|
|
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
|
|
bool fontUpdated = SetFontInternal(kDefaultFontStyle, err);
|
|
if (err.Failed() || !fontUpdated) {
|
|
err.SuppressException();
|
|
gfxFontStyle style;
|
|
style.size = kDefaultFontSize;
|
|
gfxTextPerfMetrics* tp = nullptr;
|
|
if (presShell && !presShell->IsDestroying()) {
|
|
tp = presShell->GetPresContext()->GetTextPerfMetrics();
|
|
}
|
|
int32_t perDevPixel, perCSSPixel;
|
|
GetAppUnitsValues(&perDevPixel, &perCSSPixel);
|
|
gfxFloat devToCssSize = gfxFloat(perDevPixel) / gfxFloat(perCSSPixel);
|
|
CurrentState().fontGroup =
|
|
gfxPlatform::GetPlatform()->CreateFontGroup(FontFamilyList(eFamily_sans_serif),
|
|
&style, tp,
|
|
nullptr, devToCssSize);
|
|
if (CurrentState().fontGroup) {
|
|
CurrentState().font = kDefaultFontStyle;
|
|
} else {
|
|
NS_ERROR("Default canvas font is invalid");
|
|
}
|
|
}
|
|
}
|
|
|
|
return CurrentState().fontGroup;
|
|
}
|
|
|
|
//
|
|
// line caps/joins
|
|
//
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetLineCap(const nsAString& aLinecapStyle)
|
|
{
|
|
CapStyle cap;
|
|
|
|
if (aLinecapStyle.EqualsLiteral("butt")) {
|
|
cap = CapStyle::BUTT;
|
|
} else if (aLinecapStyle.EqualsLiteral("round")) {
|
|
cap = CapStyle::ROUND;
|
|
} else if (aLinecapStyle.EqualsLiteral("square")) {
|
|
cap = CapStyle::SQUARE;
|
|
} else {
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
return;
|
|
}
|
|
|
|
CurrentState().lineCap = cap;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetLineCap(nsAString& aLinecapStyle)
|
|
{
|
|
switch (CurrentState().lineCap) {
|
|
case CapStyle::BUTT:
|
|
aLinecapStyle.AssignLiteral("butt");
|
|
break;
|
|
case CapStyle::ROUND:
|
|
aLinecapStyle.AssignLiteral("round");
|
|
break;
|
|
case CapStyle::SQUARE:
|
|
aLinecapStyle.AssignLiteral("square");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetLineJoin(const nsAString& aLinejoinStyle)
|
|
{
|
|
JoinStyle j;
|
|
|
|
if (aLinejoinStyle.EqualsLiteral("round")) {
|
|
j = JoinStyle::ROUND;
|
|
} else if (aLinejoinStyle.EqualsLiteral("bevel")) {
|
|
j = JoinStyle::BEVEL;
|
|
} else if (aLinejoinStyle.EqualsLiteral("miter")) {
|
|
j = JoinStyle::MITER_OR_BEVEL;
|
|
} else {
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
return;
|
|
}
|
|
|
|
CurrentState().lineJoin = j;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetLineJoin(nsAString& aLinejoinStyle, ErrorResult& aError)
|
|
{
|
|
switch (CurrentState().lineJoin) {
|
|
case JoinStyle::ROUND:
|
|
aLinejoinStyle.AssignLiteral("round");
|
|
break;
|
|
case JoinStyle::BEVEL:
|
|
aLinejoinStyle.AssignLiteral("bevel");
|
|
break;
|
|
case JoinStyle::MITER_OR_BEVEL:
|
|
aLinejoinStyle.AssignLiteral("miter");
|
|
break;
|
|
default:
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetLineDash(const Sequence<double>& aSegments,
|
|
ErrorResult& aRv)
|
|
{
|
|
nsTArray<mozilla::gfx::Float> dash;
|
|
|
|
for (uint32_t x = 0; x < aSegments.Length(); x++) {
|
|
if (aSegments[x] < 0.0) {
|
|
// Pattern elements must be finite "numbers" >= 0, with "finite"
|
|
// taken care of by WebIDL
|
|
return;
|
|
}
|
|
|
|
if (!dash.AppendElement(aSegments[x], fallible)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
}
|
|
if (aSegments.Length() % 2) { // If the number of elements is odd, concatenate again
|
|
for (uint32_t x = 0; x < aSegments.Length(); x++) {
|
|
if (!dash.AppendElement(aSegments[x], fallible)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
CurrentState().dash = Move(dash);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetLineDash(nsTArray<double>& aSegments) const {
|
|
const nsTArray<mozilla::gfx::Float>& dash = CurrentState().dash;
|
|
aSegments.Clear();
|
|
|
|
for (uint32_t x = 0; x < dash.Length(); x++) {
|
|
aSegments.AppendElement(dash[x]);
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetLineDashOffset(double aOffset) {
|
|
CurrentState().dashOffset = aOffset;
|
|
}
|
|
|
|
double
|
|
CanvasRenderingContext2D::LineDashOffset() const {
|
|
return CurrentState().dashOffset;
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::IsPointInPath(double aX, double aY, const CanvasWindingRule& aWinding)
|
|
{
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
EnsureUserSpacePath(aWinding);
|
|
if (!mPath) {
|
|
return false;
|
|
}
|
|
|
|
if (mPathTransformWillUpdate) {
|
|
return mPath->ContainsPoint(Point(aX, aY), mPathToDS);
|
|
}
|
|
|
|
return mPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform());
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInPath(const CanvasPath& aPath, double aX, double aY, const CanvasWindingRule& aWinding)
|
|
{
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<gfx::Path> tempPath = aPath.GetPath(aWinding, mTarget);
|
|
|
|
return tempPath->ContainsPoint(Point(aX, aY), mTarget->GetTransform());
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::IsPointInStroke(double aX, double aY)
|
|
{
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
EnsureUserSpacePath();
|
|
if (!mPath) {
|
|
return false;
|
|
}
|
|
|
|
const ContextState &state = CurrentState();
|
|
|
|
StrokeOptions strokeOptions(state.lineWidth,
|
|
state.lineJoin,
|
|
state.lineCap,
|
|
state.miterLimit,
|
|
state.dash.Length(),
|
|
state.dash.Elements(),
|
|
state.dashOffset);
|
|
|
|
if (mPathTransformWillUpdate) {
|
|
return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mPathToDS);
|
|
}
|
|
return mPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mTarget->GetTransform());
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInStroke(const CanvasPath& aPath, double aX, double aY)
|
|
{
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<gfx::Path> tempPath = aPath.GetPath(CanvasWindingRule::Nonzero, mTarget);
|
|
|
|
const ContextState &state = CurrentState();
|
|
|
|
StrokeOptions strokeOptions(state.lineWidth,
|
|
state.lineJoin,
|
|
state.lineCap,
|
|
state.miterLimit,
|
|
state.dash.Length(),
|
|
state.dash.Elements(),
|
|
state.dashOffset);
|
|
|
|
return tempPath->StrokeContainsPoint(strokeOptions, Point(aX, aY), mTarget->GetTransform());
|
|
}
|
|
|
|
// Returns a surface that contains only the part needed to draw aSourceRect.
|
|
// On entry, aSourceRect is relative to aSurface, and on return aSourceRect is
|
|
// relative to the returned surface.
|
|
static already_AddRefed<SourceSurface>
|
|
ExtractSubrect(SourceSurface* aSurface, gfx::Rect* aSourceRect, DrawTarget* aTargetDT)
|
|
{
|
|
gfx::Rect roundedOutSourceRect = *aSourceRect;
|
|
roundedOutSourceRect.RoundOut();
|
|
gfx::IntRect roundedOutSourceRectInt;
|
|
if (!roundedOutSourceRect.ToIntRect(&roundedOutSourceRectInt)) {
|
|
RefPtr<SourceSurface> surface(aSurface);
|
|
return surface.forget();
|
|
}
|
|
|
|
RefPtr<DrawTarget> subrectDT =
|
|
aTargetDT->CreateSimilarDrawTarget(roundedOutSourceRectInt.Size(), SurfaceFormat::B8G8R8A8);
|
|
|
|
if (!subrectDT) {
|
|
RefPtr<SourceSurface> surface(aSurface);
|
|
return surface.forget();
|
|
}
|
|
|
|
*aSourceRect -= roundedOutSourceRect.TopLeft();
|
|
|
|
subrectDT->CopySurface(aSurface, roundedOutSourceRectInt, IntPoint());
|
|
return subrectDT->Snapshot();
|
|
}
|
|
|
|
//
|
|
// image
|
|
//
|
|
|
|
static void
|
|
ClipImageDimension(double& aSourceCoord, double& aSourceSize, int32_t aImageSize,
|
|
double& aDestCoord, double& aDestSize)
|
|
{
|
|
double scale = aDestSize / aSourceSize;
|
|
if (aSourceCoord < 0.0) {
|
|
double destEnd = aDestCoord + aDestSize;
|
|
aDestCoord -= aSourceCoord * scale;
|
|
aDestSize = destEnd - aDestCoord;
|
|
aSourceSize += aSourceCoord;
|
|
aSourceCoord = 0.0;
|
|
}
|
|
double delta = aImageSize - (aSourceCoord + aSourceSize);
|
|
if (delta < 0.0) {
|
|
aDestSize += delta * scale;
|
|
aSourceSize = aImageSize - aSourceCoord;
|
|
}
|
|
}
|
|
|
|
// Acts like nsLayoutUtils::SurfaceFromElement, but it'll attempt
|
|
// to pull a SourceSurface from our cache. This allows us to avoid
|
|
// reoptimizing surfaces if content and canvas backends are different.
|
|
nsLayoutUtils::SurfaceFromElementResult
|
|
CanvasRenderingContext2D::CachedSurfaceFromElement(Element* aElement)
|
|
{
|
|
nsLayoutUtils::SurfaceFromElementResult res;
|
|
nsCOMPtr<nsIImageLoadingContent> imageLoader = do_QueryInterface(aElement);
|
|
if (!imageLoader) {
|
|
return res;
|
|
}
|
|
|
|
nsCOMPtr<imgIRequest> imgRequest;
|
|
imageLoader->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST,
|
|
getter_AddRefs(imgRequest));
|
|
if (!imgRequest) {
|
|
return res;
|
|
}
|
|
|
|
uint32_t status = 0;
|
|
if (NS_FAILED(imgRequest->GetImageStatus(&status)) ||
|
|
!(status & imgIRequest::STATUS_LOAD_COMPLETE)) {
|
|
return res;
|
|
}
|
|
|
|
nsCOMPtr<nsIPrincipal> principal;
|
|
if (NS_FAILED(imgRequest->GetImagePrincipal(getter_AddRefs(principal))) ||
|
|
!principal) {
|
|
return res;
|
|
}
|
|
|
|
res.mSourceSurface =
|
|
CanvasImageCache::LookupAllCanvas(aElement, mIsSkiaGL);
|
|
if (!res.mSourceSurface) {
|
|
return res;
|
|
}
|
|
|
|
int32_t corsmode = imgIRequest::CORS_NONE;
|
|
if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
|
|
res.mCORSUsed = corsmode != imgIRequest::CORS_NONE;
|
|
}
|
|
|
|
res.mSize = res.mSourceSurface->GetSize();
|
|
res.mPrincipal = principal.forget();
|
|
res.mIsWriteOnly = false;
|
|
res.mImageRequest = imgRequest.forget();
|
|
|
|
return res;
|
|
}
|
|
|
|
// drawImage(in HTMLImageElement image, in float dx, in float dy);
|
|
// -- render image from 0,0 at dx,dy top-left coords
|
|
// drawImage(in HTMLImageElement image, in float dx, in float dy, in float dw, in float dh);
|
|
// -- render image from 0,0 at dx,dy top-left coords clipping it to dw,dh
|
|
// drawImage(in HTMLImageElement image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh);
|
|
// -- render the region defined by (sx,sy,sw,wh) in image-local space into the region (dx,dy,dw,dh) on the canvas
|
|
|
|
// If only dx and dy are passed in then optional_argc should be 0. If only
|
|
// dx, dy, dw and dh are passed in then optional_argc should be 2. The only
|
|
// other valid value for optional_argc is 6 if sx, sy, sw, sh, dx, dy, dw and dh
|
|
// are all passed in.
|
|
|
|
void
|
|
CanvasRenderingContext2D::DrawImage(const CanvasImageSource& aImage,
|
|
double aSx, double aSy, double aSw,
|
|
double aSh, double aDx, double aDy,
|
|
double aDw, double aDh,
|
|
uint8_t aOptional_argc,
|
|
ErrorResult& aError)
|
|
{
|
|
if (mDrawObserver) {
|
|
mDrawObserver->DidDrawCall(CanvasDrawObserver::DrawCallType::DrawImage);
|
|
}
|
|
|
|
MOZ_ASSERT(aOptional_argc == 0 || aOptional_argc == 2 || aOptional_argc == 6);
|
|
|
|
if (!ValidateRect(aDx, aDy, aDw, aDh, true)) {
|
|
return;
|
|
}
|
|
if (aOptional_argc == 6) {
|
|
if (!ValidateRect(aSx, aSy, aSw, aSh, true)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
RefPtr<SourceSurface> srcSurf;
|
|
gfx::IntSize imgSize;
|
|
|
|
Element* element = nullptr;
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
if (aImage.IsHTMLCanvasElement()) {
|
|
HTMLCanvasElement* canvas = &aImage.GetAsHTMLCanvasElement();
|
|
element = canvas;
|
|
nsIntSize size = canvas->GetSize();
|
|
if (size.width == 0 || size.height == 0) {
|
|
aError.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
|
|
return;
|
|
}
|
|
|
|
if (canvas->IsWriteOnly()) {
|
|
SetWriteOnly();
|
|
}
|
|
} else if (aImage.IsImageBitmap()) {
|
|
ImageBitmap& imageBitmap = aImage.GetAsImageBitmap();
|
|
srcSurf = imageBitmap.PrepareForDrawTarget(mTarget);
|
|
|
|
if (!srcSurf) {
|
|
return;
|
|
}
|
|
|
|
if (imageBitmap.IsWriteOnly()) {
|
|
SetWriteOnly();
|
|
}
|
|
|
|
imgSize = gfx::IntSize(imageBitmap.Width(), imageBitmap.Height());
|
|
}
|
|
else {
|
|
if (aImage.IsHTMLImageElement()) {
|
|
HTMLImageElement* img = &aImage.GetAsHTMLImageElement();
|
|
element = img;
|
|
} else if (aImage.IsSVGImageElement()) {
|
|
SVGImageElement* img = &aImage.GetAsSVGImageElement();
|
|
element = img;
|
|
} else {
|
|
HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement();
|
|
video->MarkAsContentSource(mozilla::dom::HTMLVideoElement::CallerAPI::DRAW_IMAGE);
|
|
element = video;
|
|
}
|
|
|
|
srcSurf =
|
|
CanvasImageCache::LookupCanvas(element, mCanvasElement, &imgSize, mIsSkiaGL);
|
|
}
|
|
|
|
nsLayoutUtils::DirectDrawInfo drawInfo;
|
|
|
|
#ifdef USE_SKIA_GPU
|
|
if (mRenderingMode == RenderingMode::OpenGLBackendMode &&
|
|
mIsSkiaGL &&
|
|
!srcSurf &&
|
|
aImage.IsHTMLVideoElement() &&
|
|
AllowOpenGLCanvas()) {
|
|
mozilla::gl::GLContext* gl = gfxPlatform::GetPlatform()->GetSkiaGLGlue()->GetGLContext();
|
|
MOZ_ASSERT(gl);
|
|
|
|
HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement();
|
|
if (!video) {
|
|
return;
|
|
}
|
|
|
|
#ifdef MOZ_EME
|
|
if (video->ContainsRestrictedContent()) {
|
|
aError.Throw(NS_ERROR_NOT_AVAILABLE);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
uint16_t readyState;
|
|
if (NS_SUCCEEDED(video->GetReadyState(&readyState)) &&
|
|
readyState < nsIDOMHTMLMediaElement::HAVE_CURRENT_DATA) {
|
|
// still loading, just return
|
|
return;
|
|
}
|
|
|
|
// If it doesn't have a principal, just bail
|
|
nsCOMPtr<nsIPrincipal> principal = video->GetCurrentVideoPrincipal();
|
|
if (!principal) {
|
|
aError.Throw(NS_ERROR_NOT_AVAILABLE);
|
|
return;
|
|
}
|
|
|
|
mozilla::layers::ImageContainer* container = video->GetImageContainer();
|
|
if (!container) {
|
|
aError.Throw(NS_ERROR_NOT_AVAILABLE);
|
|
return;
|
|
}
|
|
|
|
AutoLockImage lockImage(container);
|
|
layers::Image* srcImage = lockImage.GetImage();
|
|
if (!srcImage) {
|
|
aError.Throw(NS_ERROR_NOT_AVAILABLE);
|
|
return;
|
|
}
|
|
|
|
gl->MakeCurrent();
|
|
GLuint videoTexture = 0;
|
|
gl->fGenTextures(1, &videoTexture);
|
|
// skiaGL expect upload on drawing, and uses texture 0 for texturing,
|
|
// so we must active texture 0 and bind the texture for it.
|
|
gl->fActiveTexture(LOCAL_GL_TEXTURE0);
|
|
gl->fBindTexture(LOCAL_GL_TEXTURE_2D, videoTexture);
|
|
|
|
gl->fTexImage2D(LOCAL_GL_TEXTURE_2D, 0, LOCAL_GL_RGB, srcImage->GetSize().width, srcImage->GetSize().height, 0, LOCAL_GL_RGB, LOCAL_GL_UNSIGNED_SHORT_5_6_5, nullptr);
|
|
gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE);
|
|
gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE);
|
|
gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_LINEAR);
|
|
gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_LINEAR);
|
|
|
|
const gl::OriginPos destOrigin = gl::OriginPos::TopLeft;
|
|
bool ok = gl->BlitHelper()->BlitImageToTexture(srcImage, srcImage->GetSize(),
|
|
videoTexture, LOCAL_GL_TEXTURE_2D,
|
|
destOrigin);
|
|
if (ok) {
|
|
NativeSurface texSurf;
|
|
texSurf.mType = NativeSurfaceType::OPENGL_TEXTURE;
|
|
texSurf.mFormat = SurfaceFormat::R5G6B5_UINT16;
|
|
texSurf.mSize.width = srcImage->GetSize().width;
|
|
texSurf.mSize.height = srcImage->GetSize().height;
|
|
texSurf.mSurface = (void*)((uintptr_t)videoTexture);
|
|
|
|
srcSurf = mTarget->CreateSourceSurfaceFromNativeSurface(texSurf);
|
|
if (!srcSurf) {
|
|
gl->fDeleteTextures(1, &videoTexture);
|
|
}
|
|
imgSize.width = srcImage->GetSize().width;
|
|
imgSize.height = srcImage->GetSize().height;
|
|
|
|
int32_t displayWidth = video->VideoWidth();
|
|
int32_t displayHeight = video->VideoHeight();
|
|
aSw *= (double)imgSize.width / (double)displayWidth;
|
|
aSh *= (double)imgSize.height / (double)displayHeight;
|
|
} else {
|
|
gl->fDeleteTextures(1, &videoTexture);
|
|
}
|
|
srcImage = nullptr;
|
|
|
|
if (mCanvasElement) {
|
|
CanvasUtils::DoDrawImageSecurityCheck(mCanvasElement,
|
|
principal, false,
|
|
video->GetCORSMode() != CORS_NONE);
|
|
}
|
|
}
|
|
#endif
|
|
if (!srcSurf) {
|
|
// The canvas spec says that drawImage should draw the first frame
|
|
// of animated images. We also don't want to rasterize vector images.
|
|
uint32_t sfeFlags = nsLayoutUtils::SFE_WANT_FIRST_FRAME |
|
|
nsLayoutUtils::SFE_NO_RASTERIZING_VECTORS;
|
|
|
|
nsLayoutUtils::SurfaceFromElementResult res =
|
|
CanvasRenderingContext2D::CachedSurfaceFromElement(element);
|
|
|
|
if (!res.mSourceSurface) {
|
|
res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget);
|
|
}
|
|
|
|
if (!res.mSourceSurface && !res.mDrawInfo.mImgContainer) {
|
|
// The spec says to silently do nothing in the following cases:
|
|
// - The element is still loading.
|
|
// - The image is bad, but it's not in the broken state (i.e., we could
|
|
// decode the headers and get the size).
|
|
if (!res.mIsStillLoading && !res.mHasSize) {
|
|
aError.Throw(NS_ERROR_NOT_AVAILABLE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
imgSize = res.mSize;
|
|
|
|
// Scale sw/sh based on aspect ratio
|
|
if (aImage.IsHTMLVideoElement()) {
|
|
HTMLVideoElement* video = &aImage.GetAsHTMLVideoElement();
|
|
int32_t displayWidth = video->VideoWidth();
|
|
int32_t displayHeight = video->VideoHeight();
|
|
aSw *= (double)imgSize.width / (double)displayWidth;
|
|
aSh *= (double)imgSize.height / (double)displayHeight;
|
|
}
|
|
|
|
if (mCanvasElement) {
|
|
CanvasUtils::DoDrawImageSecurityCheck(mCanvasElement,
|
|
res.mPrincipal, res.mIsWriteOnly,
|
|
res.mCORSUsed);
|
|
}
|
|
|
|
if (res.mSourceSurface) {
|
|
if (res.mImageRequest) {
|
|
CanvasImageCache::NotifyDrawImage(element, mCanvasElement, res.mSourceSurface, imgSize, mIsSkiaGL);
|
|
}
|
|
srcSurf = res.mSourceSurface;
|
|
} else {
|
|
drawInfo = res.mDrawInfo;
|
|
}
|
|
}
|
|
|
|
if (aOptional_argc == 0) {
|
|
aSx = aSy = 0.0;
|
|
aDw = aSw = (double) imgSize.width;
|
|
aDh = aSh = (double) imgSize.height;
|
|
} else if (aOptional_argc == 2) {
|
|
aSx = aSy = 0.0;
|
|
aSw = (double) imgSize.width;
|
|
aSh = (double) imgSize.height;
|
|
}
|
|
|
|
if (aSw == 0.0 || aSh == 0.0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
ClipImageDimension(aSx, aSw, imgSize.width, aDx, aDw);
|
|
ClipImageDimension(aSy, aSh, imgSize.height, aDy, aDh);
|
|
|
|
if (aSw <= 0.0 || aSh <= 0.0 ||
|
|
aDw <= 0.0 || aDh <= 0.0) {
|
|
// source and/or destination are fully clipped, so nothing is painted
|
|
return;
|
|
}
|
|
|
|
SamplingFilter samplingFilter;
|
|
AntialiasMode antialiasMode;
|
|
|
|
if (CurrentState().imageSmoothingEnabled) {
|
|
samplingFilter = gfx::SamplingFilter::LINEAR;
|
|
antialiasMode = AntialiasMode::DEFAULT;
|
|
} else {
|
|
samplingFilter = gfx::SamplingFilter::POINT;
|
|
antialiasMode = AntialiasMode::NONE;
|
|
}
|
|
|
|
const bool needBounds = NeedToCalculateBounds();
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
gfx::Rect bounds;
|
|
if (needBounds) {
|
|
bounds = gfx::Rect(aDx, aDy, aDw, aDh);
|
|
bounds = mTarget->GetTransform().TransformBounds(bounds);
|
|
}
|
|
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
if (srcSurf) {
|
|
gfx::Rect sourceRect(aSx, aSy, aSw, aSh);
|
|
if (element == mCanvasElement) {
|
|
// srcSurf is a snapshot of mTarget. If we draw to mTarget now, we'll
|
|
// trigger a COW copy of the whole canvas into srcSurf. That's a huge
|
|
// waste if sourceRect doesn't cover the whole canvas.
|
|
// We avoid copying the whole canvas by manually copying just the part
|
|
// that we need.
|
|
srcSurf = ExtractSubrect(srcSurf, &sourceRect, mTarget);
|
|
}
|
|
AdjustedTarget tempTarget(this, bounds.IsEmpty() ? nullptr : &bounds);
|
|
if (!tempTarget) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
auto op = UsedOperation();
|
|
if (!IsTargetValid() || !tempTarget) {
|
|
return;
|
|
}
|
|
tempTarget->
|
|
DrawSurface(srcSurf,
|
|
gfx::Rect(aDx, aDy, aDw, aDh),
|
|
sourceRect,
|
|
DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED),
|
|
DrawOptions(CurrentState().globalAlpha, op, antialiasMode));
|
|
} else {
|
|
DrawDirectlyToCanvas(drawInfo, &bounds,
|
|
gfx::Rect(aDx, aDy, aDw, aDh),
|
|
gfx::Rect(aSx, aSy, aSw, aSh),
|
|
imgSize);
|
|
}
|
|
|
|
RedrawUser(gfxRect(aDx, aDy, aDw, aDh));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::DrawDirectlyToCanvas(
|
|
const nsLayoutUtils::DirectDrawInfo& aImage,
|
|
gfx::Rect* aBounds,
|
|
gfx::Rect aDest,
|
|
gfx::Rect aSrc,
|
|
gfx::IntSize aImgSize)
|
|
{
|
|
MOZ_ASSERT(aSrc.width > 0 && aSrc.height > 0,
|
|
"Need positive source width and height");
|
|
|
|
AdjustedTarget tempTarget(this, aBounds->IsEmpty() ? nullptr: aBounds);
|
|
if (!tempTarget) {
|
|
return;
|
|
}
|
|
|
|
// Get any existing transforms on the context, including transformations used
|
|
// for context shadow.
|
|
Matrix matrix = tempTarget->GetTransform();
|
|
gfxMatrix contextMatrix;
|
|
contextMatrix = gfxMatrix(matrix._11, matrix._12, matrix._21,
|
|
matrix._22, matrix._31, matrix._32);
|
|
gfxSize contextScale(contextMatrix.ScaleFactors(true));
|
|
|
|
// Scale the dest rect to include the context scale.
|
|
aDest.Scale(contextScale.width, contextScale.height);
|
|
|
|
// Scale the image size to the dest rect, and adjust the source rect to match.
|
|
gfxSize scale(aDest.width / aSrc.width, aDest.height / aSrc.height);
|
|
IntSize scaledImageSize = IntSize::Ceil(aImgSize.width * scale.width,
|
|
aImgSize.height * scale.height);
|
|
aSrc.Scale(scale.width, scale.height);
|
|
|
|
// We're wrapping tempTarget's (our) DrawTarget here, so we need to restore
|
|
// the matrix even though this is a temp gfxContext.
|
|
AutoRestoreTransform autoRestoreTransform(mTarget);
|
|
|
|
RefPtr<gfxContext> context = gfxContext::CreateOrNull(tempTarget);
|
|
if (!context) {
|
|
gfxDevCrash(LogReason::InvalidContext) << "Canvas context problem";
|
|
return;
|
|
}
|
|
context->SetMatrix(contextMatrix.
|
|
Scale(1.0 / contextScale.width,
|
|
1.0 / contextScale.height).
|
|
Translate(aDest.x - aSrc.x, aDest.y - aSrc.y));
|
|
|
|
// FLAG_CLAMP is added for increased performance, since we never tile here.
|
|
uint32_t modifiedFlags = aImage.mDrawingFlags | imgIContainer::FLAG_CLAMP;
|
|
|
|
CSSIntSize sz(scaledImageSize.width, scaledImageSize.height); // XXX hmm is scaledImageSize really in CSS pixels?
|
|
SVGImageContext svgContext(sz, Nothing(), CurrentState().globalAlpha);
|
|
|
|
auto result = aImage.mImgContainer->
|
|
Draw(context, scaledImageSize,
|
|
ImageRegion::Create(gfxRect(aSrc.x, aSrc.y, aSrc.width, aSrc.height)),
|
|
aImage.mWhichFrame, SamplingFilter::GOOD, Some(svgContext), modifiedFlags);
|
|
|
|
if (result != DrawResult::SUCCESS) {
|
|
NS_WARNING("imgIContainer::Draw failed");
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::SetGlobalCompositeOperation(const nsAString& aOp,
|
|
ErrorResult& aError)
|
|
{
|
|
CompositionOp comp_op;
|
|
|
|
#define CANVAS_OP_TO_GFX_OP(cvsop, op2d) \
|
|
if (aOp.EqualsLiteral(cvsop)) \
|
|
comp_op = CompositionOp::OP_##op2d;
|
|
|
|
CANVAS_OP_TO_GFX_OP("copy", SOURCE)
|
|
else CANVAS_OP_TO_GFX_OP("source-atop", ATOP)
|
|
else CANVAS_OP_TO_GFX_OP("source-in", IN)
|
|
else CANVAS_OP_TO_GFX_OP("source-out", OUT)
|
|
else CANVAS_OP_TO_GFX_OP("source-over", OVER)
|
|
else CANVAS_OP_TO_GFX_OP("destination-in", DEST_IN)
|
|
else CANVAS_OP_TO_GFX_OP("destination-out", DEST_OUT)
|
|
else CANVAS_OP_TO_GFX_OP("destination-over", DEST_OVER)
|
|
else CANVAS_OP_TO_GFX_OP("destination-atop", DEST_ATOP)
|
|
else CANVAS_OP_TO_GFX_OP("lighter", ADD)
|
|
else CANVAS_OP_TO_GFX_OP("xor", XOR)
|
|
else CANVAS_OP_TO_GFX_OP("multiply", MULTIPLY)
|
|
else CANVAS_OP_TO_GFX_OP("screen", SCREEN)
|
|
else CANVAS_OP_TO_GFX_OP("overlay", OVERLAY)
|
|
else CANVAS_OP_TO_GFX_OP("darken", DARKEN)
|
|
else CANVAS_OP_TO_GFX_OP("lighten", LIGHTEN)
|
|
else CANVAS_OP_TO_GFX_OP("color-dodge", COLOR_DODGE)
|
|
else CANVAS_OP_TO_GFX_OP("color-burn", COLOR_BURN)
|
|
else CANVAS_OP_TO_GFX_OP("hard-light", HARD_LIGHT)
|
|
else CANVAS_OP_TO_GFX_OP("soft-light", SOFT_LIGHT)
|
|
else CANVAS_OP_TO_GFX_OP("difference", DIFFERENCE)
|
|
else CANVAS_OP_TO_GFX_OP("exclusion", EXCLUSION)
|
|
else CANVAS_OP_TO_GFX_OP("hue", HUE)
|
|
else CANVAS_OP_TO_GFX_OP("saturation", SATURATION)
|
|
else CANVAS_OP_TO_GFX_OP("color", COLOR)
|
|
else CANVAS_OP_TO_GFX_OP("luminosity", LUMINOSITY)
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
else return;
|
|
|
|
#undef CANVAS_OP_TO_GFX_OP
|
|
CurrentState().op = comp_op;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::GetGlobalCompositeOperation(nsAString& aOp,
|
|
ErrorResult& aError)
|
|
{
|
|
CompositionOp comp_op = CurrentState().op;
|
|
|
|
#define CANVAS_OP_TO_GFX_OP(cvsop, op2d) \
|
|
if (comp_op == CompositionOp::OP_##op2d) \
|
|
aOp.AssignLiteral(cvsop);
|
|
|
|
CANVAS_OP_TO_GFX_OP("copy", SOURCE)
|
|
else CANVAS_OP_TO_GFX_OP("destination-atop", DEST_ATOP)
|
|
else CANVAS_OP_TO_GFX_OP("destination-in", DEST_IN)
|
|
else CANVAS_OP_TO_GFX_OP("destination-out", DEST_OUT)
|
|
else CANVAS_OP_TO_GFX_OP("destination-over", DEST_OVER)
|
|
else CANVAS_OP_TO_GFX_OP("lighter", ADD)
|
|
else CANVAS_OP_TO_GFX_OP("source-atop", ATOP)
|
|
else CANVAS_OP_TO_GFX_OP("source-in", IN)
|
|
else CANVAS_OP_TO_GFX_OP("source-out", OUT)
|
|
else CANVAS_OP_TO_GFX_OP("source-over", OVER)
|
|
else CANVAS_OP_TO_GFX_OP("xor", XOR)
|
|
else CANVAS_OP_TO_GFX_OP("multiply", MULTIPLY)
|
|
else CANVAS_OP_TO_GFX_OP("screen", SCREEN)
|
|
else CANVAS_OP_TO_GFX_OP("overlay", OVERLAY)
|
|
else CANVAS_OP_TO_GFX_OP("darken", DARKEN)
|
|
else CANVAS_OP_TO_GFX_OP("lighten", LIGHTEN)
|
|
else CANVAS_OP_TO_GFX_OP("color-dodge", COLOR_DODGE)
|
|
else CANVAS_OP_TO_GFX_OP("color-burn", COLOR_BURN)
|
|
else CANVAS_OP_TO_GFX_OP("hard-light", HARD_LIGHT)
|
|
else CANVAS_OP_TO_GFX_OP("soft-light", SOFT_LIGHT)
|
|
else CANVAS_OP_TO_GFX_OP("difference", DIFFERENCE)
|
|
else CANVAS_OP_TO_GFX_OP("exclusion", EXCLUSION)
|
|
else CANVAS_OP_TO_GFX_OP("hue", HUE)
|
|
else CANVAS_OP_TO_GFX_OP("saturation", SATURATION)
|
|
else CANVAS_OP_TO_GFX_OP("color", COLOR)
|
|
else CANVAS_OP_TO_GFX_OP("luminosity", LUMINOSITY)
|
|
else {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
#undef CANVAS_OP_TO_GFX_OP
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::DrawWindow(nsGlobalWindow& aWindow, double aX,
|
|
double aY, double aW, double aH,
|
|
const nsAString& aBgColor,
|
|
uint32_t aFlags, ErrorResult& aError)
|
|
{
|
|
MOZ_ASSERT(aWindow.IsInnerWindow());
|
|
|
|
if (int32_t(aW) == 0 || int32_t(aH) == 0) {
|
|
return;
|
|
}
|
|
|
|
// protect against too-large surfaces that will cause allocation
|
|
// or overflow issues
|
|
if (!Factory::CheckSurfaceSize(IntSize(int32_t(aW), int32_t(aH)), 0xffff)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
// Flush layout updates
|
|
if (!(aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DO_NOT_FLUSH)) {
|
|
nsContentUtils::FlushLayoutForTree(aWindow.AsInner()->GetOuterWindow());
|
|
}
|
|
|
|
CompositionOp op = UsedOperation();
|
|
bool discardContent = GlobalAlpha() == 1.0f
|
|
&& (op == CompositionOp::OP_OVER || op == CompositionOp::OP_SOURCE);
|
|
const gfx::Rect drawRect(aX, aY, aW, aH);
|
|
EnsureTarget(discardContent ? &drawRect : nullptr);
|
|
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
// We can't allow web apps to call this until we fix at least the
|
|
// following potential security issues:
|
|
// -- rendering cross-domain IFRAMEs and then extracting the results
|
|
// -- rendering the user's theme and then extracting the results
|
|
// -- rendering native anonymous content (e.g., file input paths;
|
|
// scrollbars should be allowed)
|
|
if (!nsContentUtils::IsCallerChrome()) {
|
|
// not permitted to use DrawWindow
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
|
return;
|
|
}
|
|
|
|
RefPtr<nsPresContext> presContext;
|
|
nsIDocShell* docshell = aWindow.GetDocShell();
|
|
if (docshell) {
|
|
docshell->GetPresContext(getter_AddRefs(presContext));
|
|
}
|
|
if (!presContext) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
nscolor backgroundColor;
|
|
if (!ParseColor(aBgColor, &backgroundColor)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
nsRect r(nsPresContext::CSSPixelsToAppUnits((float)aX),
|
|
nsPresContext::CSSPixelsToAppUnits((float)aY),
|
|
nsPresContext::CSSPixelsToAppUnits((float)aW),
|
|
nsPresContext::CSSPixelsToAppUnits((float)aH));
|
|
uint32_t renderDocFlags = (nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING |
|
|
nsIPresShell::RENDER_DOCUMENT_RELATIVE);
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DRAW_CARET) {
|
|
renderDocFlags |= nsIPresShell::RENDER_CARET;
|
|
}
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DRAW_VIEW) {
|
|
renderDocFlags &= ~(nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING |
|
|
nsIPresShell::RENDER_DOCUMENT_RELATIVE);
|
|
}
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_USE_WIDGET_LAYERS) {
|
|
renderDocFlags |= nsIPresShell::RENDER_USE_WIDGET_LAYERS;
|
|
}
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_ASYNC_DECODE_IMAGES) {
|
|
renderDocFlags |= nsIPresShell::RENDER_ASYNC_DECODE_IMAGES;
|
|
}
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DO_NOT_FLUSH) {
|
|
renderDocFlags |= nsIPresShell::RENDER_DRAWWINDOW_NOT_FLUSHING;
|
|
}
|
|
|
|
// gfxContext-over-Azure may modify the DrawTarget's transform, so
|
|
// save and restore it
|
|
Matrix matrix = mTarget->GetTransform();
|
|
double sw = matrix._11 * aW;
|
|
double sh = matrix._22 * aH;
|
|
if (!sw || !sh) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<gfxContext> thebes;
|
|
RefPtr<DrawTarget> drawDT;
|
|
// Rendering directly is faster and can be done if mTarget supports Azure
|
|
// and does not need alpha blending.
|
|
// Since the pre-transaction callback calls ReturnTarget, we can't have a
|
|
// gfxContext wrapped around it when using a shared buffer provider because
|
|
// the DrawTarget's shared buffer may be unmapped in ReturnTarget.
|
|
op = CompositionOp::OP_ADD;
|
|
if (gfxPlatform::GetPlatform()->SupportsAzureContentForDrawTarget(mTarget) &&
|
|
GlobalAlpha() == 1.0f) {
|
|
op = UsedOperation();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
}
|
|
if (op == CompositionOp::OP_OVER &&
|
|
(!mBufferProvider || mBufferProvider->GetType() != LayersBackend::LAYERS_CLIENT))
|
|
{
|
|
thebes = gfxContext::CreateOrNull(mTarget);
|
|
MOZ_ASSERT(thebes); // already checked the draw target above
|
|
// (in SupportsAzureContentForDrawTarget)
|
|
thebes->SetMatrix(gfxMatrix(matrix._11, matrix._12, matrix._21,
|
|
matrix._22, matrix._31, matrix._32));
|
|
} else {
|
|
IntSize dtSize = IntSize::Ceil(sw, sh);
|
|
if (!Factory::AllowedSurfaceSize(dtSize)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
drawDT =
|
|
gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(dtSize,
|
|
SurfaceFormat::B8G8R8A8);
|
|
if (!drawDT || !drawDT->IsValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
thebes = gfxContext::CreateOrNull(drawDT);
|
|
MOZ_ASSERT(thebes); // alrady checked the draw target above
|
|
thebes->SetMatrix(gfxMatrix::Scaling(matrix._11, matrix._22));
|
|
}
|
|
|
|
nsCOMPtr<nsIPresShell> shell = presContext->PresShell();
|
|
|
|
Unused << shell->RenderDocument(r, renderDocFlags, backgroundColor, thebes);
|
|
// If this canvas was contained in the drawn window, the pre-transaction callback
|
|
// may have returned its DT. If so, we must reacquire it here.
|
|
EnsureTarget(discardContent ? &drawRect : nullptr);
|
|
|
|
if (drawDT) {
|
|
RefPtr<SourceSurface> snapshot = drawDT->Snapshot();
|
|
if (NS_WARN_IF(!snapshot)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
RefPtr<DataSourceSurface> data = snapshot->GetDataSurface();
|
|
if (!data || !Factory::AllowedSurfaceSize(data->GetSize())) {
|
|
gfxCriticalError() << "Unexpected invalid data source surface " <<
|
|
(data ? data->GetSize() : IntSize(0,0));
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
DataSourceSurface::MappedSurface rawData;
|
|
if (NS_WARN_IF(!data->Map(DataSourceSurface::READ, &rawData))) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
RefPtr<SourceSurface> source =
|
|
mTarget->CreateSourceSurfaceFromData(rawData.mData,
|
|
data->GetSize(),
|
|
rawData.mStride,
|
|
data->GetFormat());
|
|
data->Unmap();
|
|
|
|
if (!source) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
op = UsedOperation();
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
gfx::Rect destRect(0, 0, aW, aH);
|
|
gfx::Rect sourceRect(0, 0, sw, sh);
|
|
mTarget->DrawSurface(source, destRect, sourceRect,
|
|
DrawSurfaceOptions(gfx::SamplingFilter::POINT),
|
|
DrawOptions(GlobalAlpha(), op,
|
|
AntialiasMode::NONE));
|
|
} else {
|
|
mTarget->SetTransform(matrix);
|
|
}
|
|
|
|
// note that x and y are coordinates in the document that
|
|
// we're drawing; x and y are drawn to 0,0 in current user
|
|
// space.
|
|
RedrawUser(gfxRect(0, 0, aW, aH));
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::AsyncDrawXULElement(nsXULElement& aElem,
|
|
double aX, double aY,
|
|
double aW, double aH,
|
|
const nsAString& aBgColor,
|
|
uint32_t aFlags,
|
|
ErrorResult& aError)
|
|
{
|
|
// We can't allow web apps to call this until we fix at least the
|
|
// following potential security issues:
|
|
// -- rendering cross-domain IFRAMEs and then extracting the results
|
|
// -- rendering the user's theme and then extracting the results
|
|
// -- rendering native anonymous content (e.g., file input paths;
|
|
// scrollbars should be allowed)
|
|
if (!nsContentUtils::IsCallerChrome()) {
|
|
// not permitted to use DrawWindow
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
|
return;
|
|
}
|
|
|
|
#if 0
|
|
nsCOMPtr<nsIFrameLoaderOwner> loaderOwner = do_QueryInterface(&elem);
|
|
if (!loaderOwner) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
RefPtr<nsFrameLoader> frameloader = loaderOwner->GetFrameLoader();
|
|
if (!frameloader) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
PBrowserParent *child = frameloader->GetRemoteBrowser();
|
|
if (!child) {
|
|
nsIDocShell* docShell = frameLoader->GetExistingDocShell();
|
|
if (!docShell) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
nsCOMPtr<nsPIDOMWindowOuter> window = docShell->GetWindow();
|
|
if (!window) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
return DrawWindow(window->GetCurrentInnerWindow(), aX, aY, aW, aH,
|
|
aBgColor, aFlags);
|
|
}
|
|
|
|
// protect against too-large surfaces that will cause allocation
|
|
// or overflow issues
|
|
if (!Factory::CheckSurfaceSize(IntSize(aW, aH), 0xffff)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
bool flush =
|
|
(aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DO_NOT_FLUSH) == 0;
|
|
|
|
uint32_t renderDocFlags = nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING;
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DRAW_CARET) {
|
|
renderDocFlags |= nsIPresShell::RENDER_CARET;
|
|
}
|
|
if (aFlags & nsIDOMCanvasRenderingContext2D::DRAWWINDOW_DRAW_VIEW) {
|
|
renderDocFlags &= ~nsIPresShell::RENDER_IGNORE_VIEWPORT_SCROLLING;
|
|
}
|
|
|
|
nsRect rect(nsPresContext::CSSPixelsToAppUnits(aX),
|
|
nsPresContext::CSSPixelsToAppUnits(aY),
|
|
nsPresContext::CSSPixelsToAppUnits(aW),
|
|
nsPresContext::CSSPixelsToAppUnits(aH));
|
|
if (mIPC) {
|
|
PDocumentRendererParent *pdocrender =
|
|
child->SendPDocumentRendererConstructor(rect,
|
|
mThebes->CurrentMatrix(),
|
|
nsString(aBGColor),
|
|
renderDocFlags, flush,
|
|
nsIntSize(mWidth, mHeight));
|
|
if (!pdocrender)
|
|
return NS_ERROR_FAILURE;
|
|
|
|
DocumentRendererParent *docrender =
|
|
static_cast<DocumentRendererParent *>(pdocrender);
|
|
|
|
docrender->SetCanvasContext(this, mThebes);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
//
|
|
// device pixel getting/setting
|
|
//
|
|
|
|
already_AddRefed<ImageData>
|
|
CanvasRenderingContext2D::GetImageData(JSContext* aCx, double aSx,
|
|
double aSy, double aSw,
|
|
double aSh, ErrorResult& aError)
|
|
{
|
|
if (mDrawObserver) {
|
|
mDrawObserver->DidDrawCall(CanvasDrawObserver::DrawCallType::GetImageData);
|
|
}
|
|
|
|
if (!mCanvasElement && !mDocShell) {
|
|
NS_ERROR("No canvas element and no docshell in GetImageData!!!");
|
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// Check only if we have a canvas element; if we were created with a docshell,
|
|
// then it's special internal use.
|
|
if (IsWriteOnly() ||
|
|
(mCanvasElement && mCanvasElement->IsWriteOnly() && !nsContentUtils::IsCallerChrome())) {
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!IsFinite(aSx) || !IsFinite(aSy) ||
|
|
!IsFinite(aSw) || !IsFinite(aSh)) {
|
|
aError.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!aSw || !aSh) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
int32_t x = JS::ToInt32(aSx);
|
|
int32_t y = JS::ToInt32(aSy);
|
|
int32_t wi = JS::ToInt32(aSw);
|
|
int32_t hi = JS::ToInt32(aSh);
|
|
|
|
// Handle negative width and height by flipping the rectangle over in the
|
|
// relevant direction.
|
|
uint32_t w, h;
|
|
if (aSw < 0) {
|
|
w = -wi;
|
|
x -= w;
|
|
} else {
|
|
w = wi;
|
|
}
|
|
if (aSh < 0) {
|
|
h = -hi;
|
|
y -= h;
|
|
} else {
|
|
h = hi;
|
|
}
|
|
|
|
if (w == 0) {
|
|
w = 1;
|
|
}
|
|
if (h == 0) {
|
|
h = 1;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> array(aCx);
|
|
aError = GetImageDataArray(aCx, x, y, w, h, array.address());
|
|
if (aError.Failed()) {
|
|
return nullptr;
|
|
}
|
|
MOZ_ASSERT(array);
|
|
|
|
RefPtr<ImageData> imageData = new ImageData(w, h, *array);
|
|
return imageData.forget();
|
|
}
|
|
|
|
inline uint8_t PoisonValue(uint8_t v)
|
|
{
|
|
if (v==0 || v==255)
|
|
return v; //don't fuzz edges to prevent overflow/underflow
|
|
|
|
return v + rand() %3 -1;
|
|
}
|
|
|
|
static IntRect ClipImageDataTransfer(IntRect& aSrc,
|
|
const IntPoint& aDestOffset,
|
|
const IntSize& aDestBounds)
|
|
{
|
|
IntRect dest = aSrc;
|
|
dest.SafeMoveBy(aDestOffset);
|
|
dest = IntRect(IntPoint(0, 0), aDestBounds).SafeIntersect(dest);
|
|
|
|
aSrc = aSrc.SafeIntersect(dest - aDestOffset);
|
|
return aSrc + aDestOffset;
|
|
}
|
|
|
|
nsresult
|
|
CanvasRenderingContext2D::GetImageDataArray(JSContext* aCx,
|
|
int32_t aX,
|
|
int32_t aY,
|
|
uint32_t aWidth,
|
|
uint32_t aHeight,
|
|
JSObject** aRetval)
|
|
{
|
|
if (mDrawObserver) {
|
|
mDrawObserver->DidDrawCall(CanvasDrawObserver::DrawCallType::GetImageData);
|
|
}
|
|
|
|
MOZ_ASSERT(aWidth && aHeight);
|
|
|
|
bool PoisonData = Preferences::GetBool("canvas.poisondata", false);
|
|
if (PoisonData) {
|
|
int PoisonInterval = Preferences::GetInt("canvas.poisondata.interval", 300);
|
|
PoisonInterval = (PoisonInterval < 1) ? 1 : (PoisonInterval > 28800) ? 28800 : PoisonInterval;
|
|
unsigned int epoch = time(nullptr);
|
|
srand(epoch / PoisonInterval);
|
|
}
|
|
|
|
CheckedInt<uint32_t> len = CheckedInt<uint32_t>(aWidth) * aHeight * 4;
|
|
if (!len.isValid()) {
|
|
return NS_ERROR_DOM_INDEX_SIZE_ERR;
|
|
}
|
|
|
|
CheckedInt<int32_t> rightMost = CheckedInt<int32_t>(aX) + aWidth;
|
|
CheckedInt<int32_t> bottomMost = CheckedInt<int32_t>(aY) + aHeight;
|
|
|
|
if (!rightMost.isValid() || !bottomMost.isValid()) {
|
|
return NS_ERROR_DOM_SYNTAX_ERR;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> darray(aCx, JS_NewUint8ClampedArray(aCx, len.value()));
|
|
if (!darray) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
if (mZero) {
|
|
*aRetval = darray;
|
|
return NS_OK;
|
|
}
|
|
|
|
IntRect dstWriteRect(0, 0, aWidth, aHeight);
|
|
IntRect srcReadRect = ClipImageDataTransfer(dstWriteRect,
|
|
IntPoint(aX, aY),
|
|
IntSize(mWidth, mHeight));
|
|
|
|
RefPtr<DataSourceSurface> readback;
|
|
DataSourceSurface::MappedSurface rawData;
|
|
if (!srcReadRect.IsEmpty()) {
|
|
RefPtr<SourceSurface> snapshot;
|
|
if (!mTarget && mBufferProvider) {
|
|
snapshot = mBufferProvider->BorrowSnapshot();
|
|
} else {
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
snapshot = mTarget->Snapshot();
|
|
}
|
|
|
|
if (snapshot) {
|
|
readback = snapshot->GetDataSurface();
|
|
}
|
|
|
|
if (!mTarget && mBufferProvider) {
|
|
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
}
|
|
|
|
if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
}
|
|
|
|
JS::AutoCheckCannotGC nogc;
|
|
bool isShared;
|
|
uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
|
|
MOZ_ASSERT(!isShared); // Should not happen, data was created above
|
|
|
|
uint8_t* src;
|
|
uint32_t srcStride;
|
|
if (readback) {
|
|
srcStride = rawData.mStride;
|
|
src = rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
|
|
} else {
|
|
src = data;
|
|
srcStride = aWidth * 4;
|
|
}
|
|
|
|
uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
|
|
|
|
uint8_t a,r,g,b;
|
|
|
|
if (mOpaque) {
|
|
for (int32_t j = 0; j < dstWriteRect.height; ++j) {
|
|
for (int32_t i = 0; i < dstWriteRect.width; ++i) {
|
|
// XXX Is there some useful swizzle MMX we can use here?
|
|
#if MOZ_LITTLE_ENDIAN
|
|
b = *src++;
|
|
g = *src++;
|
|
r = *src++;
|
|
src++;
|
|
#else
|
|
src++;
|
|
r = *src++;
|
|
g = *src++;
|
|
b = *src++;
|
|
#endif
|
|
|
|
// Poison data for trackers if enabled
|
|
if (PoisonData) {
|
|
PoisonValue(r);
|
|
PoisonValue(g);
|
|
PoisonValue(b);
|
|
}
|
|
|
|
*dst++ = r;
|
|
*dst++ = g;
|
|
*dst++ = b;
|
|
*dst++ = 255;
|
|
}
|
|
src += srcStride - (dstWriteRect.width * 4);
|
|
dst += (aWidth * 4) - (dstWriteRect.width * 4);
|
|
}
|
|
} else
|
|
for (int32_t j = 0; j < dstWriteRect.height; ++j) {
|
|
for (int32_t i = 0; i < dstWriteRect.width; ++i) {
|
|
// XXX Is there some useful swizzle MMX we can use here?
|
|
#if MOZ_LITTLE_ENDIAN
|
|
b = *src++;
|
|
g = *src++;
|
|
r = *src++;
|
|
a = *src++;
|
|
#else
|
|
a = *src++;
|
|
r = *src++;
|
|
g = *src++;
|
|
b = *src++;
|
|
#endif
|
|
|
|
// Poison data for trackers if enabled
|
|
if (PoisonData) {
|
|
PoisonValue(a);
|
|
PoisonValue(r);
|
|
PoisonValue(g);
|
|
PoisonValue(b);
|
|
}
|
|
|
|
// Convert to non-premultiplied color
|
|
*dst++ = gfxUtils::sUnpremultiplyTable[a * 256 + r];
|
|
*dst++ = gfxUtils::sUnpremultiplyTable[a * 256 + g];
|
|
*dst++ = gfxUtils::sUnpremultiplyTable[a * 256 + b];
|
|
*dst++ = a;
|
|
}
|
|
src += srcStride - (dstWriteRect.width * 4);
|
|
dst += (aWidth * 4) - (dstWriteRect.width * 4);
|
|
}
|
|
|
|
if (readback) {
|
|
readback->Unmap();
|
|
}
|
|
|
|
*aRetval = darray;
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::EnsureErrorTarget()
|
|
{
|
|
if (sErrorTarget) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<DrawTarget> errorTarget = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget(IntSize(1, 1), SurfaceFormat::B8G8R8A8);
|
|
MOZ_ASSERT(errorTarget, "Failed to allocate the error target!");
|
|
|
|
sErrorTarget = errorTarget;
|
|
NS_ADDREF(sErrorTarget);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::FillRuleChanged()
|
|
{
|
|
if (mPath) {
|
|
mPathBuilder = mPath->CopyToBuilder(CurrentState().fillRule);
|
|
mPath = nullptr;
|
|
}
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::PutImageData(ImageData& aImageData, double aDx,
|
|
double aDy, ErrorResult& aError)
|
|
{
|
|
RootedSpiderMonkeyInterface<Uint8ClampedArray> arr(RootingCx());
|
|
DebugOnly<bool> inited = arr.Init(aImageData.GetDataObject());
|
|
MOZ_ASSERT(inited);
|
|
|
|
aError = PutImageData_explicit(JS::ToInt32(aDx), JS::ToInt32(aDy),
|
|
aImageData.Width(), aImageData.Height(),
|
|
&arr, false, 0, 0, 0, 0);
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::PutImageData(ImageData& aImageData, double aDx,
|
|
double aDy, double aDirtyX,
|
|
double aDirtyY, double aDirtyWidth,
|
|
double aDirtyHeight,
|
|
ErrorResult& aError)
|
|
{
|
|
RootedSpiderMonkeyInterface<Uint8ClampedArray> arr(RootingCx());
|
|
DebugOnly<bool> inited = arr.Init(aImageData.GetDataObject());
|
|
MOZ_ASSERT(inited);
|
|
|
|
aError = PutImageData_explicit(JS::ToInt32(aDx), JS::ToInt32(aDy),
|
|
aImageData.Width(), aImageData.Height(),
|
|
&arr, true,
|
|
JS::ToInt32(aDirtyX),
|
|
JS::ToInt32(aDirtyY),
|
|
JS::ToInt32(aDirtyWidth),
|
|
JS::ToInt32(aDirtyHeight));
|
|
}
|
|
|
|
nsresult
|
|
CanvasRenderingContext2D::PutImageData_explicit(int32_t aX, int32_t aY, uint32_t aW, uint32_t aH,
|
|
dom::Uint8ClampedArray* aArray,
|
|
bool aHasDirtyRect, int32_t aDirtyX, int32_t aDirtyY,
|
|
int32_t aDirtyWidth, int32_t aDirtyHeight)
|
|
{
|
|
if (mDrawObserver) {
|
|
mDrawObserver->DidDrawCall(CanvasDrawObserver::DrawCallType::PutImageData);
|
|
}
|
|
|
|
if (aW == 0 || aH == 0) {
|
|
return NS_ERROR_DOM_INVALID_STATE_ERR;
|
|
}
|
|
|
|
IntRect dirtyRect;
|
|
IntRect imageDataRect(0, 0, aW, aH);
|
|
|
|
if (aHasDirtyRect) {
|
|
// fix up negative dimensions
|
|
if (aDirtyWidth < 0) {
|
|
NS_ENSURE_TRUE(aDirtyWidth != INT_MIN, NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
|
|
CheckedInt32 checkedDirtyX = CheckedInt32(aDirtyX) + aDirtyWidth;
|
|
|
|
if (!checkedDirtyX.isValid())
|
|
return NS_ERROR_DOM_INDEX_SIZE_ERR;
|
|
|
|
aDirtyX = checkedDirtyX.value();
|
|
aDirtyWidth = -aDirtyWidth;
|
|
}
|
|
|
|
if (aDirtyHeight < 0) {
|
|
NS_ENSURE_TRUE(aDirtyHeight != INT_MIN, NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
|
|
CheckedInt32 checkedDirtyY = CheckedInt32(aDirtyY) + aDirtyHeight;
|
|
|
|
if (!checkedDirtyY.isValid())
|
|
return NS_ERROR_DOM_INDEX_SIZE_ERR;
|
|
|
|
aDirtyY = checkedDirtyY.value();
|
|
aDirtyHeight = -aDirtyHeight;
|
|
}
|
|
|
|
// bound the dirty rect within the imageData rectangle
|
|
dirtyRect = imageDataRect.Intersect(IntRect(aDirtyX, aDirtyY, aDirtyWidth, aDirtyHeight));
|
|
|
|
if (dirtyRect.Width() <= 0 || dirtyRect.Height() <= 0)
|
|
return NS_OK;
|
|
} else {
|
|
dirtyRect = imageDataRect;
|
|
}
|
|
|
|
IntRect srcRect = dirtyRect;
|
|
dirtyRect = ClipImageDataTransfer(srcRect, IntPoint(aX, aY),
|
|
IntSize(mWidth, mHeight));
|
|
if (dirtyRect.IsEmpty()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
aArray->ComputeLengthAndData();
|
|
|
|
uint32_t dataLen = aArray->Length();
|
|
|
|
uint32_t len = aW * aH * 4;
|
|
if (dataLen != len) {
|
|
return NS_ERROR_DOM_INVALID_STATE_ERR;
|
|
}
|
|
|
|
uint32_t copyWidth = dirtyRect.Width();
|
|
uint32_t copyHeight = dirtyRect.Height();
|
|
RefPtr<gfxImageSurface> imgsurf = new gfxImageSurface(gfx::IntSize(copyWidth, copyHeight),
|
|
SurfaceFormat::A8R8G8B8_UINT32,
|
|
false);
|
|
if (!imgsurf || imgsurf->CairoStatus()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
uint32_t copyX = dirtyRect.x - aX;
|
|
uint32_t copyY = dirtyRect.y - aY;
|
|
//uint8_t *src = aArray->Data();
|
|
uint8_t *dst = imgsurf->Data();
|
|
uint8_t* srcLine = aArray->Data() + copyY * (aW * 4) + copyX * 4;
|
|
// For opaque canvases, we must still premultiply the RGB components, but write the alpha as opaque.
|
|
uint8_t alphaMask = mOpaque ? 255 : 0;
|
|
#if 0
|
|
printf("PutImageData_explicit: dirty x=%d y=%d w=%d h=%d copy x=%d y=%d w=%d h=%d ext x=%d y=%d w=%d h=%d\n",
|
|
dirtyRect.x, dirtyRect.y, copyWidth, copyHeight,
|
|
copyX, copyY, copyWidth, copyHeight,
|
|
x, y, w, h);
|
|
#endif
|
|
for (uint32_t j = 0; j < copyHeight; j++) {
|
|
uint8_t *src = srcLine;
|
|
for (uint32_t i = 0; i < copyWidth; i++) {
|
|
uint8_t r = *src++;
|
|
uint8_t g = *src++;
|
|
uint8_t b = *src++;
|
|
uint8_t a = *src++;
|
|
|
|
// Convert to premultiplied color (losslessly if the input came from getImageData)
|
|
#if MOZ_LITTLE_ENDIAN
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + b];
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + g];
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + r];
|
|
*dst++ = a | alphaMask;
|
|
#else
|
|
*dst++ = a | alphaMask;
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + r];
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + g];
|
|
*dst++ = gfxUtils::sPremultiplyTable[a * 256 + b];
|
|
#endif
|
|
}
|
|
srcLine += aW * 4;
|
|
}
|
|
|
|
// The canvas spec says that the current path, transformation matrix, shadow attributes,
|
|
// global alpha, the clipping region, and global composition operator must not affect the
|
|
// getImageData() and putImageData() methods.
|
|
const gfx::Rect putRect(dirtyRect);
|
|
EnsureTarget(&putRect);
|
|
|
|
if (!IsTargetValid()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
RefPtr<SourceSurface> sourceSurface =
|
|
mTarget->CreateSourceSurfaceFromData(imgsurf->Data(), IntSize(copyWidth, copyHeight), imgsurf->Stride(), SurfaceFormat::B8G8R8A8);
|
|
|
|
// In certain scenarios, requesting larger than 8k image fails. Bug 803568
|
|
// covers the details of how to run into it, but the full detailed
|
|
// investigation hasn't been done to determine the underlying cause. We
|
|
// will just handle the failure to allocate the surface to avoid a crash.
|
|
if (!sourceSurface) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
mTarget->CopySurface(sourceSurface,
|
|
IntRect(0, 0,
|
|
dirtyRect.width, dirtyRect.height),
|
|
IntPoint(dirtyRect.x, dirtyRect.y));
|
|
|
|
Redraw(gfx::Rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
static already_AddRefed<ImageData>
|
|
CreateImageData(JSContext* aCx, CanvasRenderingContext2D* aContext,
|
|
uint32_t aW, uint32_t aH, ErrorResult& aError)
|
|
{
|
|
if (aW == 0)
|
|
aW = 1;
|
|
if (aH == 0)
|
|
aH = 1;
|
|
|
|
CheckedInt<uint32_t> len = CheckedInt<uint32_t>(aW) * aH * 4;
|
|
if (!len.isValid()) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// Create the fast typed array; it's initialized to 0 by default.
|
|
JSObject* darray = Uint8ClampedArray::Create(aCx, aContext, len.value());
|
|
if (!darray) {
|
|
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<mozilla::dom::ImageData> imageData =
|
|
new mozilla::dom::ImageData(aW, aH, *darray);
|
|
return imageData.forget();
|
|
}
|
|
|
|
already_AddRefed<ImageData>
|
|
CanvasRenderingContext2D::CreateImageData(JSContext* aCx, double aSw,
|
|
double aSh, ErrorResult& aError)
|
|
{
|
|
if (!aSw || !aSh) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
int32_t wi = JS::ToInt32(aSw);
|
|
int32_t hi = JS::ToInt32(aSh);
|
|
|
|
uint32_t w = Abs(wi);
|
|
uint32_t h = Abs(hi);
|
|
return mozilla::dom::CreateImageData(aCx, this, w, h, aError);
|
|
}
|
|
|
|
already_AddRefed<ImageData>
|
|
CanvasRenderingContext2D::CreateImageData(JSContext* aCx,
|
|
ImageData& aImagedata,
|
|
ErrorResult& aError)
|
|
{
|
|
return mozilla::dom::CreateImageData(aCx, this, aImagedata.Width(),
|
|
aImagedata.Height(), aError);
|
|
}
|
|
|
|
static uint8_t g2DContextLayerUserData;
|
|
|
|
|
|
uint32_t
|
|
CanvasRenderingContext2D::SkiaGLTex() const
|
|
{
|
|
if (!mTarget) {
|
|
return 0;
|
|
}
|
|
MOZ_ASSERT(IsTargetValid());
|
|
return (uint32_t)(uintptr_t)mTarget->GetNativeSurface(NativeSurfaceType::OPENGL_TEXTURE);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::RemoveDrawObserver()
|
|
{
|
|
if (mDrawObserver) {
|
|
delete mDrawObserver;
|
|
mDrawObserver = nullptr;
|
|
}
|
|
}
|
|
|
|
already_AddRefed<Layer>
|
|
CanvasRenderingContext2D::GetCanvasLayer(nsDisplayListBuilder* aBuilder,
|
|
Layer* aOldLayer,
|
|
LayerManager* aManager,
|
|
bool aMirror /* = false */)
|
|
{
|
|
if (aMirror) {
|
|
// Not supported for CanvasRenderingContext2D
|
|
return nullptr;
|
|
}
|
|
|
|
if (mOpaque || mIsSkiaGL) {
|
|
// If we're opaque then make sure we have a surface so we paint black
|
|
// instead of transparent.
|
|
// If we're using SkiaGL, then SkiaGLTex() below needs the target to
|
|
// be accessible.
|
|
EnsureTarget();
|
|
}
|
|
|
|
// Don't call EnsureTarget() ... if there isn't already a surface, then
|
|
// we have nothing to paint and there is no need to create a surface just
|
|
// to paint nothing. Also, EnsureTarget() can cause creation of a persistent
|
|
// layer manager which must NOT happen during a paint.
|
|
if ((!mBufferProvider && !mTarget) || !IsTargetValid()) {
|
|
// No DidTransactionCallback will be received, so mark the context clean
|
|
// now so future invalidations will be dispatched.
|
|
MarkContextClean();
|
|
return nullptr;
|
|
}
|
|
|
|
if (!mResetLayer && aOldLayer) {
|
|
auto userData =
|
|
static_cast<CanvasRenderingContext2DUserData*>(
|
|
aOldLayer->GetUserData(&g2DContextLayerUserData));
|
|
|
|
CanvasLayer::Data data;
|
|
|
|
if (mIsSkiaGL) {
|
|
GLuint skiaGLTex = SkiaGLTex();
|
|
if (skiaGLTex) {
|
|
SkiaGLGlue* glue = gfxPlatform::GetPlatform()->GetSkiaGLGlue();
|
|
MOZ_ASSERT(glue);
|
|
data.mGLContext = glue->GetGLContext();
|
|
data.mFrontbufferGLTex = skiaGLTex;
|
|
}
|
|
}
|
|
|
|
data.mBufferProvider = mBufferProvider;
|
|
|
|
if (userData &&
|
|
userData->IsForContext(this) &&
|
|
static_cast<CanvasLayer*>(aOldLayer)->IsDataValid(data)) {
|
|
RefPtr<Layer> ret = aOldLayer;
|
|
return ret.forget();
|
|
}
|
|
}
|
|
|
|
RefPtr<CanvasLayer> canvasLayer = aManager->CreateCanvasLayer();
|
|
if (!canvasLayer) {
|
|
NS_WARNING("CreateCanvasLayer returned null!");
|
|
// No DidTransactionCallback will be received, so mark the context clean
|
|
// now so future invalidations will be dispatched.
|
|
MarkContextClean();
|
|
return nullptr;
|
|
}
|
|
CanvasRenderingContext2DUserData* userData = nullptr;
|
|
// Make the layer tell us whenever a transaction finishes (including
|
|
// the current transaction), so we can clear our invalidation state and
|
|
// start invalidating again. We need to do this for all layers since
|
|
// callers of DrawWindow may be expecting to receive normal invalidation
|
|
// notifications after this paint.
|
|
|
|
// The layer will be destroyed when we tear down the presentation
|
|
// (at the latest), at which time this userData will be destroyed,
|
|
// releasing the reference to the element.
|
|
// The userData will receive DidTransactionCallbacks, which flush the
|
|
// the invalidation state to indicate that the canvas is up to date.
|
|
userData = new CanvasRenderingContext2DUserData(this);
|
|
canvasLayer->SetDidTransactionCallback(
|
|
CanvasRenderingContext2DUserData::DidTransactionCallback, userData);
|
|
canvasLayer->SetUserData(&g2DContextLayerUserData, userData);
|
|
|
|
CanvasLayer::Data data;
|
|
data.mSize = GetSize();
|
|
data.mHasAlpha = !mOpaque;
|
|
|
|
canvasLayer->SetPreTransactionCallback(
|
|
CanvasRenderingContext2DUserData::PreTransactionCallback, userData);
|
|
|
|
|
|
if (mIsSkiaGL) {
|
|
GLuint skiaGLTex = SkiaGLTex();
|
|
if (skiaGLTex) {
|
|
SkiaGLGlue* glue = gfxPlatform::GetPlatform()->GetSkiaGLGlue();
|
|
MOZ_ASSERT(glue);
|
|
data.mGLContext = glue->GetGLContext();
|
|
data.mFrontbufferGLTex = skiaGLTex;
|
|
}
|
|
}
|
|
|
|
data.mBufferProvider = mBufferProvider;
|
|
|
|
canvasLayer->Initialize(data);
|
|
uint32_t flags = mOpaque ? Layer::CONTENT_OPAQUE : 0;
|
|
canvasLayer->SetContentFlags(flags);
|
|
canvasLayer->Updated();
|
|
|
|
mResetLayer = false;
|
|
|
|
return canvasLayer.forget();
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::MarkContextClean()
|
|
{
|
|
if (mInvalidateCount > 0) {
|
|
mPredictManyRedrawCalls = mInvalidateCount > kCanvasMaxInvalidateCount;
|
|
}
|
|
mIsEntireFrameInvalid = false;
|
|
mInvalidateCount = 0;
|
|
}
|
|
|
|
void
|
|
CanvasRenderingContext2D::MarkContextCleanForFrameCapture()
|
|
{
|
|
mIsCapturedFrameInvalid = false;
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::IsContextCleanForFrameCapture()
|
|
{
|
|
return !mIsCapturedFrameInvalid;
|
|
}
|
|
|
|
bool
|
|
CanvasRenderingContext2D::ShouldForceInactiveLayer(LayerManager* aManager)
|
|
{
|
|
return !aManager->CanUseCanvasLayerForSize(GetSize());
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetWriteOnly() {
|
|
mWriteOnly = true;
|
|
if (mCanvasElement) {
|
|
mCanvasElement->SetWriteOnly();
|
|
}
|
|
}
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(CanvasPath, AddRef)
|
|
NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(CanvasPath, Release)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CanvasPath, mParent)
|
|
|
|
CanvasPath::CanvasPath(nsISupports* aParent)
|
|
: mParent(aParent)
|
|
{
|
|
mPathBuilder = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget()->CreatePathBuilder();
|
|
}
|
|
|
|
CanvasPath::CanvasPath(nsISupports* aParent, already_AddRefed<PathBuilder> aPathBuilder)
|
|
: mParent(aParent), mPathBuilder(aPathBuilder)
|
|
{
|
|
if (!mPathBuilder) {
|
|
mPathBuilder = gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget()->CreatePathBuilder();
|
|
}
|
|
}
|
|
|
|
JSObject*
|
|
CanvasPath::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
|
|
{
|
|
return Path2DBinding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
already_AddRefed<CanvasPath>
|
|
CanvasPath::Constructor(const GlobalObject& aGlobal, ErrorResult& aRv)
|
|
{
|
|
RefPtr<CanvasPath> path = new CanvasPath(aGlobal.GetAsSupports());
|
|
return path.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasPath>
|
|
CanvasPath::Constructor(const GlobalObject& aGlobal, CanvasPath& aCanvasPath, ErrorResult& aRv)
|
|
{
|
|
RefPtr<gfx::Path> tempPath = aCanvasPath.GetPath(CanvasWindingRule::Nonzero,
|
|
gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget());
|
|
|
|
RefPtr<CanvasPath> path = new CanvasPath(aGlobal.GetAsSupports(), tempPath->CopyToBuilder());
|
|
return path.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasPath>
|
|
CanvasPath::Constructor(const GlobalObject& aGlobal, const nsAString& aPathString, ErrorResult& aRv)
|
|
{
|
|
RefPtr<gfx::Path> tempPath = SVGContentUtils::GetPath(aPathString);
|
|
if (!tempPath) {
|
|
return Constructor(aGlobal, aRv);
|
|
}
|
|
|
|
RefPtr<CanvasPath> path = new CanvasPath(aGlobal.GetAsSupports(), tempPath->CopyToBuilder());
|
|
return path.forget();
|
|
}
|
|
|
|
void
|
|
CanvasPath::ClosePath()
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->Close();
|
|
}
|
|
|
|
void
|
|
CanvasPath::MoveTo(double aX, double aY)
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->MoveTo(Point(ToFloat(aX), ToFloat(aY)));
|
|
}
|
|
|
|
void
|
|
CanvasPath::LineTo(double aX, double aY)
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->LineTo(Point(ToFloat(aX), ToFloat(aY)));
|
|
}
|
|
|
|
void
|
|
CanvasPath::QuadraticCurveTo(double aCpx, double aCpy, double aX, double aY)
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->QuadraticBezierTo(gfx::Point(ToFloat(aCpx), ToFloat(aCpy)),
|
|
gfx::Point(ToFloat(aX), ToFloat(aY)));
|
|
}
|
|
|
|
void
|
|
CanvasPath::BezierCurveTo(double aCp1x, double aCp1y,
|
|
double aCp2x, double aCp2y,
|
|
double aX, double aY)
|
|
{
|
|
BezierTo(gfx::Point(ToFloat(aCp1x), ToFloat(aCp1y)),
|
|
gfx::Point(ToFloat(aCp2x), ToFloat(aCp2y)),
|
|
gfx::Point(ToFloat(aX), ToFloat(aY)));
|
|
}
|
|
|
|
void
|
|
CanvasPath::ArcTo(double aX1, double aY1, double aX2, double aY2, double aRadius,
|
|
ErrorResult& aError)
|
|
{
|
|
if (aRadius < 0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsurePathBuilder();
|
|
|
|
// Current point in user space!
|
|
Point p0 = mPathBuilder->CurrentPoint();
|
|
Point p1(aX1, aY1);
|
|
Point p2(aX2, aY2);
|
|
|
|
// Execute these calculations in double precision to avoid cumulative
|
|
// rounding errors.
|
|
double dir, a2, b2, c2, cosx, sinx, d, anx, any,
|
|
bnx, bny, x3, y3, x4, y4, cx, cy, angle0, angle1;
|
|
bool anticlockwise;
|
|
|
|
if (p0 == p1 || p1 == p2 || aRadius == 0) {
|
|
LineTo(p1.x, p1.y);
|
|
return;
|
|
}
|
|
|
|
// Check for colinearity
|
|
dir = (p2.x - p1.x) * (p0.y - p1.y) + (p2.y - p1.y) * (p1.x - p0.x);
|
|
if (dir == 0) {
|
|
LineTo(p1.x, p1.y);
|
|
return;
|
|
}
|
|
|
|
|
|
// XXX - Math for this code was already available from the non-azure code
|
|
// and would be well tested. Perhaps converting to bezier directly might
|
|
// be more efficient longer run.
|
|
a2 = (p0.x-aX1)*(p0.x-aX1) + (p0.y-aY1)*(p0.y-aY1);
|
|
b2 = (aX1-aX2)*(aX1-aX2) + (aY1-aY2)*(aY1-aY2);
|
|
c2 = (p0.x-aX2)*(p0.x-aX2) + (p0.y-aY2)*(p0.y-aY2);
|
|
cosx = (a2+b2-c2)/(2*sqrt(a2*b2));
|
|
|
|
sinx = sqrt(1 - cosx*cosx);
|
|
d = aRadius / ((1 - cosx) / sinx);
|
|
|
|
anx = (aX1-p0.x) / sqrt(a2);
|
|
any = (aY1-p0.y) / sqrt(a2);
|
|
bnx = (aX1-aX2) / sqrt(b2);
|
|
bny = (aY1-aY2) / sqrt(b2);
|
|
x3 = aX1 - anx*d;
|
|
y3 = aY1 - any*d;
|
|
x4 = aX1 - bnx*d;
|
|
y4 = aY1 - bny*d;
|
|
anticlockwise = (dir < 0);
|
|
cx = x3 + any*aRadius*(anticlockwise ? 1 : -1);
|
|
cy = y3 - anx*aRadius*(anticlockwise ? 1 : -1);
|
|
angle0 = atan2((y3-cy), (x3-cx));
|
|
angle1 = atan2((y4-cy), (x4-cx));
|
|
|
|
|
|
LineTo(x3, y3);
|
|
|
|
Arc(cx, cy, aRadius, angle0, angle1, anticlockwise, aError);
|
|
}
|
|
|
|
void
|
|
CanvasPath::Rect(double aX, double aY, double aW, double aH)
|
|
{
|
|
MoveTo(aX, aY);
|
|
LineTo(aX + aW, aY);
|
|
LineTo(aX + aW, aY + aH);
|
|
LineTo(aX, aY + aH);
|
|
ClosePath();
|
|
}
|
|
|
|
void
|
|
CanvasPath::Arc(double aX, double aY, double aRadius,
|
|
double aStartAngle, double aEndAngle, bool aAnticlockwise,
|
|
ErrorResult& aError)
|
|
{
|
|
if (aRadius < 0.0) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsurePathBuilder();
|
|
|
|
ArcToBezier(this, Point(aX, aY), Size(aRadius, aRadius), aStartAngle, aEndAngle, aAnticlockwise);
|
|
}
|
|
|
|
void
|
|
CanvasPath::Ellipse(double x, double y, double radiusX, double radiusY,
|
|
double rotation, double startAngle, double endAngle,
|
|
bool anticlockwise, ErrorResult& error)
|
|
{
|
|
if (radiusX < 0.0 || radiusY < 0.0) {
|
|
error.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return;
|
|
}
|
|
|
|
EnsurePathBuilder();
|
|
|
|
ArcToBezier(this, Point(x, y), Size(radiusX, radiusY), startAngle, endAngle,
|
|
anticlockwise, rotation);
|
|
}
|
|
|
|
void
|
|
CanvasPath::LineTo(const gfx::Point& aPoint)
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->LineTo(aPoint);
|
|
}
|
|
|
|
void
|
|
CanvasPath::BezierTo(const gfx::Point& aCP1,
|
|
const gfx::Point& aCP2,
|
|
const gfx::Point& aCP3)
|
|
{
|
|
EnsurePathBuilder();
|
|
|
|
mPathBuilder->BezierTo(aCP1, aCP2, aCP3);
|
|
}
|
|
|
|
void
|
|
CanvasPath::AddPath(CanvasPath& aCanvasPath, const DOMMatrix2DInit& aInit,
|
|
ErrorResult& aError)
|
|
|
|
{
|
|
RefPtr<gfx::Path> tempPath = aCanvasPath.GetPath(CanvasWindingRule::Nonzero,
|
|
gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget());
|
|
|
|
RefPtr<DOMMatrixReadOnly> matrix =
|
|
DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError);
|
|
if (aError.Failed()) {
|
|
return;
|
|
}
|
|
|
|
Matrix transform(*(matrix->GetInternal2D()));
|
|
|
|
if (!transform.IsFinite()) {
|
|
return;
|
|
}
|
|
|
|
if (!transform.IsIdentity()) {
|
|
RefPtr<PathBuilder> tempBuilder = tempPath->TransformedCopyToBuilder(transform, FillRule::FILL_WINDING);
|
|
tempPath = tempBuilder->Finish();
|
|
}
|
|
|
|
EnsurePathBuilder(); // in case a path is added to itself
|
|
tempPath->StreamToSink(mPathBuilder);
|
|
}
|
|
|
|
already_AddRefed<gfx::Path>
|
|
CanvasPath::GetPath(const CanvasWindingRule& aWinding, const DrawTarget* aTarget) const
|
|
{
|
|
FillRule fillRule = FillRule::FILL_WINDING;
|
|
if (aWinding == CanvasWindingRule::Evenodd) {
|
|
fillRule = FillRule::FILL_EVEN_ODD;
|
|
}
|
|
|
|
if (mPath &&
|
|
(mPath->GetBackendType() == aTarget->GetBackendType()) &&
|
|
(mPath->GetFillRule() == fillRule)) {
|
|
RefPtr<gfx::Path> path(mPath);
|
|
return path.forget();
|
|
}
|
|
|
|
if (!mPath) {
|
|
// if there is no path, there must be a pathbuilder
|
|
MOZ_ASSERT(mPathBuilder);
|
|
mPath = mPathBuilder->Finish();
|
|
if (!mPath) {
|
|
RefPtr<gfx::Path> path(mPath);
|
|
return path.forget();
|
|
}
|
|
|
|
mPathBuilder = nullptr;
|
|
}
|
|
|
|
// retarget our backend if we're used with a different backend
|
|
if (mPath->GetBackendType() != aTarget->GetBackendType()) {
|
|
RefPtr<PathBuilder> tmpPathBuilder = aTarget->CreatePathBuilder(fillRule);
|
|
mPath->StreamToSink(tmpPathBuilder);
|
|
mPath = tmpPathBuilder->Finish();
|
|
} else if (mPath->GetFillRule() != fillRule) {
|
|
RefPtr<PathBuilder> tmpPathBuilder = mPath->CopyToBuilder(fillRule);
|
|
mPath = tmpPathBuilder->Finish();
|
|
}
|
|
|
|
RefPtr<gfx::Path> path(mPath);
|
|
return path.forget();
|
|
}
|
|
|
|
void
|
|
CanvasPath::EnsurePathBuilder() const
|
|
{
|
|
if (mPathBuilder) {
|
|
return;
|
|
}
|
|
|
|
// if there is not pathbuilder, there must be a path
|
|
MOZ_ASSERT(mPath);
|
|
mPathBuilder = mPath->CopyToBuilder();
|
|
mPath = nullptr;
|
|
}
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|