1
0
Fork 0
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:
Jamie 2023-12-06 22:20:28 +09:00 committed by GitHub
parent dadd8b3d66
commit 8c0fb9003d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 321 additions and 9 deletions

View file

@ -42,6 +42,7 @@ util::unit_test_factory!(
globals_test,
headers_test,
http_test,
image_data_test,
internals_test,
intl_test,
io_test,

View file

@ -0,0 +1,2 @@
const data = new ImageData(2, 2, { colorSpace: "display-p3" });
postMessage(data.data.length);

View 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
View 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 };

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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>,

View file

@ -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),

View file

@ -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",

View file

@ -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.

View file

@ -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.",