Skip to content

Commit d75b4f9

Browse files
hjanuschkachromium-wpt-export-bot
authored andcommitted
Add JPEG XL canvas reftest coverage
Convert the canvas decode paths test to a PNG-backed reftest and add a WebGPU copyExternalImageToTexture case. Bug: 484065185 Change-Id: I6f9153f24fb550848f127e0da9847a1c5c3e3cd9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7666126 Commit-Queue: Helmut Januschka <[email protected]> Reviewed-by: Philip Jägenstedt <[email protected]> Cr-Commit-Position: refs/heads/main@{#1614800}
1 parent 2ae012c commit d75b4f9

4 files changed

Lines changed: 323 additions & 31 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html class="reftest-wait">
3+
<title>Reference for JPEG XL integration: canvas decode paths reftest</title>
4+
5+
<canvas id="output"></canvas>
6+
7+
<script src="resources/canvas-decode-paths-reftest.js"></script>
8+
<script>
9+
runCanvasDecodePathReftest('resources/3x3_srgb_lossless.png');
10+
</script>
11+
</html>
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<!DOCTYPE html>
2+
<title>JPEG XL integration: canvas WebGPU decode path</title>
3+
<link rel="help" href="https://github.com/web-platform-tests/interop-jpegxl">
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
7+
<script>
8+
async function loadImage(src) {
9+
const image = new Image();
10+
await new Promise((resolve, reject) => {
11+
image.onload = resolve;
12+
image.onerror = () => reject(new Error(`image load failed: ${src}`));
13+
image.src = src;
14+
});
15+
return image;
16+
}
17+
18+
function sampleCenterPixelWith2d(image) {
19+
const canvas = document.createElement('canvas');
20+
canvas.width = 3;
21+
canvas.height = 3;
22+
23+
const ctx = canvas.getContext('2d');
24+
assert_true(!!ctx, '2D context available');
25+
ctx.drawImage(image, 0, 0);
26+
return ctx.getImageData(1, 1, 1, 1).data;
27+
}
28+
29+
function assertPixelsApproxEqual(actual, expected, tolerance, messagePrefix) {
30+
for (let i = 0; i < 4; ++i) {
31+
assert_approx_equals(
32+
actual[i], expected[i], tolerance, `${messagePrefix} channel ${i}`);
33+
}
34+
}
35+
36+
async function sampleCenterPixelWithWebGPU(image) {
37+
if (!('gpu' in navigator)) {
38+
return null;
39+
}
40+
41+
const adapter = await navigator.gpu.requestAdapter();
42+
if (!adapter) {
43+
return null;
44+
}
45+
46+
const device = await adapter.requestDevice();
47+
const width = image.naturalWidth || image.width;
48+
const height = image.naturalHeight || image.height;
49+
assert_greater_than(width, 0);
50+
assert_greater_than(height, 0);
51+
52+
const texture = device.createTexture({
53+
size: {width, height, depthOrArrayLayers: 1},
54+
format: 'rgba8unorm',
55+
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
56+
});
57+
58+
device.queue.copyExternalImageToTexture(
59+
{source: image},
60+
{texture},
61+
{width, height, depthOrArrayLayers: 1});
62+
63+
const buffer = device.createBuffer({
64+
size: 4,
65+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
66+
});
67+
68+
const encoder = device.createCommandEncoder();
69+
encoder.copyTextureToBuffer(
70+
{texture, origin: {x: 1, y: 1, z: 0}},
71+
{buffer},
72+
{width: 1, height: 1, depthOrArrayLayers: 1});
73+
device.queue.submit([encoder.finish()]);
74+
75+
await buffer.mapAsync(GPUMapMode.READ);
76+
const pixel = new Uint8Array(buffer.getMappedRange()).slice(0, 4);
77+
78+
buffer.unmap();
79+
buffer.destroy();
80+
texture.destroy();
81+
82+
return pixel;
83+
}
84+
85+
promise_test(async () => {
86+
const [jxlImage, pngImage] = await Promise.all([
87+
loadImage('resources/3x3_srgb_lossless.jxl'),
88+
loadImage('resources/3x3_srgb_lossless.png'),
89+
]);
90+
91+
const actual = await sampleCenterPixelWithWebGPU(jxlImage);
92+
if (!actual) {
93+
return;
94+
}
95+
96+
const expected = sampleCenterPixelWith2d(pngImage);
97+
assertPixelsApproxEqual(actual, expected, 1, 'WebGPU copyExternalImageToTexture');
98+
}, 'WebGPU copyExternalImageToTexture path with JPEG XL image.');
99+
</script>

jpegxl/canvas-decode-paths.html

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,14 @@
11
<!DOCTYPE html>
2-
<title>JPEG XL integration: canvas decode paths</title>
2+
<html class="reftest-wait">
3+
<title>JPEG XL integration: canvas decode paths reftest</title>
34
<link rel="help" href="https://github.com/web-platform-tests/interop-jpegxl">
4-
<script src="/resources/testharness.js"></script>
5-
<script src="/resources/testharnessreport.js"></script>
5+
<link rel="match" href="canvas-decode-paths-ref.html">
6+
<meta name="assert" content="Canvas decode paths should match PNG reference output for drawImage, createImageBitmap, WebGL texture upload, and rgba-float16 readback conversion.">
67

7-
<script>
8-
promise_test(async () => {
9-
const image = new Image();
10-
const loaded = new Promise((resolve, reject) => {
11-
image.onload = resolve;
12-
image.onerror = () => reject(new Error('image decode failed'));
13-
});
14-
image.src = 'resources/3x3_srgb_lossless.jxl';
15-
await loaded;
16-
17-
const canvas = document.createElement('canvas');
18-
canvas.width = 3;
19-
canvas.height = 3;
20-
const ctx = canvas.getContext('2d');
21-
ctx.drawImage(image, 0, 0);
8+
<canvas id="output"></canvas>
229

23-
const p = ctx.getImageData(1, 1, 1, 1).data;
24-
const nonZero = p[0] !== 0 || p[1] !== 0 || p[2] !== 0 || p[3] !== 0;
25-
assert_true(nonZero, '2D canvas contains decoded JPEG XL pixels');
26-
}, 'Canvas 2D drawImage path with JPEG XL image.');
27-
28-
promise_test(async () => {
29-
const response = await fetch('resources/3x3_srgb_lossless.jxl');
30-
const blob = await response.blob();
31-
const bitmap = await createImageBitmap(blob);
32-
assert_equals(bitmap.width, 3);
33-
assert_equals(bitmap.height, 3);
34-
bitmap.close();
35-
}, 'createImageBitmap() decodes JPEG XL blob.');
10+
<script src="resources/canvas-decode-paths-reftest.js"></script>
11+
<script>
12+
runCanvasDecodePathReftest('resources/3x3_srgb_lossless.jxl');
3613
</script>
14+
</html>
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
const TILE_SIZE = 3;
2+
const SCALE = 20;
3+
function createTileCanvas() {
4+
const canvas = document.createElement('canvas');
5+
canvas.width = TILE_SIZE;
6+
canvas.height = TILE_SIZE;
7+
return canvas;
8+
}
9+
10+
function fillUnsupported(canvas) {
11+
const ctx = canvas.getContext('2d');
12+
ctx.fillStyle = 'rgb(255, 0, 255)';
13+
ctx.fillRect(0, 0, TILE_SIZE, TILE_SIZE);
14+
}
15+
16+
function fillError() {
17+
const output = document.getElementById('output');
18+
output.width = 40;
19+
output.height = 40;
20+
const ctx = output.getContext('2d');
21+
ctx.fillStyle = 'rgb(255, 0, 0)';
22+
ctx.fillRect(0, 0, output.width, output.height);
23+
}
24+
25+
async function loadImage(src) {
26+
const image = new Image();
27+
await new Promise((resolve, reject) => {
28+
image.onload = resolve;
29+
image.onerror = () => reject(new Error(`image load failed: ${src}`));
30+
image.src = src;
31+
});
32+
return image;
33+
}
34+
35+
async function renderDrawImageTile(image) {
36+
const canvas = createTileCanvas();
37+
const ctx = canvas.getContext('2d');
38+
ctx.drawImage(image, 0, 0);
39+
return canvas;
40+
}
41+
42+
async function renderImageBitmapTile(src) {
43+
const canvas = createTileCanvas();
44+
const ctx = canvas.getContext('2d');
45+
46+
try {
47+
const response = await fetch(src);
48+
const blob = await response.blob();
49+
const bitmap = await createImageBitmap(blob);
50+
ctx.drawImage(bitmap, 0, 0);
51+
bitmap.close();
52+
return canvas;
53+
} catch (error) {
54+
fillUnsupported(canvas);
55+
return canvas;
56+
}
57+
}
58+
59+
function makeImageDataFromWebGLPixels(pixels) {
60+
const flipped = new Uint8ClampedArray(pixels.length);
61+
const rowWidth = TILE_SIZE * 4;
62+
for (let y = 0; y < TILE_SIZE; ++y) {
63+
const sourceOffset = (TILE_SIZE - 1 - y) * rowWidth;
64+
const destinationOffset = y * rowWidth;
65+
flipped.set(pixels.subarray(sourceOffset, sourceOffset + rowWidth),
66+
destinationOffset);
67+
}
68+
return new ImageData(flipped, TILE_SIZE, TILE_SIZE);
69+
}
70+
71+
function renderWebGLTile(image) {
72+
const outputCanvas = createTileCanvas();
73+
const outputContext = outputCanvas.getContext('2d');
74+
75+
const webglCanvas = createTileCanvas();
76+
const gl = webglCanvas.getContext('webgl');
77+
if (!gl) {
78+
fillUnsupported(outputCanvas);
79+
return outputCanvas;
80+
}
81+
82+
const texture = gl.createTexture();
83+
const framebuffer = gl.createFramebuffer();
84+
if (!texture || !framebuffer) {
85+
fillUnsupported(outputCanvas);
86+
return outputCanvas;
87+
}
88+
89+
gl.bindTexture(gl.TEXTURE_2D, texture);
90+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
91+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
92+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
93+
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
94+
95+
try {
96+
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
97+
} catch (error) {
98+
fillUnsupported(outputCanvas);
99+
gl.deleteFramebuffer(framebuffer);
100+
gl.deleteTexture(texture);
101+
return outputCanvas;
102+
}
103+
104+
gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
105+
gl.framebufferTexture2D(
106+
gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
107+
108+
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
109+
fillUnsupported(outputCanvas);
110+
gl.deleteFramebuffer(framebuffer);
111+
gl.deleteTexture(texture);
112+
return outputCanvas;
113+
}
114+
115+
const pixels = new Uint8Array(TILE_SIZE * TILE_SIZE * 4);
116+
gl.readPixels(0, 0, TILE_SIZE, TILE_SIZE, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
117+
118+
const imageData = makeImageDataFromWebGLPixels(pixels);
119+
outputContext.putImageData(imageData, 0, 0);
120+
121+
gl.deleteFramebuffer(framebuffer);
122+
gl.deleteTexture(texture);
123+
return outputCanvas;
124+
}
125+
126+
function convertFloatChannelToByte(value) {
127+
if (!Number.isFinite(value)) {
128+
return 0;
129+
}
130+
if (value <= 0) {
131+
return 0;
132+
}
133+
if (value >= 1) {
134+
return 255;
135+
}
136+
return Math.round(value * 255);
137+
}
138+
139+
function renderFloat16Tile(image) {
140+
const canvas = createTileCanvas();
141+
const ctx = canvas.getContext('2d');
142+
ctx.drawImage(image, 0, 0);
143+
144+
let floatData;
145+
try {
146+
floatData =
147+
ctx.getImageData(0, 0, TILE_SIZE, TILE_SIZE,
148+
{pixelFormat: 'rgba-float16'}).data;
149+
} catch (error) {
150+
fillUnsupported(canvas);
151+
return canvas;
152+
}
153+
154+
if (!(floatData instanceof Float16Array)) {
155+
fillUnsupported(canvas);
156+
return canvas;
157+
}
158+
159+
const converted = new Uint8ClampedArray(floatData.length);
160+
for (let i = 0; i < floatData.length; ++i) {
161+
converted[i] = convertFloatChannelToByte(floatData[i]);
162+
}
163+
164+
ctx.putImageData(new ImageData(converted, TILE_SIZE, TILE_SIZE), 0, 0);
165+
return canvas;
166+
}
167+
168+
function drawOutputTiles(tiles) {
169+
const output = document.getElementById('output');
170+
output.width = TILE_SIZE * SCALE * tiles.length;
171+
output.height = TILE_SIZE * SCALE;
172+
173+
const ctx = output.getContext('2d');
174+
ctx.imageSmoothingEnabled = false;
175+
176+
const scaledTileSize = TILE_SIZE * SCALE;
177+
for (let i = 0; i < tiles.length; ++i) {
178+
ctx.drawImage(
179+
tiles[i],
180+
0,
181+
0,
182+
TILE_SIZE,
183+
TILE_SIZE,
184+
i * scaledTileSize,
185+
0,
186+
scaledTileSize,
187+
scaledTileSize);
188+
}
189+
}
190+
191+
async function runCanvasDecodePathReftest(source) {
192+
try {
193+
const image = await loadImage(source);
194+
const drawImageTile = await renderDrawImageTile(image);
195+
const imageBitmapTile = await renderImageBitmapTile(source);
196+
const webglTile = renderWebGLTile(image);
197+
const float16Tile = renderFloat16Tile(image);
198+
drawOutputTiles([drawImageTile, imageBitmapTile, webglTile, float16Tile]);
199+
} catch (error) {
200+
fillError();
201+
} finally {
202+
document.documentElement.classList.remove('reftest-wait');
203+
}
204+
}

0 commit comments

Comments
 (0)