From 77cdcd6158535ae27999cfe8ed4ee4812320a120 Mon Sep 17 00:00:00 2001 From: Andrew Osmond Date: Wed, 13 Sep 2023 11:20:47 -0400 Subject: [PATCH] Bug 1850871 - Resize canvas in ImageBitmapRenderingContext::TransferFromImageBitmap. This patch makes it so that when a new ImageBitmap is provided to an ImageBitmapRenderingContext, we resize its owning canvas rather than crop or scale the given ImageBitmap to fit inside the canvas dimensions. See discussion in https://github.com/whatwg/html/issues/7833. Differential Revision: https://phabricator.services.mozilla.com/D188126 --- dom/canvas/ImageBitmapRenderingContext.cpp | 22 ++++++ dom/canvas/OffscreenCanvas.cpp | 17 ++++ dom/canvas/OffscreenCanvas.h | 2 + dom/canvas/test/test_bitmaprenderer.html | 91 +++++++++++----------- dom/html/HTMLCanvasElement.cpp | 20 +++++ dom/html/HTMLCanvasElement.h | 5 ++ 6 files changed, 112 insertions(+), 45 deletions(-) diff --git a/dom/canvas/ImageBitmapRenderingContext.cpp b/dom/canvas/ImageBitmapRenderingContext.cpp index c50e7790c5fd1..2516f451a37ae 100644 --- a/dom/canvas/ImageBitmapRenderingContext.cpp +++ b/dom/canvas/ImageBitmapRenderingContext.cpp @@ -80,6 +80,18 @@ void ImageBitmapRenderingContext::TransferFromImageBitmap( return; } + // Note that this is reentrant and will call back into SetDimensions. + if (mCanvasElement) { + mCanvasElement->SetSize(mImage->GetSize(), aRv); + } else if (mOffscreenCanvas) { + mOffscreenCanvas->SetSize(mImage->GetSize(), aRv); + } + + if (NS_WARN_IF(aRv.Failed())) { + mImage = nullptr; + return; + } + if (aImageBitmap->IsWriteOnly()) { if (mCanvasElement) { mCanvasElement->SetWriteOnly(); @@ -96,6 +108,16 @@ NS_IMETHODIMP ImageBitmapRenderingContext::SetDimensions(int32_t aWidth, int32_t aHeight) { mWidth = aWidth; mHeight = aHeight; + + if (mOffscreenCanvas) { + OffscreenCanvasDisplayData data; + data.mSize = {mWidth, mHeight}; + data.mIsOpaque = GetIsOpaque(); + data.mIsAlphaPremult = true; + data.mDoPaintCallbacks = false; + mOffscreenCanvas->UpdateDisplayData(data); + } + return NS_OK; } diff --git a/dom/canvas/OffscreenCanvas.cpp b/dom/canvas/OffscreenCanvas.cpp index 09e58fedff8ca..01ad5f112eb60 100644 --- a/dom/canvas/OffscreenCanvas.cpp +++ b/dom/canvas/OffscreenCanvas.cpp @@ -147,6 +147,23 @@ void OffscreenCanvas::SetHeight(uint32_t aHeight, ErrorResult& aRv) { CanvasAttrChanged(); } +void OffscreenCanvas::SetSize(const nsIntSize& aSize, ErrorResult& aRv) { + if (mNeutered) { + aRv.ThrowInvalidStateError( + "Cannot set dimensions of placeholder canvas transferred to worker."); + return; + } + + if (NS_WARN_IF(aSize.IsEmpty())) { + aRv.ThrowRangeError("OffscreenCanvas size is empty, must be non-empty."); + return; + } + + mWidth = aSize.width; + mHeight = aSize.height; + CanvasAttrChanged(); +} + void OffscreenCanvas::GetContext( JSContext* aCx, const OffscreenRenderingContextId& aContextId, JS::Handle aContextOptions, diff --git a/dom/canvas/OffscreenCanvas.h b/dom/canvas/OffscreenCanvas.h index 0e1fc682379c0..ffe899385c548 100644 --- a/dom/canvas/OffscreenCanvas.h +++ b/dom/canvas/OffscreenCanvas.h @@ -150,6 +150,8 @@ class OffscreenCanvas final : public DOMEventTargetHelper, bool MayNeuter() const { return !mNeutered && !mCurrentContext; } + void SetSize(const nsIntSize& aSize, ErrorResult& aRv); + nsIPrincipal* GetExpandedReader() const { return mExpandedReader; } void SetWriteOnly(RefPtr&& aExpandedReader); diff --git a/dom/canvas/test/test_bitmaprenderer.html b/dom/canvas/test/test_bitmaprenderer.html index bd6d8ee846f9f..94787046b5c5b 100644 --- a/dom/canvas/test/test_bitmaprenderer.html +++ b/dom/canvas/test/test_bitmaprenderer.html @@ -43,11 +43,11 @@ ctx.fillStyle = "#00FF00"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); - var canvasRef = createCanvas(90, 90); + var canvasRef = createCanvas(canvasWidth, canvasHeight); var ctx = canvasRef.getContext("2d"); // Clear with black transparent first ctx.fillStyle = "rgba(0, 0, 0, 0)"; - ctx.fillRect(0, 0, 90, 90); + ctx.fillRect(0, 0, canvasWidth, canvasHeight); ctx.fillStyle = "#00FF00"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); @@ -89,52 +89,53 @@ }); } -function scaleTest() { - var canvas1 = createCanvas(64, 64); - var ctx = canvas1.getContext("2d"); +function createSolidGreenBitmap(width, height) { + const canvas = createCanvas(width, height); + const ctx = canvas.getContext("2d"); ctx.fillStyle = "#00FF00"; - ctx.fillRect(0, 0, 64, 64); - - var canvas2 = createCanvas(64, 64); - var ctx2 = canvas2.getContext("2d"); - ctx2.fillStyle = "#00FF00"; - ctx2.fillRect(0, 0, 64, 64); + ctx.fillRect(0, 0, width, height); + const bitmap = createImageBitmap(canvas); + document.body.removeChild(canvas); + return bitmap; +} - var p1 = createImageBitmap(canvas1); - var p2 = createImageBitmap(canvas2); - Promise.all([p1, p2]).then(async function(bitmaps) { - document.body.removeChild(canvas1); - document.body.removeChild(canvas2); +async function scaleTestCase(name, refWidth, refHeight, testWidth, testHeight, canvasWidth, canvasHeight) { + const refBitmap = await createSolidGreenBitmap(refWidth, refHeight); + const refCanvas = createCanvas(refWidth, refHeight); + const refCtx = refCanvas.getContext("bitmaprenderer"); + refCtx.transferFromImageBitmap(refBitmap); + is(refCanvas.width, refWidth, name + ": canvas width " + refCanvas.width + " is reference " + refWidth); + is(refCanvas.height, refHeight, name + ": canvas height " + refCanvas.height + " is reference " + refHeight); + const refSnapshot = await snapshotWindow(window); + document.body.removeChild(refCanvas); + + const bitmap = await createSolidGreenBitmap(testWidth, testHeight); + const canvas = createCanvas(canvasWidth, canvasHeight); + const ctx = canvas.getContext("bitmaprenderer"); + ctx.transferFromImageBitmap(bitmap); + is(canvas.width, testWidth, name + ": canvas width " + canvas.width + " is bitmap " + testWidth); + is(canvas.height, testHeight, name + ": canvas height " + canvas.height + " is bitmap " + testHeight); + if (refWidth !== testWidth) { + canvas.width = refWidth; + } + if (refHeight !== testHeight) { + canvas.height = refHeight; + } + const snapshot = await snapshotWindow(window); + document.body.removeChild(canvas); + + const results = compareSnapshots(snapshot, refSnapshot, true); + ok(results[0], name + ": screenshots should be the same"); + return Promise.resolve(); +} - // Create a large canvas then shrink. - var canvas3 = createCanvas(128, 128); - var ctx3 = canvas3.getContext("bitmaprenderer"); - ctx3.transferFromImageBitmap(bitmaps[0]); - var snapshotLargeRef = await snapshotWindow(window); - - canvas3.width = 32; - canvas3.height = 32; - var snapshotSmall = await snapshotWindow(window); - document.body.removeChild(canvas3); - - // Create a small canvas then grow. - var canvas4 = createCanvas(32, 32); - var ctx4 = canvas4.getContext("bitmaprenderer"); - ctx4.transferFromImageBitmap(bitmaps[1]); - var snapshotSmallRef = await snapshotWindow(window); - - canvas4.width = 128; - canvas4.height = 128; - var snapshotLarge = await snapshotWindow(window); - document.body.removeChild(canvas4); - - var resultsLarge = compareSnapshots(snapshotLarge, snapshotLargeRef, true); - ok(resultsLarge[0], "Screenshots should be the same"); - - var resultsSmall = compareSnapshots(snapshotSmall, snapshotSmallRef, true); - ok(resultsSmall[0], "Screenshots should be the same"); - runTestOnWorker(); - }); +async function scaleTest() { + await scaleTestCase("grow_unscaled", 64, 64, 64, 64, 32, 32); // Canvas grows, no scaling. + await scaleTestCase("grow_downscaled", 64, 64, 128, 128, 32, 32); // Canvas grows, scales down. + await scaleTestCase("same_downscaled", 64, 64, 128, 128, 128, 128); // Canvas unchanged, scales down. + await scaleTestCase("shrink_unscaled", 64, 64, 64, 64, 128, 128); // Canvas shrinks, no scaling. + await scaleTestCase("shrink_downscaled", 64, 64, 128, 128, 256, 256); // Canvas shrinks, scales down. + runTestOnWorker(); } function runTestOnWorker() { diff --git a/dom/html/HTMLCanvasElement.cpp b/dom/html/HTMLCanvasElement.cpp index c213960e6bb1b..d76f9a28176f8 100644 --- a/dom/html/HTMLCanvasElement.cpp +++ b/dom/html/HTMLCanvasElement.cpp @@ -1095,6 +1095,26 @@ void HTMLCanvasElement::SetHeight(uint32_t aHeight, ErrorResult& aRv) { SetUnsignedIntAttr(nsGkAtoms::height, aHeight, DEFAULT_CANVAS_HEIGHT, aRv); } +void HTMLCanvasElement::SetSize(const nsIntSize& aSize, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.ThrowInvalidStateError( + "Cannot set width of placeholder canvas transferred to " + "OffscreenCanvas."); + return; + } + + if (NS_WARN_IF(aSize.IsEmpty())) { + aRv.ThrowRangeError("Canvas size is empty, must be non-empty."); + return; + } + + SetUnsignedIntAttr(nsGkAtoms::width, aSize.width, DEFAULT_CANVAS_WIDTH, aRv); + MOZ_ASSERT(!aRv.Failed()); + SetUnsignedIntAttr(nsGkAtoms::height, aSize.height, DEFAULT_CANVAS_HEIGHT, + aRv); + MOZ_ASSERT(!aRv.Failed()); +} + void HTMLCanvasElement::FlushOffscreenCanvas() { if (mOffscreenDisplay) { mOffscreenDisplay->FlushForDisplay(); diff --git a/dom/html/HTMLCanvasElement.h b/dom/html/HTMLCanvasElement.h index 757f537e8ffd9..0deb7336cc85c 100644 --- a/dom/html/HTMLCanvasElement.h +++ b/dom/html/HTMLCanvasElement.h @@ -175,6 +175,11 @@ class HTMLCanvasElement final : public nsGenericHTMLElement, */ nsIntSize GetSize(); + /** + * Set the size in pixels of this canvas element. + */ + void SetSize(const nsIntSize& aSize, ErrorResult& aRv); + /** * Determine whether the canvas is write-only. */