diff --git a/cli/tests/integration/js_unit_tests.rs b/cli/tests/integration/js_unit_tests.rs index bdab489268..165ab25bf4 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 0000000000..bf920d2f38 --- /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 0000000000..ea5d64dbb1 --- /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(); + 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 0000000000..a753a34cfc --- /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 a07f4b814e..e151806a19 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 9b9a55c238..9a70e15017 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 a68b6344ec..a5b77d6b83 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, diff --git a/runtime/js/98_global_scope.js b/runtime/js/98_global_scope.js index cc2fe3f9d9..406ea50f54 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 b4eed19f0d..6dff4c9306 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 07f6b6ba94..1867c1ed57 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 3b4b3b9d40..2c9aa8ceb5 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.",