summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie <5964236+jamsinclair@users.noreply.github.com>2023-12-06 22:20:28 +0900
committerGitHub <noreply@github.com>2023-12-06 14:20:28 +0100
commit8c0fb9003d874fcbee0b1a6f6ee30dfb58c668bc (patch)
tree643966c1543346f803e373329d6c8bb28e199621
parentdadd8b3d660fd2fd56803f29e1d8b6dd7a2adde9 (diff)
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)
-rw-r--r--cli/tests/integration/js_unit_tests.rs1
-rw-r--r--cli/tests/testdata/workers/image_data_worker.ts2
-rw-r--r--cli/tests/unit/image_data_test.ts53
-rw-r--r--ext/web/16_image_data.js215
-rw-r--r--ext/web/internal.d.ts4
-rw-r--r--ext/web/lib.deno_web.d.ts28
-rw-r--r--ext/web/lib.rs1
-rw-r--r--runtime/js/98_global_scope.js2
-rw-r--r--tools/core_import_map.json1
-rwxr-xr-xtools/wpt.ts17
-rw-r--r--tools/wpt/expectation.json6
11 files changed, 321 insertions, 9 deletions
diff --git a/cli/tests/integration/js_unit_tests.rs b/cli/tests/integration/js_unit_tests.rs
index bdab48926..165ab25bf 100644
--- a/cli/tests/integration/js_unit_tests.rs
+++ b/cli/tests/integration/js_unit_tests.rs
@@ -42,6 +42,7 @@ util::unit_test_factory!(
globals_test,
headers_test,
http_test,
+ image_data_test,
internals_test,
intl_test,
io_test,
diff --git a/cli/tests/testdata/workers/image_data_worker.ts b/cli/tests/testdata/workers/image_data_worker.ts
new file mode 100644
index 000000000..bf920d2f3
--- /dev/null
+++ b/cli/tests/testdata/workers/image_data_worker.ts
@@ -0,0 +1,2 @@
+const data = new ImageData(2, 2, { colorSpace: "display-p3" });
+postMessage(data.data.length);
diff --git a/cli/tests/unit/image_data_test.ts b/cli/tests/unit/image_data_test.ts
new file mode 100644
index 000000000..ea5d64dbb
--- /dev/null
+++ b/cli/tests/unit/image_data_test.ts
@@ -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;
+ },
+);
diff --git a/ext/web/16_image_data.js b/ext/web/16_image_data.js
new file mode 100644
index 000000000..a753a34cf
--- /dev/null
+++ b/ext/web/16_image_data.js
@@ -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 };
diff --git a/ext/web/internal.d.ts b/ext/web/internal.d.ts
index a07f4b814..e151806a1 100644
--- a/ext/web/internal.d.ts
+++ b/ext/web/internal.d.ts
@@ -111,3 +111,7 @@ declare module "ext:deno_web/13_message_port.js" {
transferables: Transferable[];
}
}
+
+declare module "ext:deno_web/16_image_data.js" {
+ const ImageData: typeof ImageData;
+}
diff --git a/ext/web/lib.deno_web.d.ts b/ext/web/lib.deno_web.d.ts
index 9b9a55c23..9a70e1501 100644
--- a/ext/web/lib.deno_web.d.ts
+++ b/ext/web/lib.deno_web.d.ts
@@ -1237,3 +1237,31 @@ declare var DecompressionStream: {
declare function reportError(
error: any,
): 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;
+};
diff --git a/ext/web/lib.rs b/ext/web/lib.rs
index a68b6344e..a5b77d6b8 100644
--- a/ext/web/lib.rs
+++ b/ext/web/lib.rs
@@ -117,6 +117,7 @@ deno_core::extension!(deno_web,
"13_message_port.js",
"14_compression.js",
"15_performance.js",
+ "16_image_data.js",
],
options = {
blob_store: Arc<BlobStore>,
diff --git a/runtime/js/98_global_scope.js b/runtime/js/98_global_scope.js
index cc2fe3f9d..406ea50f5 100644
--- a/runtime/js/98_global_scope.js
+++ b/runtime/js/98_global_scope.js
@@ -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 webStorage from "ext:deno_webstorage/01_webstorage.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
const windowOrWorkerGlobalScope = {
@@ -67,6 +68,7 @@ const windowOrWorkerGlobalScope = {
FileReader: util.nonEnumerable(fileReader.FileReader),
FormData: util.nonEnumerable(formData.FormData),
Headers: util.nonEnumerable(headers.Headers),
+ ImageData: util.nonEnumerable(imageData.ImageData),
MessageEvent: util.nonEnumerable(event.MessageEvent),
Performance: util.nonEnumerable(performance.Performance),
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
diff --git a/tools/core_import_map.json b/tools/core_import_map.json
index b4eed19f0..6dff4c930 100644
--- a/tools/core_import_map.json
+++ b/tools/core_import_map.json
@@ -222,6 +222,7 @@
"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/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_websocket/01_websocket.js": "../ext/websocket/01_websocket.js",
"ext:deno_websocket/02_websocketstream.js": "../ext/websocket/02_websocketstream.js",
diff --git a/tools/wpt.ts b/tools/wpt.ts
index 07f6b6ba9..1867c1ed5 100755
--- a/tools/wpt.ts
+++ b/tools/wpt.ts
@@ -711,14 +711,15 @@ function discoverTestsToRun(
1,
) as ManifestTestVariation[]
) {
- if (!path) continue;
- const url = new URL(path, "http://web-platform.test:8000");
- if (
- !url.pathname.endsWith(".any.html") &&
- !url.pathname.endsWith(".window.html") &&
- !url.pathname.endsWith(".worker.html") &&
- !url.pathname.endsWith(".worker-module.html")
- ) {
+ // Test keys ending with ".html" include their own html boilerplate.
+ // Test keys ending with ".js" will have the necessary boilerplate generated and
+ // the manifest path will contain the full path to the generated html test file.
+ // See: https://web-platform-tests.org/writing-tests/testharness.html
+ if (!key.endsWith(".html") && !key.endsWith(".js")) continue;
+
+ const testHtmlPath = path ?? `${prefix}/${key}`;
+ const url = new URL(testHtmlPath, "http://web-platform.test:8000");
+ if (!url.pathname.endsWith(".html")) {
continue;
}
// These tests require an HTTP2 compatible server.
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 3b4b3b9d4..2c9aa8ceb 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -7011,6 +7011,11 @@
}
},
"embedded-content": {
+ "the-canvas-element": {
+ "imagedata.html": [
+ "ImageData(buffer, w, opt h), Uint8ClampedArray argument type check"
+ ]
+ },
"the-iframe-element": {
"cross-origin-to-whom-part-2.window.html": false,
"cross-origin-to-whom.window.html": false,
@@ -8389,7 +8394,6 @@
"interface-objects": {
"001.worker.html": [
"The SharedWorker interface object should be exposed.",
- "The ImageData interface object should be exposed.",
"The ImageBitmap interface object should be exposed.",
"The CanvasGradient interface object should be exposed.",
"The CanvasPattern interface object should be exposed.",