mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-28 02:27:19 +02:00
LibWeb: Don't crash when creating empty bitmap from a HTMLCanvasElement
This commit is contained in:
committed by
Shannon Booth
parent
bd83f5bde6
commit
48417152df
Notes:
github-actions[bot]
2025-07-20 04:28:03 +00:00
Author: https://github.com/tcl3 Commit: https://github.com/LadybirdBrowser/ladybird/commit/48417152dfa Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/5527 Reviewed-by: https://github.com/shannonbooth ✅
@@ -0,0 +1,175 @@
|
||||
function makeCanvas() {
|
||||
return new Promise(resolve => {
|
||||
var testCanvas = document.createElement("canvas");
|
||||
testCanvas.width = 20;
|
||||
testCanvas.height = 20;
|
||||
var testCtx = testCanvas.getContext("2d");
|
||||
testCtx.fillStyle = "rgb(255, 0, 0)";
|
||||
testCtx.fillRect(0, 0, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 255, 0)";
|
||||
testCtx.fillRect(10, 0, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 0, 255)";
|
||||
testCtx.fillRect(0, 10, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 0, 0)";
|
||||
testCtx.fillRect(10, 10, 10, 10);
|
||||
resolve(testCanvas);
|
||||
});
|
||||
}
|
||||
|
||||
function makeOffscreenCanvas() {
|
||||
return new Promise(resolve => {
|
||||
let canvas = new OffscreenCanvas(20, 20);
|
||||
var testCtx = canvas.getContext("2d");
|
||||
testCtx.fillStyle = "rgb(255, 0, 0)";
|
||||
testCtx.fillRect(0, 0, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 255, 0)";
|
||||
testCtx.fillRect(10, 0, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 0, 255)";
|
||||
testCtx.fillRect(0, 10, 10, 10);
|
||||
testCtx.fillStyle = "rgb(0, 0, 0)";
|
||||
testCtx.fillRect(10, 10, 10, 10);
|
||||
resolve(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
function makeMakeVideo(src) {
|
||||
return function () {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var video = document.createElement("video");
|
||||
video.oncanplaythrough = function() {
|
||||
resolve(video);
|
||||
};
|
||||
video.onerror = reject;
|
||||
|
||||
// preload=auto is required to ensure a frame is available once
|
||||
// canplaythrough is fired. The default of preload=metadata does not
|
||||
// gaurantee this.
|
||||
video.preload = "auto";
|
||||
video.src = getVideoURI(src);
|
||||
|
||||
// Prevent WebKit from garbage collecting event handlers.
|
||||
window._video = video;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeVideo() {
|
||||
return makeMakeVideo("../../../../../images/pattern")();
|
||||
}
|
||||
|
||||
var imageBitmapDataUrlVideoPromise = self.GLOBAL && self.GLOBAL.isWorker()
|
||||
// /common/media.js can't load in a Worker so we don't have getVideoURI
|
||||
? null
|
||||
: fetch(getVideoURI("../../../../../images/pattern"))
|
||||
.then(response => Promise.all([response.headers.get("Content-Type"), response.arrayBuffer()]))
|
||||
.then(([type, data]) => {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var video = document.createElement("video");
|
||||
video.oncanplaythrough = function() {
|
||||
resolve(video);
|
||||
};
|
||||
video.onerror = reject;
|
||||
|
||||
var encoded = btoa(String.fromCodePoint(...new Uint8Array(data)));
|
||||
var dataUrl = `data:${type};base64,${encoded}`;
|
||||
|
||||
// preload=auto is required to ensure a frame is available once
|
||||
// canplaythrough is fired. The default of preload=metadata does not
|
||||
// gaurantee this.
|
||||
video.preload = "auto";
|
||||
video.src = dataUrl;
|
||||
|
||||
// Prevent WebKit from garbage collecting event handlers.
|
||||
window._dataVideo = video;
|
||||
});
|
||||
});
|
||||
|
||||
function makeDataUrlVideo() {
|
||||
return imageBitmapDataUrlVideoPromise;
|
||||
}
|
||||
|
||||
function makeMakeHTMLImage(src) {
|
||||
return function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var img = new Image();
|
||||
img.onload = function() {
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeMakeSVGImage(src) {
|
||||
return function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var image = document.createElementNS("http://www.w3.org/2000/svg", "image");
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = reject;
|
||||
image.setAttribute("externalResourcesRequired", "true");
|
||||
image.setAttributeNS("http://www.w3.org/1999/xlink", 'xlink:href', src);
|
||||
document.body.appendChild(image);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function makeImageData() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var width = 20, height = 20;
|
||||
var imgData = new ImageData(width, height);
|
||||
for (var i = 0; i < width * height * 4; i += 4) {
|
||||
imgData.data[i] = 0;
|
||||
imgData.data[i + 1] = 0;
|
||||
imgData.data[i + 2] = 0;
|
||||
imgData.data[i + 3] = 255; //alpha channel: 255
|
||||
}
|
||||
var halfWidth = width / 2;
|
||||
var halfHeight = height / 2;
|
||||
// initialize to R, G, B, Black, with each one 10*10 pixels
|
||||
for (var i = 0; i < halfHeight; i++)
|
||||
for (var j = 0; j < halfWidth; j++)
|
||||
imgData.data[i * width * 4 + j * 4] = 255;
|
||||
for (var i = 0; i < halfHeight; i++)
|
||||
for (var j = halfWidth; j < width; j++)
|
||||
imgData.data[i * width * 4 + j * 4 + 1] = 255;
|
||||
for (var i = halfHeight; i < height; i++)
|
||||
for (var j = 0; j < halfWidth; j++)
|
||||
imgData.data[i * width * 4 + j * 4 + 2] = 255;
|
||||
resolve(imgData);
|
||||
});
|
||||
}
|
||||
|
||||
function makeImageBitmap() {
|
||||
return makeCanvas().then(canvas => {
|
||||
return createImageBitmap(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
function makeBlob(src) {
|
||||
return function () {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", src);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
xhr.onload = function() {
|
||||
resolve(xhr.response);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var imageSourceTypes = [
|
||||
{ name: 'an HTMLCanvasElement', factory: makeCanvas },
|
||||
{ name: 'an HTMLVideoElement', factory: makeVideo },
|
||||
{ name: 'an HTMLVideoElement from a data URL', factory: makeDataUrlVideo },
|
||||
{ name: 'a bitmap HTMLImageElement', factory: makeMakeHTMLImage("../../../../../images/pattern.png") },
|
||||
{ name: 'a vector HTMLImageElement', factory: makeMakeHTMLImage("../../../../../images/pattern.svg") },
|
||||
{ name: 'a bitmap SVGImageElement', factory: makeMakeSVGImage("../../../../../images/pattern.png") },
|
||||
{ name: 'a vector SVGImageElement', factory: makeMakeSVGImage("../../../../../images/pattern.svg") },
|
||||
{ name: 'an OffscreenCanvas', factory: makeOffscreenCanvas },
|
||||
{ name: 'an ImageData', factory: makeImageData },
|
||||
{ name: 'an ImageBitmap', factory: makeImageBitmap },
|
||||
{ name: 'a Blob', factory: makeBlob("../../../../../images/pattern.png") },
|
||||
];
|
||||
@@ -0,0 +1,239 @@
|
||||
<!doctype html>
|
||||
<script src="../../../../../resources/testharness.js"></script>
|
||||
<script src="../../../../../resources/testharnessreport.js"></script>
|
||||
<script src="../../../../../common/media.js"></script>
|
||||
<script src="common.sub.js"></script>
|
||||
<script>
|
||||
|
||||
function makeOversizedCanvas() {
|
||||
|
||||
return new Promise(resolve => {
|
||||
let canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', '100000000');
|
||||
canvas.setAttribute('height', '100000000');
|
||||
resolve(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
function makeOversizedOffscreenCanvas() {
|
||||
return new Promise(resolve =>{
|
||||
let canvas = new OffscreenCanvas(100000000, 100000000);
|
||||
resolve(canvas);
|
||||
});
|
||||
}
|
||||
|
||||
function makeInvalidBlob() {
|
||||
return new Promise(resolve => {
|
||||
resolve(new Blob()); // Blob with no data cannot be decoded.
|
||||
});
|
||||
}
|
||||
|
||||
function makeBrokenImage() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.src = "data:,x";
|
||||
image.onload = reject;
|
||||
image.onerror = () => resolve(image);
|
||||
});
|
||||
}
|
||||
|
||||
function makeAvailableButBrokenImage(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.src = path;
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
testCases = [
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and sw set to 0',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return promise_rejects_js(t, RangeError,
|
||||
createImageBitmap(source, 0, 0, 0, 10));
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and sh set to 0',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return promise_rejects_js(t, RangeError,
|
||||
createImageBitmap(source, 0, 0, 10, 0));
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and oversized ' +
|
||||
'(unallocatable) crop region',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return createImageBitmap(source, 0, 0, 100000000, 100000000)
|
||||
.then(i => {
|
||||
assert_equals(i.width, 100000000);
|
||||
assert_equals(i.height, 100000000);
|
||||
})
|
||||
.catch(e => {
|
||||
// This case is not explicitly documented in the specification for
|
||||
// createImageBitmap, but it is expected that internal failures
|
||||
// cause InvalidStateError.
|
||||
//
|
||||
// Note: https://bugs.chromium.org/p/chromium/issues/detail?id=799025
|
||||
assert_throws_dom("InvalidStateError", () => { throw e });
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and ' +
|
||||
'a value of 0 int resizeWidth',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return createImageBitmap(source, {resizeWidth:0, resizeHeight:10})
|
||||
.catch(e => {
|
||||
assert_throws_dom("InvalidStateError", () => { throw e });
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and ' +
|
||||
'a value of 0 in resizeHeight',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return createImageBitmap(source, {resizeWidth:10, resizeHeight:0})
|
||||
.catch(e => {
|
||||
assert_throws_dom("InvalidStateError", () => { throw e });
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and ' +
|
||||
'a value between 0 and 1 in resizeWidth',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return createImageBitmap(source, {resizeWidth:0.5, resizeHeight:10})
|
||||
.catch(e => {
|
||||
assert_throws_dom("InvalidStateError", () => { throw e });
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
description: 'createImageBitmap with <sourceType> source and ' +
|
||||
'a value between 0 and 1 in resizeHeight',
|
||||
promiseTestFunction:
|
||||
(source, t) => {
|
||||
return createImageBitmap(source, {resizeWidth:10, resizeHeight:0.5})
|
||||
.catch(e => {
|
||||
assert_throws_dom("InvalidStateError", () => { throw e });
|
||||
});
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
// Generate the test matrix for each sourceType + testCase combo.
|
||||
imageSourceTypes.forEach(imageSourceType => {
|
||||
testCases.forEach(testCase => {
|
||||
let description = testCase.description.replace('<sourceType>',
|
||||
imageSourceType.name);
|
||||
promise_test( t => {
|
||||
return imageSourceType.factory().then(source => {
|
||||
return testCase.promiseTestFunction(source, t);
|
||||
});
|
||||
}, description);
|
||||
});
|
||||
});
|
||||
|
||||
promise_test( t => {
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(undefined));
|
||||
}, "createImageBitmap with undefined image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(null));
|
||||
}, "createImageBitmap with null image source.");
|
||||
|
||||
promise_test( t => {
|
||||
var context = document.createElement("canvas").getContext("2d");
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(context));
|
||||
}, "createImageBitmap with CanvasRenderingContext2D image source.");
|
||||
|
||||
promise_test( t => {
|
||||
var context = document.createElement("canvas").getContext("webgl");
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(context));
|
||||
}, "createImageBitmap with WebGLRenderingContext image source.");
|
||||
|
||||
promise_test( t => {
|
||||
var buffer = new Uint8Array();
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(buffer));
|
||||
}, "createImageBitmap with Uint8Array image source.");
|
||||
|
||||
promise_test( t => {
|
||||
var buffer = new ArrayBuffer(8);
|
||||
return promise_rejects_js(t, TypeError, createImageBitmap(buffer));
|
||||
}, "createImageBitmap with ArrayBuffer image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(new Image()));
|
||||
}, "createImageBitmap with empty image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(document.createElement('video')));
|
||||
}, "createImageBitmap with empty video source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeOversizedCanvas().then(canvas => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(canvas));
|
||||
});
|
||||
}, "createImageBitmap with an oversized canvas source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeOversizedOffscreenCanvas().then(offscreenCanvas => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(offscreenCanvas));
|
||||
});
|
||||
}, "createImageBitmap with an invalid OffscreenCanvas source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeInvalidBlob().then(blob => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(blob));
|
||||
});
|
||||
}, "createImageBitmap with an undecodable blob source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeBrokenImage().then(image => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(image));
|
||||
});
|
||||
}, "createImageBitmap with a broken image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeAvailableButBrokenImage("../../../../../images/undecodable.png").then(image => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(image));
|
||||
});
|
||||
}, "createImageBitmap with an available but undecodable image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeAvailableButBrokenImage("../../../../../images/red-zeroheight.svg").then(image => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(image));
|
||||
});
|
||||
}, "createImageBitmap with an available but zero height image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeAvailableButBrokenImage("../../../../../images/red-zerowidth.svg").then(image => {
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(image));
|
||||
});
|
||||
}, "createImageBitmap with an available but zero width image source.");
|
||||
|
||||
promise_test( t => {
|
||||
return makeImageBitmap().then(bitmap => {
|
||||
bitmap.close()
|
||||
return promise_rejects_dom(t, "InvalidStateError",
|
||||
createImageBitmap(bitmap));
|
||||
});
|
||||
}, "createImageBitmap with a closed ImageBitmap.");
|
||||
</script>
|
||||
Reference in New Issue
Block a user