Skip to content

Commit

Permalink
Bug 1850871 - Resize canvas in ImageBitmapRenderingContext::TransferF…
Browse files Browse the repository at this point in the history
…romImageBitmap. r=gfx-reviewers,lsalzman

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 whatwg/html#7833.

Differential Revision: https://phabricator.services.mozilla.com/D188126
  • Loading branch information
aosmond committed Oct 16, 2023
1 parent 30fcbc5 commit 0329c1a
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 51 deletions.
22 changes: 22 additions & 0 deletions dom/canvas/ImageBitmapRenderingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}

Expand Down
17 changes: 17 additions & 0 deletions dom/canvas/OffscreenCanvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<JS::Value> aContextOptions,
Expand Down
2 changes: 2 additions & 0 deletions dom/canvas/OffscreenCanvas.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<nsIPrincipal>&& aExpandedReader);
Expand Down
96 changes: 51 additions & 45 deletions dom/canvas/test/test_bitmaprenderer.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -89,52 +89,58 @@
});
}

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 + ": refCanvas width " + refCanvas.width + " is ref " + refWidth);
is(refCanvas.height, refHeight, name + ": refCanvas height " + refCanvas.height + " is ref " + 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;
is(canvas.width, refWidth, name + ": canvas width " + canvas.width + " is ref " + refWidth);
}
if (refHeight !== testHeight) {
canvas.height = refHeight;
is(canvas.height, refHeight, name + ": canvas height " + canvas.height + " is ref " + 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("grow_upscaled", 64, 64, 32, 32, 16, 16); // Canvas grows, scales up.
await scaleTestCase("same_downscaled", 64, 64, 128, 128, 128, 128); // Canvas unchanged, scales down.
await scaleTestCase("same_upscaled", 64, 64, 32, 32, 32, 32); // Canvas unchanged, scales up.
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.
await scaleTestCase("shrink_upscaled", 64, 64, 32, 32, 256, 256); // Canvas shrinks, scales up.
runTestOnWorker();
}

function runTestOnWorker() {
Expand Down
20 changes: 20 additions & 0 deletions dom/html/HTMLCanvasElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,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();
Expand Down
5 changes: 5 additions & 0 deletions dom/html/HTMLCanvasElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
6 changes: 3 additions & 3 deletions layout/generic/nsHTMLCanvasFrame.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,14 @@ class nsDisplayCanvas final : public nsPaintedDisplayItem {
if (!surface || !surface->IsValid()) {
return;
}
gfx::IntSize size = surface->GetSize();

transform = gfxUtils::SnapTransform(
transform, gfxRect(0, 0, size.width, size.height), nullptr);
transform, gfxRect(0, 0, canvasSizeInPx.width, canvasSizeInPx.height),
nullptr);
aCtx->Multiply(transform);

aCtx->GetDrawTarget()->FillRect(
Rect(0, 0, size.width, size.height),
Rect(0, 0, canvasSizeInPx.width, canvasSizeInPx.height),
SurfacePattern(surface, ExtendMode::CLAMP, Matrix(),
nsLayoutUtils::GetSamplingFilterForFrame(f)));
return;
Expand Down

This file was deleted.

0 comments on commit 0329c1a

Please sign in to comment.