mirror of
https://github.com/denoland/deno.git
synced 2025-09-26 12:19:12 +00:00
feat(ext/web): add ImageData Web API (#21183)
Fixes #19288 Adds the `ImageData` Web API. This would be beneficial to projects using `ImageData` as a convenient transport layer for pixel data. This is common in Web Assembly projects that manipulate images. Having this global available in Deno would improve compatibility of existing JS libraries. **References** - [MDN ImageData Web API](https://developer.mozilla.org/en-US/docs/Web/API/ImageData) - [whatwg HTML Standard Canvas Spec](https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation)
This commit is contained in:
parent
dadd8b3d66
commit
8c0fb9003d
11 changed files with 321 additions and 9 deletions
|
@ -42,6 +42,7 @@ util::unit_test_factory!(
|
||||||
globals_test,
|
globals_test,
|
||||||
headers_test,
|
headers_test,
|
||||||
http_test,
|
http_test,
|
||||||
|
image_data_test,
|
||||||
internals_test,
|
internals_test,
|
||||||
intl_test,
|
intl_test,
|
||||||
io_test,
|
io_test,
|
||||||
|
|
2
cli/tests/testdata/workers/image_data_worker.ts
vendored
Normal file
2
cli/tests/testdata/workers/image_data_worker.ts
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
const data = new ImageData(2, 2, { colorSpace: "display-p3" });
|
||||||
|
postMessage(data.data.length);
|
53
cli/tests/unit/image_data_test.ts
Normal file
53
cli/tests/unit/image_data_test.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import { assertEquals } from "./test_util.ts";
|
||||||
|
|
||||||
|
Deno.test(function imageDataInitializedWithSourceWidthAndHeight() {
|
||||||
|
const imageData = new ImageData(16, 9);
|
||||||
|
|
||||||
|
assertEquals(imageData.width, 16);
|
||||||
|
assertEquals(imageData.height, 9);
|
||||||
|
assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
|
||||||
|
assertEquals(imageData.colorSpace, "srgb");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(function imageDataInitializedWithImageDataAndWidth() {
|
||||||
|
const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16);
|
||||||
|
|
||||||
|
assertEquals(imageData.width, 16);
|
||||||
|
assertEquals(imageData.height, 9);
|
||||||
|
assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
|
||||||
|
assertEquals(imageData.colorSpace, "srgb");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
function imageDataInitializedWithImageDataAndWidthAndHeightAndColorSpace() {
|
||||||
|
const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16, 9, {
|
||||||
|
colorSpace: "display-p3",
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(imageData.width, 16);
|
||||||
|
assertEquals(imageData.height, 9);
|
||||||
|
assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
|
||||||
|
assertEquals(imageData.colorSpace, "display-p3");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
async function imageDataUsedInWorker() {
|
||||||
|
const { promise, resolve } = Promise.withResolvers<void>();
|
||||||
|
const url = import.meta.resolve(
|
||||||
|
"../testdata/workers/image_data_worker.ts",
|
||||||
|
);
|
||||||
|
const expectedData = 16;
|
||||||
|
|
||||||
|
const worker = new Worker(url, { type: "module" });
|
||||||
|
worker.onmessage = function (e) {
|
||||||
|
assertEquals(expectedData, e.data);
|
||||||
|
worker.terminate();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
},
|
||||||
|
);
|
215
ext/web/16_image_data.js
Normal file
215
ext/web/16_image_data.js
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
import * as webidl from "ext:deno_webidl/00_webidl.js";
|
||||||
|
import DOMException from "ext:deno_web/01_dom_exception.js";
|
||||||
|
import { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
|
||||||
|
const primordials = globalThis.__bootstrap.primordials;
|
||||||
|
const {
|
||||||
|
ObjectPrototypeIsPrototypeOf,
|
||||||
|
SymbolFor,
|
||||||
|
TypedArrayPrototypeGetLength,
|
||||||
|
TypedArrayPrototypeGetSymbolToStringTag,
|
||||||
|
Uint8ClampedArray,
|
||||||
|
} = primordials;
|
||||||
|
|
||||||
|
webidl.converters["PredefinedColorSpace"] = webidl.createEnumConverter(
|
||||||
|
"PredefinedColorSpace",
|
||||||
|
[
|
||||||
|
"srgb",
|
||||||
|
"display-p3",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
webidl.converters["ImageDataSettings"] = webidl.createDictionaryConverter(
|
||||||
|
"ImageDataSettings",
|
||||||
|
[
|
||||||
|
{ key: "colorSpace", converter: webidl.converters["PredefinedColorSpace"] },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
class ImageData {
|
||||||
|
/** @type {number} */
|
||||||
|
#width;
|
||||||
|
/** @type {height} */
|
||||||
|
#height;
|
||||||
|
/** @type {Uint8Array} */
|
||||||
|
#data;
|
||||||
|
/** @type {'srgb' | 'display-p3'} */
|
||||||
|
#colorSpace;
|
||||||
|
|
||||||
|
constructor(arg0, arg1, arg2 = undefined, arg3 = undefined) {
|
||||||
|
webidl.requiredArguments(
|
||||||
|
arguments.length,
|
||||||
|
2,
|
||||||
|
'Failed to construct "ImageData"',
|
||||||
|
);
|
||||||
|
this[webidl.brand] = webidl.brand;
|
||||||
|
|
||||||
|
let sourceWidth;
|
||||||
|
let sourceHeight;
|
||||||
|
let data;
|
||||||
|
let settings;
|
||||||
|
const prefix = "Failed to construct 'ImageData'";
|
||||||
|
|
||||||
|
// Overload: new ImageData(data, sw [, sh [, settings ] ])
|
||||||
|
if (
|
||||||
|
arguments.length > 3 ||
|
||||||
|
TypedArrayPrototypeGetSymbolToStringTag(arg0) === "Uint8ClampedArray"
|
||||||
|
) {
|
||||||
|
data = webidl.converters.Uint8ClampedArray(arg0, prefix, "Argument 1");
|
||||||
|
sourceWidth = webidl.converters["unsigned long"](
|
||||||
|
arg1,
|
||||||
|
prefix,
|
||||||
|
"Argument 2",
|
||||||
|
);
|
||||||
|
const dataLength = TypedArrayPrototypeGetLength(data);
|
||||||
|
|
||||||
|
if (webidl.type(arg2) !== "Undefined") {
|
||||||
|
sourceHeight = webidl.converters["unsigned long"](
|
||||||
|
arg2,
|
||||||
|
prefix,
|
||||||
|
"Argument 3",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings = webidl.converters["ImageDataSettings"](
|
||||||
|
arg3,
|
||||||
|
prefix,
|
||||||
|
"Argument 4",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (dataLength === 0) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The input data has zero elements.",
|
||||||
|
"InvalidStateError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLength % 4 !== 0) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The input data length is not a multiple of 4.",
|
||||||
|
"InvalidStateError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceWidth < 1) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The source width is zero or not a number.",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webidl.type(sourceHeight) !== "Undefined" && sourceHeight < 1) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The source height is zero or not a number.",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLength / 4 % sourceWidth !== 0) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The input data length is not a multiple of (4 * width).",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
webidl.type(sourceHeight) !== "Undefined" &&
|
||||||
|
(sourceWidth * sourceHeight * 4 !== dataLength)
|
||||||
|
) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The input data length is not equal to (4 * width * height).",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webidl.type(sourceHeight) === "Undefined") {
|
||||||
|
this.#height = dataLength / 4 / sourceWidth;
|
||||||
|
} else {
|
||||||
|
this.#height = sourceHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#colorSpace = settings.colorSpace ?? "srgb";
|
||||||
|
this.#width = sourceWidth;
|
||||||
|
this.#data = data;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overload: new ImageData(sw, sh [, settings])
|
||||||
|
sourceWidth = webidl.converters["unsigned long"](
|
||||||
|
arg0,
|
||||||
|
prefix,
|
||||||
|
"Argument 1",
|
||||||
|
);
|
||||||
|
sourceHeight = webidl.converters["unsigned long"](
|
||||||
|
arg1,
|
||||||
|
prefix,
|
||||||
|
"Argument 2",
|
||||||
|
);
|
||||||
|
|
||||||
|
settings = webidl.converters["ImageDataSettings"](
|
||||||
|
arg2,
|
||||||
|
prefix,
|
||||||
|
"Argument 3",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourceWidth < 1) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The source width is zero or not a number.",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sourceHeight < 1) {
|
||||||
|
throw new DOMException(
|
||||||
|
"Failed to construct 'ImageData': The source height is zero or not a number.",
|
||||||
|
"IndexSizeError",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#colorSpace = settings.colorSpace ?? "srgb";
|
||||||
|
this.#width = sourceWidth;
|
||||||
|
this.#height = sourceHeight;
|
||||||
|
this.#data = new Uint8ClampedArray(sourceWidth * sourceHeight * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
webidl.assertBranded(this, ImageDataPrototype);
|
||||||
|
return this.#width;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
webidl.assertBranded(this, ImageDataPrototype);
|
||||||
|
return this.#height;
|
||||||
|
}
|
||||||
|
|
||||||
|
get data() {
|
||||||
|
webidl.assertBranded(this, ImageDataPrototype);
|
||||||
|
return this.#data;
|
||||||
|
}
|
||||||
|
|
||||||
|
get colorSpace() {
|
||||||
|
webidl.assertBranded(this, ImageDataPrototype);
|
||||||
|
return this.#colorSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
|
||||||
|
return inspect(
|
||||||
|
createFilteredInspectProxy({
|
||||||
|
object: this,
|
||||||
|
evaluate: ObjectPrototypeIsPrototypeOf(ImageDataPrototype, this),
|
||||||
|
keys: [
|
||||||
|
"data",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"colorSpace",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
inspectOptions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageDataPrototype = ImageData.prototype;
|
||||||
|
|
||||||
|
export { ImageData };
|
4
ext/web/internal.d.ts
vendored
4
ext/web/internal.d.ts
vendored
|
@ -111,3 +111,7 @@ declare module "ext:deno_web/13_message_port.js" {
|
||||||
transferables: Transferable[];
|
transferables: Transferable[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "ext:deno_web/16_image_data.js" {
|
||||||
|
const ImageData: typeof ImageData;
|
||||||
|
}
|
||||||
|
|
28
ext/web/lib.deno_web.d.ts
vendored
28
ext/web/lib.deno_web.d.ts
vendored
|
@ -1237,3 +1237,31 @@ declare var DecompressionStream: {
|
||||||
declare function reportError(
|
declare function reportError(
|
||||||
error: any,
|
error: any,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/** @category Web APIs */
|
||||||
|
type PredefinedColorSpace = "srgb" | "display-p3";
|
||||||
|
|
||||||
|
/** @category Web APIs */
|
||||||
|
interface ImageDataSettings {
|
||||||
|
readonly colorSpace?: PredefinedColorSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @category Web APIs */
|
||||||
|
interface ImageData {
|
||||||
|
readonly colorSpace: PredefinedColorSpace;
|
||||||
|
readonly data: Uint8ClampedArray;
|
||||||
|
readonly height: number;
|
||||||
|
readonly width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @category Web APIs */
|
||||||
|
declare var ImageData: {
|
||||||
|
prototype: ImageData;
|
||||||
|
new (sw: number, sh: number, settings?: ImageDataSettings): ImageData;
|
||||||
|
new (
|
||||||
|
data: Uint8ClampedArray,
|
||||||
|
sw: number,
|
||||||
|
sh?: number,
|
||||||
|
settings?: ImageDataSettings,
|
||||||
|
): ImageData;
|
||||||
|
};
|
||||||
|
|
|
@ -117,6 +117,7 @@ deno_core::extension!(deno_web,
|
||||||
"13_message_port.js",
|
"13_message_port.js",
|
||||||
"14_compression.js",
|
"14_compression.js",
|
||||||
"15_performance.js",
|
"15_performance.js",
|
||||||
|
"16_image_data.js",
|
||||||
],
|
],
|
||||||
options = {
|
options = {
|
||||||
blob_store: Arc<BlobStore>,
|
blob_store: Arc<BlobStore>,
|
||||||
|
|
|
@ -42,6 +42,7 @@ import * as abortSignal from "ext:deno_web/03_abort_signal.js";
|
||||||
import * as globalInterfaces from "ext:deno_web/04_global_interfaces.js";
|
import * as globalInterfaces from "ext:deno_web/04_global_interfaces.js";
|
||||||
import * as webStorage from "ext:deno_webstorage/01_webstorage.js";
|
import * as webStorage from "ext:deno_webstorage/01_webstorage.js";
|
||||||
import * as prompt from "ext:runtime/41_prompt.js";
|
import * as prompt from "ext:runtime/41_prompt.js";
|
||||||
|
import * as imageData from "ext:deno_web/16_image_data.js";
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope
|
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope
|
||||||
const windowOrWorkerGlobalScope = {
|
const windowOrWorkerGlobalScope = {
|
||||||
|
@ -67,6 +68,7 @@ const windowOrWorkerGlobalScope = {
|
||||||
FileReader: util.nonEnumerable(fileReader.FileReader),
|
FileReader: util.nonEnumerable(fileReader.FileReader),
|
||||||
FormData: util.nonEnumerable(formData.FormData),
|
FormData: util.nonEnumerable(formData.FormData),
|
||||||
Headers: util.nonEnumerable(headers.Headers),
|
Headers: util.nonEnumerable(headers.Headers),
|
||||||
|
ImageData: util.nonEnumerable(imageData.ImageData),
|
||||||
MessageEvent: util.nonEnumerable(event.MessageEvent),
|
MessageEvent: util.nonEnumerable(event.MessageEvent),
|
||||||
Performance: util.nonEnumerable(performance.Performance),
|
Performance: util.nonEnumerable(performance.Performance),
|
||||||
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
|
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
|
||||||
|
|
|
@ -222,6 +222,7 @@
|
||||||
"ext:deno_web/13_message_port.js": "../ext/web/13_message_port.js",
|
"ext:deno_web/13_message_port.js": "../ext/web/13_message_port.js",
|
||||||
"ext:deno_web/14_compression.js": "../ext/web/14_compression.js",
|
"ext:deno_web/14_compression.js": "../ext/web/14_compression.js",
|
||||||
"ext:deno_web/15_performance.js": "../ext/web/15_performance.js",
|
"ext:deno_web/15_performance.js": "../ext/web/15_performance.js",
|
||||||
|
"ext:deno_web/16_image_data.js": "../ext/web/16_image_data.js",
|
||||||
"ext:deno_webidl/00_webidl.js": "../ext/webidl/00_webidl.js",
|
"ext:deno_webidl/00_webidl.js": "../ext/webidl/00_webidl.js",
|
||||||
"ext:deno_websocket/01_websocket.js": "../ext/websocket/01_websocket.js",
|
"ext:deno_websocket/01_websocket.js": "../ext/websocket/01_websocket.js",
|
||||||
"ext:deno_websocket/02_websocketstream.js": "../ext/websocket/02_websocketstream.js",
|
"ext:deno_websocket/02_websocketstream.js": "../ext/websocket/02_websocketstream.js",
|
||||||
|
|
17
tools/wpt.ts
17
tools/wpt.ts
|
@ -711,14 +711,15 @@ function discoverTestsToRun(
|
||||||
1,
|
1,
|
||||||
) as ManifestTestVariation[]
|
) as ManifestTestVariation[]
|
||||||
) {
|
) {
|
||||||
if (!path) continue;
|
// Test keys ending with ".html" include their own html boilerplate.
|
||||||
const url = new URL(path, "http://web-platform.test:8000");
|
// Test keys ending with ".js" will have the necessary boilerplate generated and
|
||||||
if (
|
// the manifest path will contain the full path to the generated html test file.
|
||||||
!url.pathname.endsWith(".any.html") &&
|
// See: https://web-platform-tests.org/writing-tests/testharness.html
|
||||||
!url.pathname.endsWith(".window.html") &&
|
if (!key.endsWith(".html") && !key.endsWith(".js")) continue;
|
||||||
!url.pathname.endsWith(".worker.html") &&
|
|
||||||
!url.pathname.endsWith(".worker-module.html")
|
const testHtmlPath = path ?? `${prefix}/${key}`;
|
||||||
) {
|
const url = new URL(testHtmlPath, "http://web-platform.test:8000");
|
||||||
|
if (!url.pathname.endsWith(".html")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// These tests require an HTTP2 compatible server.
|
// These tests require an HTTP2 compatible server.
|
||||||
|
|
|
@ -7011,6 +7011,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"embedded-content": {
|
"embedded-content": {
|
||||||
|
"the-canvas-element": {
|
||||||
|
"imagedata.html": [
|
||||||
|
"ImageData(buffer, w, opt h), Uint8ClampedArray argument type check"
|
||||||
|
]
|
||||||
|
},
|
||||||
"the-iframe-element": {
|
"the-iframe-element": {
|
||||||
"cross-origin-to-whom-part-2.window.html": false,
|
"cross-origin-to-whom-part-2.window.html": false,
|
||||||
"cross-origin-to-whom.window.html": false,
|
"cross-origin-to-whom.window.html": false,
|
||||||
|
@ -8389,7 +8394,6 @@
|
||||||
"interface-objects": {
|
"interface-objects": {
|
||||||
"001.worker.html": [
|
"001.worker.html": [
|
||||||
"The SharedWorker interface object should be exposed.",
|
"The SharedWorker interface object should be exposed.",
|
||||||
"The ImageData interface object should be exposed.",
|
|
||||||
"The ImageBitmap interface object should be exposed.",
|
"The ImageBitmap interface object should be exposed.",
|
||||||
"The CanvasGradient interface object should be exposed.",
|
"The CanvasGradient interface object should be exposed.",
|
||||||
"The CanvasPattern interface object should be exposed.",
|
"The CanvasPattern interface object should be exposed.",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue