Skip to content

Commit

Permalink
Throw an exception in transferToImageBitmap 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.

OffscreenCanvas.transferToImageBitmap however is supposed to release
the canvas content, leaving the offscreen canvas empty. We cannot
release the recording if layers are incomplete, and if we kept the
layer content alive for later, we would not be leaving the canvas
empty as the spec requires.

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

Bug: 1484741
Change-Id: Ic770b51a0343faf0b2c7477624d69f59187ce97f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4939633
Commit-Queue: Jean-Philippe Gravel <[email protected]>
Reviewed-by: Fernando Serboncini <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1212692}
  • Loading branch information
graveljp authored and chromium-wpt-export-bot committed Oct 20, 2023
1 parent 78a4d14 commit 3375400
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 181 deletions.

This file was deleted.

This file was deleted.

This file was deleted.

34 changes: 34 additions & 0 deletions html/canvas/offscreen/layers/2d.layer.transferToImageBitmap.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. -->
<title>OffscreenCanvas test: 2d.layer.transferToImageBitmap</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.transferToImageBitmap</h1>
<p class="desc">Check that calling transferToImageBitmap in a layer throws an exception.</p>


<script>
var t = async_test("Check that calling transferToImageBitmap 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');

// `transferToImageBitmap` shouldn't throw on it's own.
canvas.transferToImageBitmap();
// Make sure the exception isn't caused by calling the function twice.
canvas.transferToImageBitmap();
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError",
() => canvas.transferToImageBitmap());
t.done();

});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py.
// OffscreenCanvas test in a worker:2d.layer.transferToImageBitmap
// Description:Check that calling transferToImageBitmap 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 transferToImageBitmap 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');

// `transferToImageBitmap` shouldn't throw on it's own.
canvas.transferToImageBitmap();
// Make sure the exception isn't caused by calling the function twice.
canvas.transferToImageBitmap();
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError",
() => canvas.transferToImageBitmap());
t.done();
});
done();
31 changes: 20 additions & 11 deletions html/canvas/offscreen/layers/2d.layer.unclosed-nested.w.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,29 @@ <h1>2d.layer.unclosed-nested</h1>
const canvas = new OffscreenCanvas(200, 200);
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'rgba(0, 0, 255, 1)';
ctx.fillRect(60, 60, 75, 50);
ctx.globalAlpha = 0.5;
// `transferToImageBitmap` is used to transfer the test result to the
// worker's parent, but `transferToImageBitmap` can't be called on canvas
// with unclosed layers. We can however draw to a separate offscreen canvas
// and write it to the main canvas using `drawImage`.
const canvas2 = new OffscreenCanvas(200, 200);
const ctx2 = canvas2.getContext('2d');

ctx.beginLayer();
ctx.fillStyle = 'rgba(225, 0, 0, 1)';
ctx.fillRect(50, 50, 75, 50);
ctx2.fillStyle = 'rgba(0, 0, 255, 1)';
ctx2.fillRect(60, 60, 75, 50);
ctx2.globalAlpha = 0.5;

ctx.beginLayer();
ctx.fillStyle = 'rgba(0, 255, 0, 1)';
ctx.fillRect(70, 70, 75, 50);
ctx2.beginLayer();
ctx2.fillStyle = 'rgba(225, 0, 0, 1)';
ctx2.fillRect(50, 50, 75, 50);

ctx.endLayer();
// Missing ctx.endLayer() here.
ctx2.beginLayer();
ctx2.fillStyle = 'rgba(0, 255, 0, 1)';
ctx2.fillRect(70, 70, 75, 50);

ctx2.endLayer();
// Missing ctx2.endLayer() here.

ctx.drawImage(canvas2, 0, 0);

const bitmap = canvas.transferToImageBitmap();
self.postMessage(bitmap, bitmap);
Expand Down
23 changes: 16 additions & 7 deletions html/canvas/offscreen/layers/2d.layer.unclosed.w.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,23 @@ <h1>2d.layer.unclosed</h1>
const canvas = new OffscreenCanvas(200, 200);
const ctx = canvas.getContext('2d');

ctx.fillStyle = 'purple';
ctx.fillRect(60, 60, 75, 50);
ctx.globalAlpha = 0.5;
// `transferToImageBitmap` is used to transfer the test result to the
// worker's parent, but `transferToImageBitmap` can't be called on canvas
// with unclosed layers. We can however draw to a separate offscreen canvas
// and write it to the main canvas using `drawImage`.
const canvas2 = new OffscreenCanvas(200, 200);
const ctx2 = canvas2.getContext('2d');

ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
ctx.fillRect(40, 40, 75, 50);
ctx.fillStyle = 'grey';
ctx.fillRect(50, 50, 75, 50);
ctx2.fillStyle = 'purple';
ctx2.fillRect(60, 60, 75, 50);
ctx2.globalAlpha = 0.5;

ctx2.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
ctx2.fillRect(40, 40, 75, 50);
ctx2.fillStyle = 'grey';
ctx2.fillRect(50, 50, 75, 50);

ctx.drawImage(canvas2, 0, 0);

const bitmap = canvas.transferToImageBitmap();
self.postMessage(bitmap, bitmap);
Expand Down
110 changes: 57 additions & 53 deletions html/canvas/tools/yaml-new/layers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,29 @@
desc: Check that layers are rendered even if not closed.
size: [200, 200]
code: |
ctx.fillStyle = 'purple';
ctx.fillRect(60, 60, 75, 50);
ctx.globalAlpha = 0.5;
{% set ns = namespace(ctx='ctx') %}
{% if canvas_type == 'worker' %}
// `transferToImageBitmap` is used to transfer the test result to the
// worker's parent, but `transferToImageBitmap` can't be called on canvas
// with unclosed layers. We can however draw to a separate offscreen canvas
// and write it to the main canvas using `drawImage`.
const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }});
const ctx2 = canvas2.getContext('2d');
{% set ns.ctx = 'ctx2' %}
{% endif %}
ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
ctx.fillRect(40, 40, 75, 50);
ctx.fillStyle = 'grey';
ctx.fillRect(50, 50, 75, 50);
{{ ns.ctx }}.fillStyle = 'purple';
{{ ns.ctx }}.fillRect(60, 60, 75, 50);
{{ ns.ctx }}.globalAlpha = 0.5;
{{ ns.ctx }}.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
{{ ns.ctx }}.fillRect(40, 40, 75, 50);
{{ ns.ctx }}.fillStyle = 'grey';
{{ ns.ctx }}.fillRect(50, 50, 75, 50);
{% if canvas_type == 'worker' %}
ctx.drawImage(canvas2, 0, 0);
{% endif %}
reference: |
ctx.fillStyle = 'purple';
ctx.fillRect(60, 60, 75, 50);
Expand All @@ -336,20 +351,35 @@
desc: Check that layers are rendered even if not closed.
size: [200, 200]
code: |
ctx.fillStyle = 'rgba(0, 0, 255, 1)';
ctx.fillRect(60, 60, 75, 50);
ctx.globalAlpha = 0.5;
{% set ns = namespace(ctx='ctx') %}
{% if canvas_type == 'worker' %}
// `transferToImageBitmap` is used to transfer the test result to the
// worker's parent, but `transferToImageBitmap` can't be called on canvas
// with unclosed layers. We can however draw to a separate offscreen canvas
// and write it to the main canvas using `drawImage`.
const canvas2 = new OffscreenCanvas({{ size[0] }}, {{ size[1] }});
const ctx2 = canvas2.getContext('2d');
{% set ns.ctx = 'ctx2' %}
{% endif %}
ctx.beginLayer();
ctx.fillStyle = 'rgba(225, 0, 0, 1)';
ctx.fillRect(50, 50, 75, 50);
{{ ns.ctx }}.fillStyle = 'rgba(0, 0, 255, 1)';
{{ ns.ctx }}.fillRect(60, 60, 75, 50);
{{ ns.ctx }}.globalAlpha = 0.5;
ctx.beginLayer();
ctx.fillStyle = 'rgba(0, 255, 0, 1)';
ctx.fillRect(70, 70, 75, 50);
{{ ns.ctx }}.beginLayer();
{{ ns.ctx }}.fillStyle = 'rgba(225, 0, 0, 1)';
{{ ns.ctx }}.fillRect(50, 50, 75, 50);
ctx.endLayer();
// Missing ctx.endLayer() here.
{{ ns.ctx }}.beginLayer();
{{ ns.ctx }}.fillStyle = 'rgba(0, 255, 0, 1)';
{{ ns.ctx }}.fillRect(70, 70, 75, 50);
{{ ns.ctx }}.endLayer();
// Missing {{ ns.ctx }}.endLayer() here.
{% if canvas_type == 'worker' %}
ctx.drawImage(canvas2, 0, 0);
{% endif %}
reference: |
const canvas1 = document.createElement('canvas');
const ctx1 = canvas1.getContext('2d');
Expand Down Expand Up @@ -443,44 +473,18 @@
canvasType: ['HTMLCanvas']
flush_canvas: canvas.toDataURL();


- name: 2d.layer.render-opportunities.transferToImageBitmap
desc: Checks that transferToImageBitmap flushes and rebuilds the state stack.
size: [200, 200]
- name: 2d.layer.transferToImageBitmap
desc: Check that calling transferToImageBitmap in a layer throws an exception.
canvasType: ['OffscreenCanvas', 'Worker']
code: |
ctx.fillStyle = 'purple';
ctx.fillRect(60, 60, 75, 50);
ctx.globalAlpha = 0.5;
ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
ctx.fillRect(40, 40, 75, 50);
ctx.fillStyle = 'grey';
ctx.fillRect(50, 50, 75, 50);
// Force a flush and restoration of the state stack.
// `transferToImageBitmap` clears the frame but preserves render states.
// `transferToImageBitmap` shouldn't throw on it's own.
canvas.transferToImageBitmap();
ctx.fillRect(70, 70, 75, 50);
ctx.fillStyle = 'orange';
ctx.fillRect(80, 80, 75, 50);
ctx.endLayer();
ctx.fillRect(80, 40, 75, 50);
reference: |
ctx.fillStyle = 'purple';
ctx.globalAlpha = 0.5;
ctx.beginLayer({filter: {name: 'dropShadow', dx: -2, dy: 2}});
ctx.fillStyle = 'grey';
ctx.fillRect(70, 70, 75, 50);
ctx.fillStyle = 'orange';
ctx.fillRect(80, 80, 75, 50);
ctx.endLayer();
ctx.fillRect(80, 40, 75, 50);
// Make sure the exception isn't caused by calling the function twice.
canvas.transferToImageBitmap();
// Calling again inside a layer should throw.
ctx.beginLayer();
assert_throws_dom("InvalidStateError",
() => canvas.transferToImageBitmap());
- name: 2d.layer.several-complex
desc: >-
Expand Down

0 comments on commit 3375400

Please sign in to comment.