/* -*- 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/. */
// 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.");
// 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());
} elseif (!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]));
} elseif (state.gradientStyles[aStyle] &&
state.gradientStyles[aStyle]->GetType() ==
CanvasGradient::Type::LINEAR) { auto gradient = static_cast<CanvasLinearGradient*>(
state.gradientStyles[aStyle].get());
/* 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;
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 (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;
}
// 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;
}
/* 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;
// 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;
}
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;
}
/* * 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;
}
// 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. constbool 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();
~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();
}
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");
}
if (!mContext) { return aRv.ThrowSyntaxError("No canvas context");
}
auto color = mContext->ParseColor(
aColorstr, CanvasRenderingContext2D::ResolveCurrentColor::No); if (!color) { return aRv.ThrowSyntaxError("Invalid color");
}
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]); if (auto* autoSVGFiltersObserver =
tmp->mStyleStack[i].autoSVGFiltersObserver.get()) { /* * XXXjwatt: I don't think this is doing anything useful. All we do under * this function is clear a raw C-style (i.e. not strong) pointer. That's * clearly not helping in breaking any cycles. The fact that we MOZ_CRASH * in OnRenderingChange if that pointer is null indicates that this isn't * even doing anything useful in terms of preventing further invalidation * from any observed filters.
*/
autoSVGFiltersObserver->Detach();
}
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
Maybe<nscolor> CanvasRenderingContext2D::ParseColor( const nsACString& aString, ResolveCurrentColor aResolveCurrentColor) { auto entry = mColorStyleCache.Lookup(aString); if (!entry) {
entry.Set(ParseColorSlow(aString));
}
constauto& data = entry.Data(); if (data.mWasCurrentColor && mCanvasElement &&
aResolveCurrentColor == ResolveCurrentColor::Yes) { // If it was currentColor, get the value of the color property, flushing // style if necessary.
RefPtr<const ComputedStyle> canvasStyle =
nsComputedDOMStyle::GetComputedStyle(mCanvasElement); if (canvasStyle) { return Some(canvasStyle->StyleText()->mColor.ToColor());
}
} return data.mColor;
}
void CanvasRenderingContext2D::ResetBitmap(bool aFreeBuffer) { 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; if (aFreeBuffer) {
mBufferProvider = nullptr;
} elseif (mBufferProvider) { // Try to keep the buffer around. However, we still need to clear the // contents as if it was recreated before next use.
mBufferNeedsClear = true;
}
// Since the target changes the backing texture will change, and this will // no longer be valid.
mIsEntireFrameInvalid = false;
mPredictManyRedrawCalls = false;
mFrameCaptureState = FrameCaptureState::CLEAN;
}
void CanvasRenderingContext2D::OnRemoteCanvasLost() { // We only lose context / data if we are using remote canvas, which is only // for accelerated targets. if (!mBufferProvider || !mBufferProvider->IsAccelerated() || mIsContextLost) { return;
}
// 2. Set context's context lost to true.
mIsContextLost = mAllowContextRestore = true;
// 3. Reset the rendering context to its default state given context.
ClearTarget();
// We dispatch because it isn't safe to call into the script event handlers, // and we don't want to mutate our state in CanvasShutdownManager.
NS_DispatchToCurrentThread(NS_NewCancelableRunnableFunction( "CanvasRenderingContext2D::OnRemoteCanvasLost", [self = RefPtr{this}] { // 4. Let shouldRestore be the result of firing an event named // contextlost at canvas, with the cancelable attribute initialized to // true.
self->mAllowContextRestore = self->DispatchEvent(
u"contextlost"_ns, CanBubble::eNo, Cancelable::eYes);
}));
}
void CanvasRenderingContext2D::OnRemoteCanvasRestored() { // We never lost our context if it was not a remote canvas, nor can we restore // if we have already shutdown. if (mHasShutdown || !mIsContextLost || !mAllowContextRestore) { return;
}
// We dispatch because it isn't safe to call into the script event handlers, // and we don't want to mutate our state in CanvasShutdownManager.
NS_DispatchToCurrentThread(NS_NewCancelableRunnableFunction( "CanvasRenderingContext2D::OnRemoteCanvasRestored",
[self = RefPtr{this}] { // 5. If shouldRestore is false, then abort these steps. if (!self->mHasShutdown && self->mIsContextLost &&
self->mAllowContextRestore) { // 7. Set context's context lost to false.
self->mIsContextLost = false;
// 6. Attempt to restore context by creating a backing storage using // context's attributes and associating them with context. If this // fails, then abort these steps. if (!self->EnsureTarget()) {
self->mIsContextLost = true; return;
}
// 8. Fire an event named contextrestored at canvas.
self->DispatchEvent(u"contextrestored"_ns, CanBubble::eNo,
Cancelable::eNo);
}
}));
}
// 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(')');
}
}
if (mCanvasElement) {
SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement);
mCanvasElement->InvalidateCanvasContent(nullptr);
} elseif (mOffscreenCanvas) {
mOffscreenCanvas->QueueCommitToCompositor();
} else {
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
}
if (mPredictManyRedrawCalls || mInvalidateCount > kCanvasMaxInvalidateCount) {
Redraw(); return;
}
if (mCanvasElement) {
SVGObserverUtils::InvalidateDirectRenderingObservers(mCanvasElement);
mCanvasElement->InvalidateCanvasContent(&aR);
} elseif (mOffscreenCanvas) {
mOffscreenCanvas->QueueCommitToCompositor();
} else {
NS_ASSERTION(mDocShell, "Redraw with no canvas element or docshell!");
}
}
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) { 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::BorrowTarget(const IntRect& aPersistedRect, bool aNeedsClear) { // We are attempting to request a DrawTarget from the current // PersistentBufferProvider. However, if the provider needs to be refreshed, // or if it is accelerated and the application has requested that we disallow // acceleration, then we skip trying to use this provider so that it will be // recreated by EnsureTarget later. if (!mBufferProvider || mBufferProvider->RequiresRefresh() ||
(mBufferProvider->IsAccelerated() && UseSoftwareRendering())) { returnfalse;
}
mTarget = mBufferProvider->BorrowDrawTarget(aPersistedRect); if (!mTarget || !mTarget->IsValid()) { if (mTarget) {
mBufferProvider->ReturnDrawTarget(mTarget.forget());
} returnfalse;
} if (mBufferNeedsClear) { if (mBufferProvider->PreservesDrawingState()) { // If the buffer provider preserves the clip and transform state, then // we must ensure it is cleared before reusing the target. if (!mTarget->RemoveAllClips()) {
mBufferProvider->ReturnDrawTarget(mTarget.forget()); returnfalse;
}
mTarget->SetTransform(Matrix());
} // If the canvas was reset, then we need to clear the target in case its // contents was somehow preserved. We only need to clear the target if // the operation doesn't fill the entire canvas. if (aNeedsClear) {
mTarget->ClearRect(gfx::Rect(mTarget->GetRect()));
}
} if (!mBufferProvider->PreservesDrawingState() || mBufferNeedsClear) {
RestoreClipsAndTransformToTarget();
}
mBufferNeedsClear = false; returntrue;
}
bool CanvasRenderingContext2D::EnsureTarget(ErrorResult& aError, const gfx::Rect* aCoveredRect, bool aWillClear, bool aSkipTransform) { if (AlreadyShutDown()) {
gfxCriticalNoteOnce << "Attempt to render into a Canvas2d after shutdown.";
SetErrorState();
aError.ThrowInvalidStateError( "Cannot use canvas after shutdown initiated."); returnfalse;
}
// The spec doesn't say what to do in this case, but Chrome silently fails // without throwing an error. We should at least throw if the canvas is // permanently disabled. if (NS_WARN_IF(mIsContextLost)) { if (!mAllowContextRestore) {
aError.ThrowInvalidStateError( "Cannot use canvas as context is lost forever.");
} returnfalse;
}
if (mTarget) { if (mTarget == sErrorTarget.get()) {
aError.ThrowInvalidStateError("Canvas is already in error state."); returnfalse;
} returntrue;
}
// Check that the dimensions are sane if (mWidth > StaticPrefs::gfx_canvas_max_size() ||
mHeight > StaticPrefs::gfx_canvas_max_size()) {
SetErrorState();
aError.ThrowInvalidStateError("Canvas exceeds max size."); returnfalse;
}
if (mWidth < 0 || mHeight < 0) {
SetErrorState();
aError.ThrowInvalidStateError("Canvas has invalid size."); returnfalse;
}
// 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 &&
(aSkipTransform ? *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 (constauto& style : mStyleStack) { if (!canDiscardContent) { break;
} for (constauto& clipOrTransform : style.clipsAndTransforms) { if (clipOrTransform.IsClip()) {
canDiscardContent = false; break;
}
}
}
if (!TryAcceleratedTarget(newTarget, newProvider) &&
!TrySharedTarget(newTarget, newProvider) &&
!TryBasicTarget(newTarget, newProvider, aError)) {
gfxCriticalError(
CriticalLog::DefaultOptions(Factory::ReasonableSurfaceSize(GetSize())))
<< "Failed borrow shared and basic targets.";
SetErrorState(); returnfalse;
}
MOZ_ASSERT(newTarget);
MOZ_ASSERT(newProvider);
bool needsClear =
!canDiscardContent || (mBufferProvider && mBufferNeedsClear); 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 && !mBufferNeedsClear &&
CopyBufferProvider(*mBufferProvider, *newTarget, persistedRect)) {
needsClear = false;
}
if (needsClear) {
newTarget->ClearRect(canvasRect);
}
// Ensure any Path state is compatible with the type of DrawTarget used. This // may require making a copy with the correct type if they (rarely) mismatch. if (mPathBuilder &&
mPathBuilder->GetBackendType() != newTarget->GetBackendType()) {
RefPtr<Path> path = mPathBuilder->Finish();
mPathBuilder = newTarget->CreatePathBuilder(path->GetFillRule());
path->StreamToSink(mPathBuilder);
} if (mPath && mPath->GetBackendType() != newTarget->GetBackendType()) {
RefPtr<PathBuilder> builder =
newTarget->CreatePathBuilder(mPath->GetFillRule());
mPath->StreamToSink(builder);
mPath = builder->Finish();
}
// 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;
returntrue;
}
void CanvasRenderingContext2D::SetInitialState() { // Set up the initial canvas defaults
mPathBuilder = nullptr;
mPath = nullptr;
mPathPruned = false;
mPathTransform = Matrix();
mPathTransformDirty = false;
mStyleStack.Clear();
ContextState* state = mStyleStack.AppendElement();
state->globalAlpha = 1.0;
// clear transforms, clips, etc.
SetInitialState();
}
void CanvasRenderingContext2D::RegisterAllocation() { // XXX - It would make more sense to track the allocation in // PeristentBufferProvider, rather than here. staticbool registered = false; // FIXME: Disable the reporter for now, see bug 1241865 if (!registered && false) {
registered = true;
RegisterStrongMemoryReporter(new Canvas2dPixelsReporter());
}
}
bool CanvasRenderingContext2D::TryAcceleratedTarget(
RefPtr<gfx::DrawTarget>& aOutDT,
RefPtr<layers::PersistentBufferProvider>& aOutProvider) { if (!XRE_IsContentProcess()) { // Only allow accelerated contexts to be created in a content process to // ensure it is remoted appropriately and run on the correct parent or // GPU process threads. returnfalse;
} 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;
} // Don't try creating an accelerate DrawTarget if either acceleration failed // previously or if the application expects acceleration to be slow. if (!mAllowAcceleration || UseSoftwareRendering()) { returnfalse;
}
if (mCanvasElement) {
MOZ_ASSERT(NS_IsMainThread());
WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); if (NS_WARN_IF(!renderer)) { returnfalse;
}
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; returnfalse;
}
if (mCanvasElement) {
MOZ_ASSERT(NS_IsMainThread());
WindowRenderer* renderer = WindowRendererFromCanvasElement(mCanvasElement); if (NS_WARN_IF(!renderer)) { returnfalse;
}
// 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,
ErrorResult& aError) {
aOutDT = gfxPlatform::GetPlatform()->CreateOffscreenCanvasDrawTarget(
GetSize(), GetSurfaceFormat(), UseSoftwareRendering()); if (!aOutDT) {
aError.ThrowInvalidStateError("Canvas could not create basic draw target."); returnfalse;
}
// See Bug 1524554 - this forces DT initialization.
aOutDT->ClearRect(gfx::Rect());
if (!aOutDT->IsValid()) {
aOutDT = nullptr;
aError.ThrowInvalidStateError("Canvas could not init basic draw target."); returnfalse;
}
aOutProvider = new PersistentBufferProviderBasic(aOutDT); returntrue;
}
PersistentBufferProvider* CanvasRenderingContext2D::GetBufferProvider() { if (mBufferProvider && mBufferNeedsClear) { // Force the buffer to clear before it is used.
EnsureTarget();
} return mBufferProvider;
}
void CanvasRenderingContext2D::ClearTarget(int32_t aWidth, int32_t aHeight) { // Only free the buffer provider if the size no longer matches. bool freeBuffer = aWidth != mWidth || aHeight != mHeight;
ResetBitmap(freeBuffer);
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 (!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 = CanvasTextBaseline::Middle;
}
}
}
void CanvasRenderingContext2D::ReturnTarget(bool aForceReset) { if (mTarget && mBufferProvider && mTarget != sErrorTarget.get()) {
CurrentState().transform = mTarget->GetTransform(); if (aForceReset || !mBufferProvider->PreservesDrawingState()) { for (constauto& style : mStyleStack) { for (constauto& 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();
}
if (ret && ShouldResistFingerprinting(RFPTarget::CanvasRandomization)) { bool randomize = true; // Skip randomization if we are doing user characteristics data collection. // During data collection, we'll 1) set the pref to true 2) be in the main // thread and 3) be in chrome code (JS Window Actor). if (StaticPrefs::
privacy_resistFingerprinting_randomization_canvas_disable_for_chrome()) { bool isCallerChrome =
NS_IsMainThread() && nsContentUtils::IsCallerChrome(); if (isCallerChrome) {
randomize = false;
}
} if (randomize) {
nsRFPService::RandomizePixels(
GetCookieJarSettings(), ret.get(), out_imageSize->width,
out_imageSize->height,
out_imageSize->width * out_imageSize->height * 4,
SurfaceFormat::A8R8G8B8_UINT32);
}
}
// 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() || mIsContextLost, "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(aTarget);
RefPtr<SourceSurface> retSurface = snapshot;
mBufferProvider->ReturnSnapshot(snapshot.forget()); return retSurface.forget();
}
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;
}
EnsureTarget(); if (!IsTargetValid()) { return;
}
for (constauto& clipOrTransform : CurrentState().clipsAndTransforms) { if (clipOrTransform.IsClip()) {
mTarget->PopClip();
}
}
already_AddRefed<DOMMatrix> CanvasRenderingContext2D::GetTransform(
ErrorResult& aError) { // If we are silently failing, then we still need to return a transform while // we are in the process of recovering.
Matrix transform; if (EnsureTarget(aError)) {
transform = mTarget->GetTransform();
} elseif (aError.Failed()) { return nullptr;
}
RefPtr<DOMMatrix> matrix = new DOMMatrix(GetParentObject(), transform); return matrix.forget();
}
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;
}
if (aSource.IsHTMLCanvasElement()) {
HTMLCanvasElement* canvas = &aSource.GetAsHTMLCanvasElement();
element = canvas;
CSSIntSize 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);
if (videoFrame->CodedWidth() == 0) {
aError.ThrowInvalidStateError("Passed-in canvas has width 0"); return nullptr;
}
if (videoFrame->CodedHeight() == 0) {
aError.ThrowInvalidStateError("Passed-in canvas has height 0"); return nullptr;
}
} else { // Special case for ImageBitmap
ImageBitmap& imgBitmap = aSource.GetAsImageBitmap(); if (!EnsureTarget(aError)) { return nullptr;
}
MOZ_ASSERT(IsTargetValid());
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();
}
if (!EnsureTarget(aError)) { return nullptr;
}
MOZ_ASSERT(IsTargetValid());
// 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; if (offscreenCanvas) {
res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, flags,
mTarget);
} elseif (videoFrame) {
res = nsLayoutUtils::SurfaceFromVideoFrame(videoFrame, flags, mTarget);
} else {
res = 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;
}
static already_AddRefed<const ComputedStyle> GetFontStyleForServo(
Element* aElement, const nsACString& aFont, PresShell* aPresShell,
nsACString& aOutUsedFont, ErrorResult& aError) {
RefPtr<StyleLockedDeclarationBlock> 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();
// Have to get a parent ComputedStyle for inherit-like relative values (2em, // bolder, etc.)
RefPtr<const ComputedStyle> parentStyle; if (aElement) {
parentStyle = nsComputedDOMStyle::GetComputedStyle(aElement); if (NS_WARN_IF(aPresShell->IsDestroying())) { // The flush might've killed the shell.
aError.Throw(NS_ERROR_FAILURE); return nullptr;
}
} if (!parentStyle) {
RefPtr<StyleLockedDeclarationBlock> declarations =
CreateFontDeclarationForServo("10px sans-serif"_ns,
aPresShell->GetDocument());
MOZ_ASSERT(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) { float px = sc->StyleFont()->mFont.size.ToCSSPixels();
Servo_DeclarationBlock_SetLengthValue(declarations, eCSSProperty_font_size,
px, eCSSUnit_Pixel);
}
// The font getter is required to be reserialized based on what we // parsed (including having line-height removed). // If we failed to reserialize, ignore this attempt to set the value.
Servo_SerializeFontValueForCanvas(declarations, &aOutUsedFont); if (aOutUsedFont.IsEmpty()) { return nullptr;
}
static already_AddRefed<const ComputedStyle> ResolveFilterStyleForServo( const nsACString& aFilterString, const ComputedStyle* aParentStyle,
PresShell* aPresShell, ErrorResult& aError) {
RefPtr<StyleLockedDeclarationBlock> 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;
}
if (NS_WARN_IF(!urlExtraData)) { // Provided we have a FontFaceSetImpl object, this should only happen on // worker threads, where we failed to initialize the worker before it was // shutdown.
aError.ThrowInvalidStateError("Missing URLExtraData"); returnfalse;
}
// In addition to unparseable values, reject 'inherit' and 'initial'. if (Servo_DeclarationBlock_HasCSSWideKeyword(declarations, aProperty)) { return nullptr;
}
static GeckoFontMetrics GetFontMetricsFromCanvas(void* aContext) { auto* ctx = static_cast<CanvasRenderingContext2D*>(aContext); auto* fontGroup = ctx->GetCurrentFontStyle(); if (!fontGroup) { // Shouldn't happen, but just in case... return plausible values for a // 10px font (canvas default size). return {Length::FromPixels(5.0),
Length::FromPixels(5.0),
Length::FromPixels(8.0),
Length::FromPixels(10.0),
Length::FromPixels(8.0),
Length::FromPixels(10.0),
0.0f,
0.0f};
} auto metrics = fontGroup->GetMetricsForCSSUnits(nsFontMetrics::eHorizontal); return {Length::FromPixels(metrics.xHeight),
Length::FromPixels(metrics.zeroWidth),
Length::FromPixels(metrics.capHeight),
Length::FromPixels(metrics.ideographicWidth),
Length::FromPixels(metrics.maxAscent),
Length::FromPixels(fontGroup->GetStyle()->size),
0.0f,
0.0f};
}
void CanvasRenderingContext2D::ParseSpacing(const nsACString& aSpacing, float* aValue,
nsACString& aNormalized) { // Normalize whitespace in the string before trying to parse it, as we want // to store it in normalized form, and this allows a simple check against the // 'normal' keyword, which is not accepted.
nsAutoCString normalized(aSpacing);
normalized.CompressWhitespace(true, true);
ToLowerCase(normalized); if (normalized.EqualsLiteral("normal")) { return;
} float value; if (!Servo_ParseLengthWithoutStyleContext(&normalized, &value,
GetFontMetricsFromCanvas, this)) { if (!GetPresShell()) { return;
} // This will parse aSpacing as a <length-percentage>...
RefPtr<const ComputedStyle> style =
ResolveStyleForProperty(eCSSProperty_letter_spacing, aSpacing); if (!style) { return;
} // ...but only <length> is allowed according to the canvas spec. if (!style->StyleText()->mLetterSpacing.IsLength()) { return;
}
value = style->StyleText()->mLetterSpacing.AsLength().ToCSSPixels();
}
aNormalized = normalized;
*aValue = value;
}
float GetLineHeight(Type aType) const override { // This is used if a filter is added through `url()`, and if the SVG // filter being referred to is using line-height units. switch (aType) { case Type::This: { constauto wm = GetWritingModeForType(aType); constauto lh = ReflowInput::CalcLineHeightForCanvas(
mLineHeight, mFont, mFontLanguage, mFontExplicitLanguage,
mPresContext, wm); return nsPresContext::AppUnitsToFloatCSSPixels(lh);
} case Type::Root: { return SVGContentUtils::GetLineHeight(
mPresContext->Document()->GetRootElement());
}
}
MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); return 1.0f;
}
private:
GeckoFontMetrics GetFontMetricsForType(Type aType) const override { switch (aType) { case Type::This: { if (!mCanvasStyle) { return DefaultFontMetrics();
} return Gecko_GetFontMetrics(
mPresContext, WritingMode(mCanvasStyle).IsVertical(),
mCanvasStyle->StyleFont(), mCanvasStyle->StyleFont()->mFont.size, /* aUseUserFontSet = */ true, /* aRetrieveMathScales */ false);
} case Type::Root: return GetFontMetrics(mPresContext->Document()->GetRootElement()); default:
MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); return DefaultFontMetrics();
}
}
WritingMode GetWritingModeForType(Type aType) const override { switch (aType) { case Type::This: return mCanvasStyle ? WritingMode(mCanvasStyle) : WritingMode(); case Type::Root: return GetWritingMode(mPresContext->Document()->GetRootElement()); default:
MOZ_ASSERT_UNREACHABLE("Was a new value added to the enumeration?"); return WritingMode();
}
}
// 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. staticbool FiltersNeedFrameFlush(Span<const StyleFilter> aFilters) { for (constauto& filter : aFilters) { if (filter.IsUrl()) { returntrue;
}
} returnfalse;
}
RefPtr<PresShell> presShell = GetPresShell(); if (!mOffscreenCanvas && (!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 = writeOnly; return;
}
// The PresContext is only used with URL filters and we don't allow those to // be used on worker threads.
nsPresContext* presContext = nullptr; if (presShell) { if (aFlushIfNeeded &&
FiltersNeedFrameFlush(CurrentState().filterChain.AsSpan())) {
presShell->FlushPendingNotifications(FlushType::Frames);
}
if (MOZ_UNLIKELY(presShell->IsDestroying())) { return;
}
// 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)) { returnfalse;
}
// 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;
} returntrue;
}
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;
}
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;
if (!ValidateRect(aX, aY, aW, aH, true)) { return;
}
EnsureTarget(); if (!IsTargetValid()) { return;
}
constbool 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 == CanvasLineJoin::Round) {
cap = CapStyle::ROUND;
}
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); auto op = target.UsedOperation(); if (!target) { return;
}
if (!aW) {
CapStyle cap = CapStyle::BUTT; if (CurrentState().lineJoin == CanvasLineJoin::Round) {
cap = CapStyle::ROUND;
}
AdjustedTarget target(this, bounds.IsEmpty() ? nullptr : &bounds, true); auto op = target.UsedOperation(); if (!target) { return;
}
// 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)) { returnfalse;
}
EnsureCapped();
mPathBuilder->MoveTo(Point(aX, aY)); if (aW == 0 && aH == 0) { return;
}
mPathBuilder->LineTo(Point(aX + aW, aY));
mPathBuilder->LineTo(Point(aX + aW, aY + aH));
mPathBuilder->LineTo(Point(aX, aY + aH));
mPathBuilder->Close();
}
// https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-roundrect staticvoid RoundRectImpl(
PathBuilder* aPathBuilder, const Maybe<Matrix>& aTransform, double aX, double aY, double aW, double aH, const UnrestrictedDoubleOrDOMPointInitOrUnrestrictedDoubleOrDOMPointInitSequence&
aRadii,
ErrorResult& aError) { // Step 1. If any of x, y, w, or h are infinite or NaN, then return. if (!std::isfinite(aX) || !std::isfinite(aY) || !std::isfinite(aW) ||
!std::isfinite(aH)) { return;
}
nsTArray<OwningUnrestrictedDoubleOrDOMPointInit> radii; // Step 2. If radii is an unrestricted double or DOMPointInit, then set radii // to « radii ». if (aRadii.IsUnrestrictedDouble()) {
radii.AppendElement()->SetAsUnrestrictedDouble() =
aRadii.GetAsUnrestrictedDouble();
} elseif (aRadii.IsDOMPointInit()) {
radii.AppendElement()->SetAsDOMPointInit() = aRadii.GetAsDOMPointInit();
} else {
radii = aRadii.GetAsUnrestrictedDoubleOrDOMPointInitSequence(); // Step 3. If radii is not a list of size one, two, three, or // four, then throw a RangeError. if (radii.Length() < 1 || radii.Length() > 4) {
aError.ThrowRangeError("Can have between 1 and 4 radii"); return;
}
}
// Step 4. Let normalizedRadii be an empty list.
AutoTArray<Size, 4> normalizedRadii;
// Step 5. For each radius of radii: for (constauto& radius : radii) { // Step 5.1. If radius is a DOMPointInit: if (radius.IsDOMPointInit()) { const DOMPointInit& point = radius.GetAsDOMPointInit(); // Step 5.1.1. If radius["x"] or radius["y"] is infinite or NaN, then // return. if (!std::isfinite(point.mX) || !std::isfinite(point.mY)) { return;
}
// Step 5.1.2. If radius["x"] or radius["y"] is negative, then // throw a RangeError. if (point.mX < 0 || point.mY < 0) {
aError.ThrowRangeError("Radius can not be negative"); return;
}
// Step 5.2. If radius is a unrestricted double: double r = radius.GetAsUnrestrictedDouble(); // Step 5.2.1. If radius is infinite or NaN, then return. if (!std::isfinite(r)) { return;
}
// Step 5.2.2. If radius is negative, then throw a RangeError. if (r < 0) {
aError.ThrowRangeError("Radius can not be negative"); return;
}
// Step 5.2.3. Otherwise append «[ "x" → radius, "y" → radius ]» to // normalizedRadii.
normalizedRadii.AppendElement(Size(gfx::Float(r), gfx::Float(r)));
}
// Step 6. Let upperLeft, upperRight, lowerRight, and lowerLeft be null.
Size upperLeft, upperRight, lowerRight, lowerLeft;
if (normalizedRadii.Length() == 4) { // Step 7. If normalizedRadii's size is 4, then set upperLeft to // normalizedRadii[0], set upperRight to normalizedRadii[1], set lowerRight // to normalizedRadii[2], and set lowerLeft to normalizedRadii[3].
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerRight = normalizedRadii[2];
lowerLeft = normalizedRadii[3];
} elseif (normalizedRadii.Length() == 3) { // Step 8. If normalizedRadii's size is 3, then set upperLeft to // normalizedRadii[0], set upperRight and lowerLeft to normalizedRadii[1], // and set lowerRight to normalizedRadii[2].
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerRight = normalizedRadii[2];
lowerLeft = normalizedRadii[1];
} elseif (normalizedRadii.Length() == 2) { // Step 9. If normalizedRadii's size is 2, then set upperLeft and lowerRight // to normalizedRadii[0] and set upperRight and lowerLeft to // normalizedRadii[1].
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[1];
lowerRight = normalizedRadii[0];
lowerLeft = normalizedRadii[1];
} else { // Step 10. If normalizedRadii's size is 1, then set upperLeft, upperRight, // lowerRight, and lowerLeft to normalizedRadii[0].
MOZ_ASSERT(normalizedRadii.Length() == 1);
upperLeft = normalizedRadii[0];
upperRight = normalizedRadii[0];
lowerRight = normalizedRadii[0];
lowerLeft = normalizedRadii[0];
}
// This is not as specified but copied from Chrome. // XXX Maybe if we implemented Step 12 (the path algorithm) per // spec this wouldn't be needed? Float x(aX), y(aY), w(aW), h(aH); bool clockwise = true; if (w < 0) { // Horizontal flip
clockwise = false;
x += w;
w = -w;
std::swap(upperLeft, upperRight);
std::swap(lowerLeft, lowerRight);
}
if (h < 0) { // Vertical flip
clockwise = !clockwise;
y += h;
h = -h;
std::swap(upperLeft, lowerLeft);
std::swap(upperRight, lowerRight);
}
// Step 11. Corner curves must not overlap. Scale all radii to prevent this: // Step 11.1. Let top be upperLeft["x"] + upperRight["x"]. Float top = upperLeft.width + upperRight.width; // Step 11.2. Let right be upperRight["y"] + lowerRight["y"]. Float right = upperRight.height + lowerRight.height; // Step 11.3. Let bottom be lowerRight["x"] + lowerLeft["x"]. Float bottom = lowerRight.width + lowerLeft.width; // Step 11.4. Let left be upperLeft["y"] + lowerLeft["y"]. Float left = upperLeft.height + lowerLeft.height; // Step 11.5. Let scale be the minimum value of the ratios w / top, h / right, // w / bottom, h / left. Float scale = std::min({w / top, h / right, w / bottom, h / left}); // Step 11.6. If scale is less than 1, then set the x and y members of // upperLeft, upperRight, lowerLeft, and lowerRight to their current values // multiplied by scale. if (scale < 1.0f) {
upperLeft = upperLeft * scale;
upperRight = upperRight * scale;
lowerLeft = lowerLeft * scale;
lowerRight = lowerRight * scale;
}
// Step 12. Create a new subpath: // Step 13. Mark the subpath as closed. // Note: Implemented by AppendRoundedRectToPath, which is shared with CSS // borders etc.
gfx::Rect rect{x, y, w, h};
RectCornerRadii cornerRadii(upperLeft, upperRight, lowerRight, lowerLeft);
AppendRoundedRectToPath(aPathBuilder, rect, cornerRadii, clockwise,
aTransform);
// Step 14. Create a new subpath with the point (x, y) as the only point in // the subpath. // XXX We don't seem to be doing this for ::Rect either?
}
// NOTE: IsTargetValid() may be false here (mTarget == sErrorTarget) but we // go ahead and create a path anyway since callers depend on that. if (NS_WARN_IF(!mTarget)) { returnfalse;
}
FillRule fillRule = CurrentState().fillRule;
if (mPathTransformDirty) {
FlushPathTransform();
}
SetFontInternal(aFont, aError); if (aError.Failed()) { return;
}
// Setting the font attribute magically resets fontVariantCaps and // fontStretch to normal. // (spec unclear, cf. https://github.com/whatwg/html/issues/8103)
SetFontVariantCaps(CanvasFontVariantCaps::Normal);
SetFontStretch(CanvasFontStretch::Normal);
// If letterSpacing or wordSpacing is present, recompute to account for // changes to font-relative dimensions.
UpdateSpacing();
}
staticfloat QuantizeFontSize(float aSize) { // Based on the Veltkamp-Dekker float-splitting algorithm, see e.g. // https://indico.cern.ch/event/313684/contributions/1687773/attachments/600513/826490/FPArith-Part2.pdf // A 32-bit float has 24 bits of precision (23 stored, plus an implicit 1 bit // at the start of the mantissa).
constexpr int bitsToDrop = 17; // leaving 7 bits of precision
constexpr int scale = 1 << bitsToDrop; float d = aSize * (scale + 1); float t = d - aSize; return d - t;
}
// 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);
// Quantize font size to avoid filling caches with thousands of fonts that // differ by imperceptibly-tiny size deltas.
resizedFont.size = StyleCSSPixelLength::FromPixels(
QuantizeFontSize(resizedFont.size.ToCSSPixels()));
// fontStretch handling: if fontStretch is not 'normal', apply it; // if it is normal, then use whatever the shorthand set. // XXX(jfkthame) The interaction between the shorthand and the separate attr // here is not clearly spec'd, and we may want to reconsider it (or revise // the available values); see https://github.com/whatwg/html/issues/8103. switch (CurrentState().fontStretch) { case CanvasFontStretch::Normal: // Leave whatever the shorthand set. break; case CanvasFontStretch::Ultra_condensed:
resizedFont.stretch = StyleFontStretch::ULTRA_CONDENSED; break; case CanvasFontStretch::Extra_condensed:
resizedFont.stretch = StyleFontStretch::EXTRA_CONDENSED; break; case CanvasFontStretch::Condensed:
resizedFont.stretch = StyleFontStretch::CONDENSED; break; case CanvasFontStretch::Semi_condensed:
resizedFont.stretch = StyleFontStretch::SEMI_CONDENSED; break; case CanvasFontStretch::Semi_expanded:
resizedFont.stretch = StyleFontStretch::SEMI_EXPANDED; break; case CanvasFontStretch::Expanded:
resizedFont.stretch = StyleFontStretch::EXPANDED; break; case CanvasFontStretch::Extra_expanded:
resizedFont.stretch = StyleFontStretch::EXTRA_EXPANDED; break; case CanvasFontStretch::Ultra_expanded:
resizedFont.stretch = StyleFontStretch::ULTRA_EXPANDED; break; default:
MOZ_ASSERT_UNREACHABLE("unknown stretch value"); break;
}
// fontVariantCaps handling: if fontVariantCaps is not 'normal', apply it; // if it is, then use the smallCaps boolean from the shorthand. // XXX(jfkthame) The interaction between the shorthand and the separate attr // here is not clearly spec'd, and we may want to reconsider it (or revise // the available values); see https://github.com/whatwg/html/issues/8103. switch (CurrentState().fontVariantCaps) { case CanvasFontVariantCaps::Normal: // Leave whatever the shorthand set. break; case CanvasFontVariantCaps::Small_caps:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_SMALLCAPS; break; case CanvasFontVariantCaps::All_small_caps:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_ALLSMALL; break; case CanvasFontVariantCaps::Petite_caps:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_PETITECAPS; break; case CanvasFontVariantCaps::All_petite_caps:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_ALLPETITE; break; case CanvasFontVariantCaps::Unicase:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_UNICASE; break; case CanvasFontVariantCaps::Titling_caps:
resizedFont.variantCaps = NS_FONT_VARIANT_CAPS_TITLING; break; default:
MOZ_ASSERT_UNREACHABLE("unknown caps value"); break;
}
staticvoid 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());
aUsedFont.Append(" ");
}
// font-stretch is serialized using CSS Fonts 3 keywords, not percentages. if (!aStyle.stretch.IsNormal() &&
Servo_FontStretch_SerializeKeyword(&aStyle.stretch, &aUsedFont)) {
aUsedFont.Append(" ");
}
if (aStyle.variantCaps == NS_FONT_VARIANT_CAPS_SMALLCAPS) {
aUsedFont.Append("small-caps ");
}
// Serialize the computed (not specified) size, and the family name(s).
aUsedFont.AppendFloat(aStyle.size);
aUsedFont.Append("px ");
aUsedFont.Append(FamilyListToString(aList));
}
if (NS_WARN_IF(!urlExtraData)) { // Provided we have a FontFaceSetImpl object, this should only happen on // worker threads, where we failed to initialize the worker before it was // shutdown.
aError.ThrowInvalidStateError("Missing URLExtraData"); returnfalse;
}
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; bool smallCaps = false; if (!ServoCSSParser::ParseFontShorthandForMatching(
aFont, urlExtraData, list, fontStyle.style, fontStyle.stretch,
fontStyle.weight, &size, &smallCaps)) { returnfalse;
}
switch (CurrentState().fontStretch) { case CanvasFontStretch::Normal: // Leave whatever the shorthand set. break; case CanvasFontStretch::Ultra_condensed:
fontStyle.stretch = StyleFontStretch::ULTRA_CONDENSED; break; case CanvasFontStretch::Extra_condensed:
fontStyle.stretch = StyleFontStretch::EXTRA_CONDENSED; break; case CanvasFontStretch::Condensed:
fontStyle.stretch = StyleFontStretch::CONDENSED; break; case CanvasFontStretch::Semi_condensed:
fontStyle.stretch = StyleFontStretch::SEMI_CONDENSED; break; case CanvasFontStretch::Semi_expanded:
fontStyle.stretch = StyleFontStretch::SEMI_EXPANDED; break; case CanvasFontStretch::Expanded:
fontStyle.stretch = StyleFontStretch::EXPANDED; break; case CanvasFontStretch::Extra_expanded:
fontStyle.stretch = StyleFontStretch::EXTRA_EXPANDED; break; case CanvasFontStretch::Ultra_expanded:
fontStyle.stretch = StyleFontStretch::ULTRA_EXPANDED; break; default:
MOZ_ASSERT_UNREACHABLE("unknown stretch value"); break;
}
// fontVariantCaps handling: if fontVariantCaps is not 'normal', apply it; // if it is, then use the smallCaps boolean from the shorthand. // XXX(jfkthame) The interaction between the shorthand and the separate attr // here is not clearly spec'd, and we may want to reconsider it (or revise // the available values); see https://github.com/whatwg/html/issues/8103. switch (CurrentState().fontVariantCaps) { case CanvasFontVariantCaps::Normal:
fontStyle.variantCaps = smallCaps ? NS_FONT_VARIANT_CAPS_SMALLCAPS
: NS_FONT_VARIANT_CAPS_NORMAL; break; case CanvasFontVariantCaps::Small_caps:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_SMALLCAPS; break; case CanvasFontVariantCaps::All_small_caps:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_ALLSMALL; break; case CanvasFontVariantCaps::Petite_caps:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_PETITECAPS; break; case CanvasFontVariantCaps::All_petite_caps:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_ALLPETITE; break; case CanvasFontVariantCaps::Unicase:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_UNICASE; break; case CanvasFontVariantCaps::Titling_caps:
fontStyle.variantCaps = NS_FONT_VARIANT_CAPS_TITLING; break; default:
MOZ_ASSERT_UNREACHABLE("unknown caps value"); break;
} // If variantCaps is set, we need to disable a gfxFont fast-path.
fontStyle.noFallbackVariantFeatures =
(fontStyle.variantCaps == NS_FONT_VARIANT_CAPS_NORMAL);
// Set the kerning feature, if required by the fontKerning attribute.
gfxFontFeature setting{TRUETYPE_TAG('k', 'e', 'r', 'n'), 0}; switch (CurrentState().fontKerning) { case CanvasFontKerning::None:
setting.mValue = 0;
fontStyle.featureSettings.AppendElement(setting); break; case CanvasFontKerning::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();
}
} else { // Pass the OS default language, to behave similarly to HTML or canvas- // element content with no language tag.
language = nsLanguageAtomService::GetService()->GetLocaleLanguage();
}
// TODO: Cache fontGroups in the Worker (use an nsFontCache?)
gfxFontGroup* fontGroup = new gfxFontGroup(nullptr, // aPresContext
list, // aFontFamilyList
&fontStyle, // aStyle
language, // aLanguage
explicitLanguage, // aExplicitLanguage
nullptr, // aTextPerf
fontFaceSetImpl, // aUserFontSet
1.0, // aDevToCssSize
StyleFontVariantEmoji::Normal);
CurrentState().fontGroup = fontGroup;
SerializeFontForCanvas(list, fontStyle, CurrentState().font);
CurrentState().fontFont = nsFont(StyleFontFamily{list, false, false},
StyleCSSPixelLength::FromPixels(size));
CurrentState().fontFont.variantCaps = fontStyle.variantCaps;
CurrentState().fontLanguage = nullptr;
CurrentState().fontExplicitLanguage = false; // We don't have any computed style, assume normal height.
CurrentState().fontLineHeight = StyleLineHeight::Normal(); returntrue;
}
void CanvasRenderingContext2D::UpdateSpacing() { auto state = CurrentState(); if (!state.letterSpacingStr.IsEmpty()) {
SetLetterSpacing(state.letterSpacingStr);
} if (!state.wordSpacingStr.IsEmpty()) {
SetWordSpacing(state.wordSpacingStr);
}
}
/* * 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). * We also replace characters with Bidi type Segment Separator or Block * Separator. * @param str The string whose whitespace characters to replace.
*/ staticinlinevoid TextReplaceWhitespaceCharacters(nsAutoString& aStr) {
aStr.ReplaceChar(u"\x09\x0A\x0B\x0C\x0D\x1C\x1D\x1E\x1F\x85\x2029",
char16_t(' '));
}
void CanvasRenderingContext2D::FillText(const nsAString& aText, double aX, double aY, const Optional<double>& aMaxWidth,
ErrorResult& aError) { // We try to match the most commonly observed strings used by canvas // fingerprinting scripts. We do a prefix match, because that means having to // match fewer bytes and sometimes the strings is followed by a few random // characters. // - Cwm fjordbank gly // Used by FingerprintJS // (https://github.com/fingerprintjs/fingerprintjs/blob/4c4b2c8455e701b8341b2b766d1939cf5de4b615/src/sources/canvas.ts#L119) // and others // - Hel$&?6%){mZ+#@ // - <@nv45. F1n63r,Pr1n71n6! // Usually there are at most a handful (usually ~1/2) fillText calls by // fingerprinters if (mFillTextCalls <= 5) { if (StringBeginsWith(aText, u"Cwm fjord"_ns) ||
StringBeginsWith(aText, u"Hel$&?6%"_ns) ||
StringBeginsWith(aText, u"<@nv45. "_ns)) {
mFeatureUsage |= CanvasFeatureUsage::KnownFingerprintText;
}
mFillTextCalls++;
}
/** * Used for nsBidiPresUtils::ProcessText
*/ struct MOZ_STACK_CLASS CanvasBidiProcessor final
: public nsBidiPresUtils::BidiProcessor { using Style = CanvasRenderingContext2D::Style;
~CanvasBidiProcessor() { // notify front-end code if we encountered missing glyphs in any script if (mMissingFonts) {
mMissingFonts->Flush();
}
}
class PropertyProvider : public gfxTextRun::PropertyProvider { public: explicit PropertyProvider(const CanvasBidiProcessor& aProcessor)
: mProcessor(aProcessor) {}
void GetSpacing(gfxTextRun::Range aRange,
gfxFont::Spacing* aSpacing) const { for (auto i = aRange.start; i < aRange.end; ++i) { auto* charGlyphs = mProcessor.mTextRun->GetCharacterGlyphs(); if (i == mProcessor.mTextRun->GetLength() - 1 ||
(charGlyphs[i + 1].IsClusterStart() &&
charGlyphs[i + 1].IsLigatureGroupStart())) { // Currently we add all the letterspacing to the right of the glyph, // which is similar to Chrome's behavior, though the LTR vs RTL // asymmetry seems unfortunate. if (mProcessor.mTextRun->IsRightToLeft()) {
aSpacing->mAfter = 0;
aSpacing->mBefore = mProcessor.mLetterSpacing;
} else {
aSpacing->mBefore = 0;
aSpacing->mAfter = mProcessor.mLetterSpacing;
}
} else {
aSpacing->mBefore = 0;
aSpacing->mAfter = 0;
} if (charGlyphs[i].CharIsSpace()) { if (mProcessor.mTextRun->IsRightToLeft()) {
aSpacing->mBefore += mProcessor.mWordSpacing;
} else {
aSpacing->mAfter += mProcessor.mWordSpacing;
}
}
aSpacing++;
}
}
// 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);
}
// 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, &provider);
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; const ContextState& state = mCtx->CurrentState();
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 = nullptr;
// position of the left side of the string, alphabetic baseline
gfx::Point mPt;
// current font
gfxFontGroup* mFontgrp = nullptr;
// palette cache for COLR font rendering
mozilla::gfx::PaletteCache& mPaletteCache;
// spacing adjustments to be applied
gfx::Float mLetterSpacing = 0.0f;
gfx::Float mWordSpacing = 0.0f;
// 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 = 0;
// operation (fill or stroke)
CanvasRenderingContext2D::TextDrawOperation mOp =
CanvasRenderingContext2D::TextDrawOperation::FILL;
// 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 = gfx::ShapedTextFlags();
// Count of how many times SetText has been called on this processor.
uint32_t mSetTextCount = 0;
// true iff the bounding box should be measured bool mDoMeasureBoundingBox = false;
// true if future SetText calls should be ignored bool mIgnoreSetText = false;
};
// replace all the whitespace characters with U+0020 SPACE
nsAutoString textToDraw(aText);
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 || std::isnan(aMaxWidth.Value()))) {
textToDraw.Truncate();
}
RefPtr<const ComputedStyle> canvasStyle; if (mCanvasElement) {
canvasStyle = nsComputedDOMStyle::GetComputedStyle(mCanvasElement);
}
// Get text direction, either from the property or inherited from context. const ContextState& state = CurrentState(); bool isRTL; switch (state.textDirection) { case CanvasDirection::Ltr:
isRTL = false; break; case CanvasDirection::Rtl:
isRTL = true; break; case CanvasDirection::Inherit: if (canvasStyle) {
isRTL =
canvasStyle->StyleVisibility()->mDirection == StyleDirection::Rtl;
} elseif (document) {
isRTL = GET_BIDI_OPTION_DIRECTION(document->GetBidiOptions()) ==
IBMBIDI_TEXTDIRECTION_RTL;
} else {
isRTL = false;
} break; default:
MOZ_CRASH("unknown direction!");
}
// This is only needed to know if we can know the drawing bounding box easily. constbool doCalculateBounds = NeedToCalculateBounds(); if (presShell && presShell->IsDestroying()) {
aError = NS_ERROR_FAILURE; return nullptr;
}
if (presContext) { // ensure user font set is up to date
presContext->Document()->FlushUserFontSet();
currentFontStyle->SetUserFontSet(presContext->GetUserFontSet());
}
if (!std::isfinite(aX) || !std::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 MakeUnique<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(mPaletteCache);
// 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();
switch (state.textRendering) { case CanvasTextRendering::Auto: if (state.fontFont.size.ToCSSPixels() <
StaticPrefs::browser_display_auto_quality_min_font_size()) {
processor.mTextRunFlags |= gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED;
} else {
processor.mTextRunFlags &= ~gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED;
} break; case CanvasTextRendering::OptimizeSpeed:
processor.mTextRunFlags |= gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; break; case CanvasTextRendering::OptimizeLegibility: case CanvasTextRendering::GeometricPrecision:
processor.mTextRunFlags &= ~gfx::ShapedTextFlags::TEXT_OPTIMIZE_SPEED; break; default:
MOZ_CRASH("unknown textRendering!");
}
// 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;
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;
}
// offset pt.y (or pt.x, for vertical text) based on text baseline
gfxFloat baselineAnchor;
switch (state.textBaseline) { case CanvasTextBaseline::Hanging:
baselineAnchor = font->GetBaselines(fontOrientation).mHanging; break; case CanvasTextBaseline::Top:
baselineAnchor = fontMetrics.emAscent; break; case CanvasTextBaseline::Middle:
baselineAnchor = (fontMetrics.emAscent - fontMetrics.emDescent) * .5f; break; case CanvasTextBaseline::Alphabetic:
baselineAnchor = font->GetBaselines(fontOrientation).mAlphabetic; break; case CanvasTextBaseline::Ideographic:
baselineAnchor = font->GetBaselines(fontOrientation).mIdeographic; break; case CanvasTextBaseline::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. if (runOrientation != gfx::ShapedTextFlags::TEXT_ORIENT_HORIZONTAL) { if (fontOrientation == nsFontMetrics::eVertical) { // 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; auto baselines = font->GetBaselines(fontOrientation); return MakeUnique<TextMetrics>(
totalWidth, actualBoundingBoxLeft, actualBoundingBoxRight,
fontMetrics.maxAscent - baselineAnchor, // fontBBAscent
fontMetrics.maxDescent + baselineAnchor, // fontBBDescent
actualBoundingBoxAscent, actualBoundingBoxDescent,
fontMetrics.emAscent - baselineAnchor, // emHeightAscent
fontMetrics.emDescent + baselineAnchor, // emHeightDescent
baselines.mHanging - baselineAnchor,
baselines.mAlphabetic - baselineAnchor,
baselines.mIdeographic - baselineAnchor);
}
// 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));
}
Matrix oldTransform = mTarget->GetTransform(); bool restoreTransform = false; // 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);
restoreTransform = true;
}
// save the previous bounding box
gfxRect boundingBox = processor.mBoundingBox;
// don't ever need to measure the bounding box twice
processor.mDoMeasureBoundingBox = false;
// 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; constfloat 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); constauto* sans =
Servo_FontFamily_Generic(StyleGenericFontFamily::SansSerif);
fontGroup = new gfxFontGroup(
presContext, sans->families, &style, language, explicitLanguage,
presContext ? presContext->GetTextPerfMetrics() : nullptr, nullptr,
devToCssSize, StyleFontVariantEmoji::Normal); if (fontGroup) {
CurrentState().font = kDefaultFontStyle;
} else {
NS_ERROR("Default canvas font is invalid");
}
}
}
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;
}
}
}
// Check for site-specific permission and return false if no permission. if (mCanvasElement) {
nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc(); if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
aSubjectPrincipal)) { returnfalse;
}
} elseif (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) { returnfalse;
}
EnsureUserSpacePath(aWinding); if (!mPath) { returnfalse;
}
// Check for site-specific permission and return false if no permission. if (mCanvasElement) {
nsCOMPtr<Document> ownerDoc = mCanvasElement->OwnerDoc(); if (!CanvasUtils::IsImageExtractionAllowed(ownerDoc, aCx,
aSubjectPrincipal)) { returnfalse;
}
} elseif (mOffscreenCanvas && mOffscreenCanvas->ShouldResistFingerprinting(
RFPTarget::CanvasImageExtractionPrompt)) { returnfalse;
}
EnsureUserSpacePath(); if (!mPath) { returnfalse;
}
// 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();
}
// Try to extract an optimized sub-surface. if (RefPtr<SourceSurface> surface =
aSurface->ExtractSubrect(roundedOutSourceRectInt)) {
*aSourceRect -= roundedOutSourceRect.TopLeft(); return surface.forget();
}
// 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, mTarget); if (!res.mSourceSurface) { return res;
}
auto& sdv = sd.ref().get_SurfaceDescriptorGPUVideo(); constauto& sdvType = sdv.type(); if (sdvType ==
layers::SurfaceDescriptorGPUVideo::TSurfaceDescriptorRemoteDecoder) { auto& sdrd = sdv.get_SurfaceDescriptorRemoteDecoder(); auto& subdesc = sdrd.subdesc(); constauto& subdescType = subdesc.type(); if (subdescType == layers::RemoteDecoderVideoSubDescriptor::Tnull_t) { return sd;
} if (subdescType == layers::RemoteDecoderVideoSubDescriptor::
TSurfaceDescriptorMacIOSurface) { return sd;
} if (subdescType ==
layers::RemoteDecoderVideoSubDescriptor::TSurfaceDescriptorD3D10 &&
StaticPrefs::gfx_canvas_remote_use_draw_image_fast_path_d3d()) { auto& descD3D10 = subdesc.get_SurfaceDescriptorD3D10(); if (descD3D10.gpuProcessQueryId().isSome() &&
descD3D10.gpuProcessQueryId().ref().mOnlyForOverlay) { return Nothing();
} // Clear FileHandleWrapper, since FileHandleWrapper::mHandle could not be // cross process delivered by using Shmem. Cross-process delivery of // FileHandleWrapper::mHandle is not possible simply by using shmen. When // it is tried, parent side process just causes crash during destroying // FileHandleWrapper.
descD3D10.handle() = nullptr; return sd;
}
}
return Nothing();
}
// 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.
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_ALLOW_UNCROPPED_UNSCALED;
if (offscreenCanvas) {
res = nsLayoutUtils::SurfaceFromOffscreenCanvas(offscreenCanvas, sfeFlags,
mTarget);
} elseif (videoFrame) {
res = nsLayoutUtils::SurfaceFromVideoFrame(videoFrame, sfeFlags, mTarget);
} else {
res = CanvasRenderingContext2D::CachedSurfaceFromElement(element); if (!res.mSourceSurface) {
HTMLVideoElement* video = HTMLVideoElement::FromNodeOrNull(element); if (video && mBufferProvider->IsAccelerated() &&
mTarget->IsRecording() &&
!(!NeedToApplyFilter() && NeedToDrawShadow())) {
res = nsLayoutUtils::SurfaceFromElement(
video, sfeFlags, mTarget, /* aOptimizeSourceSurface */ false);
surfaceDescriptor = MaybeGetSurfaceDescriptorForRemoteCanvas(res); if (surfaceDescriptor.isNothing() && res.mLayersImage) { if ((res.mSourceSurface = res.mLayersImage->GetAsSourceSurface())) {
RefPtr<SourceSurface> opt =
mTarget->OptimizeSourceSurface(res.mSourceSurface); if (opt) {
res.mSourceSurface = opt;
}
}
}
} else {
res = nsLayoutUtils::SurfaceFromElement(element, sfeFlags, mTarget);
}
}
}
if (surfaceDescriptor.isNothing()) {
srcSurf = res.GetSourceSurface();
}
if (!srcSurf && surfaceDescriptor.isNothing() &&
!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\"");
} elseif (videoFrame) {
aError.ThrowInvalidStateError("Passed-in video frame is \"broken\"");
} return;
}
// Any provided coordinates are in the display space, or the same as the // intrinsic size. In order to get to the surface coordinate space, we may // need to adjust for scaling and/or cropping. If no source coordinates are // provided, then we can just directly use the actual surface size. if (aOptional_argc == 0) {
aSx = clipOriginX;
aSy = clipOriginY;
aSw = clipWidth;
aSh = clipHeight;
aDw = (double)intrinsicImgSize.width;
aDh = (double)intrinsicImgSize.height;
} elseif (aOptional_argc == 2) {
aSx = clipOriginX;
aSy = clipOriginY;
aSw = clipWidth;
aSh = clipHeight;
} elseif (cropRect || intrinsicImgSize != imgSize) { // We need to first scale between the cropped size and the intrinsic size, // and then adjust for the offset from the crop rect. double scaleXToCrop = clipWidth / intrinsicImgSize.width; double scaleYToCrop = clipHeight / intrinsicImgSize.height;
aSx = aSx * scaleXToCrop + clipOriginX;
aSy = aSy * scaleYToCrop + clipOriginY;
aSw = aSw * scaleXToCrop;
aSh = aSh * scaleYToCrop;
}
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;
if (!IsTargetValid()) {
aError.Throw(NS_ERROR_FAILURE); return;
}
if (srcSurf || surfaceDescriptor.isSome()) {
gfx::Rect sourceRect(aSx, aSy, aSw, aSh); if (srcSurf && ((element && element == mCanvasElement) ||
(offscreenCanvas && offscreenCanvas == mOffscreenCanvas))) { // 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); // The SFE result may inadvertently keep the snapshot alive, forcing a // copy when MarkChanged is called. Clear out possibly the last reference // to the original snapshot to avoid this.
res.mSourceSurface = nullptr;
}
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;
}
if (srcSurf) {
MOZ_ASSERT(surfaceDescriptor.isNothing());
// 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);
// 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;
}
// 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;
}
Maybe<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.emplace(mTarget);
thebes.ref().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;
}
Unused << presShell->RenderDocument(r, renderDocFlags, *backgroundColor,
&thebes.ref()); // 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. if (!EnsureTarget(aError, discardContent ? &drawRect : nullptr)) { return;
}
MOZ_ASSERT(IsTargetValid());
if (drawDT) {
RefPtr<SourceSurface> snapshot = drawDT->Snapshot(); if (NS_WARN_IF(!snapshot)) {
aError.Throw(NS_ERROR_FAILURE); return;
}
// 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) { 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. if (IsWriteOnly() ||
(mCanvasElement && !mCanvasElement->CallerCanRead(aSubjectPrincipal)) ||
(mOffscreenCanvas &&
!mOffscreenCanvas->CallerCanRead(aSubjectPrincipal))) { // 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;
}
// Restrict the typed array length to INT32_MAX because that's all we support.
CheckedInt<uint32_t> len = CheckedInt<uint32_t>(aWidth) * aHeight * 4; if (!len.isValid() || len.value() > INT32_MAX) { return NS_ERROR_DOM_INDEX_SIZE_ERR;
}
// Check for site-specific permission.
CanvasUtils::ImageExtraction permission =
CanvasUtils::ImageExtraction::Unrestricted; if (mCanvasElement) {
permission = CanvasUtils::ImageExtractionResult(mCanvasElement, aCx,
aSubjectPrincipal);
} elseif (mOffscreenCanvas) {
permission = CanvasUtils::ImageExtractionResult(mOffscreenCanvas, aCx,
aSubjectPrincipal);
}
// Clone the data source surface if canvas randomization is enabled. We need // to do this because we don't want to alter the actual image buffer. // Otherwise, we will provide inconsistent image data with multiple calls. // // Note that we don't need to clone if we will use the place holder because // the place holder doesn't use actual image data. if (permission == CanvasUtils::ImageExtraction::Randomize) { if (readback) {
readback = CreateDataSourceSurfaceByCloning(readback);
}
}
do {
uint8_t* randomData; if (permission == CanvasUtils::ImageExtraction::Placeholder) { // 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();
} elseif (permission == CanvasUtils::ImageExtraction::Randomize) { // Apply the random noises if canvan randomization is enabled. We don't // need to calculate random noises if we are going to use the place // holder.
JS::AutoCheckCannotGC nogc; bool isShared;
uint8_t* data = JS_GetUint8ClampedArrayData(darray, &isShared, nogc);
MOZ_ASSERT(!isShared); // Should not happen, data was created above
if (permission == CanvasUtils::ImageExtraction::Placeholder) {
FillPlaceholderCanvas(randomData, len.value(), data); break;
}
if (aHasDirtyRect) { // fix up negative dimensions if (aDirtyWidth < 0) { if (aDirtyWidth == INT_MIN) { return aRv.ThrowInvalidStateError("Dirty width is invalid");
}
// 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); if (!EnsureTarget(aRv, &putRect, true, true)) { return;
}
// 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);
}
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(), aError); if (aError.Failed()) { return nullptr;
}
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(); returnfalse;
}
if (!mBufferProvider) { // Force the creation of a buffer provider.
EnsureTarget();
ReturnTarget(); if (!mBufferProvider) {
MarkContextClean(); returnfalse;
}
}
inlinevoid CanvasPath::EnsureCapped() const { // If there were zero-length segments emitted that were pruned, we need to // emit a LineTo to ensure that caps are generated for the segment. if (mPruned) {
mPathBuilder->LineTo(mPathBuilder->CurrentPoint());
mPruned = false;
}
}
inlinevoid CanvasPath::EnsureActive() const { // If the path is not active, then adding an op to the path may cause the path // to add the first point of the op as the initial point instead of the actual // current point. if (mPruned && !mPathBuilder->IsActive()) {
mPathBuilder->MoveTo(mPathBuilder->CurrentPoint());
mPruned = false;
}
}
if (!mPath) { // if there is no path, there must be a pathbuilder
MOZ_ASSERT(mPathBuilder);
EnsureCapped();
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();
} elseif (mPath->GetFillRule() != fillRule) {
Path::SetFillRule(mPath, fillRule);
}
// 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.
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.