Skip to content

Commit

Permalink
Throw an exception in putImageData if canvas layers are opened
Browse files Browse the repository at this point in the history
This API is incompatible with how the 2D canvas is rasterized when
it contains unclosed layers. Because layers can have filters that get
applied on their final content, they can't be presented until they are
closed. Instead, we normally keep the layer content alive after a
flush, so that it can be presented in a later frame when the layer is
finally closed.

putImageData however is supposed to write to the canvas bitmap
wholesale, bypassing all render states. This means that we can't write
to the layer's content because the written pixels would then get
filtered when the layer is closed. We can't write to the main canvas
either because these pixels would later be overwritten by the layer's
result with draw calls that potentially happened before (e.g. in the
sequence `beginLayer(); fillRect(); putImageData(); endLayer();`,
`fillRect()` would write over `putImageData()`.

This behavior is part of the current 2D Canvas Layer spec draft:
Explainer: https:/fserb/canvas2D/blob/master/spec/layers.md
Spec draft: whatwg/html#9537

Change-Id: I266a3155c32919a68dbbb093e4aff9b1dd13a3b5
Bug: 1484741
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4943172
Reviewed-by: Fernando Serboncini <[email protected]>
Commit-Queue: Jean-Philippe Gravel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1212741}
  • Loading branch information
graveljp authored and chromium-wpt-export-bot committed Oct 20, 2023
1 parent 0d569c2 commit 6e6f9fb
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 191 deletions.
35 changes: 35 additions & 0 deletions html/canvas/element/layers/2d.layer.putImageData.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>Canvas test: 2d.layer.putImageData</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>
<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css">
<body class="show_output">

<h1>2d.layer.putImageData</h1>
<p class="desc">Check that calling putImageData in a layer throws an exception.</p>


<p class="output">Actual output:</p>
<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>

<ul id="d"></ul>
<script>
var t = async_test("Check that calling putImageData in a layer throws an exception.");
_addTest(function(canvas, ctx) {

const canvas2 = new OffscreenCanvas(100, 50);
const ctx2 = canvas2.getContext('2d')
const data = ctx2.getImageData(0, 0, 1, 1);
// `putImageData` shouldn't throw on it's own.
ctx.putImageData(data, 0, 0);
// Make sure the exception isn't caused by calling the function twice.
ctx.putImageData(data, 0, 0);
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError", () => ctx.putImageData(data, 0, 0));

});
</script>

This file was deleted.

This file was deleted.

36 changes: 36 additions & 0 deletions html/canvas/offscreen/layers/2d.layer.putImageData.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>OffscreenCanvas test: 2d.layer.putImageData</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/html/canvas/resources/canvas-tests.js"></script>

<h1>2d.layer.putImageData</h1>
<p class="desc">Check that calling putImageData in a layer throws an exception.</p>


<script>
var t = async_test("Check that calling putImageData in a layer throws an exception.");
var t_pass = t.done.bind(t);
var t_fail = t.step_func(function(reason) {
throw reason;
});
t.step(function() {

var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');

const canvas2 = new OffscreenCanvas(100, 50);
const ctx2 = canvas2.getContext('2d')
const data = ctx2.getImageData(0, 0, 1, 1);
// `putImageData` shouldn't throw on it's own.
ctx.putImageData(data, 0, 0);
// Make sure the exception isn't caused by calling the function twice.
ctx.putImageData(data, 0, 0);
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError", () => ctx.putImageData(data, 0, 0));
t.done();

});
</script>
31 changes: 31 additions & 0 deletions html/canvas/offscreen/layers/2d.layer.putImageData.worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
// OffscreenCanvas test in a worker:2d.layer.putImageData
// Description:Check that calling putImageData in a layer throws an exception.
// Note:

importScripts("/resources/testharness.js");
importScripts("/html/canvas/resources/canvas-tests.js");

var t = async_test("Check that calling putImageData in a layer throws an exception.");
var t_pass = t.done.bind(t);
var t_fail = t.step_func(function(reason) {
throw reason;
});
t.step(function() {

var canvas = new OffscreenCanvas(100, 50);
var ctx = canvas.getContext('2d');

const canvas2 = new OffscreenCanvas(100, 50);
const ctx2 = canvas2.getContext('2d')
const data = ctx2.getImageData(0, 0, 1, 1);
// `putImageData` shouldn't throw on it's own.
ctx.putImageData(data, 0, 0);
// Make sure the exception isn't caused by calling the function twice.
ctx.putImageData(data, 0, 0);
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError", () => ctx.putImageData(data, 0, 0));
t.done();
});
done();

This file was deleted.

This file was deleted.

This file was deleted.

19 changes: 14 additions & 5 deletions html/canvas/tools/yaml-new/layers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -459,11 +459,6 @@
test_type: "promise"
flush_canvas: |-
await new Promise(resolve => requestAnimationFrame(resolve));
putImageData:
flush_canvas: |-
const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }});
const ctx2 = canvas2.getContext('2d');
ctx.putImageData(ctx2.getImageData(0, 0, 1, 1), 0, 0);
toBlob:
test_type: "promise"
canvasType: ['HTMLCanvas']
Expand All @@ -473,6 +468,20 @@
canvasType: ['HTMLCanvas']
flush_canvas: canvas.toDataURL();

- name: 2d.layer.putImageData
desc: Check that calling putImageData in a layer throws an exception.
code: |
const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }});
const ctx2 = canvas2.getContext('2d')
const data = ctx2.getImageData(0, 0, 1, 1);
// `putImageData` shouldn't throw on it's own.
ctx.putImageData(data, 0, 0);
// Make sure the exception isn't caused by calling the function twice.
ctx.putImageData(data, 0, 0);
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError", () => ctx.putImageData(data, 0, 0));
- name: 2d.layer.transferToImageBitmap
desc: Check that calling transferToImageBitmap in a layer throws an exception.
canvasType: ['OffscreenCanvas', 'Worker']
Expand Down

0 comments on commit 6e6f9fb

Please sign in to comment.