mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -05: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,
|
||||
headers_test,
|
||||
http_test,
|
||||
image_data_test,
|
||||
internals_test,
|
||||
intl_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[];
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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",
|
||||
|
|
17
tools/wpt.ts
17
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.
|
||||
|
|
|
@ -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.",
|
||||
|
|
Loading…
Reference in a new issue