зеркало из https://github.com/mozilla/gecko-dev.git
6158 строки
197 KiB
C++
6158 строки
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 "nsMathUtils.h"
|
|
|
|
#include "nsContentUtils.h"
|
|
|
|
#include "mozilla/intl/BidiEmbeddingLevel.h"
|
|
#include "mozilla/PresShell.h"
|
|
#include "mozilla/PresShellInlines.h"
|
|
#include "mozilla/SVGImageContext.h"
|
|
#include "mozilla/SVGObserverUtils.h"
|
|
#include "mozilla/dom/Document.h"
|
|
#include "mozilla/dom/FontFaceSetImpl.h"
|
|
#include "mozilla/dom/FontFaceSet.h"
|
|
#include "mozilla/dom/HTMLCanvasElement.h"
|
|
#include "mozilla/dom/GeneratePlaceholderCanvasData.h"
|
|
#include "nsPresContext.h"
|
|
|
|
#include "nsIInterfaceRequestorUtils.h"
|
|
#include "nsIFrame.h"
|
|
#include "nsError.h"
|
|
|
|
#include "nsCSSPseudoElements.h"
|
|
#include "nsComputedDOMStyle.h"
|
|
|
|
#include "nsPrintfCString.h"
|
|
|
|
#include "nsReadableUtils.h"
|
|
|
|
#include "nsColor.h"
|
|
#include "nsGfxCIID.h"
|
|
#include "nsIDocShell.h"
|
|
#include "nsPIDOMWindow.h"
|
|
#include "nsDisplayList.h"
|
|
#include "nsFocusManager.h"
|
|
|
|
#include "nsTArray.h"
|
|
|
|
#include "ImageEncoder.h"
|
|
#include "ImageRegion.h"
|
|
|
|
#include "gfxContext.h"
|
|
#include "gfxPlatform.h"
|
|
#include "gfxFont.h"
|
|
#include "gfxBlur.h"
|
|
#include "gfxTextRun.h"
|
|
#include "gfxUtils.h"
|
|
|
|
#include "nsFrameLoader.h"
|
|
#include "nsBidiPresUtils.h"
|
|
#include "Layers.h"
|
|
#include "LayerUserData.h"
|
|
#include "CanvasUtils.h"
|
|
#include "nsIMemoryReporter.h"
|
|
#include "nsStyleUtil.h"
|
|
#include "CanvasImageCache.h"
|
|
#include "DrawTargetWebgl.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "jsapi.h"
|
|
#include "jsfriendapi.h"
|
|
#include "js/Array.h" // JS::GetArrayLength
|
|
#include "js/Conversions.h"
|
|
#include "js/experimental/TypedData.h" // JS_NewUint8ClampedArray, JS_GetUint8ClampedArrayData
|
|
#include "js/HeapAPI.h"
|
|
#include "js/PropertyAndElement.h" // JS_GetElement
|
|
#include "js/Warnings.h" // JS::WarnASCII
|
|
|
|
#include "mozilla/Alignment.h"
|
|
#include "mozilla/Assertions.h"
|
|
#include "mozilla/CheckedInt.h"
|
|
#include "mozilla/DebugOnly.h"
|
|
#include "mozilla/dom/CanvasGradient.h"
|
|
#include "mozilla/dom/CanvasPattern.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/FilterInstance.h"
|
|
#include "mozilla/gfx/2D.h"
|
|
#include "mozilla/gfx/Tools.h"
|
|
#include "mozilla/gfx/PathHelpers.h"
|
|
#include "mozilla/gfx/DataSurfaceHelpers.h"
|
|
#include "mozilla/gfx/PatternHelpers.h"
|
|
#include "mozilla/gfx/Swizzle.h"
|
|
#include "mozilla/layers/PersistentBufferProvider.h"
|
|
#include "mozilla/MathAlgorithms.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/ServoBindings.h"
|
|
#include "mozilla/StaticPrefs_gfx.h"
|
|
#include "mozilla/Telemetry.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/TextMetrics.h"
|
|
#include "mozilla/FloatingPoint.h"
|
|
#include "nsGlobalWindow.h"
|
|
#include "nsDeviceContext.h"
|
|
#include "nsFontMetrics.h"
|
|
#include "nsLayoutUtils.h"
|
|
#include "Units.h"
|
|
#include "mozilla/CycleCollectedJSRuntime.h"
|
|
#include "mozilla/ServoCSSParser.h"
|
|
#include "mozilla/ServoStyleSet.h"
|
|
#include "mozilla/SVGContentUtils.h"
|
|
#include "mozilla/layers/CanvasClient.h"
|
|
#include "mozilla/layers/WebRenderUserData.h"
|
|
#include "mozilla/layers/WebRenderCanvasRenderer.h"
|
|
#include "WindowRenderer.h"
|
|
|
|
#undef free // apparently defined by some windows header, clashing with a
|
|
// free() method in SkTypes.h
|
|
|
|
#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::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 Atomic<int64_t> gCanvasAzureMemoryUsed(0);
|
|
|
|
// Adds Save() / Restore() calls to the scope.
|
|
class MOZ_RAII AutoSaveRestore {
|
|
public:
|
|
explicit AutoSaveRestore(CanvasRenderingContext2D* aCtx) : mCtx(aCtx) {
|
|
mCtx->Save();
|
|
}
|
|
~AutoSaveRestore() { mCtx->Restore(); }
|
|
|
|
private:
|
|
RefPtr<CanvasRenderingContext2D> mCtx;
|
|
};
|
|
|
|
// 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() = default;
|
|
|
|
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 CanvasConicGradient : public CanvasGradient {
|
|
public:
|
|
CanvasConicGradient(CanvasRenderingContext2D* aContext, Float aAngle,
|
|
const Point& aCenter)
|
|
: CanvasGradient(aContext, Type::CONIC),
|
|
mAngle(aAngle),
|
|
mCenter(aCenter) {}
|
|
|
|
const Float mAngle;
|
|
const Point mCenter;
|
|
};
|
|
|
|
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, bool* aIsColor) const {
|
|
const ContextState& state = CurrentState();
|
|
bool opaque = false;
|
|
bool color = false;
|
|
if (state.globalAlpha >= 1.0) {
|
|
if (state.patternStyles[aStyle] && state.patternStyles[aStyle]->mSurface) {
|
|
opaque = IsOpaque(state.patternStyles[aStyle]->mSurface->GetFormat());
|
|
} else if (!state.gradientStyles[aStyle]) {
|
|
// TODO: for gradient patterns we could check that all stops are opaque
|
|
// colors.
|
|
// it's a color pattern.
|
|
opaque = sRGBColor::FromABGR(state.colorStyles[aStyle]).a >= 1.0;
|
|
color = true;
|
|
}
|
|
}
|
|
if (aIsColor) {
|
|
*aIsColor = color;
|
|
}
|
|
return opaque;
|
|
}
|
|
|
|
// 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:
|
|
using Style = CanvasRenderingContext2D::Style;
|
|
using ContextState = CanvasRenderingContext2D::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.gradientStyles[aStyle] &&
|
|
state.gradientStyles[aStyle]->GetType() ==
|
|
CanvasGradient::Type::CONIC) {
|
|
auto gradient =
|
|
static_cast<CanvasConicGradient*>(state.gradientStyles[aStyle].get());
|
|
|
|
mPattern.InitConicGradientPattern(
|
|
gradient->mCenter, gradient->mAngle, 0, 1,
|
|
gradient->GetGradientStopsForTarget(aRT));
|
|
} else if (state.patternStyles[aStyle]) {
|
|
aCtx->DoSecurityCheck(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:
|
|
using ContextState = CanvasRenderingContext2D::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);
|
|
}
|
|
|
|
if (!mFinalTarget->CanCreateSimilarDrawTarget(mSourceGraphicRect.Size(),
|
|
SurfaceFormat::B8G8R8A8)) {
|
|
mTarget = mFinalTarget;
|
|
mCtx = nullptr;
|
|
mFinalTarget = nullptr;
|
|
return;
|
|
}
|
|
|
|
mTarget = mFinalTarget->CreateSimilarDrawTarget(mSourceGraphicRect.Size(),
|
|
SurfaceFormat::B8G8R8A8);
|
|
|
|
if (mTarget) {
|
|
// See bug 1524554.
|
|
mTarget->ClearRect(gfx::Rect());
|
|
}
|
|
|
|
if (!mTarget || !mTarget->IsValid()) {
|
|
// 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) {
|
|
// See bug 1524554.
|
|
dt->ClearRect(gfx::Rect());
|
|
}
|
|
|
|
if (!dt || !dt->IsValid()) {
|
|
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:
|
|
using ContextState = CanvasRenderingContext2D::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();
|
|
if (!bounds.ToIntRect(&mTempRect) ||
|
|
!mFinalTarget->CanCreateSimilarDrawTarget(mTempRect.Size(),
|
|
SurfaceFormat::B8G8R8A8)) {
|
|
mTarget = mFinalTarget;
|
|
mCtx = nullptr;
|
|
mFinalTarget = nullptr;
|
|
return;
|
|
}
|
|
|
|
mTarget = mFinalTarget->CreateShadowDrawTarget(
|
|
mTempRect.Size(), SurfaceFormat::B8G8R8A8, mSigma);
|
|
|
|
if (mTarget) {
|
|
// See bug 1524554.
|
|
mTarget->ClearRect(gfx::Rect());
|
|
}
|
|
|
|
if (!mTarget || !mTarget->IsValid()) {
|
|
// 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(),
|
|
ShadowOptions(ToDeviceColor(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:
|
|
using ContextState = CanvasRenderingContext2D::ContextState;
|
|
|
|
explicit AdjustedTarget(CanvasRenderingContext2D* aCtx,
|
|
const gfx::Rect* aBounds = nullptr,
|
|
bool aAllowOptimization = false)
|
|
: mCtx(aCtx),
|
|
mOptimizeShadow(false),
|
|
mUsedOperation(aCtx->CurrentState().op) {
|
|
// 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() || !boundsAfterFilter.IsFinite()) {
|
|
return;
|
|
}
|
|
|
|
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.
|
|
const bool applyFilter = aCtx->NeedToApplyFilter();
|
|
if (aCtx->NeedToDrawShadow()) {
|
|
if (aAllowOptimization && !applyFilter) {
|
|
// If only drawing a shadow and no filter, then avoid buffering to an
|
|
// intermediate target while drawing the shadow directly to the final
|
|
// target. When doing so, we want to use the actual composition op
|
|
// instead of OP_OVER.
|
|
mTarget = aCtx->mTarget;
|
|
if (mTarget && mTarget->IsValid()) {
|
|
mOptimizeShadow = true;
|
|
return;
|
|
}
|
|
}
|
|
mShadowTarget = MakeUnique<AdjustedTargetForShadow>(
|
|
aCtx, aCtx->mTarget, boundsAfterFilter, mUsedOperation);
|
|
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.
|
|
mUsedOperation = CompositionOp::OP_OVER;
|
|
}
|
|
|
|
// Now set up the filter draw target.
|
|
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), mUsedOperation);
|
|
mTarget = mFilterTarget->DT();
|
|
mUsedOperation = CompositionOp::OP_OVER;
|
|
}
|
|
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; }
|
|
|
|
CompositionOp UsedOperation() const { return mUsedOperation; }
|
|
|
|
ShadowOptions ShadowParams() const {
|
|
const ContextState& state = mCtx->CurrentState();
|
|
return ShadowOptions(ToDeviceColor(state.shadowColor), state.shadowOffset,
|
|
state.ShadowBlurSigma());
|
|
}
|
|
|
|
void Fill(const Path* aPath, const Pattern& aPattern,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
mTarget->DrawShadow(aPath, aPattern, ShadowParams(), aOptions);
|
|
}
|
|
mTarget->Fill(aPath, aPattern, aOptions);
|
|
}
|
|
|
|
void FillRect(const Rect& aRect, const Pattern& aPattern,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
RefPtr<Path> path = MakePathForRect(*mTarget, aRect);
|
|
mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions);
|
|
}
|
|
mTarget->FillRect(aRect, aPattern, aOptions);
|
|
}
|
|
|
|
void Stroke(const Path* aPath, const Pattern& aPattern,
|
|
const StrokeOptions& aStrokeOptions,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
mTarget->DrawShadow(aPath, aPattern, ShadowParams(), aOptions,
|
|
&aStrokeOptions);
|
|
}
|
|
mTarget->Stroke(aPath, aPattern, aStrokeOptions, aOptions);
|
|
}
|
|
|
|
void StrokeRect(const Rect& aRect, const Pattern& aPattern,
|
|
const StrokeOptions& aStrokeOptions,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
RefPtr<Path> path = MakePathForRect(*mTarget, aRect);
|
|
mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions,
|
|
&aStrokeOptions);
|
|
}
|
|
mTarget->StrokeRect(aRect, aPattern, aStrokeOptions, aOptions);
|
|
}
|
|
|
|
void StrokeLine(const Point& aStart, const Point& aEnd,
|
|
const Pattern& aPattern, const StrokeOptions& aStrokeOptions,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
RefPtr<PathBuilder> builder = mTarget->CreatePathBuilder();
|
|
builder->MoveTo(aStart);
|
|
builder->LineTo(aEnd);
|
|
RefPtr<Path> path = builder->Finish();
|
|
mTarget->DrawShadow(path, aPattern, ShadowParams(), aOptions,
|
|
&aStrokeOptions);
|
|
}
|
|
mTarget->StrokeLine(aStart, aEnd, aPattern, aStrokeOptions, aOptions);
|
|
}
|
|
|
|
void DrawSurface(SourceSurface* aSurface, const Rect& aDest,
|
|
const Rect& aSource, const DrawSurfaceOptions& aSurfOptions,
|
|
const DrawOptions& aOptions) {
|
|
if (mOptimizeShadow) {
|
|
RefPtr<Path> path = MakePathForRect(*mTarget, aSource);
|
|
ShadowOptions shadowParams(ShadowParams());
|
|
SurfacePattern pattern(aSurface, ExtendMode::CLAMP, Matrix(),
|
|
shadowParams.BlurRadius() > 1
|
|
? SamplingFilter::POINT
|
|
: aSurfOptions.mSamplingFilter);
|
|
Matrix matrix = Matrix::Scaling(aDest.width / aSource.width,
|
|
aDest.height / aSource.height);
|
|
matrix.PreTranslate(-aSource.x, -aSource.y);
|
|
matrix.PostTranslate(aDest.x, aDest.y);
|
|
AutoRestoreTransform autoRestoreTransform(mTarget);
|
|
mTarget->ConcatTransform(matrix);
|
|
mTarget->DrawShadow(path, pattern, shadowParams, aOptions);
|
|
}
|
|
mTarget->DrawSurface(aSurface, aDest, aSource, aSurfOptions, aOptions);
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
CanvasRenderingContext2D* mCtx;
|
|
bool mOptimizeShadow;
|
|
CompositionOp mUsedOperation;
|
|
RefPtr<DrawTarget> mTarget;
|
|
UniquePtr<AdjustedTargetForShadow> mShadowTarget;
|
|
UniquePtr<AdjustedTargetForFilter> mFilterTarget;
|
|
};
|
|
|
|
void CanvasPattern::SetTransform(const DOMMatrix2DInit& aInit,
|
|
ErrorResult& aError) {
|
|
RefPtr<DOMMatrixReadOnly> matrix =
|
|
DOMMatrixReadOnly::FromMatrix(GetParentObject(), aInit, aError);
|
|
if (aError.Failed()) {
|
|
return;
|
|
}
|
|
const auto* matrix2D = matrix->GetInternal2D();
|
|
if (!matrix2D->IsFinite()) {
|
|
return;
|
|
}
|
|
mTransform = Matrix(*matrix2D);
|
|
}
|
|
|
|
void CanvasGradient::AddColorStop(float aOffset, const nsACString& aColorstr,
|
|
ErrorResult& aRv) {
|
|
if (aOffset < 0.0 || aOffset > 1.0) {
|
|
return aRv.ThrowIndexSizeError("Offset out of 0-1.0 range");
|
|
}
|
|
|
|
PresShell* presShell = mContext ? mContext->GetPresShell() : nullptr;
|
|
ServoStyleSet* styleSet = presShell ? presShell->StyleSet() : nullptr;
|
|
|
|
nscolor color;
|
|
bool ok = ServoCSSParser::ComputeColor(styleSet, NS_RGB(0, 0, 0), aColorstr,
|
|
&color);
|
|
if (!ok) {
|
|
return aRv.ThrowSyntaxError("Invalid color");
|
|
}
|
|
|
|
GradientStop newStop;
|
|
|
|
newStop.offset = aOffset;
|
|
newStop.color = ToDeviceColor(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() = default;
|
|
|
|
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;
|
|
}
|
|
|
|
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.
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCanvasElement)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mOffscreenCanvas)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell)
|
|
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 autoSVGFiltersObserver =
|
|
tmp->mStyleStack[i].autoSVGFiltersObserver.get();
|
|
if (autoSVGFiltersObserver) {
|
|
// XXXjwatt: I don't think this call achieves anything. See the comment
|
|
// that documents this function.
|
|
SVGObserverUtils::DetachFromCanvasContext(autoSVGFiltersObserver);
|
|
}
|
|
ImplCycleCollectionUnlink(tmp->mStyleStack[i].autoSVGFiltersObserver);
|
|
}
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CanvasRenderingContext2D)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCanvasElement)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOffscreenCanvas)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell)
|
|
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].autoSVGFiltersObserver,
|
|
"RAII SVG Filters Observer");
|
|
}
|
|
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->HasKnownLiveWrapper()) {
|
|
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->HasKnownLiveWrapper();
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(CanvasRenderingContext2D)
|
|
return nsCCUncollectableMarker::sGeneration && tmp->HasKnownLiveWrapper();
|
|
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::ContextState::ContextState() = default;
|
|
|
|
CanvasRenderingContext2D::ContextState::ContextState(const ContextState& aOther)
|
|
: fontGroup(aOther.fontGroup),
|
|
fontLanguage(aOther.fontLanguage),
|
|
fontFont(aOther.fontFont),
|
|
gradientStyles(aOther.gradientStyles),
|
|
patternStyles(aOther.patternStyles),
|
|
colorStyles(aOther.colorStyles),
|
|
font(aOther.font),
|
|
textAlign(aOther.textAlign),
|
|
textBaseline(aOther.textBaseline),
|
|
textDirection(aOther.textDirection),
|
|
fontKerning(aOther.fontKerning),
|
|
shadowColor(aOther.shadowColor),
|
|
transform(aOther.transform),
|
|
shadowOffset(aOther.shadowOffset),
|
|
lineWidth(aOther.lineWidth),
|
|
miterLimit(aOther.miterLimit),
|
|
globalAlpha(aOther.globalAlpha),
|
|
shadowBlur(aOther.shadowBlur),
|
|
dash(aOther.dash.Clone()),
|
|
dashOffset(aOther.dashOffset),
|
|
op(aOther.op),
|
|
fillRule(aOther.fillRule),
|
|
lineCap(aOther.lineCap),
|
|
lineJoin(aOther.lineJoin),
|
|
filterString(aOther.filterString),
|
|
filterChain(aOther.filterChain),
|
|
autoSVGFiltersObserver(aOther.autoSVGFiltersObserver),
|
|
filter(aOther.filter),
|
|
filterAdditionalImages(aOther.filterAdditionalImages.Clone()),
|
|
filterSourceGraphicTainted(aOther.filterSourceGraphicTainted),
|
|
imageSmoothingEnabled(aOther.imageSmoothingEnabled),
|
|
fontExplicitLanguage(aOther.fontExplicitLanguage) {}
|
|
|
|
CanvasRenderingContext2D::ContextState::~ContextState() = default;
|
|
|
|
void CanvasRenderingContext2D::ContextState::SetColorStyle(Style aWhichStyle,
|
|
nscolor aColor) {
|
|
colorStyles[aWhichStyle] = aColor;
|
|
gradientStyles[aWhichStyle] = nullptr;
|
|
patternStyles[aWhichStyle] = nullptr;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::ContextState::SetPatternStyle(
|
|
Style aWhichStyle, CanvasPattern* aPat) {
|
|
gradientStyles[aWhichStyle] = nullptr;
|
|
patternStyles[aWhichStyle] = aPat;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::ContextState::SetGradientStyle(
|
|
Style aWhichStyle, CanvasGradient* aGrad) {
|
|
gradientStyles[aWhichStyle] = aGrad;
|
|
patternStyles[aWhichStyle] = nullptr;
|
|
}
|
|
|
|
/**
|
|
** CanvasRenderingContext2D impl
|
|
**/
|
|
|
|
// Initialize our static variables.
|
|
MOZ_THREAD_LOCAL(uintptr_t) CanvasRenderingContext2D::sNumLivingContexts;
|
|
MOZ_THREAD_LOCAL(DrawTarget*) CanvasRenderingContext2D::sErrorTarget;
|
|
|
|
CanvasRenderingContext2D::CanvasRenderingContext2D(
|
|
layers::LayersBackend aCompositorBackend)
|
|
: // these are the default values from the Canvas spec
|
|
mWidth(0),
|
|
mHeight(0),
|
|
mZero(false),
|
|
mOpaqueAttrValue(false),
|
|
mContextAttributesHasAlpha(true),
|
|
mOpaque(false),
|
|
mResetLayer(true),
|
|
mIPC(false),
|
|
mHasPendingStableStateCallback(false),
|
|
mIsEntireFrameInvalid(false),
|
|
mPredictManyRedrawCalls(false),
|
|
mFrameCaptureState(FrameCaptureState::CLEAN,
|
|
"CanvasRenderingContext2D::mFrameCaptureState"),
|
|
mPathTransformWillUpdate(false),
|
|
mInvalidateCount(0),
|
|
mWriteOnly(false) {
|
|
sNumLivingContexts.infallibleInit();
|
|
sErrorTarget.infallibleInit();
|
|
sNumLivingContexts.set(sNumLivingContexts.get() + 1);
|
|
}
|
|
|
|
CanvasRenderingContext2D::~CanvasRenderingContext2D() {
|
|
RemovePostRefreshObserver();
|
|
RemoveShutdownObserver();
|
|
Reset();
|
|
|
|
sNumLivingContexts.set(sNumLivingContexts.get() - 1);
|
|
if (sNumLivingContexts.get() == 0 && sErrorTarget.get()) {
|
|
RefPtr<DrawTarget> target = dont_AddRef(sErrorTarget.get());
|
|
sErrorTarget.set(nullptr);
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::Initialize() { AddShutdownObserver(); }
|
|
|
|
JSObject* CanvasRenderingContext2D::WrapObject(
|
|
JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
|
|
return CanvasRenderingContext2D_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::ParseColor(const nsACString& aString,
|
|
nscolor* aColor) {
|
|
Document* document = mCanvasElement ? mCanvasElement->OwnerDoc() : nullptr;
|
|
css::Loader* loader = document ? document->CSSLoader() : nullptr;
|
|
|
|
PresShell* presShell = GetPresShell();
|
|
ServoStyleSet* set = presShell ? presShell->StyleSet() : nullptr;
|
|
|
|
// First, try computing the color without handling currentcolor.
|
|
bool wasCurrentColor = false;
|
|
if (!ServoCSSParser::ComputeColor(set, NS_RGB(0, 0, 0), aString, aColor,
|
|
&wasCurrentColor, loader)) {
|
|
return false;
|
|
}
|
|
|
|
if (wasCurrentColor && mCanvasElement) {
|
|
// Otherwise, get the value of the color property, flushing style
|
|
// if necessary.
|
|
RefPtr<const ComputedStyle> canvasStyle =
|
|
nsComputedDOMStyle::GetComputedStyle(mCanvasElement);
|
|
if (canvasStyle) {
|
|
*aColor = canvasStyle->StyleText()->mColor.ToColor();
|
|
}
|
|
// Beware that the presShell could be gone here.
|
|
}
|
|
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;
|
|
|
|
// Since the target changes the backing texture will change, and this will
|
|
// no longer be valid.
|
|
mIsEntireFrameInvalid = false;
|
|
mPredictManyRedrawCalls = false;
|
|
mFrameCaptureState = FrameCaptureState::CLEAN;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::OnShutdown() {
|
|
mShutdownObserver = nullptr;
|
|
|
|
RefPtr<PersistentBufferProvider> provider = mBufferProvider;
|
|
|
|
Reset();
|
|
|
|
if (provider) {
|
|
provider->OnShutdown();
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::AddShutdownObserver() {
|
|
MOZ_ASSERT(!mShutdownObserver);
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
mShutdownObserver = new CanvasShutdownObserver(this);
|
|
nsContentUtils::RegisterShutdownObserver(mShutdownObserver);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::RemoveShutdownObserver() {
|
|
if (mShutdownObserver) {
|
|
mShutdownObserver->OnShutdown();
|
|
mShutdownObserver = nullptr;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetStyleFromString(const nsACString& aStr,
|
|
Style aWhichStyle) {
|
|
MOZ_ASSERT(!aStr.IsVoid());
|
|
|
|
nscolor color;
|
|
if (!ParseColor(aStr, &color)) {
|
|
return;
|
|
}
|
|
|
|
CurrentState().SetColorStyle(aWhichStyle, color);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::GetStyleAsUnion(
|
|
OwningUTF8StringOrCanvasGradientOrCanvasPattern& 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.SetAsUTF8String());
|
|
}
|
|
}
|
|
|
|
// static
|
|
void CanvasRenderingContext2D::StyleColorToString(const nscolor& aColor,
|
|
nsACString& aStr) {
|
|
aStr.Truncate();
|
|
// 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) {
|
|
aStr.AppendPrintf("#%02x%02x%02x", NS_GET_R(aColor), NS_GET_G(aColor),
|
|
NS_GET_B(aColor));
|
|
} else {
|
|
aStr.AppendPrintf("rgba(%d, %d, %d, ", NS_GET_R(aColor), NS_GET_G(aColor),
|
|
NS_GET_B(aColor));
|
|
aStr.AppendFloat(nsStyleUtil::ColorComponentToFloat(NS_GET_A(aColor)));
|
|
aStr.Append(')');
|
|
}
|
|
}
|
|
|
|
nsresult CanvasRenderingContext2D::Redraw() {
|
|
mFrameCaptureState = FrameCaptureState::DIRTY;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
return NS_OK;
|
|
}
|
|
|
|
mIsEntireFrameInvalid = true;
|
|
|
|
if (mCanvasElement) {
|
|
SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement);
|
|
mCanvasElement->InvalidateCanvasContent(nullptr);
|
|
} else if (mOffscreenCanvas) {
|
|
mOffscreenCanvas->QueueCommitToCompositor();
|
|
} else {
|
|
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::Redraw(const gfx::Rect& aR) {
|
|
mFrameCaptureState = FrameCaptureState::DIRTY;
|
|
|
|
++mInvalidateCount;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
return;
|
|
}
|
|
|
|
if (mPredictManyRedrawCalls || mInvalidateCount > kCanvasMaxInvalidateCount) {
|
|
Redraw();
|
|
return;
|
|
}
|
|
|
|
if (mCanvasElement) {
|
|
SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement);
|
|
mCanvasElement->InvalidateCanvasContent(&aR);
|
|
} else if (mOffscreenCanvas) {
|
|
mOffscreenCanvas->QueueCommitToCompositor();
|
|
} else {
|
|
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::DidRefresh() {}
|
|
|
|
void CanvasRenderingContext2D::RedrawUser(const gfxRect& aR) {
|
|
mFrameCaptureState = FrameCaptureState::DIRTY;
|
|
|
|
if (mIsEntireFrameInvalid) {
|
|
++mInvalidateCount;
|
|
return;
|
|
}
|
|
|
|
gfx::Rect newr = mTarget->GetTransform().TransformBounds(ToRect(aR));
|
|
Redraw(newr);
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::CopyBufferProvider(
|
|
PersistentBufferProvider& aOld, DrawTarget& aTarget, IntRect aCopyRect) {
|
|
// Borrowing the snapshot must be done after ReturnTarget.
|
|
RefPtr<SourceSurface> snapshot = aOld.BorrowSnapshot();
|
|
|
|
if (!snapshot) {
|
|
return false;
|
|
}
|
|
|
|
aTarget.CopySurface(snapshot, aCopyRect, IntPoint());
|
|
aOld.ReturnSnapshot(snapshot.forget());
|
|
return true;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::Demote() {}
|
|
|
|
void CanvasRenderingContext2D::ScheduleStableStateCallback() {
|
|
if (mHasPendingStableStateCallback) {
|
|
return;
|
|
}
|
|
mHasPendingStableStateCallback = true;
|
|
|
|
nsContentUtils::RunInStableState(
|
|
NewRunnableMethod("dom::CanvasRenderingContext2D::OnStableState", 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 (auto& style : mStyleStack) {
|
|
for (auto clipOrTransform = style.clipsAndTransforms.begin();
|
|
clipOrTransform != style.clipsAndTransforms.end(); clipOrTransform++) {
|
|
if (clipOrTransform->IsClip()) {
|
|
if (mClipsNeedConverting) {
|
|
// We have possibly changed backends, so we need to convert the clips
|
|
// in case they are no longer compatible with mTarget.
|
|
RefPtr<PathBuilder> pathBuilder = mTarget->CreatePathBuilder();
|
|
clipOrTransform->clip->StreamToSink(pathBuilder);
|
|
clipOrTransform->clip = pathBuilder->Finish();
|
|
}
|
|
mTarget->PushClip(clipOrTransform->clip);
|
|
} else {
|
|
mTarget->SetTransform(clipOrTransform->transform);
|
|
}
|
|
}
|
|
}
|
|
|
|
mClipsNeedConverting = false;
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::EnsureTarget(const gfx::Rect* aCoveredRect,
|
|
bool aWillClear) {
|
|
if (AlreadyShutDown()) {
|
|
gfxCriticalError() << "Attempt to render into a Canvas2d after shutdown.";
|
|
SetErrorState();
|
|
return false;
|
|
}
|
|
|
|
if (mTarget) {
|
|
return mTarget != sErrorTarget.get();
|
|
}
|
|
|
|
// Check that the dimensions are sane
|
|
if (mWidth > StaticPrefs::gfx_canvas_max_size() ||
|
|
mHeight > StaticPrefs::gfx_canvas_max_size() || mWidth < 0 ||
|
|
mHeight < 0) {
|
|
SetErrorState();
|
|
return false;
|
|
}
|
|
|
|
// 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 && !mBufferProvider->RequiresRefresh()) {
|
|
mTarget = mBufferProvider->BorrowDrawTarget(persistedRect);
|
|
if (mTarget && mTarget->IsValid()) {
|
|
if (!mBufferProvider->PreservesDrawingState()) {
|
|
RestoreClipsAndTransformToTarget();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
RefPtr<DrawTarget> newTarget;
|
|
RefPtr<PersistentBufferProvider> newProvider;
|
|
|
|
if (!TryAcceleratedTarget(newTarget, newProvider) &&
|
|
!TrySharedTarget(newTarget, newProvider) &&
|
|
!TryBasicTarget(newTarget, newProvider)) {
|
|
gfxCriticalError(
|
|
CriticalLog::DefaultOptions(Factory::ReasonableSurfaceSize(GetSize())))
|
|
<< "Failed borrow shared and basic targets.";
|
|
|
|
SetErrorState();
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(newTarget);
|
|
MOZ_ASSERT(newProvider);
|
|
|
|
bool needsClear = !canDiscardContent;
|
|
if (newTarget->GetBackendType() == gfx::BackendType::SKIA &&
|
|
(needsClear || !aWillClear)) {
|
|
// Skia expects the unused X channel to contains 0xFF 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.
|
|
newTarget->ClearRect(canvasRect);
|
|
needsClear = false;
|
|
}
|
|
|
|
// Try to copy data from the previous buffer provider if there is one.
|
|
if (!canDiscardContent && mBufferProvider &&
|
|
CopyBufferProvider(*mBufferProvider, *newTarget, persistedRect)) {
|
|
needsClear = false;
|
|
}
|
|
|
|
if (needsClear) {
|
|
newTarget->ClearRect(canvasRect);
|
|
}
|
|
|
|
mTarget = std::move(newTarget);
|
|
mBufferProvider = std::move(newProvider);
|
|
|
|
RegisterAllocation();
|
|
AddZoneWaitingForGC();
|
|
|
|
RestoreClipsAndTransformToTarget();
|
|
|
|
// Force a full layer transaction since we didn't have a layer before
|
|
// and now we might need one.
|
|
if (mCanvasElement) {
|
|
mCanvasElement->InvalidateCanvas();
|
|
}
|
|
// EnsureTarget hasn't drawn anything. Preserve mFrameCaptureState.
|
|
FrameCaptureState captureState = mFrameCaptureState;
|
|
// Calling Redraw() tells our invalidation machinery that the entire
|
|
// canvas is already invalid, which can speed up future drawing.
|
|
Redraw();
|
|
mFrameCaptureState = captureState;
|
|
|
|
return true;
|
|
}
|
|
|
|
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.get()) {
|
|
gCanvasAzureMemoryUsed -= mWidth * mHeight * 4;
|
|
}
|
|
|
|
mTarget = sErrorTarget.get();
|
|
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());
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::AddZoneWaitingForGC() {
|
|
JSObject* wrapper = GetWrapperPreserveColor();
|
|
if (wrapper) {
|
|
CycleCollectedJSRuntime::Get()->AddZoneWaitingForGC(
|
|
JS::GetObjectZone(wrapper));
|
|
}
|
|
}
|
|
|
|
static WindowRenderer* WindowRendererFromCanvasElement(
|
|
nsINode* aCanvasElement) {
|
|
if (!aCanvasElement) {
|
|
return nullptr;
|
|
}
|
|
|
|
return nsContentUtils::WindowRendererForDocument(aCanvasElement->OwnerDoc());
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::TryAcceleratedTarget(
|
|
RefPtr<gfx::DrawTarget>& aOutDT,
|
|
RefPtr<layers::PersistentBufferProvider>& aOutProvider) {
|
|
if (mBufferProvider && mBufferProvider->IsAccelerated() &&
|
|
mBufferProvider->RequiresRefresh()) {
|
|
// If there is already a provider and we got here, then the provider needs
|
|
// to be refreshed and we should avoid using acceleration in the future.
|
|
mAllowAcceleration = false;
|
|
}
|
|
if (!mAllowAcceleration) {
|
|
return false;
|
|
}
|
|
aOutDT = DrawTargetWebgl::Create(GetSize(), GetSurfaceFormat());
|
|
if (!aOutDT) {
|
|
return false;
|
|
}
|
|
|
|
aOutProvider = new PersistentBufferProviderAccelerated(aOutDT);
|
|
return true;
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::TrySharedTarget(
|
|
RefPtr<gfx::DrawTarget>& aOutDT,
|
|
RefPtr<layers::PersistentBufferProvider>& aOutProvider) {
|
|
aOutDT = nullptr;
|
|
aOutProvider = nullptr;
|
|
|
|
if (!mCanvasElement) {
|
|
return false;
|
|
}
|
|
|
|
if (mBufferProvider && mBufferProvider->IsShared()) {
|
|
// we are already using a shared buffer provider, we are allocating a new
|
|
// one because the current one failed so let's just fall back to the basic
|
|
// provider.
|
|
mClipsNeedConverting = true;
|
|
return false;
|
|
}
|
|
|
|
WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement);
|
|
|
|
if (!renderer) {
|
|
return false;
|
|
}
|
|
|
|
aOutProvider =
|
|
renderer->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;
|
|
}
|
|
|
|
// See Bug 1524554 - this forces DT initialization.
|
|
aOutDT->ClearRect(gfx::Rect());
|
|
|
|
if (!aOutDT->IsValid()) {
|
|
aOutDT = nullptr;
|
|
return false;
|
|
}
|
|
|
|
aOutProvider = new PersistentBufferProviderBasic(aOutDT);
|
|
return true;
|
|
}
|
|
|
|
Maybe<SurfaceDescriptor> CanvasRenderingContext2D::GetFrontBuffer(
|
|
WebGLFramebufferJS*, const bool webvr) {
|
|
if (mBufferProvider) {
|
|
return mBufferProvider->GetFrontBuffer();
|
|
}
|
|
return Nothing();
|
|
}
|
|
|
|
PresShell* CanvasRenderingContext2D::GetPresShell() {
|
|
if (mCanvasElement) {
|
|
return mCanvasElement->OwnerDoc()->GetPresShell();
|
|
}
|
|
if (mDocShell) {
|
|
return mDocShell->GetPresShell();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
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::AddAssociatedMemory() {
|
|
JSObject* wrapper = GetWrapperMaybeDead();
|
|
if (wrapper) {
|
|
JS::AddAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this),
|
|
JS::MemoryUse::DOMBinding);
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::RemoveAssociatedMemory() {
|
|
JSObject* wrapper = GetWrapperMaybeDead();
|
|
if (wrapper) {
|
|
JS::RemoveAssociatedMemory(wrapper, BindingJSObjectMallocBytes(this),
|
|
JS::MemoryUse::DOMBinding);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
// Update the memory size associated with the wrapper object when we change
|
|
// the dimensions. Note that we need to keep updating dying wrappers before
|
|
// they are finalized so that the memory accounting balances out.
|
|
RemoveAssociatedMemory();
|
|
mWidth = aWidth;
|
|
mHeight = aHeight;
|
|
AddAssociatedMemory();
|
|
}
|
|
|
|
if (mOffscreenCanvas) {
|
|
OffscreenCanvasDisplayData data;
|
|
data.mSize = {mWidth, mHeight};
|
|
data.mIsOpaque = mOpaque;
|
|
data.mIsAlphaPremult = true;
|
|
data.mDoPaintCallbacks = true;
|
|
mOffscreenCanvas->UpdateDisplayData(data);
|
|
}
|
|
|
|
if (!mCanvasElement || !mCanvasElement->IsInComposedDoc()) {
|
|
return;
|
|
}
|
|
|
|
// For vertical writing-mode, unless text-orientation is sideways,
|
|
// we'll modify the initial value of textBaseline to 'middle'.
|
|
RefPtr<const ComputedStyle> canvasStyle =
|
|
nsComputedDOMStyle::GetComputedStyle(mCanvasElement);
|
|
if (canvasStyle) {
|
|
WritingMode wm(canvasStyle);
|
|
if (wm.IsVertical() && !wm.IsSideways()) {
|
|
CurrentState().textBaseline = TextBaseline::MIDDLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::ReturnTarget(bool aForceReset) {
|
|
if (mTarget && mBufferProvider && mTarget != sErrorTarget.get()) {
|
|
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;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetOpaqueValueFromOpaqueAttr(
|
|
bool aOpaqueAttrValue) {
|
|
if (aOpaqueAttrValue != mOpaqueAttrValue) {
|
|
mOpaqueAttrValue = aOpaqueAttrValue;
|
|
UpdateIsOpaque();
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::UpdateIsOpaque() {
|
|
mOpaque = !mContextAttributesHasAlpha || mOpaqueAttrValue;
|
|
ClearTarget();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
mContextAttributesHasAlpha = attributes.mAlpha;
|
|
UpdateIsOpaque();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
UniquePtr<uint8_t[]> CanvasRenderingContext2D::GetImageBuffer(
|
|
int32_t* aFormat) {
|
|
UniquePtr<uint8_t[]> ret;
|
|
|
|
*aFormat = 0;
|
|
|
|
if (!mBufferProvider) {
|
|
if (!EnsureTarget()) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
RefPtr<SourceSurface> snapshot = mBufferProvider->BorrowSnapshot();
|
|
if (snapshot) {
|
|
RefPtr<DataSourceSurface> data = snapshot->GetDataSurface();
|
|
if (data && data->GetSize() == GetSize()) {
|
|
*aFormat = imgIEncoder::INPUT_FORMAT_HOSTARGB;
|
|
ret = SurfaceToPackedBGRA(data);
|
|
}
|
|
}
|
|
|
|
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
|
|
return ret;
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
CanvasRenderingContext2D::GetInputStream(const char* aMimeType,
|
|
const nsAString& 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;
|
|
}
|
|
|
|
return ImageEncoder::GetInputStream(mWidth, mHeight, imageBuffer.get(),
|
|
format, encoder, aEncoderOptions,
|
|
aStream);
|
|
}
|
|
|
|
already_AddRefed<mozilla::gfx::SourceSurface>
|
|
CanvasRenderingContext2D::GetSurfaceSnapshot(gfxAlphaType* aOutAlphaType) {
|
|
if (aOutAlphaType) {
|
|
*aOutAlphaType = (mOpaque ? gfxAlphaType::Opaque : gfxAlphaType::Premult);
|
|
}
|
|
|
|
// For GetSurfaceSnapshot we always call EnsureTarget even if mBufferProvider
|
|
// already exists, otherwise we get performance issues. See bug 1567054.
|
|
if (!EnsureTarget()) {
|
|
MOZ_ASSERT(
|
|
mTarget == sErrorTarget.get(),
|
|
"On EnsureTarget failure mTarget should be set to sErrorTarget.");
|
|
// In rare circumstances we may have failed to create an error target.
|
|
return mTarget ? mTarget->Snapshot() : nullptr;
|
|
}
|
|
|
|
// The concept of BorrowSnapshot seems a bit broken here, but the original
|
|
// code in GetSurfaceSnapshot just returned a snapshot from mTarget, which
|
|
// amounts to breaking the concept implicitly.
|
|
RefPtr<SourceSurface> snapshot = mBufferProvider->BorrowSnapshot();
|
|
RefPtr<SourceSurface> retSurface = snapshot;
|
|
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
return retSurface.forget();
|
|
}
|
|
|
|
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.RemoveLastElement();
|
|
|
|
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::GenericNaN();
|
|
ctm = Matrix(NaN, NaN, NaN, NaN, NaN, NaN);
|
|
}
|
|
|
|
MatrixToJSObject(aCx, ctm, aResult, aError);
|
|
}
|
|
|
|
//
|
|
// colors
|
|
//
|
|
|
|
void CanvasRenderingContext2D::SetStyleFromUnion(
|
|
const UTF8StringOrCanvasGradientOrCanvasPattern& aValue,
|
|
Style aWhichStyle) {
|
|
if (aValue.IsUTF8String()) {
|
|
SetStyleFromString(aValue.GetAsUTF8String(), aWhichStyle);
|
|
return;
|
|
}
|
|
|
|
if (aValue.IsCanvasGradient()) {
|
|
SetStyleFromGradient(aValue.GetAsCanvasGradient(), aWhichStyle);
|
|
return;
|
|
}
|
|
|
|
if (aValue.IsCanvasPattern()) {
|
|
CanvasPattern& pattern = aValue.GetAsCanvasPattern();
|
|
SetStyleFromPattern(pattern, aWhichStyle);
|
|
if (pattern.mForceWriteOnly) {
|
|
SetWriteOnly();
|
|
}
|
|
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.ThrowIndexSizeError("Negative radius");
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasGradient> grad = new CanvasRadialGradient(
|
|
this, Point(aX0, aY0), aR0, Point(aX1, aY1), aR1);
|
|
|
|
return grad.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasGradient> CanvasRenderingContext2D::CreateConicGradient(
|
|
double aAngle, double aCx, double aCy) {
|
|
return MakeAndAddRef<CanvasConicGradient>(this, aAngle, Point(aCx, aCy));
|
|
}
|
|
|
|
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.ThrowSyntaxError("Invalid pattern keyword");
|
|
return nullptr;
|
|
}
|
|
|
|
Element* element = nullptr;
|
|
OffscreenCanvas* offscreenCanvas = nullptr;
|
|
|
|
if (aSource.IsHTMLCanvasElement()) {
|
|
HTMLCanvasElement* canvas = &aSource.GetAsHTMLCanvasElement();
|
|
element = canvas;
|
|
|
|
nsIntSize size = canvas->GetSize();
|
|
if (size.width == 0) {
|
|
aError.ThrowInvalidStateError("Passed-in canvas has width 0");
|
|
return nullptr;
|
|
}
|
|
|
|
if (size.height == 0) {
|
|
aError.ThrowInvalidStateError("Passed-in canvas has height 0");
|
|
return nullptr;
|
|
}
|
|
|
|
// Special case for Canvas, which could be an Azure canvas!
|
|
nsICanvasRenderingContextInternal* srcCanvas = canvas->GetCurrentContext();
|
|
if (srcCanvas) {
|
|
// This might not be an Azure canvas!
|
|
RefPtr<SourceSurface> srcSurf = srcCanvas->GetSurfaceSnapshot();
|
|
if (!srcSurf) {
|
|
aError.ThrowInvalidStateError(
|
|
"CanvasRenderingContext2D.createPattern() failed to snapshot source"
|
|
"canvas.");
|
|
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();
|
|
element = img;
|
|
} else if (aSource.IsSVGImageElement()) {
|
|
SVGImageElement* img = &aSource.GetAsSVGImageElement();
|
|
element = img;
|
|
} else if (aSource.IsHTMLVideoElement()) {
|
|
auto& video = aSource.GetAsHTMLVideoElement();
|
|
video.LogVisibility(
|
|
mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_PATTERN);
|
|
element = &video;
|
|
} else if (aSource.IsOffscreenCanvas()) {
|
|
offscreenCanvas = &aSource.GetAsOffscreenCanvas();
|
|
|
|
nsIntSize size = offscreenCanvas->GetWidthHeight();
|
|
if (size.width == 0) {
|
|
aError.ThrowInvalidStateError("Passed-in canvas has width 0");
|
|
return nullptr;
|
|
}
|
|
|
|
if (size.height == 0) {
|
|
aError.ThrowInvalidStateError("Passed-in canvas has height 0");
|
|
return nullptr;
|
|
}
|
|
|
|
nsICanvasRenderingContextInternal* srcCanvas =
|
|
offscreenCanvas->GetContext();
|
|
if (srcCanvas) {
|
|
RefPtr<SourceSurface> srcSurf = srcCanvas->GetSurfaceSnapshot();
|
|
if (!srcSurf) {
|
|
aError.ThrowInvalidStateError(
|
|
"Passed-in canvas failed to create snapshot");
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasPattern> pat = new CanvasPattern(
|
|
this, srcSurf, repeatMode, srcCanvas->PrincipalOrNull(),
|
|
offscreenCanvas->IsWriteOnly(), false);
|
|
|
|
return pat.forget();
|
|
}
|
|
} 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) {
|
|
aError.ThrowInvalidStateError(
|
|
"Passed-in ImageBitmap has been transferred");
|
|
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
|
|
auto flags = nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE |
|
|
nsLayoutUtils::SFE_EXACT_SIZE_SURFACE;
|
|
SurfaceFromElementResult res =
|
|
offscreenCanvas
|
|
? nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, flags,
|
|
mTarget)
|
|
: nsLayoutUtils::SurfaceFromElement(element, flags, mTarget);
|
|
|
|
// Per spec, we should throw here for the HTMLImageElement and SVGImageElement
|
|
// cases if the image request state is "broken". In terms of the infromation
|
|
// in "res", the "broken" state corresponds to not having a size and not being
|
|
// still-loading (so there is no size forthcoming).
|
|
if (aSource.IsHTMLImageElement() || aSource.IsSVGImageElement()) {
|
|
if (!res.mIsStillLoading && !res.mHasSize) {
|
|
aError.ThrowInvalidStateError(
|
|
"Passed-in image's current request's state is \"broken\"");
|
|
return nullptr;
|
|
}
|
|
|
|
if (res.mSize.width == 0 || res.mSize.height == 0) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Is the "fully decodable" check already done in SurfaceFromElement? It's
|
|
// not clear how to do it from here, exactly.
|
|
}
|
|
|
|
RefPtr<SourceSurface> surface = res.GetSourceSurface();
|
|
if (!surface) {
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<CanvasPattern> pat =
|
|
new CanvasPattern(this, surface, repeatMode, res.mPrincipal,
|
|
res.mIsWriteOnly, res.mCORSUsed);
|
|
return pat.forget();
|
|
}
|
|
|
|
//
|
|
// shadows
|
|
//
|
|
void CanvasRenderingContext2D::SetShadowColor(const nsACString& aShadowColor) {
|
|
nscolor color;
|
|
if (!ParseColor(aShadowColor, &color)) {
|
|
return;
|
|
}
|
|
|
|
CurrentState().shadowColor = color;
|
|
}
|
|
|
|
//
|
|
// filters
|
|
//
|
|
|
|
static already_AddRefed<RawServoDeclarationBlock> CreateDeclarationForServo(
|
|
nsCSSPropertyID aProperty, const nsACString& aPropertyValue,
|
|
Document* aDocument) {
|
|
ServoCSSParser::ParsingEnvironment env{aDocument->DefaultStyleAttrURLData(),
|
|
aDocument->GetCompatibilityMode(),
|
|
aDocument->CSSLoader()};
|
|
RefPtr<RawServoDeclarationBlock> servoDeclarations =
|
|
ServoCSSParser::ParseProperty(aProperty, aPropertyValue, env);
|
|
|
|
if (!servoDeclarations) {
|
|
// We got a syntax error. The spec says this value must be ignored.
|
|
return nullptr;
|
|
}
|
|
|
|
// From canvas spec, force to set line-height property to 'normal' font
|
|
// property.
|
|
if (aProperty == eCSSProperty_font) {
|
|
const nsCString normalString = "normal"_ns;
|
|
Servo_DeclarationBlock_SetPropertyById(
|
|
servoDeclarations, eCSSProperty_line_height, &normalString, false,
|
|
env.mUrlExtraData, ParsingMode::Default, env.mCompatMode, env.mLoader,
|
|
env.mRuleType, {});
|
|
}
|
|
|
|
return servoDeclarations.forget();
|
|
}
|
|
|
|
static already_AddRefed<RawServoDeclarationBlock> CreateFontDeclarationForServo(
|
|
const nsACString& aFont, Document* aDocument) {
|
|
return CreateDeclarationForServo(eCSSProperty_font, aFont, aDocument);
|
|
}
|
|
|
|
static already_AddRefed<const ComputedStyle> GetFontStyleForServo(
|
|
Element* aElement, const nsACString& aFont, PresShell* aPresShell,
|
|
nsACString& aOutUsedFont, ErrorResult& aError) {
|
|
RefPtr<RawServoDeclarationBlock> declarations =
|
|
CreateFontDeclarationForServo(aFont, aPresShell->GetDocument());
|
|
if (!declarations) {
|
|
// 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 'none'.
|
|
if (Servo_DeclarationBlock_HasCSSWideKeyword(declarations,
|
|
eCSSProperty_font_size_adjust)) {
|
|
return nullptr;
|
|
}
|
|
|
|
ServoStyleSet* styleSet = aPresShell->StyleSet();
|
|
|
|
RefPtr<const ComputedStyle> parentStyle;
|
|
// have to get a parent ComputedStyle for inherit-like relative
|
|
// values (2em, bolder, etc.)
|
|
if (aElement && aElement->IsInComposedDoc()) {
|
|
parentStyle = nsComputedDOMStyle::GetComputedStyle(aElement);
|
|
if (!parentStyle) {
|
|
// The flush killed the shell, so we couldn't get any meaningful style
|
|
// back.
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
} else {
|
|
RefPtr<RawServoDeclarationBlock> declarations =
|
|
CreateFontDeclarationForServo("10px sans-serif"_ns,
|
|
aPresShell->GetDocument());
|
|
MOZ_ASSERT(declarations);
|
|
|
|
parentStyle =
|
|
aPresShell->StyleSet()->ResolveForDeclarations(nullptr, declarations);
|
|
}
|
|
|
|
MOZ_RELEASE_ASSERT(parentStyle, "Should have a valid parent style");
|
|
|
|
MOZ_ASSERT(!aPresShell->IsDestroying(),
|
|
"We should have returned an error above if the presshell is "
|
|
"being destroyed.");
|
|
|
|
RefPtr<const ComputedStyle> sc =
|
|
styleSet->ResolveForDeclarations(parentStyle, declarations);
|
|
|
|
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-font
|
|
// The font-size component must be converted to CSS px for reserialization,
|
|
// so we update the declarations with the value from the computed style.
|
|
if (!sc->StyleFont()->mFont.family.is_system_font) {
|
|
nsAutoCString computedFontSize;
|
|
sc->GetComputedPropertyValue(eCSSProperty_font_size, computedFontSize);
|
|
Servo_DeclarationBlock_SetPropertyById(
|
|
declarations, eCSSProperty_font_size, &computedFontSize, false, nullptr,
|
|
ParsingMode::Default, eCompatibility_FullStandards, nullptr,
|
|
StyleCssRuleType::Style, {});
|
|
}
|
|
|
|
// The font getter is required to be reserialized based on what we
|
|
// parsed (including having line-height removed).
|
|
Servo_SerializeFontValueForCanvas(declarations, &aOutUsedFont);
|
|
return sc.forget();
|
|
}
|
|
|
|
static already_AddRefed<RawServoDeclarationBlock>
|
|
CreateFilterDeclarationForServo(const nsACString& aFilter,
|
|
Document* aDocument) {
|
|
return CreateDeclarationForServo(eCSSProperty_filter, aFilter, aDocument);
|
|
}
|
|
|
|
static already_AddRefed<const ComputedStyle> ResolveFilterStyleForServo(
|
|
const nsACString& aFilterString, const ComputedStyle* aParentStyle,
|
|
PresShell* aPresShell, ErrorResult& aError) {
|
|
RefPtr<RawServoDeclarationBlock> declarations =
|
|
CreateFilterDeclarationForServo(aFilterString, aPresShell->GetDocument());
|
|
if (!declarations) {
|
|
// 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 (Servo_DeclarationBlock_HasCSSWideKeyword(declarations,
|
|
eCSSProperty_filter)) {
|
|
return nullptr;
|
|
}
|
|
|
|
ServoStyleSet* styleSet = aPresShell->StyleSet();
|
|
RefPtr<const ComputedStyle> computedValues =
|
|
styleSet->ResolveForDeclarations(aParentStyle, declarations);
|
|
|
|
return computedValues.forget();
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::ParseFilter(
|
|
const nsACString& aString, StyleOwnedSlice<StyleFilter>& aFilterChain,
|
|
ErrorResult& aError) {
|
|
RefPtr<PresShell> presShell = GetPresShell();
|
|
if (NS_WARN_IF(!presShell)) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return false;
|
|
}
|
|
|
|
nsAutoCString usedFont; // unused
|
|
|
|
RefPtr<const ComputedStyle> parentStyle = GetFontStyleForServo(
|
|
mCanvasElement, GetFont(), presShell, usedFont, aError);
|
|
if (!parentStyle) {
|
|
return false;
|
|
}
|
|
|
|
RefPtr<const ComputedStyle> style =
|
|
ResolveFilterStyleForServo(aString, parentStyle, presShell, aError);
|
|
if (!style) {
|
|
return false;
|
|
}
|
|
|
|
aFilterChain = style->StyleEffects()->mFilters;
|
|
return true;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetFilter(const nsACString& aFilter,
|
|
ErrorResult& aError) {
|
|
StyleOwnedSlice<StyleFilter> filterChain;
|
|
if (ParseFilter(aFilter, filterChain, aError)) {
|
|
CurrentState().filterString = aFilter;
|
|
CurrentState().filterChain = std::move(filterChain);
|
|
if (mCanvasElement) {
|
|
CurrentState().autoSVGFiltersObserver =
|
|
SVGObserverUtils::ObserveFiltersForCanvasContext(
|
|
this, mCanvasElement, CurrentState().filterChain.AsSpan());
|
|
UpdateFilter();
|
|
}
|
|
}
|
|
}
|
|
|
|
class CanvasUserSpaceMetrics : public UserSpaceMetricsWithSize {
|
|
public:
|
|
CanvasUserSpaceMetrics(const gfx::IntSize& aSize, const nsFont& aFont,
|
|
nsAtom* aFontLanguage, bool aExplicitLanguage,
|
|
nsPresContext* aPresContext)
|
|
: mSize(aSize),
|
|
mFont(aFont),
|
|
mFontLanguage(aFontLanguage),
|
|
mExplicitLanguage(aExplicitLanguage),
|
|
mPresContext(aPresContext) {}
|
|
|
|
virtual float GetEmLength() const override {
|
|
return mFont.size.ToCSSPixels();
|
|
}
|
|
|
|
virtual float GetExLength() const override {
|
|
nsFontMetrics::Params params;
|
|
params.language = mFontLanguage;
|
|
params.explicitLanguage = mExplicitLanguage;
|
|
params.textPerf = mPresContext->GetTextPerfMetrics();
|
|
params.featureValueLookup = mPresContext->GetFontFeatureValuesLookup();
|
|
RefPtr<nsFontMetrics> fontMetrics =
|
|
mPresContext->GetMetricsFor(mFont, params);
|
|
return NSAppUnitsToFloatPixels(fontMetrics->XHeight(),
|
|
AppUnitsPerCSSPixel());
|
|
}
|
|
|
|
virtual gfx::Size GetSize() const override { return Size(mSize); }
|
|
|
|
private:
|
|
gfx::IntSize mSize;
|
|
const nsFont& mFont;
|
|
nsAtom* mFontLanguage;
|
|
bool mExplicitLanguage;
|
|
nsPresContext* mPresContext;
|
|
};
|
|
|
|
// The filter might reference an SVG filter that is declared inside this
|
|
// document. Flush frames so that we'll have a SVGFilterFrame to work
|
|
// with.
|
|
static bool FiltersNeedFrameFlush(Span<const StyleFilter> aFilters) {
|
|
for (const auto& filter : aFilters) {
|
|
if (filter.IsUrl()) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::UpdateFilter() {
|
|
RefPtr<PresShell> 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;
|
|
}
|
|
|
|
if (FiltersNeedFrameFlush(CurrentState().filterChain.AsSpan())) {
|
|
presShell->FlushPendingNotifications(FlushType::Frames);
|
|
}
|
|
|
|
MOZ_RELEASE_ASSERT(!mStyleStack.IsEmpty());
|
|
if (MOZ_UNLIKELY(presShell->IsDestroying())) {
|
|
return;
|
|
}
|
|
|
|
const bool sourceGraphicIsTainted =
|
|
mCanvasElement && mCanvasElement->IsWriteOnly();
|
|
|
|
CurrentState().filter = FilterInstance::GetFilterDescription(
|
|
mCanvasElement, CurrentState().filterChain.AsSpan(),
|
|
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, true);
|
|
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]) {
|
|
auto& style = state->patternStyles[Style::FILL];
|
|
CanvasPattern::RepeatMode repeat = style->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;
|
|
if ((limitx || limity) && style->mTransform.IsRectilinear()) {
|
|
// For rectilinear transforms, we can just get the transformed pattern
|
|
// bounds and intersect them with the fill rectangle bounds.
|
|
// TODO: If the transform is not rectilinear, then we would need a fully
|
|
// general clip path to represent the X and Y clip planes bounding the
|
|
// pattern. For such cases, it would be more efficient to rely on Skia's
|
|
// Decal tiling mode rather than trying to generate a path. Until then,
|
|
// just punt to relying on the default Clamp mode.
|
|
gfx::Rect patternBounds(style->mSurface->GetRect());
|
|
patternBounds = style->mTransform.TransformBounds(patternBounds);
|
|
if (style->mTransform.HasNonAxisAlignedTransform()) {
|
|
// If there is an rotation (90 or 270 degrees), the X axis of the
|
|
// pattern projects onto the Y axis of the geometry, and vice versa.
|
|
std::swap(limitx, limity);
|
|
}
|
|
// We always need to execute painting for non-over operators, even if
|
|
// we end up with w/h = 0. The default Rect::Intersect can cause both
|
|
// dimensions to become empty if either dimension individually fails
|
|
// to overlap, which is unsuitable. Instead, we need to independently
|
|
// limit the supplied rectangle on each dimension as required.
|
|
if (limitx) {
|
|
double x2 = aX + aW;
|
|
aX = std::max(aX, double(patternBounds.x));
|
|
aW = std::max(std::min(x2, double(patternBounds.XMost())) - aX, 0.0);
|
|
}
|
|
if (limity) {
|
|
double y2 = aY + aH;
|
|
aY = std::max(aY, double(patternBounds.y));
|
|
aH = std::max(std::min(y2, double(patternBounds.YMost())) - aY, 0.0);
|
|
}
|
|
}
|
|
}
|
|
state = nullptr;
|
|
|
|
bool isColor;
|
|
bool discardContent = PatternIsOpaque(Style::FILL, &isColor) &&
|
|
(CurrentState().op == CompositionOp::OP_OVER ||
|
|
CurrentState().op == CompositionOp::OP_SOURCE);
|
|
const gfx::Rect fillRect(aX, aY, aW, aH);
|
|
EnsureTarget(discardContent ? &fillRect : nullptr, discardContent && isColor);
|
|
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, true);
|
|
CompositionOp op = target.UsedOperation();
|
|
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);
|
|
}
|
|
|
|
if (!IsTargetValid()) {
|
|
return;
|
|
}
|
|
|
|
if (!aH) {
|
|
CapStyle cap = CapStyle::BUTT;
|
|
if (CurrentState().lineJoin == JoinStyle::ROUND) {
|
|
cap = CapStyle::ROUND;
|
|
}
|
|
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true);
|
|
auto op = target.UsedOperation();
|
|
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, true);
|
|
auto op = target.UsedOperation();
|
|
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, true);
|
|
auto op = target.UsedOperation();
|
|
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, true);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = target.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, true);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = target.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, true);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = target.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, true);
|
|
if (!target) {
|
|
return;
|
|
}
|
|
|
|
auto op = target.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(Element& aElement) {
|
|
if (!aElement.State().HasState(ElementState::FOCUSRING)) {
|
|
return false;
|
|
}
|
|
|
|
HTMLCanvasElement* canvas = GetCanvas();
|
|
if (!canvas || !aElement.IsInclusiveDescendantOf(canvas)) {
|
|
return false;
|
|
}
|
|
|
|
EnsureUserSpacePath();
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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 nsACString& aFont,
|
|
ErrorResult& aError) {
|
|
SetFontInternal(aFont, aError);
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::SetFontInternal(const nsACString& aFont,
|
|
ErrorResult& aError) {
|
|
RefPtr<PresShell> presShell = GetPresShell();
|
|
if (!presShell) {
|
|
return SetFontInternalDisconnected(aFont, aError);
|
|
}
|
|
|
|
nsCString usedFont;
|
|
RefPtr<const ComputedStyle> sc =
|
|
GetFontStyleForServo(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.
|
|
// FIXME: Nobody initializes mAllowZoom for servo?
|
|
// 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.ScaledBy(1.0f / c->CSSToDevPixelScale().scale);
|
|
|
|
// Our FontKerning constants (see the enum definition) are the same as the
|
|
// NS_FONT_KERNING_* values so we can simply assign here.
|
|
resizedFont.kerning = uint8_t(CurrentState().fontKerning);
|
|
|
|
c->Document()->FlushUserFontSet();
|
|
|
|
nsFontMetrics::Params params;
|
|
params.language = fontStyle->mLanguage;
|
|
params.explicitLanguage = fontStyle->mExplicitLanguage;
|
|
params.userFontSet = c->GetUserFontSet();
|
|
params.textPerf = c->GetTextPerfMetrics();
|
|
RefPtr<nsFontMetrics> metrics = c->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;
|
|
}
|
|
|
|
static nsAutoCString FamilyListToString(
|
|
const StyleFontFamilyList& aFamilyList) {
|
|
return StringJoin(","_ns, aFamilyList.list.AsSpan(),
|
|
[](nsACString& dst, const StyleSingleFontFamily& name) {
|
|
name.AppendToString(dst);
|
|
});
|
|
}
|
|
|
|
static void SerializeFontForCanvas(const StyleFontFamilyList& aList,
|
|
const gfxFontStyle& aStyle,
|
|
nsACString& aUsedFont) {
|
|
// Re-serialize the font shorthand as required by the canvas spec.
|
|
aUsedFont.Truncate();
|
|
|
|
if (!aStyle.style.IsNormal()) {
|
|
aStyle.style.ToString(aUsedFont);
|
|
aUsedFont.Append(" ");
|
|
}
|
|
|
|
// font-weight is serialized as a number
|
|
if (!aStyle.weight.IsNormal()) {
|
|
aUsedFont.AppendFloat(aStyle.weight.ToFloat());
|
|
}
|
|
|
|
// font-stretch is serialized using CSS Fonts 3 keywords, not percentages.
|
|
if (!aStyle.stretch.IsNormal() &&
|
|
Servo_FontStretch_SerializeKeyword(&aStyle.stretch, &aUsedFont)) {
|
|
aUsedFont.Append(" ");
|
|
}
|
|
|
|
// Serialize the computed (not specified) size, and the family name(s).
|
|
aUsedFont.AppendFloat(aStyle.size);
|
|
aUsedFont.Append("px ");
|
|
aUsedFont.Append(FamilyListToString(aList));
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::SetFontInternalDisconnected(
|
|
const nsACString& aFont, ErrorResult& aError) {
|
|
FontFaceSet* fontFaceSet = nullptr;
|
|
if (mCanvasElement) {
|
|
fontFaceSet = mCanvasElement->OwnerDoc()->Fonts();
|
|
} else {
|
|
nsIGlobalObject* global = GetParentObject();
|
|
fontFaceSet = global ? global->Fonts() : nullptr;
|
|
}
|
|
|
|
FontFaceSetImpl* fontFaceSetImpl =
|
|
fontFaceSet ? fontFaceSet->GetImpl() : nullptr;
|
|
RefPtr<URLExtraData> urlExtraData =
|
|
fontFaceSetImpl ? fontFaceSetImpl->GetURLExtraData() : nullptr;
|
|
|
|
if (fontFaceSetImpl) {
|
|
fontFaceSetImpl->FlushUserFontSet();
|
|
}
|
|
|
|
// In the OffscreenCanvas case we don't have the context necessary to call
|
|
// GetFontStyleForServo(), as we do in the main-thread canvas context, so
|
|
// instead we borrow ParseFontShorthandForMatching to parse the attribute.
|
|
StyleComputedFontStyleDescriptor style(
|
|
StyleComputedFontStyleDescriptor::Normal());
|
|
StyleFontFamilyList list;
|
|
gfxFontStyle fontStyle;
|
|
float size = 0.0f;
|
|
if (!ServoCSSParser::ParseFontShorthandForMatching(
|
|
aFont, urlExtraData, list, fontStyle.style, fontStyle.stretch,
|
|
fontStyle.weight, &size)) {
|
|
return false;
|
|
}
|
|
|
|
fontStyle.size = size;
|
|
|
|
// Set the kerning feature, if required by the fontKerning attribute.
|
|
gfxFontFeature setting{TRUETYPE_TAG('k', 'e', 'r', 'n'), 0};
|
|
switch (CurrentState().fontKerning) {
|
|
case FontKerning::NONE:
|
|
setting.mValue = 0;
|
|
fontStyle.featureSettings.AppendElement(setting);
|
|
break;
|
|
case FontKerning::NORMAL:
|
|
setting.mValue = 1;
|
|
fontStyle.featureSettings.AppendElement(setting);
|
|
break;
|
|
default:
|
|
// auto case implies use user agent default
|
|
break;
|
|
}
|
|
|
|
// If we have a canvas element, get its lang (if known).
|
|
RefPtr<nsAtom> language;
|
|
bool explicitLanguage = false;
|
|
if (mCanvasElement) {
|
|
language = mCanvasElement->FragmentOrElement::GetLang();
|
|
if (language) {
|
|
explicitLanguage = true;
|
|
} else {
|
|
language = mCanvasElement->OwnerDoc()->GetLanguageForStyle();
|
|
}
|
|
}
|
|
// TODO: For workers, should we be passing a language? Where from?
|
|
|
|
// TODO: Cache fontGroups in the Worker (use an nsFontCache?)
|
|
gfxFontGroup* fontGroup = gfxPlatform::GetPlatform()->CreateFontGroup(
|
|
nullptr, // aPresContext
|
|
list, // aFontFamilyList
|
|
&fontStyle, // aStyle
|
|
language, // aLanguage
|
|
explicitLanguage, // aExplicitLanguage
|
|
nullptr, // aTextPerf
|
|
fontFaceSetImpl, // aUserFontSet
|
|
1.0); // aDevToCssSize
|
|
CurrentState().fontGroup = fontGroup;
|
|
SerializeFontForCanvas(list, fontStyle, CurrentState().font);
|
|
CurrentState().fontFont = nsFont(StyleFontFamily{list, false, false},
|
|
StyleCSSPixelLength::FromPixels(size));
|
|
CurrentState().fontLanguage = nullptr;
|
|
CurrentState().fontExplicitLanguage = false;
|
|
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;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetDirection(const nsAString& aDirection) {
|
|
if (aDirection.EqualsLiteral("ltr")) {
|
|
CurrentState().textDirection = TextDirection::LTR;
|
|
} else if (aDirection.EqualsLiteral("rtl")) {
|
|
CurrentState().textDirection = TextDirection::RTL;
|
|
} else if (aDirection.EqualsLiteral("inherit")) {
|
|
CurrentState().textDirection = TextDirection::INHERIT;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::GetDirection(nsAString& aDirection) {
|
|
switch (CurrentState().textDirection) {
|
|
case TextDirection::LTR:
|
|
aDirection.AssignLiteral("ltr");
|
|
break;
|
|
case TextDirection::RTL:
|
|
aDirection.AssignLiteral("rtl");
|
|
break;
|
|
case TextDirection::INHERIT:
|
|
aDirection.AssignLiteral("inherit");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::SetFontKerning(const nsAString& aFontKerning) {
|
|
auto oldValue = CurrentState().fontKerning;
|
|
if (aFontKerning.EqualsLiteral("auto")) {
|
|
CurrentState().fontKerning = FontKerning::AUTO;
|
|
} else if (aFontKerning.EqualsLiteral("normal")) {
|
|
CurrentState().fontKerning = FontKerning::NORMAL;
|
|
} else if (aFontKerning.EqualsLiteral("none")) {
|
|
CurrentState().fontKerning = FontKerning::NONE;
|
|
}
|
|
if (CurrentState().fontKerning != oldValue) {
|
|
CurrentState().fontGroup = nullptr;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::GetFontKerning(nsAString& aFontKerning) {
|
|
switch (CurrentState().fontKerning) {
|
|
case FontKerning::AUTO:
|
|
aFontKerning.AssignLiteral("auto");
|
|
break;
|
|
case FontKerning::NORMAL:
|
|
aFontKerning.AssignLiteral("normal");
|
|
break;
|
|
case FontKerning::NONE:
|
|
aFontKerning.AssignLiteral("none");
|
|
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(u"\x09\x0A\x0B\x0C\x0D", char16_t(' '));
|
|
}
|
|
|
|
void CanvasRenderingContext2D::FillText(const nsAString& aText, double aX,
|
|
double aY,
|
|
const Optional<double>& aMaxWidth,
|
|
ErrorResult& aError) {
|
|
DebugOnly<TextMetrics*> metrics = DrawOrMeasureText(
|
|
aText, aX, aY, aMaxWidth, TextDrawOperation::FILL, aError);
|
|
MOZ_ASSERT(!metrics); // drawing operation never returns TextMetrics
|
|
}
|
|
|
|
void CanvasRenderingContext2D::StrokeText(const nsAString& aText, double aX,
|
|
double aY,
|
|
const Optional<double>& aMaxWidth,
|
|
ErrorResult& aError) {
|
|
DebugOnly<TextMetrics*> metrics = DrawOrMeasureText(
|
|
aText, aX, aY, aMaxWidth, TextDrawOperation::STROKE, aError);
|
|
MOZ_ASSERT(!metrics); // drawing operation never returns TextMetrics
|
|
}
|
|
|
|
TextMetrics* CanvasRenderingContext2D::MeasureText(const nsAString& aRawText,
|
|
ErrorResult& aError) {
|
|
Optional<double> maxWidth;
|
|
return DrawOrMeasureText(aRawText, 0, 0, maxWidth, TextDrawOperation::MEASURE,
|
|
aError);
|
|
}
|
|
|
|
/**
|
|
* Used for nsBidiPresUtils::ProcessText
|
|
*/
|
|
struct MOZ_STACK_CLASS CanvasBidiProcessor final
|
|
: public nsBidiPresUtils::BidiProcessor {
|
|
using Style = CanvasRenderingContext2D::Style;
|
|
|
|
CanvasBidiProcessor()
|
|
: nsBidiPresUtils::BidiProcessor(),
|
|
mCtx(nullptr),
|
|
mFontgrp(nullptr),
|
|
mAppUnitsPerDevPixel(0),
|
|
mOp(CanvasRenderingContext2D::TextDrawOperation::FILL),
|
|
mTextRunFlags(),
|
|
mSetTextCount(0),
|
|
mDoMeasureBoundingBox(false),
|
|
mIgnoreSetText(false) {
|
|
if (StaticPrefs::gfx_missing_fonts_notify()) {
|
|
mMissingFonts = MakeUnique<gfxMissingFontRecorder>();
|
|
}
|
|
}
|
|
|
|
~CanvasBidiProcessor() {
|
|
// notify front-end code if we encountered missing glyphs in any script
|
|
if (mMissingFonts) {
|
|
mMissingFonts->Flush();
|
|
}
|
|
}
|
|
|
|
using ContextState = CanvasRenderingContext2D::ContextState;
|
|
|
|
void SetText(const char16_t* aText, int32_t aLength,
|
|
intl::BidiDirection aDirection) override {
|
|
if (mIgnoreSetText) {
|
|
// We've been told to ignore SetText because the processor is only ever
|
|
// handling a single, fixed string.
|
|
MOZ_ASSERT(mTextRun && mTextRun->GetLength() == uint32_t(aLength));
|
|
return;
|
|
}
|
|
mSetTextCount++;
|
|
auto* pfl = gfxPlatformFontList::PlatformFontList();
|
|
pfl->Lock();
|
|
mFontgrp->CheckForUpdatedPlatformList();
|
|
mFontgrp->UpdateUserFonts(); // ensure user font generation is current
|
|
// adjust flags for current direction run
|
|
gfx::ShapedTextFlags flags = mTextRunFlags;
|
|
if (aDirection == intl::BidiDirection::RTL) {
|
|
flags |= gfx::ShapedTextFlags::TEXT_IS_RTL;
|
|
} else {
|
|
flags &= ~gfx::ShapedTextFlags::TEXT_IS_RTL;
|
|
}
|
|
mTextRun = mFontgrp->MakeTextRun(
|
|
aText, aLength, mDrawTarget, mAppUnitsPerDevPixel, flags,
|
|
nsTextFrameUtils::Flags::DontSkipDrawingForPendingUserFonts,
|
|
mMissingFonts.get());
|
|
pfl->Unlock();
|
|
}
|
|
|
|
nscoord GetWidth() override {
|
|
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::CONIC: {
|
|
auto conic = static_cast<CanvasConicGradient*>(gradient);
|
|
pattern = new gfxPattern(conic->mCenter.x, conic->mCenter.y,
|
|
conic->mAngle, 0, 1);
|
|
break;
|
|
}
|
|
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, radial or conic 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, pat->mTransform);
|
|
pattern->SetExtend(CvtCanvasRepeatToGfxRepeat(pat->mRepeat));
|
|
return pattern.forget();
|
|
}
|
|
|
|
void DrawText(nscoord aXOffset, nscoord aWidth) override {
|
|
gfx::Point point = mPt;
|
|
bool rtl = mTextRun->IsRightToLeft();
|
|
bool verticalRun = mTextRun->IsVertical();
|
|
RefPtr<gfxPattern> pattern;
|
|
|
|
float& 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);
|
|
|
|
params.allowGDI = false;
|
|
|
|
const ContextState* state = &mCtx->CurrentState();
|
|
if (state->StyleIsColor(style)) { // Color
|
|
nscolor fontColor = state->colorStyles[style];
|
|
if (style == Style::FILL) {
|
|
params.context->SetColor(sRGBColor::FromABGR(fontColor));
|
|
} else {
|
|
params.textStrokeColor = fontColor;
|
|
}
|
|
} else {
|
|
if (state->gradientStyles[style]) { // Gradient
|
|
pattern = GetGradientFor(style);
|
|
} else if (state->patternStyles[style]) { // Pattern
|
|
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 = target.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
|
|
gfx::Point mPt;
|
|
|
|
// current font
|
|
gfxFontGroup* mFontgrp;
|
|
|
|
// to record any unsupported characters found in the text,
|
|
// and notify front-end if it is interested
|
|
UniquePtr<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
|
|
gfx::ShapedTextFlags mTextRunFlags;
|
|
|
|
// Count of how many times SetText has been called on this processor.
|
|
uint32_t mSetTextCount;
|
|
|
|
// true iff the bounding box should be measured
|
|
bool mDoMeasureBoundingBox;
|
|
|
|
// true if future SetText calls should be ignored
|
|
bool mIgnoreSetText;
|
|
};
|
|
|
|
TextMetrics* CanvasRenderingContext2D::DrawOrMeasureText(
|
|
const nsAString& aRawText, float aX, float aY,
|
|
const Optional<double>& aMaxWidth, TextDrawOperation aOp,
|
|
ErrorResult& aError) {
|
|
// Approximated baselines. In an ideal world, we'd read the baseline info
|
|
// directly from the font (where available). Alas we currently lack
|
|
// that functionality. These numbers are best guesses and should
|
|
// suffice for now. Both are fractions of the em ascent/descent from the
|
|
// alphabetic baseline.
|
|
const double kHangingBaselineDefault = 0.8; // fraction of ascent
|
|
const double kIdeographicBaselineDefault = 0.5; // fraction of descent
|
|
|
|
gfxFontGroup* currentFontStyle = GetCurrentFontStyle();
|
|
if (NS_WARN_IF(!currentFontStyle)) {
|
|
aError = NS_ERROR_FAILURE;
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<PresShell> presShell = GetPresShell();
|
|
Document* document = presShell ? presShell->GetDocument() : nullptr;
|
|
|
|
// 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();
|
|
}
|
|
|
|
RefPtr<const ComputedStyle> canvasStyle;
|
|
if (mCanvasElement && mCanvasElement->IsInComposedDoc()) {
|
|
// try to find the closest context
|
|
canvasStyle = nsComputedDOMStyle::GetComputedStyle(mCanvasElement);
|
|
if (!canvasStyle) {
|
|
aError = NS_ERROR_FAILURE;
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
// Get text direction, either from the property or inherited from context.
|
|
const ContextState& state = CurrentState();
|
|
bool isRTL;
|
|
switch (state.textDirection) {
|
|
case TextDirection::LTR:
|
|
isRTL = false;
|
|
break;
|
|
case TextDirection::RTL:
|
|
isRTL = true;
|
|
break;
|
|
case TextDirection::INHERIT:
|
|
if (canvasStyle) {
|
|
isRTL =
|
|
canvasStyle->StyleVisibility()->mDirection == StyleDirection::Rtl;
|
|
} else if (document) {
|
|
isRTL = GET_BIDI_OPTION_DIRECTION(document->GetBidiOptions()) ==
|
|
IBMBIDI_TEXTDIRECTION_RTL;
|
|
} else {
|
|
isRTL = false;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// This is only needed to know if we can know the drawing bounding box easily.
|
|
const bool doCalculateBounds = NeedToCalculateBounds();
|
|
if (presShell && presShell->IsDestroying()) {
|
|
aError = NS_ERROR_FAILURE;
|
|
return nullptr;
|
|
}
|
|
|
|
nsPresContext* presContext =
|
|
presShell ? presShell->GetPresContext() : nullptr;
|
|
|
|
if (presContext) {
|
|
// ensure user font set is up to date
|
|
presContext->Document()->FlushUserFontSet();
|
|
currentFontStyle->SetUserFontSet(presContext->GetUserFontSet());
|
|
}
|
|
|
|
if (currentFontStyle->GetStyle()->size == 0.0F) {
|
|
aError = NS_OK;
|
|
if (aOp == TextDrawOperation::MEASURE) {
|
|
return new TextMetrics(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
0.0, 0.0);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
if (!IsFinite(aX) || !IsFinite(aY)) {
|
|
aError = NS_OK;
|
|
// This may not be correct - what should TextMetrics contain in the case of
|
|
// infinite width or height?
|
|
if (aOp == TextDrawOperation::MEASURE) {
|
|
return new TextMetrics(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
|
|
0.0, 0.0);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
CanvasBidiProcessor processor;
|
|
|
|
// If we don't have a ComputedStyle, 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, presContext, canvasStyle->StyleFont(),
|
|
canvasStyle->StyleText(), 0)
|
|
: gfx::ShapedTextFlags();
|
|
|
|
GetAppUnitsValues(&processor.mAppUnitsPerDevPixel, nullptr);
|
|
processor.mPt = gfx::Point(aX, aY);
|
|
processor.mDrawTarget = gfxPlatform::ThreadLocalScreenReferenceDrawTarget();
|
|
|
|
// 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 ||
|
|
aOp == TextDrawOperation::MEASURE;
|
|
processor.mFontgrp = currentFontStyle;
|
|
|
|
nscoord totalWidthCoord;
|
|
|
|
processor.mFontgrp
|
|
->UpdateUserFonts(); // ensure user font generation is current
|
|
RefPtr<gfxFont> font = processor.mFontgrp->GetFirstValidFont();
|
|
const gfxFont::Metrics& fontMetrics =
|
|
font->GetMetrics(nsFontMetrics::eHorizontal);
|
|
|
|
// calls bidi algo twice since it needs the full text width and the
|
|
// bounding boxes before rendering anything
|
|
aError = nsBidiPresUtils::ProcessText(
|
|
textToDraw.get(), textToDraw.Length(),
|
|
isRTL ? intl::BidiEmbeddingLevel::RTL() : intl::BidiEmbeddingLevel::LTR(),
|
|
presContext, processor, nsBidiPresUtils::MODE_MEASURE, nullptr, 0,
|
|
&totalWidthCoord, &mBidiEngine);
|
|
if (aError.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
// If ProcessText only called SetText once, we're dealing with a single run,
|
|
// and so we don't need to repeat SetText and textRun construction at drawing
|
|
// time below; we can just re-use the existing textRun.
|
|
if (processor.mSetTextCount == 1) {
|
|
processor.mIgnoreSetText = true;
|
|
}
|
|
|
|
float totalWidth = float(totalWidthCoord) / processor.mAppUnitsPerDevPixel;
|
|
|
|
// offset pt.x based on text align
|
|
gfxFloat anchorX;
|
|
|
|
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;
|
|
}
|
|
|
|
float offsetX = anchorX * totalWidth;
|
|
processor.mPt.x -= offsetX;
|
|
|
|
// offset pt.y (or pt.x, for vertical text) based on text baseline
|
|
gfxFloat baselineAnchor;
|
|
|
|
switch (state.textBaseline) {
|
|
case TextBaseline::HANGING:
|
|
baselineAnchor = fontMetrics.emAscent * kHangingBaselineDefault;
|
|
break;
|
|
case TextBaseline::TOP:
|
|
baselineAnchor = fontMetrics.emAscent;
|
|
break;
|
|
case TextBaseline::MIDDLE:
|
|
baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f;
|
|
break;
|
|
case TextBaseline::ALPHABETIC:
|
|
baselineAnchor = 0;
|
|
break;
|
|
case TextBaseline::IDEOGRAPHIC:
|
|
baselineAnchor = -fontMetrics.emDescent * kIdeographicBaselineDefault;
|
|
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.
|
|
gfx::ShapedTextFlags runOrientation =
|
|
(processor.mTextRunFlags & gfx::ShapedTextFlags::TEXT_ORIENT_MASK);
|
|
if (runOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL) {
|
|
if (runOrientation == gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_MIXED ||
|
|
runOrientation == gfx::ShapedTextFlags::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;
|
|
}
|
|
|
|
// if only measuring, don't need to do any more work
|
|
if (aOp == TextDrawOperation::MEASURE) {
|
|
aError = NS_OK;
|
|
// Note that actualBoundingBoxLeft measures the distance in the leftward
|
|
// direction, so its sign is reversed from our usual physical coordinates.
|
|
double actualBoundingBoxLeft = offsetX - processor.mBoundingBox.X();
|
|
double actualBoundingBoxRight = processor.mBoundingBox.XMost() - offsetX;
|
|
double actualBoundingBoxAscent =
|
|
-processor.mBoundingBox.Y() - baselineAnchor;
|
|
double actualBoundingBoxDescent =
|
|
processor.mBoundingBox.YMost() + baselineAnchor;
|
|
double hangingBaseline =
|
|
fontMetrics.emAscent * kHangingBaselineDefault - baselineAnchor;
|
|
double ideographicBaseline =
|
|
-fontMetrics.emDescent * kIdeographicBaselineDefault - baselineAnchor;
|
|
return new TextMetrics(
|
|
totalWidth, actualBoundingBoxLeft, actualBoundingBoxRight,
|
|
fontMetrics.maxAscent - baselineAnchor, // fontBBAscent
|
|
fontMetrics.maxDescent + baselineAnchor, // fontBBDescent
|
|
actualBoundingBoxAscent, actualBoundingBoxDescent,
|
|
fontMetrics.emAscent - baselineAnchor, // emHeightAscent
|
|
-fontMetrics.emDescent - baselineAnchor, // emHeightDescent
|
|
hangingBaseline,
|
|
-baselineAnchor, // alphabeticBaseline
|
|
ideographicBaseline);
|
|
}
|
|
|
|
// If we did not actually calculate bounds, set up a simple bounding box
|
|
// based on the text position and advance.
|
|
if (!doCalculateBounds) {
|
|
processor.mBoundingBox.width = totalWidth;
|
|
processor.mBoundingBox.MoveBy(gfxPoint(processor.mPt.x, processor.mPt.y));
|
|
}
|
|
|
|
processor.mPt.x *= processor.mAppUnitsPerDevPixel;
|
|
processor.mPt.y *= processor.mAppUnitsPerDevPixel;
|
|
|
|
EnsureTarget();
|
|
if (!IsTargetValid()) {
|
|
aError = NS_ERROR_FAILURE;
|
|
return nullptr;
|
|
}
|
|
|
|
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;
|
|
|
|
aError = nsBidiPresUtils::ProcessText(
|
|
textToDraw.get(), textToDraw.Length(),
|
|
isRTL ? intl::BidiEmbeddingLevel::RTL() : intl::BidiEmbeddingLevel::LTR(),
|
|
presContext, processor, nsBidiPresUtils::MODE_DRAW, nullptr, 0, nullptr,
|
|
&mBidiEngine);
|
|
|
|
if (aError.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
mTarget->SetTransform(oldTransform);
|
|
|
|
if (aOp == CanvasRenderingContext2D::TextDrawOperation::FILL &&
|
|
!doCalculateBounds) {
|
|
RedrawUser(boundingBox);
|
|
} else {
|
|
Redraw();
|
|
}
|
|
|
|
aError = NS_OK;
|
|
return nullptr;
|
|
}
|
|
|
|
gfxFontGroup* CanvasRenderingContext2D::GetCurrentFontStyle() {
|
|
// Use lazy (re)initialization for the fontGroup since it's rather expensive.
|
|
|
|
RefPtr<PresShell> presShell = GetPresShell();
|
|
nsPresContext* presContext =
|
|
presShell ? presShell->GetPresContext() : nullptr;
|
|
|
|
// If we have a cached fontGroup, check that it is valid for the current
|
|
// prescontext; if not, we need to discard and re-create it.
|
|
RefPtr<gfxFontGroup>& fontGroup = CurrentState().fontGroup;
|
|
if (fontGroup) {
|
|
if (fontGroup->GetPresContext() != presContext) {
|
|
fontGroup = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!fontGroup) {
|
|
ErrorResult err;
|
|
constexpr auto kDefaultFontStyle = "10px sans-serif"_ns;
|
|
const float kDefaultFontSize = 10.0;
|
|
// If the font has already been set, we're re-creating the fontGroup
|
|
// and should re-use the existing font attribute; if not, we initialize
|
|
// it to the canvas default.
|
|
const nsCString& currentFont = CurrentState().font;
|
|
bool fontUpdated = SetFontInternal(
|
|
currentFont.IsEmpty() ? kDefaultFontStyle : currentFont, err);
|
|
if (err.Failed() || !fontUpdated) {
|
|
err.SuppressException();
|
|
// XXX Should we get a default lang from the prescontext or something?
|
|
nsAtom* language = nsGkAtoms::x_western;
|
|
bool explicitLanguage = false;
|
|
gfxFontStyle style;
|
|
style.size = kDefaultFontSize;
|
|
int32_t perDevPixel, perCSSPixel;
|
|
GetAppUnitsValues(&perDevPixel, &perCSSPixel);
|
|
gfxFloat devToCssSize = gfxFloat(perDevPixel) / gfxFloat(perCSSPixel);
|
|
const auto* sans =
|
|
Servo_FontFamily_Generic(StyleGenericFontFamily::SansSerif);
|
|
fontGroup = gfxPlatform::GetPlatform()->CreateFontGroup(
|
|
presContext, sans->families, &style, language, explicitLanguage,
|
|
presContext ? presContext->GetTextPerfMetrics() : nullptr, nullptr,
|
|
devToCssSize);
|
|
if (fontGroup) {
|
|
CurrentState().font = kDefaultFontStyle;
|
|
} else {
|
|
NS_ERROR("Default canvas font is invalid");
|
|
}
|
|
}
|
|
} else {
|
|
// The fontgroup needs to check if its cached families/faces are valid.
|
|
fontGroup->CheckForUpdatedPlatformList();
|
|
}
|
|
|
|
return 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 = std::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(JSContext* aCx, double aX,
|
|
double aY,
|
|
const CanvasWindingRule& aWinding,
|
|
nsIPrincipal& aSubjectPrincipal) {
|
|
return IsPointInPath(aCx, aX, aY, aWinding, Some(&aSubjectPrincipal));
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInPath(
|
|
JSContext* aCx, double aX, double aY, const CanvasWindingRule& aWinding,
|
|
Maybe<nsIPrincipal*> aSubjectPrincipal) {
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
// Check for site-specific permission and return false if no permission.
|
|
if (mCanvasElement) {
|
|
nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc();
|
|
if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
|
|
aSubjectPrincipal)) {
|
|
return false;
|
|
}
|
|
} else if (mOffscreenCanvas &&
|
|
mOffscreenCanvas->ShouldResistFingerprinting()) {
|
|
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(JSContext* aCx,
|
|
const CanvasPath& aPath, double aX,
|
|
double aY,
|
|
const CanvasWindingRule& aWinding,
|
|
nsIPrincipal& aSubjectPrincipal) {
|
|
return IsPointInPath(aCx, aPath, aX, aY, aWinding, Some(&aSubjectPrincipal));
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInPath(JSContext* aCx,
|
|
const CanvasPath& aPath, double aX,
|
|
double aY,
|
|
const CanvasWindingRule& aWinding,
|
|
Maybe<nsIPrincipal*>) {
|
|
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(
|
|
JSContext* aCx, double aX, double aY, nsIPrincipal& aSubjectPrincipal) {
|
|
return IsPointInStroke(aCx, aX, aY, Some(&aSubjectPrincipal));
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInStroke(
|
|
JSContext* aCx, double aX, double aY,
|
|
Maybe<nsIPrincipal*> aSubjectPrincipal) {
|
|
if (!FloatValidate(aX, aY)) {
|
|
return false;
|
|
}
|
|
|
|
// Check for site-specific permission and return false if no permission.
|
|
if (mCanvasElement) {
|
|
nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc();
|
|
if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
|
|
aSubjectPrincipal)) {
|
|
return false;
|
|
}
|
|
} else if (mOffscreenCanvas &&
|
|
mOffscreenCanvas->ShouldResistFingerprinting()) {
|
|
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(
|
|
JSContext* aCx, const CanvasPath& aPath, double aX, double aY,
|
|
nsIPrincipal& aSubjectPrincipal) {
|
|
return IsPointInStroke(aCx, aPath, aX, aY, Some(&aSubjectPrincipal));
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::IsPointInStroke(JSContext* aCx,
|
|
const CanvasPath& aPath,
|
|
double aX, double aY,
|
|
Maybe<nsIPrincipal*>) {
|
|
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) {
|
|
// See bug 1524554.
|
|
subrectDT->ClearRect(gfx::Rect());
|
|
}
|
|
|
|
if (!subrectDT || !subrectDT->IsValid()) {
|
|
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.
|
|
SurfaceFromElementResult CanvasRenderingContext2D::CachedSurfaceFromElement(
|
|
Element* aElement) {
|
|
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;
|
|
}
|
|
|
|
if (NS_FAILED(imgRequest->GetHadCrossOriginRedirects(
|
|
&res.mHadCrossOriginRedirects))) {
|
|
return res;
|
|
}
|
|
|
|
res.mSourceSurface = CanvasImageCache::LookupAllCanvas(aElement);
|
|
if (!res.mSourceSurface) {
|
|
return res;
|
|
}
|
|
|
|
int32_t corsmode = CORS_NONE;
|
|
if (NS_SUCCEEDED(imgRequest->GetCORSMode(&corsmode))) {
|
|
res.mCORSUsed = corsmode != CORS_NONE;
|
|
}
|
|
|
|
res.mSize = res.mIntrinsicSize = res.mSourceSurface->GetSize();
|
|
res.mPrincipal = std::move(principal);
|
|
res.mImageRequest = std::move(imgRequest);
|
|
res.mIsWriteOnly = CheckWriteOnlySecurity(res.mCORSUsed, res.mPrincipal,
|
|
res.mHadCrossOriginRedirects);
|
|
|
|
return res;
|
|
}
|
|
|
|
static void SwapScaleWidthHeightForRotation(gfx::Rect& aRect,
|
|
VideoInfo::Rotation aDegrees) {
|
|
if (aDegrees == VideoInfo::Rotation::kDegree_90 ||
|
|
aDegrees == VideoInfo::Rotation::kDegree_270) {
|
|
std::swap(aRect.width, aRect.height);
|
|
}
|
|
}
|
|
|
|
static Matrix ComputeRotationMatrix(gfxFloat aRotatedWidth,
|
|
gfxFloat aRotatedHeight,
|
|
VideoInfo::Rotation aDegrees) {
|
|
Matrix shiftVideoCenterToOrigin;
|
|
if (aDegrees == VideoInfo::Rotation::kDegree_90 ||
|
|
aDegrees == VideoInfo::Rotation::kDegree_270) {
|
|
shiftVideoCenterToOrigin =
|
|
Matrix::Translation(-aRotatedHeight / 2.0, -aRotatedWidth / 2.0);
|
|
} else {
|
|
shiftVideoCenterToOrigin =
|
|
Matrix::Translation(-aRotatedWidth / 2.0, -aRotatedHeight / 2.0);
|
|
}
|
|
|
|
Matrix rotation = Matrix::Rotation(gfx::Float(aDegrees / 180.0 * M_PI));
|
|
Matrix shiftLeftTopToOrigin =
|
|
Matrix::Translation(aRotatedWidth / 2.0, aRotatedHeight / 2.0);
|
|
return shiftVideoCenterToOrigin * rotation * shiftLeftTopToOrigin;
|
|
}
|
|
|
|
// 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) {
|
|
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;
|
|
gfx::IntSize intrinsicImgSize;
|
|
Element* element = nullptr;
|
|
OffscreenCanvas* offscreenCanvas = 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) {
|
|
return aError.ThrowInvalidStateError("Passed-in canvas is empty");
|
|
}
|
|
|
|
if (canvas->IsWriteOnly()) {
|
|
SetWriteOnly();
|
|
}
|
|
} else if (aImage.IsOffscreenCanvas()) {
|
|
offscreenCanvas = &aImage.GetAsOffscreenCanvas();
|
|
nsIntSize size = offscreenCanvas->GetWidthHeight();
|
|
if (size.IsEmpty()) {
|
|
return aError.ThrowInvalidStateError("Passed-in canvas is empty");
|
|
}
|
|
|
|
srcSurf = offscreenCanvas->GetSurfaceSnapshot();
|
|
if (srcSurf) {
|
|
imgSize = intrinsicImgSize = srcSurf->GetSize();
|
|
}
|
|
|
|
if (offscreenCanvas->IsWriteOnly()) {
|
|
SetWriteOnly();
|
|
}
|
|
} else if (aImage.IsImageBitmap()) {
|
|
ImageBitmap& imageBitmap = aImage.GetAsImageBitmap();
|
|
srcSurf = imageBitmap.PrepareForDrawTarget(mTarget);
|
|
|
|
if (!srcSurf) {
|
|
if (imageBitmap.IsClosed()) {
|
|
aError.ThrowInvalidStateError("Passed-in ImageBitmap is closed");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (imageBitmap.IsWriteOnly()) {
|
|
SetWriteOnly();
|
|
}
|
|
|
|
imgSize = intrinsicImgSize =
|
|
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->LogVisibility(
|
|
mozilla::dom::HTMLVideoElement::CallerAPI::DRAW_IMAGE);
|
|
element = video;
|
|
}
|
|
|
|
srcSurf = CanvasImageCache::LookupCanvas(element, mCanvasElement, &imgSize,
|
|
&intrinsicImgSize);
|
|
}
|
|
|
|
DirectDrawInfo drawInfo;
|
|
|
|
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_IF_IMAGE |
|
|
nsLayoutUtils::SFE_NO_RASTERIZING_VECTORS |
|
|
nsLayoutUtils::SFE_EXACT_SIZE_SURFACE;
|
|
|
|
SurfaceFromElementResult res;
|
|
if (offscreenCanvas) {
|
|
res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, sfeFlags,
|
|
mTarget);
|
|
} else {
|
|
res = CanvasRenderingContext2D::CachedSurfaceFromElement(element);
|
|
if (!res.mSourceSurface) {
|
|
res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget);
|
|
}
|
|
}
|
|
|
|
if (!res.mSourceSurface && !res.mDrawInfo.mImgContainer) {
|
|
// https://html.spec.whatwg.org/#check-the-usability-of-the-image-argument:
|
|
//
|
|
// Only throw if the request is broken and the element is an
|
|
// HTMLImageElement / SVGImageElement. Note that even for those 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 &&
|
|
(aImage.IsHTMLImageElement() || aImage.IsSVGImageElement())) {
|
|
aError.ThrowInvalidStateError("Passed-in image is \"broken\"");
|
|
}
|
|
return;
|
|
}
|
|
|
|
imgSize = res.mSize;
|
|
intrinsicImgSize = res.mIntrinsicSize;
|
|
DoSecurityCheck(res.mPrincipal, res.mIsWriteOnly, res.mCORSUsed);
|
|
|
|
if (res.mSourceSurface) {
|
|
if (res.mImageRequest) {
|
|
CanvasImageCache::NotifyDrawImage(element, mCanvasElement,
|
|
res.mSourceSurface, imgSize,
|
|
intrinsicImgSize);
|
|
}
|
|
srcSurf = res.mSourceSurface;
|
|
} else {
|
|
drawInfo = res.mDrawInfo;
|
|
}
|
|
}
|
|
|
|
if (aOptional_argc == 0) {
|
|
aSx = aSy = 0.0;
|
|
aSw = (double)imgSize.width;
|
|
aSh = (double)imgSize.height;
|
|
aDw = (double)intrinsicImgSize.width;
|
|
aDh = (double)intrinsicImgSize.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) {
|
|
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;
|
|
}
|
|
|
|
// Per spec, the smoothing setting applies only to scaling up a bitmap image.
|
|
// When down-scaling the user agent is free to choose whether or not to smooth
|
|
// the image. Nearest sampling when down-scaling is rarely desirable and
|
|
// smoothing when down-scaling matches chromium's behavior.
|
|
// If any dimension is up-scaled, we consider the image as being up-scaled.
|
|
auto scale = mTarget->GetTransform().ScaleFactors();
|
|
bool isDownScale =
|
|
aDw * Abs(scale.xScale) < aSw && aDh * Abs(scale.yScale) < aSh;
|
|
|
|
SamplingFilter samplingFilter;
|
|
AntialiasMode antialiasMode;
|
|
|
|
if (CurrentState().imageSmoothingEnabled || isDownScale) {
|
|
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, true);
|
|
if (!tempTarget) {
|
|
gfxWarning() << "Invalid adjusted target in Canvas2D "
|
|
<< gfx::hexa((DrawTarget*)mTarget) << ", "
|
|
<< NeedToDrawShadow() << NeedToApplyFilter();
|
|
return;
|
|
}
|
|
|
|
auto op = tempTarget.UsedOperation();
|
|
if (!IsTargetValid() || !tempTarget) {
|
|
return;
|
|
}
|
|
|
|
VideoInfo::Rotation rotationDeg = VideoInfo::Rotation::kDegree_0;
|
|
if (HTMLVideoElement* video = HTMLVideoElement::FromNodeOrNull(element)) {
|
|
rotationDeg = video->RotationDegrees();
|
|
}
|
|
|
|
gfx::Rect destRect(aDx, aDy, aDw, aDh);
|
|
|
|
Matrix transform;
|
|
if (rotationDeg != VideoInfo::Rotation::kDegree_0) {
|
|
Matrix preTransform = ComputeRotationMatrix(aDw, aDh, rotationDeg);
|
|
transform = preTransform * Matrix::Translation(aDx, aDy);
|
|
|
|
SwapScaleWidthHeightForRotation(destRect, rotationDeg);
|
|
// When rotation exists, aDx, aDy is handled by transform, Since aDest.x
|
|
// aDest.y handling of DrawSurface() does not care about the rotation.
|
|
destRect.x = 0;
|
|
destRect.y = 0;
|
|
}
|
|
Matrix currentTransform = tempTarget->GetTransform();
|
|
transform *= currentTransform;
|
|
|
|
tempTarget->SetTransform(transform);
|
|
|
|
tempTarget.DrawSurface(
|
|
srcSurf, destRect, sourceRect,
|
|
DrawSurfaceOptions(samplingFilter, SamplingBounds::UNBOUNDED),
|
|
DrawOptions(CurrentState().globalAlpha, op, antialiasMode));
|
|
|
|
tempTarget->SetTransform(currentTransform);
|
|
|
|
} 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 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 = ThebesMatrix(matrix);
|
|
MatrixScalesDouble contextScale = contextMatrix.ScaleFactors();
|
|
|
|
// Scale the dest rect to include the context scale.
|
|
aDest.Scale((float)contextScale.xScale, (float)contextScale.yScale);
|
|
|
|
// Scale the image size to the dest rect, and adjust the source rect to match.
|
|
MatrixScalesDouble scale(aDest.width / aSrc.width,
|
|
aDest.height / aSrc.height);
|
|
IntSize scaledImageSize =
|
|
IntSize::Ceil(static_cast<float>(scale.xScale * aImgSize.width),
|
|
static_cast<float>(scale.yScale * aImgSize.height));
|
|
aSrc.Scale(static_cast<float>(scale.xScale),
|
|
static_cast<float>(scale.yScale));
|
|
|
|
// 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->SetMatrixDouble(
|
|
contextMatrix
|
|
.PreScale(1.0 / contextScale.xScale, 1.0 / contextScale.yScale)
|
|
.PreTranslate(aDest.x - aSrc.x, aDest.y - aSrc.y));
|
|
|
|
context->SetOp(tempTarget.UsedOperation());
|
|
|
|
// FLAG_CLAMP is added for increased performance, since we never tile here.
|
|
uint32_t modifiedFlags = aImage.mDrawingFlags | imgIContainer::FLAG_CLAMP;
|
|
|
|
// XXX hmm is scaledImageSize really in CSS pixels?
|
|
CSSIntSize sz(scaledImageSize.width, scaledImageSize.height);
|
|
SVGImageContext svgContext(Some(sz));
|
|
|
|
auto result = aImage.mImgContainer->Draw(
|
|
context, scaledImageSize,
|
|
ImageRegion::Create(gfxRect(aSrc.x, aSrc.y, aSrc.width, aSrc.height)),
|
|
aImage.mWhichFrame, SamplingFilter::GOOD, svgContext, modifiedFlags,
|
|
CurrentState().globalAlpha);
|
|
|
|
if (result != ImgDrawResult::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(nsGlobalWindowInner& aWindow,
|
|
double aX, double aY, double aW,
|
|
double aH, const nsACString& aBgColor,
|
|
uint32_t aFlags,
|
|
nsIPrincipal& aSubjectPrincipal,
|
|
ErrorResult& aError) {
|
|
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;
|
|
}
|
|
|
|
Document* doc = aWindow.GetExtantDoc();
|
|
if (doc && aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) {
|
|
doc->WarnOnceAbout(
|
|
DeprecatedOperations::eDrawWindowCanvasRenderingContext2D);
|
|
}
|
|
|
|
// Flush layout updates
|
|
if (!(aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DO_NOT_FLUSH)) {
|
|
nsContentUtils::FlushLayoutForTree(aWindow.GetOuterWindow());
|
|
}
|
|
|
|
CompositionOp op = CurrentState().op;
|
|
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;
|
|
}
|
|
|
|
RefPtr<nsPresContext> presContext;
|
|
nsIDocShell* docshell = aWindow.GetDocShell();
|
|
if (docshell) {
|
|
presContext = docshell->GetPresContext();
|
|
}
|
|
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));
|
|
RenderDocumentFlags renderDocFlags =
|
|
(RenderDocumentFlags::IgnoreViewportScrolling |
|
|
RenderDocumentFlags::DocumentRelative);
|
|
if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DRAW_CARET) {
|
|
renderDocFlags |= RenderDocumentFlags::DrawCaret;
|
|
}
|
|
if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DRAW_VIEW) {
|
|
renderDocFlags &= ~(RenderDocumentFlags::IgnoreViewportScrolling |
|
|
RenderDocumentFlags::DocumentRelative);
|
|
}
|
|
if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_USE_WIDGET_LAYERS) {
|
|
renderDocFlags |= RenderDocumentFlags::UseWidgetLayers;
|
|
}
|
|
if (aFlags &
|
|
CanvasRenderingContext2D_Binding::DRAWWINDOW_ASYNC_DECODE_IMAGES) {
|
|
renderDocFlags |= RenderDocumentFlags::AsyncDecodeImages;
|
|
}
|
|
if (aFlags & CanvasRenderingContext2D_Binding::DRAWWINDOW_DO_NOT_FLUSH) {
|
|
renderDocFlags |= RenderDocumentFlags::DrawWindowNotFlushing;
|
|
}
|
|
|
|
// 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 = CurrentState().op;
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
}
|
|
if (op == CompositionOp::OP_OVER &&
|
|
(!mBufferProvider || !mBufferProvider->IsShared())) {
|
|
thebes = gfxContext::CreateOrNull(mTarget);
|
|
MOZ_ASSERT(thebes); // already checked the draw target above
|
|
// (in SupportsAzureContentForDrawTarget)
|
|
thebes->SetMatrix(matrix);
|
|
} else {
|
|
IntSize dtSize = IntSize::Ceil(sw, sh);
|
|
if (!Factory::AllowedSurfaceSize(dtSize)) {
|
|
// attempt to limit the DT to what will actually cover the target
|
|
Size limitSize(mTarget->GetSize());
|
|
limitSize.Scale(matrix._11, matrix._22);
|
|
dtSize = Min(dtSize, IntSize::Ceil(limitSize));
|
|
// if the DT is still too big, then error
|
|
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(Matrix::Scaling(matrix._11, matrix._22));
|
|
}
|
|
|
|
RefPtr<PresShell> presShell = presContext->PresShell();
|
|
|
|
Unused << presShell->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;
|
|
}
|
|
|
|
op = CurrentState().op;
|
|
if (!IsTargetValid()) {
|
|
aError.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
gfx::Rect destRect(0, 0, aW, aH);
|
|
gfx::Rect sourceRect(0, 0, sw, sh);
|
|
mTarget->DrawSurface(snapshot, 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));
|
|
}
|
|
|
|
//
|
|
// device pixel getting/setting
|
|
//
|
|
|
|
already_AddRefed<ImageData> CanvasRenderingContext2D::GetImageData(
|
|
JSContext* aCx, int32_t aSx, int32_t aSy, int32_t aSw, int32_t aSh,
|
|
nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) {
|
|
return GetImageData(aCx, aSx, aSy, aSw, aSh, Some(&aSubjectPrincipal),
|
|
aError);
|
|
}
|
|
|
|
already_AddRefed<ImageData> CanvasRenderingContext2D::GetImageData(
|
|
JSContext* aCx, int32_t aSx, int32_t aSy, int32_t aSw, int32_t aSh,
|
|
Maybe<nsIPrincipal*> aSubjectPrincipal, ErrorResult& aError) {
|
|
if (!mCanvasElement && !mDocShell && !mOffscreenCanvas) {
|
|
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.
|
|
// FIXME(aosmond): OffscreenCanvas security check??!
|
|
if (IsWriteOnly() ||
|
|
(mCanvasElement && !mCanvasElement->CallerCanRead(aCx))) {
|
|
// XXX ERRMSG we need to report an error to developers here! (bug 329026)
|
|
aError.Throw(NS_ERROR_DOM_SECURITY_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
if (!aSw || !aSh) {
|
|
aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
|
|
return nullptr;
|
|
}
|
|
|
|
// Handle negative width and height by flipping the rectangle over in the
|
|
// relevant direction.
|
|
uint32_t w, h;
|
|
if (aSw < 0) {
|
|
w = -aSw;
|
|
aSx -= w;
|
|
} else {
|
|
w = aSw;
|
|
}
|
|
if (aSh < 0) {
|
|
h = -aSh;
|
|
aSy -= h;
|
|
} else {
|
|
h = aSh;
|
|
}
|
|
|
|
if (w == 0) {
|
|
w = 1;
|
|
}
|
|
if (h == 0) {
|
|
h = 1;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> array(aCx);
|
|
aError = GetImageDataArray(aCx, aSx, aSy, w, h, aSubjectPrincipal,
|
|
array.address());
|
|
if (aError.Failed()) {
|
|
return nullptr;
|
|
}
|
|
MOZ_ASSERT(array);
|
|
return MakeAndAddRef<ImageData>(w, h, *array);
|
|
}
|
|
|
|
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,
|
|
Maybe<nsIPrincipal*> aSubjectPrincipal, JSObject** aRetval) {
|
|
MOZ_ASSERT(aWidth && aHeight);
|
|
|
|
// Restrict the typed array length to INT32_MAX because that's all we support
|
|
// in dom::TypedArray::ComputeState.
|
|
CheckedInt<uint32_t> len = CheckedInt<uint32_t>(aWidth) * aHeight * 4;
|
|
if (!len.isValid() || len.value() > INT32_MAX) {
|
|
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));
|
|
if (srcReadRect.IsEmpty()) {
|
|
*aRetval = darray;
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!mBufferProvider) {
|
|
if (!EnsureTarget()) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
}
|
|
|
|
RefPtr<SourceSurface> snapshot = mBufferProvider->BorrowSnapshot();
|
|
if (!snapshot) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
RefPtr<DataSourceSurface> readback = snapshot->GetDataSurface();
|
|
mBufferProvider->ReturnSnapshot(snapshot.forget());
|
|
|
|
DataSourceSurface::MappedSurface rawData;
|
|
if (!readback || !readback->Map(DataSourceSurface::READ, &rawData)) {
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
|
|
// Check for site-specific permission. This check is not needed if the
|
|
// canvas was created with a docshell (that is only done for special
|
|
// internal uses).
|
|
bool usePlaceholder = false;
|
|
if (mCanvasElement) {
|
|
nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc();
|
|
usePlaceholder = !CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
|
|
aSubjectPrincipal);
|
|
} else if (mOffscreenCanvas) {
|
|
usePlaceholder = mOffscreenCanvas->ShouldResistFingerprinting();
|
|
}
|
|
|
|
do {
|
|
uint8_t* randomData;
|
|
if (usePlaceholder) {
|
|
// Since we cannot call any GC-able functions (like requesting the RNG
|
|
// service) after we call JS_GetUint8ClampedArrayData, we will
|
|
// pre-generate the randomness required for GeneratePlaceholderCanvasData.
|
|
randomData = TryToGenerateRandomDataForPlaceholderCanvasData();
|
|
}
|
|
|
|
JS::AutoCheckCannotGC nogc;
|
|
bool isShared;
|
|
uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
|
|
MOZ_ASSERT(!isShared); // Should not happen, data was created above
|
|
|
|
if (usePlaceholder) {
|
|
FillPlaceholderCanvas(randomData, len.value(), data);
|
|
break;
|
|
}
|
|
|
|
uint32_t srcStride = rawData.mStride;
|
|
uint8_t* src =
|
|
rawData.mData + srcReadRect.y * srcStride + srcReadRect.x * 4;
|
|
|
|
uint8_t* dst = data + dstWriteRect.y * (aWidth * 4) + dstWriteRect.x * 4;
|
|
|
|
if (mOpaque) {
|
|
SwizzleData(src, srcStride, SurfaceFormat::X8R8G8B8_UINT32, dst,
|
|
aWidth * 4, SurfaceFormat::R8G8B8A8, dstWriteRect.Size());
|
|
} else {
|
|
UnpremultiplyData(src, srcStride, SurfaceFormat::A8R8G8B8_UINT32, dst,
|
|
aWidth * 4, SurfaceFormat::R8G8B8A8,
|
|
dstWriteRect.Size());
|
|
}
|
|
} while (false);
|
|
|
|
readback->Unmap();
|
|
*aRetval = darray;
|
|
return NS_OK;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::EnsureErrorTarget() {
|
|
if (sErrorTarget.get()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<DrawTarget> errorTarget =
|
|
gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget(
|
|
IntSize(1, 1), SurfaceFormat::B8G8R8A8);
|
|
MOZ_ASSERT(errorTarget, "Failed to allocate the error target!");
|
|
|
|
sErrorTarget.set(errorTarget.forget().take());
|
|
}
|
|
|
|
void CanvasRenderingContext2D::FillRuleChanged() {
|
|
if (mPath) {
|
|
mPathBuilder = mPath->CopyToBuilder(CurrentState().fillRule);
|
|
mPath = nullptr;
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx,
|
|
int32_t aDy, ErrorResult& aRv) {
|
|
RootedSpiderMonkeyInterface<Uint8ClampedArray> arr(RootingCx());
|
|
PutImageData_explicit(aDx, aDy, aImageData, false, 0, 0, 0, 0, aRv);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::PutImageData(ImageData& aImageData, int32_t aDx,
|
|
int32_t aDy, int32_t aDirtyX,
|
|
int32_t aDirtyY,
|
|
int32_t aDirtyWidth,
|
|
int32_t aDirtyHeight,
|
|
ErrorResult& aRv) {
|
|
PutImageData_explicit(aDx, aDy, aImageData, true, aDirtyX, aDirtyY,
|
|
aDirtyWidth, aDirtyHeight, aRv);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::PutImageData_explicit(
|
|
int32_t aX, int32_t aY, ImageData& aImageData, bool aHasDirtyRect,
|
|
int32_t aDirtyX, int32_t aDirtyY, int32_t aDirtyWidth, int32_t aDirtyHeight,
|
|
ErrorResult& aRv) {
|
|
RootedSpiderMonkeyInterface<Uint8ClampedArray> arr(RootingCx());
|
|
if (!arr.Init(aImageData.GetDataObject())) {
|
|
return aRv.ThrowInvalidStateError(
|
|
"Failed to extract Uint8ClampedArray from ImageData (security check "
|
|
"failed?)");
|
|
}
|
|
|
|
const uint32_t width = aImageData.Width();
|
|
const uint32_t height = aImageData.Height();
|
|
if (width == 0 || height == 0) {
|
|
return aRv.ThrowInvalidStateError("Passed-in image is empty");
|
|
}
|
|
|
|
IntRect dirtyRect;
|
|
IntRect imageDataRect(0, 0, width, height);
|
|
|
|
if (aHasDirtyRect) {
|
|
// fix up negative dimensions
|
|
if (aDirtyWidth < 0) {
|
|
if (aDirtyWidth == INT_MIN) {
|
|
return aRv.ThrowInvalidStateError("Dirty width is invalid");
|
|
}
|
|
|
|
CheckedInt32 checkedDirtyX = CheckedInt32(aDirtyX) + aDirtyWidth;
|
|
|
|
if (!checkedDirtyX.isValid()) {
|
|
return aRv.ThrowInvalidStateError("Dirty width is invalid");
|
|
}
|
|
|
|
aDirtyX = checkedDirtyX.value();
|
|
aDirtyWidth = -aDirtyWidth;
|
|
}
|
|
|
|
if (aDirtyHeight < 0) {
|
|
if (aDirtyHeight == INT_MIN) {
|
|
return aRv.ThrowInvalidStateError("Dirty height is invalid");
|
|
}
|
|
|
|
CheckedInt32 checkedDirtyY = CheckedInt32(aDirtyY) + aDirtyHeight;
|
|
|
|
if (!checkedDirtyY.isValid()) {
|
|
return aRv.ThrowInvalidStateError("Dirty height is invalid");
|
|
}
|
|
|
|
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;
|
|
}
|
|
} else {
|
|
dirtyRect = imageDataRect;
|
|
}
|
|
|
|
IntRect srcRect = dirtyRect;
|
|
dirtyRect = ClipImageDataTransfer(srcRect, IntPoint(aX, aY),
|
|
IntSize(mWidth, mHeight));
|
|
if (dirtyRect.IsEmpty()) {
|
|
return;
|
|
}
|
|
|
|
arr.ComputeState();
|
|
|
|
uint32_t dataLen = arr.Length();
|
|
|
|
uint32_t len = width * height * 4;
|
|
if (dataLen != len) {
|
|
return aRv.ThrowInvalidStateError("Invalid width or height");
|
|
}
|
|
|
|
// 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 aRv.Throw(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
DataSourceSurface::MappedSurface map;
|
|
RefPtr<DataSourceSurface> sourceSurface;
|
|
uint8_t* lockedBits = nullptr;
|
|
uint8_t* dstData;
|
|
IntSize dstSize;
|
|
int32_t dstStride;
|
|
SurfaceFormat dstFormat;
|
|
if (mTarget->LockBits(&lockedBits, &dstSize, &dstStride, &dstFormat)) {
|
|
dstData = lockedBits + dirtyRect.y * dstStride + dirtyRect.x * 4;
|
|
} else {
|
|
sourceSurface = Factory::CreateDataSourceSurface(
|
|
dirtyRect.Size(), SurfaceFormat::B8G8R8A8, false);
|
|
|
|
// 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 aRv.Throw(NS_ERROR_FAILURE);
|
|
}
|
|
if (!sourceSurface->Map(DataSourceSurface::READ_WRITE, &map)) {
|
|
return aRv.Throw(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
dstData = map.mData;
|
|
if (!dstData) {
|
|
return aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
}
|
|
dstStride = map.mStride;
|
|
dstFormat = sourceSurface->GetFormat();
|
|
}
|
|
|
|
uint8_t* srcData = arr.Data() + srcRect.y * (width * 4) + srcRect.x * 4;
|
|
|
|
PremultiplyData(
|
|
srcData, width * 4, SurfaceFormat::R8G8B8A8, dstData, dstStride,
|
|
mOpaque ? SurfaceFormat::X8R8G8B8_UINT32 : SurfaceFormat::A8R8G8B8_UINT32,
|
|
dirtyRect.Size());
|
|
|
|
if (lockedBits) {
|
|
mTarget->ReleaseBits(lockedBits);
|
|
} else if (sourceSurface) {
|
|
sourceSurface->Unmap();
|
|
mTarget->CopySurface(sourceSurface, dirtyRect - dirtyRect.TopLeft(),
|
|
dirtyRect.TopLeft());
|
|
}
|
|
|
|
Redraw(
|
|
gfx::Rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height));
|
|
}
|
|
|
|
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;
|
|
|
|
// Restrict the typed array length to INT32_MAX because that's all we support
|
|
// in dom::TypedArray::ComputeState.
|
|
CheckedInt<uint32_t> len = CheckedInt<uint32_t>(aW) * aH * 4;
|
|
if (!len.isValid() || len.value() > INT32_MAX) {
|
|
aError.ThrowIndexSizeError("Invalid width or height");
|
|
return nullptr;
|
|
}
|
|
|
|
// Create the fast typed array; it's initialized to 0 by default.
|
|
JSObject* darray = Uint8ClampedArray::Create(aCx, aContext, len.value());
|
|
if (!darray) {
|
|
// TODO: Should use OOMReporter.
|
|
aError.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return nullptr;
|
|
}
|
|
|
|
return do_AddRef(new ImageData(aW, aH, *darray));
|
|
}
|
|
|
|
already_AddRefed<ImageData> CanvasRenderingContext2D::CreateImageData(
|
|
JSContext* aCx, int32_t aSw, int32_t aSh, ErrorResult& aError) {
|
|
if (!aSw || !aSh) {
|
|
aError.ThrowIndexSizeError("Invalid width or height");
|
|
return nullptr;
|
|
}
|
|
|
|
uint32_t w = Abs(aSw);
|
|
uint32_t h = Abs(aSh);
|
|
return dom::CreateImageData(aCx, this, w, h, aError);
|
|
}
|
|
|
|
already_AddRefed<ImageData> CanvasRenderingContext2D::CreateImageData(
|
|
JSContext* aCx, ImageData& aImagedata, ErrorResult& aError) {
|
|
return dom::CreateImageData(aCx, this, aImagedata.Width(),
|
|
aImagedata.Height(), aError);
|
|
}
|
|
|
|
void CanvasRenderingContext2D::OnMemoryPressure() {
|
|
if (mBufferProvider) {
|
|
mBufferProvider->OnMemoryPressure();
|
|
}
|
|
}
|
|
|
|
void CanvasRenderingContext2D::OnBeforePaintTransaction() {
|
|
if (!mTarget) return;
|
|
OnStableState();
|
|
}
|
|
|
|
void CanvasRenderingContext2D::OnDidPaintTransaction() { MarkContextClean(); }
|
|
|
|
bool CanvasRenderingContext2D::UpdateWebRenderCanvasData(
|
|
nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) {
|
|
if (mOpaque) {
|
|
// If we're opaque then make sure we have a surface so we paint black
|
|
// instead of transparent.
|
|
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 && !IsTargetValid()) {
|
|
// No DidTransactionCallback will be received, so mark the context clean
|
|
// now so future invalidations will be dispatched.
|
|
MarkContextClean();
|
|
// Clear CanvasRenderer of WebRenderCanvasData
|
|
aCanvasData->ClearCanvasRenderer();
|
|
return false;
|
|
}
|
|
|
|
auto renderer = aCanvasData->GetCanvasRenderer();
|
|
|
|
if (!mResetLayer && renderer) {
|
|
CanvasRendererData data;
|
|
data.mContext = this;
|
|
data.mSize = GetSize();
|
|
|
|
if (renderer->IsDataValid(data)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
renderer = aCanvasData->CreateCanvasRenderer();
|
|
if (!InitializeCanvasRenderer(aBuilder, renderer)) {
|
|
// Clear CanvasRenderer of WebRenderCanvasData
|
|
aCanvasData->ClearCanvasRenderer();
|
|
return false;
|
|
}
|
|
|
|
MOZ_ASSERT(renderer);
|
|
mResetLayer = false;
|
|
return true;
|
|
}
|
|
|
|
bool CanvasRenderingContext2D::InitializeCanvasRenderer(
|
|
nsDisplayListBuilder* aBuilder, CanvasRenderer* aRenderer) {
|
|
CanvasRendererData data;
|
|
data.mContext = this;
|
|
data.mSize = GetSize();
|
|
data.mIsOpaque = mOpaque;
|
|
data.mDoPaintCallbacks = true;
|
|
|
|
if (!mBufferProvider) {
|
|
// Force the creation of a buffer provider.
|
|
EnsureTarget();
|
|
ReturnTarget();
|
|
if (!mBufferProvider) {
|
|
MarkContextClean();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
aRenderer->Initialize(data);
|
|
aRenderer->SetDirty();
|
|
return true;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::MarkContextClean() {
|
|
if (mInvalidateCount > 0) {
|
|
mPredictManyRedrawCalls = mInvalidateCount > kCanvasMaxInvalidateCount;
|
|
}
|
|
mIsEntireFrameInvalid = false;
|
|
mInvalidateCount = 0;
|
|
}
|
|
|
|
void CanvasRenderingContext2D::GetAppUnitsValues(int32_t* aPerDevPixel,
|
|
int32_t* aPerCSSPixel) {
|
|
// If we don't have a canvas element, we just return something generic.
|
|
if (aPerDevPixel) {
|
|
*aPerDevPixel = 60;
|
|
}
|
|
if (aPerCSSPixel) {
|
|
*aPerCSSPixel = 60;
|
|
}
|
|
PresShell* presShell = GetPresShell();
|
|
if (!presShell) {
|
|
return;
|
|
}
|
|
nsPresContext* presContext = presShell->GetPresContext();
|
|
if (!presContext) {
|
|
return;
|
|
}
|
|
if (aPerDevPixel) {
|
|
*aPerDevPixel = presContext->AppUnitsPerDevPixel();
|
|
}
|
|
if (aPerCSSPixel) {
|
|
*aPerCSSPixel = AppUnitsPerCSSPixel();
|
|
}
|
|
}
|
|
|
|
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::ThreadLocalScreenReferenceDrawTarget()->CreatePathBuilder();
|
|
}
|
|
|
|
CanvasPath::CanvasPath(nsISupports* aParent,
|
|
already_AddRefed<PathBuilder> aPathBuilder)
|
|
: mParent(aParent), mPathBuilder(aPathBuilder) {
|
|
if (!mPathBuilder) {
|
|
mPathBuilder = gfxPlatform::ThreadLocalScreenReferenceDrawTarget()
|
|
->CreatePathBuilder();
|
|
}
|
|
}
|
|
|
|
JSObject* CanvasPath::WrapObject(JSContext* aCx,
|
|
JS::Handle<JSObject*> aGivenProto) {
|
|
return Path2D_Binding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
already_AddRefed<CanvasPath> CanvasPath::Constructor(
|
|
const GlobalObject& aGlobal) {
|
|
RefPtr<CanvasPath> path = new CanvasPath(aGlobal.GetAsSupports());
|
|
return path.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasPath> CanvasPath::Constructor(
|
|
const GlobalObject& aGlobal, CanvasPath& aCanvasPath) {
|
|
RefPtr<gfx::DrawTarget> drawTarget =
|
|
gfxPlatform::ThreadLocalScreenReferenceDrawTarget();
|
|
RefPtr<gfx::Path> tempPath =
|
|
aCanvasPath.GetPath(CanvasWindingRule::Nonzero, drawTarget.get());
|
|
|
|
RefPtr<CanvasPath> path =
|
|
new CanvasPath(aGlobal.GetAsSupports(), tempPath->CopyToBuilder());
|
|
return path.forget();
|
|
}
|
|
|
|
already_AddRefed<CanvasPath> CanvasPath::Constructor(
|
|
const GlobalObject& aGlobal, const nsAString& aPathString) {
|
|
RefPtr<gfx::Path> tempPath = SVGContentUtils::GetPath(aPathString);
|
|
if (!tempPath) {
|
|
return Constructor(aGlobal);
|
|
}
|
|
|
|
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) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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& aError) {
|
|
if (radiusX < 0.0 || radiusY < 0.0) {
|
|
return aError.ThrowIndexSizeError("Negative radius");
|
|
}
|
|
|
|
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::DrawTarget> drawTarget =
|
|
gfxPlatform::ThreadLocalScreenReferenceDrawTarget();
|
|
RefPtr<gfx::Path> tempPath =
|
|
aCanvasPath.GetPath(CanvasWindingRule::Nonzero, drawTarget.get());
|
|
|
|
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;
|
|
}
|
|
|
|
size_t BindingJSObjectMallocBytes(CanvasRenderingContext2D* aContext) {
|
|
IntSize size = aContext->GetSize();
|
|
|
|
// TODO: Bug 1552137: No memory will be allocated if either dimension is
|
|
// greater than gfxPrefs::gfx_canvas_max_size(). We should check this here
|
|
// too.
|
|
|
|
CheckedInt<uint32_t> bytes =
|
|
CheckedInt<uint32_t>(size.width) * size.height * 4;
|
|
if (!bytes.isValid()) {
|
|
return 0;
|
|
}
|
|
|
|
return bytes.value();
|
|
}
|
|
|
|
} // namespace mozilla::dom
|