diff --git a/Cargo.lock b/Cargo.lock
index b8b8e735c362cb..3da8b82000020e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -491,6 +491,12 @@ version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
+[[package]]
+name = "bytemuck"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6"
+
[[package]]
name = "byteorder"
version = "1.5.0"
@@ -628,6 +634,12 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
[[package]]
name = "colorchoice"
version = "1.0.0"
@@ -1124,6 +1136,17 @@ dependencies = [
"url",
]
+[[package]]
+name = "deno_canvas"
+version = "0.1.0"
+dependencies = [
+ "deno_core",
+ "deno_webgpu",
+ "image",
+ "serde",
+ "tokio",
+]
+
[[package]]
name = "deno_config"
version = "0.8.1"
@@ -1621,6 +1644,7 @@ dependencies = [
"deno_ast",
"deno_broadcast_channel",
"deno_cache",
+ "deno_canvas",
"deno_console",
"deno_core",
"deno_cron",
@@ -2402,6 +2426,15 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "fdeflate"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "209098dd6dfc4445aa6111f0e98653ac323eaa4dfd212c9ca3931bf9955c31bd"
+dependencies = [
+ "simd-adler32",
+]
+
[[package]]
name = "ff"
version = "0.13.0"
@@ -3168,6 +3201,20 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
+[[package]]
+name = "image"
+version = "0.24.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "num-rational",
+ "num-traits",
+ "png",
+]
+
[[package]]
name = "import_map"
version = "0.18.2"
@@ -3713,6 +3760,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
dependencies = [
"adler",
+ "simd-adler32",
]
[[package]]
@@ -3947,6 +3995,17 @@ dependencies = [
"num-traits",
]
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.17"
@@ -4381,6 +4440,19 @@ dependencies = [
"syn 2.0.48",
]
+[[package]]
+name = "png"
+version = "0.17.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64"
+dependencies = [
+ "bitflags 1.3.2",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
[[package]]
name = "polyval"
version = "0.6.1"
@@ -5332,6 +5404,12 @@ dependencies = [
"rand_core",
]
+[[package]]
+name = "simd-adler32"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
+
[[package]]
name = "simd-json"
version = "0.13.4"
diff --git a/Cargo.toml b/Cargo.toml
index c6bb7c52ac6535..dc626f090e5f88 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ members = [
"test_util",
"ext/broadcast_channel",
"ext/cache",
+ "ext/canvas",
"ext/console",
"ext/cron",
"ext/crypto",
@@ -58,6 +59,7 @@ denokv_remote = "0.5.0"
# exts
deno_broadcast_channel = { version = "0.126.0", path = "./ext/broadcast_channel" }
deno_cache = { version = "0.64.0", path = "./ext/cache" }
+deno_canvas = { version = "0.1.0", path = "./ext/canvas" }
deno_console = { version = "0.132.0", path = "./ext/console" }
deno_cron = { version = "0.12.0", path = "./ext/cron" }
deno_crypto = { version = "0.146.0", path = "./ext/crypto" }
diff --git a/cli/build.rs b/cli/build.rs
index d3f428c508b98c..5fd6ca4d50e68b 100644
--- a/cli/build.rs
+++ b/cli/build.rs
@@ -152,6 +152,7 @@ mod ts {
op_crate_libs.insert("deno.webgpu", deno_webgpu_get_declaration());
op_crate_libs.insert("deno.websocket", deno_websocket::get_declaration());
op_crate_libs.insert("deno.webstorage", deno_webstorage::get_declaration());
+ op_crate_libs.insert("deno.canvas", deno_canvas::get_declaration());
op_crate_libs.insert("deno.crypto", deno_crypto::get_declaration());
op_crate_libs.insert(
"deno.broadcast_channel",
diff --git a/cli/tests/integration/js_unit_tests.rs b/cli/tests/integration/js_unit_tests.rs
index 8fbeb61e15850b..7680ee7a177564 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_bitmap_test,
image_data_test,
internals_test,
intl_test,
diff --git a/cli/tests/node_compat/test/common/index.js b/cli/tests/node_compat/test/common/index.js
index 986b8ea1a5af71..9f5b4814c197c8 100644
--- a/cli/tests/node_compat/test/common/index.js
+++ b/cli/tests/node_compat/test/common/index.js
@@ -34,6 +34,7 @@ let knownGlobals = [
closed,
confirm,
console,
+ createImageBitmap,
crypto,
Deno,
dispatchEvent,
diff --git a/cli/tests/unit/image_bitmap_test.ts b/cli/tests/unit/image_bitmap_test.ts
new file mode 100644
index 00000000000000..364f2a1677afe1
--- /dev/null
+++ b/cli/tests/unit/image_bitmap_test.ts
@@ -0,0 +1,92 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { assertEquals } from "./test_util.ts";
+
+function generateNumberedData(n: number): Uint8ClampedArray {
+ return new Uint8ClampedArray(
+ Array.from({ length: n }, (_, i) => [i + 1, 0, 0, 1]).flat(),
+ );
+}
+
+Deno.test(async function imageBitmapDirect() {
+ const data = generateNumberedData(3);
+ const imageData = new ImageData(data, 3, 1);
+ const imageBitmap = await createImageBitmap(imageData);
+ assertEquals(
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ Deno[Deno.internal].getBitmapData(imageBitmap),
+ new Uint8Array(data.buffer),
+ );
+});
+
+Deno.test(async function imageBitmapCrop() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, 1, 1, 1, 1);
+ assertEquals(
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ Deno[Deno.internal].getBitmapData(imageBitmap),
+ new Uint8Array([5, 0, 0, 1]),
+ );
+});
+
+Deno.test(async function imageBitmapCropPartialNegative() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, -1, -1, 2, 2);
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 1, 0, 0, 1
+ ]));
+});
+
+Deno.test(async function imageBitmapCropGreater() {
+ const data = generateNumberedData(3 * 3);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, -1, -1, 5, 5);
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 4, 0, 0, 1, 5, 0, 0, 1, 6, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 7, 0, 0, 1, 8, 0, 0, 1, 9, 0, 0, 1, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ ]));
+});
+
+Deno.test(async function imageBitmapScale() {
+ const data = generateNumberedData(3);
+ const imageData = new ImageData(data, 3, 1);
+ const imageBitmap = await createImageBitmap(imageData, {
+ resizeHeight: 5,
+ resizeWidth: 5,
+ resizeQuality: "pixelated",
+ });
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1,
+ 1, 0, 0, 1, 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1, 3, 0, 0, 1
+ ]));
+});
+
+Deno.test(async function imageBitmapFlipY() {
+ const data = generateNumberedData(9);
+ const imageData = new ImageData(data, 3, 3);
+ const imageBitmap = await createImageBitmap(imageData, {
+ imageOrientation: "flipY",
+ });
+ // @ts-ignore: Deno[Deno.internal].core allowed
+ // deno-fmt-ignore
+ assertEquals(Deno[Deno.internal].getBitmapData(imageBitmap), new Uint8Array([
+ 7, 0, 0, 1, 8, 0, 0, 1, 9, 0, 0, 1,
+ 4, 0, 0, 1, 5, 0, 0, 1, 6, 0, 0, 1,
+ 1, 0, 0, 1, 2, 0, 0, 1, 3, 0, 0, 1,
+ ]));
+});
diff --git a/cli/tsc/dts/lib.deno.shared_globals.d.ts b/cli/tsc/dts/lib.deno.shared_globals.d.ts
index f4d19c8e9d6fbc..86bf8237eceb0c 100644
--- a/cli/tsc/dts/lib.deno.shared_globals.d.ts
+++ b/cli/tsc/dts/lib.deno.shared_globals.d.ts
@@ -8,6 +8,8 @@
///
///
///
+///
+///
///
///
///
diff --git a/cli/tsc/dts/lib.deno.window.d.ts b/cli/tsc/dts/lib.deno.window.d.ts
index eaab7c3c237f89..83c385aa06e1ec 100644
--- a/cli/tsc/dts/lib.deno.window.d.ts
+++ b/cli/tsc/dts/lib.deno.window.d.ts
@@ -3,7 +3,6 @@
///
///
///
-///
///
///
///
diff --git a/cli/tsc/mod.rs b/cli/tsc/mod.rs
index 04450b8d097aea..4c7101d5cb9b51 100644
--- a/cli/tsc/mod.rs
+++ b/cli/tsc/mod.rs
@@ -94,6 +94,7 @@ pub fn get_types_declaration_file_text() -> String {
"deno.webgpu",
"deno.websocket",
"deno.webstorage",
+ "deno.canvas",
"deno.crypto",
"deno.broadcast_channel",
"deno.net",
diff --git a/ext/canvas/01_image.js b/ext/canvas/01_image.js
new file mode 100644
index 00000000000000..f87b227b3fd331
--- /dev/null
+++ b/ext/canvas/01_image.js
@@ -0,0 +1,552 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+import { core, internals, primordials } from "ext:core/mod.js";
+const ops = core.ops;
+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";
+import { BlobPrototype } from "ext:deno_web/09_file.js";
+import { sniffImage } from "ext:deno_web/01_mimesniff.js";
+const {
+ ObjectPrototypeIsPrototypeOf,
+ Symbol,
+ SymbolFor,
+ TypeError,
+ TypedArrayPrototypeGetBuffer,
+ TypedArrayPrototypeGetLength,
+ TypedArrayPrototypeGetSymbolToStringTag,
+ Uint8Array,
+ Uint8ClampedArray,
+ MathCeil,
+ PromiseResolve,
+ PromiseReject,
+ RangeError,
+} = primordials;
+
+webidl.converters["PredefinedColorSpace"] = webidl.createEnumConverter(
+ "PredefinedColorSpace",
+ [
+ "srgb",
+ "display-p3",
+ ],
+);
+
+webidl.converters["ImageDataSettings"] = webidl.createDictionaryConverter(
+ "ImageDataSettings",
+ [
+ { key: "colorSpace", converter: webidl.converters["PredefinedColorSpace"] },
+ ],
+);
+
+webidl.converters["ImageOrientation"] = webidl.createEnumConverter(
+ "ImageOrientation",
+ [
+ "from-image",
+ "flipY",
+ ],
+);
+
+webidl.converters["PremultiplyAlpha"] = webidl.createEnumConverter(
+ "PremultiplyAlpha",
+ [
+ "none",
+ "premultiply",
+ "default",
+ ],
+);
+
+webidl.converters["ColorSpaceConversion"] = webidl.createEnumConverter(
+ "ColorSpaceConversion",
+ [
+ "none",
+ "default",
+ ],
+);
+
+webidl.converters["ResizeQuality"] = webidl.createEnumConverter(
+ "ResizeQuality",
+ [
+ "pixelated",
+ "low",
+ "medium",
+ "high",
+ ],
+);
+
+webidl.converters["ImageBitmapOptions"] = webidl.createDictionaryConverter(
+ "ImageBitmapOptions",
+ [
+ {
+ key: "imageOrientation",
+ converter: webidl.converters["ImageOrientation"],
+ defaultValue: "from-image",
+ },
+ {
+ key: "premultiplyAlpha",
+ converter: webidl.converters["PremultiplyAlpha"],
+ defaultValue: "default",
+ },
+ {
+ key: "colorSpaceConversion",
+ converter: webidl.converters["ColorSpaceConversion"],
+ defaultValue: "default",
+ },
+ {
+ key: "resizeWidth",
+ converter: (v, prefix, context, opts) =>
+ webidl.converters["unsigned long"](v, prefix, context, {
+ ...opts,
+ enforceRange: true,
+ }),
+ },
+ {
+ key: "resizeHeight",
+ converter: (v, prefix, context, opts) =>
+ webidl.converters["unsigned long"](v, prefix, context, {
+ ...opts,
+ enforceRange: true,
+ }),
+ },
+ {
+ key: "resizeQuality",
+ converter: webidl.converters["ResizeQuality"],
+ defaultValue: "low",
+ },
+ ],
+);
+
+const _data = Symbol("[[data]]");
+const _width = Symbol("[[width]]");
+const _height = Symbol("[[height]]");
+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;
+
+const _bitmapData = Symbol("[[bitmapData]]");
+const _detached = Symbol("[[detached]]");
+class ImageBitmap {
+ [_width];
+ [_height];
+ [_bitmapData];
+ [_detached];
+
+ constructor() {
+ webidl.illegalConstructor();
+ }
+
+ get width() {
+ webidl.assertBranded(this, ImageBitmapPrototype);
+ if (this[_detached]) {
+ return 0;
+ }
+
+ return this[_width];
+ }
+
+ get height() {
+ webidl.assertBranded(this, ImageBitmapPrototype);
+ if (this[_detached]) {
+ return 0;
+ }
+
+ return this[_height];
+ }
+
+ close() {
+ webidl.assertBranded(this, ImageBitmapPrototype);
+ this[_detached] = true;
+ this[_bitmapData] = null;
+ }
+
+ [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
+ return inspect(
+ createFilteredInspectProxy({
+ object: this,
+ evaluate: ObjectPrototypeIsPrototypeOf(ImageBitmapPrototype, this),
+ keys: [
+ "width",
+ "height",
+ ],
+ }),
+ inspectOptions,
+ );
+ }
+}
+const ImageBitmapPrototype = ImageBitmap.prototype;
+
+function createImageBitmap(
+ image,
+ sxOrOptions = undefined,
+ sy = undefined,
+ sw = undefined,
+ sh = undefined,
+ options = undefined,
+) {
+ const prefix = "Failed to call 'createImageBitmap'";
+
+ // Overload: createImageBitmap(image [, options ])
+ if (arguments.length < 3) {
+ options = webidl.converters["ImageBitmapOptions"](
+ sxOrOptions,
+ prefix,
+ "Argument 2",
+ );
+ } else {
+ // Overload: createImageBitmap(image, sx, sy, sw, sh [, options ])
+ sxOrOptions = webidl.converters["long"](sxOrOptions, prefix, "Argument 2");
+ sy = webidl.converters["long"](sy, prefix, "Argument 3");
+ sw = webidl.converters["long"](sw, prefix, "Argument 4");
+ sh = webidl.converters["long"](sh, prefix, "Argument 5");
+ options = webidl.converters["ImageBitmapOptions"](
+ options,
+ prefix,
+ "Argument 6",
+ );
+
+ if (sw === 0) {
+ return PromiseReject(new RangeError("sw has to be greater than 0"));
+ }
+
+ if (sh === 0) {
+ return PromiseReject(new RangeError("sh has to be greater than 0"));
+ }
+ }
+
+ if (options.resizeWidth === 0) {
+ return PromiseReject(
+ new DOMException(
+ "options.resizeWidth has to be greater than 0",
+ "InvalidStateError",
+ ),
+ );
+ }
+ if (options.resizeHeight === 0) {
+ return PromiseReject(
+ new DOMException(
+ "options.resizeWidth has to be greater than 0",
+ "InvalidStateError",
+ ),
+ );
+ }
+
+ const imageBitmap = webidl.createBranded(ImageBitmap);
+
+ if (ObjectPrototypeIsPrototypeOf(ImageDataPrototype, image)) {
+ const processedImage = processImage(
+ image[_data],
+ image[_width],
+ image[_height],
+ sxOrOptions,
+ sy,
+ sw,
+ sh,
+ options,
+ );
+ imageBitmap[_bitmapData] = processedImage.data;
+ imageBitmap[_width] = processedImage.outputWidth;
+ imageBitmap[_height] = processedImage.outputHeight;
+ return PromiseResolve(imageBitmap);
+ }
+ if (ObjectPrototypeIsPrototypeOf(BlobPrototype, image)) {
+ return (async () => {
+ const data = await image.arrayBuffer();
+ const mimetype = sniffImage(image.type);
+ if (mimetype !== "image/png") {
+ throw new DOMException(
+ `Unsupported type '${image.type}'`,
+ "InvalidStateError",
+ );
+ }
+ const { data: imageData, width, height } = ops.op_image_decode_png(data);
+ const processedImage = processImage(
+ imageData,
+ width,
+ height,
+ sxOrOptions,
+ sy,
+ sw,
+ sh,
+ options,
+ );
+ imageBitmap[_bitmapData] = processedImage.data;
+ imageBitmap[_width] = processedImage.outputWidth;
+ imageBitmap[_height] = processedImage.outputHeight;
+ return imageBitmap;
+ })();
+ } else {
+ return PromiseReject(new TypeError("Invalid or unsupported image value"));
+ }
+}
+
+function processImage(input, width, height, sx, sy, sw, sh, options) {
+ let sourceRectangle;
+
+ if (
+ sx !== undefined && sy !== undefined && sw !== undefined && sh !== undefined
+ ) {
+ sourceRectangle = [
+ [sx, sy],
+ [sx + sw, sy],
+ [sx + sw, sy + sh],
+ [sx, sy + sh],
+ ];
+ } else {
+ sourceRectangle = [
+ [0, 0],
+ [width, 0],
+ [width, height],
+ [0, height],
+ ];
+ }
+ const widthOfSourceRect = sourceRectangle[1][0] - sourceRectangle[0][0];
+ const heightOfSourceRect = sourceRectangle[3][1] - sourceRectangle[0][1];
+
+ let outputWidth;
+ if (options.resizeWidth !== undefined) {
+ outputWidth = options.resizeWidth;
+ } else if (options.resizeHeight !== undefined) {
+ outputWidth = MathCeil(
+ (widthOfSourceRect * options.resizeHeight) / heightOfSourceRect,
+ );
+ } else {
+ outputWidth = widthOfSourceRect;
+ }
+
+ let outputHeight;
+ if (options.resizeHeight !== undefined) {
+ outputHeight = options.resizeHeight;
+ } else if (options.resizeWidth !== undefined) {
+ outputHeight = MathCeil(
+ (heightOfSourceRect * options.resizeWidth) / widthOfSourceRect,
+ );
+ } else {
+ outputHeight = heightOfSourceRect;
+ }
+
+ if (options.colorSpaceConversion === "none") {
+ throw new TypeError("options.colorSpaceConversion 'none' is not supported");
+ }
+
+ /*
+ * The cropping works differently than the spec specifies:
+ * The spec states to create an infinite surface and place the top-left corner
+ * of the image a 0,0 and crop based on sourceRectangle.
+ *
+ * We instead create a surface the size of sourceRectangle, and position
+ * the image at the correct location, which is the inverse of the x & y of
+ * sourceRectangle's top-left corner.
+ */
+ const data = ops.op_image_process(
+ new Uint8Array(TypedArrayPrototypeGetBuffer(input)),
+ {
+ width,
+ height,
+ surfaceWidth: widthOfSourceRect,
+ surfaceHeight: heightOfSourceRect,
+ inputX: sourceRectangle[0][0] * -1, // input_x
+ inputY: sourceRectangle[0][1] * -1, // input_y
+ outputWidth,
+ outputHeight,
+ resizeQuality: options.resizeQuality,
+ flipY: options.imageOrientation === "flipY",
+ premultiply: options.premultiplyAlpha === "default"
+ ? null
+ : (options.premultiplyAlpha === "premultiply"),
+ },
+ );
+
+ return {
+ data,
+ outputWidth,
+ outputHeight,
+ };
+}
+
+function getBitmapData(imageBitmap) {
+ return imageBitmap[_bitmapData];
+}
+
+internals.getBitmapData = getBitmapData;
+
+export { _bitmapData, _detached, createImageBitmap, ImageBitmap, ImageData };
diff --git a/ext/canvas/Cargo.toml b/ext/canvas/Cargo.toml
new file mode 100644
index 00000000000000..de5372d0c87fb4
--- /dev/null
+++ b/ext/canvas/Cargo.toml
@@ -0,0 +1,21 @@
+# Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+[package]
+name = "deno_canvas"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+readme = "README.md"
+repository.workspace = true
+description = "OffscreenCanvas implementation for Deno"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+deno_core.workspace = true
+deno_webgpu.workspace = true
+image = { version = "0.24.7", default-features = false, features = ["png"] }
+serde = { workspace = true, features = ["derive"] }
+tokio = { workspace = true, features = ["full"] }
diff --git a/ext/canvas/README.md b/ext/canvas/README.md
new file mode 100644
index 00000000000000..cf013677e786b1
--- /dev/null
+++ b/ext/canvas/README.md
@@ -0,0 +1,3 @@
+# deno_canvas
+
+Extension that implements various OffscreenCanvas related APIs.
diff --git a/ext/canvas/lib.deno_canvas.d.ts b/ext/canvas/lib.deno_canvas.d.ts
new file mode 100644
index 00000000000000..28d57d583a4833
--- /dev/null
+++ b/ext/canvas/lib.deno_canvas.d.ts
@@ -0,0 +1,87 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-var
+
+///
+///
+
+/** @category Web APIs */
+declare type PredefinedColorSpace = "srgb" | "display-p3";
+
+/** @category Web APIs */
+declare interface ImageDataSettings {
+ readonly colorSpace?: PredefinedColorSpace;
+}
+
+/** @category Web APIs */
+declare 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;
+};
+
+/** @category Web APIs */
+declare type ColorSpaceConversion = "default" | "none";
+
+/** @category Web APIs */
+declare type ImageOrientation = "flipY" | "from-image" | "none";
+
+/** @category Web APIs */
+declare type PremultiplyAlpha = "default" | "none" | "premultiply";
+
+/** @category Web APIs */
+declare type ResizeQuality = "high" | "low" | "medium" | "pixelated";
+
+/** @category Web APIs */
+declare type ImageBitmapSource = Blob | ImageData;
+
+/** @category Web APIs */
+interface ImageBitmapOptions {
+ colorSpaceConversion?: ColorSpaceConversion;
+ imageOrientation?: ImageOrientation;
+ premultiplyAlpha?: PremultiplyAlpha;
+ resizeHeight?: number;
+ resizeQuality?: ResizeQuality;
+ resizeWidth?: number;
+}
+
+/** @category Web APIs */
+declare function createImageBitmap(
+ image: ImageBitmapSource,
+ options?: ImageBitmapOptions,
+): Promise;
+/** @category Web APIs */
+declare function createImageBitmap(
+ image: ImageBitmapSource,
+ sx: number,
+ sy: number,
+ sw: number,
+ sh: number,
+ options?: ImageBitmapOptions,
+): Promise;
+
+/** @category Web APIs */
+interface ImageBitmap {
+ readonly height: number;
+ readonly width: number;
+ close(): void;
+}
+
+/** @category Web APIs */
+declare var ImageBitmap: {
+ prototype: ImageBitmap;
+ new (): ImageBitmap;
+};
diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs
new file mode 100644
index 00000000000000..b05332c3f12af2
--- /dev/null
+++ b/ext/canvas/lib.rs
@@ -0,0 +1,153 @@
+// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::error::type_error;
+use deno_core::error::AnyError;
+use deno_core::op2;
+use deno_core::ToJsBuffer;
+use image::imageops::FilterType;
+use image::ColorType;
+use image::ImageDecoder;
+use image::Pixel;
+use image::RgbaImage;
+use serde::Deserialize;
+use serde::Serialize;
+use std::path::PathBuf;
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "snake_case")]
+enum ImageResizeQuality {
+ Pixelated,
+ Low,
+ Medium,
+ High,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ImageProcessArgs {
+ width: u32,
+ height: u32,
+ surface_width: u32,
+ surface_height: u32,
+ input_x: i64,
+ input_y: i64,
+ output_width: u32,
+ output_height: u32,
+ resize_quality: ImageResizeQuality,
+ flip_y: bool,
+ premultiply: Option,
+}
+
+#[op2]
+#[serde]
+fn op_image_process(
+ #[buffer] buf: &[u8],
+ #[serde] args: ImageProcessArgs,
+) -> Result {
+ let view =
+ RgbaImage::from_vec(args.width, args.height, buf.to_vec()).unwrap();
+
+ let surface = if !(args.width == args.surface_width
+ && args.height == args.surface_height
+ && args.input_x == 0
+ && args.input_y == 0)
+ {
+ let mut surface = RgbaImage::new(args.surface_width, args.surface_height);
+
+ image::imageops::overlay(&mut surface, &view, args.input_x, args.input_y);
+
+ surface
+ } else {
+ view
+ };
+
+ let filter_type = match args.resize_quality {
+ ImageResizeQuality::Pixelated => FilterType::Nearest,
+ ImageResizeQuality::Low => FilterType::Triangle,
+ ImageResizeQuality::Medium => FilterType::CatmullRom,
+ ImageResizeQuality::High => FilterType::Lanczos3,
+ };
+
+ let mut image_out = image::imageops::resize(
+ &surface,
+ args.output_width,
+ args.output_height,
+ filter_type,
+ );
+
+ if args.flip_y {
+ image::imageops::flip_vertical_in_place(&mut image_out);
+ }
+
+ // ignore 9.
+
+ if let Some(premultiply) = args.premultiply {
+ let is_not_premultiplied = image_out.pixels().any(|pixel| {
+ (pixel.0[0].max(pixel.0[1]).max(pixel.0[2])) > (255 * pixel.0[3])
+ });
+
+ if premultiply {
+ if is_not_premultiplied {
+ for pixel in image_out.pixels_mut() {
+ let alpha = pixel.0[3];
+ pixel.apply_without_alpha(|channel| {
+ (channel as f32 * (alpha as f32 / 255.0)) as u8
+ })
+ }
+ }
+ } else if !is_not_premultiplied {
+ for pixel in image_out.pixels_mut() {
+ let alpha = pixel.0[3];
+ pixel.apply_without_alpha(|channel| {
+ (channel as f32 / (alpha as f32 / 255.0)) as u8
+ })
+ }
+ }
+ }
+
+ Ok(image_out.to_vec().into())
+}
+
+#[derive(Debug, Serialize)]
+struct DecodedPng {
+ data: ToJsBuffer,
+ width: u32,
+ height: u32,
+}
+
+#[op2]
+#[serde]
+fn op_image_decode_png(#[buffer] buf: &[u8]) -> Result {
+ let png = image::codecs::png::PngDecoder::new(buf)?;
+
+ let (width, height) = png.dimensions();
+
+ // TODO(@crowlKats): maybe use DynamicImage https://docs.rs/image/0.24.7/image/enum.DynamicImage.html ?
+ if png.color_type() != ColorType::Rgba8 {
+ return Err(type_error(format!(
+ "Color type '{:?}' not supported",
+ png.color_type()
+ )));
+ }
+
+ let mut png_data = Vec::with_capacity(png.total_bytes() as usize);
+
+ png.read_image(&mut png_data)?;
+
+ Ok(DecodedPng {
+ data: png_data.into(),
+ width,
+ height,
+ })
+}
+
+deno_core::extension!(
+ deno_canvas,
+ deps = [deno_webidl, deno_web, deno_webgpu],
+ ops = [op_image_process, op_image_decode_png],
+ lazy_loaded_esm = ["01_image.js"],
+);
+
+pub fn get_declaration() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_canvas.d.ts")
+}
diff --git a/ext/web/01_mimesniff.js b/ext/web/01_mimesniff.js
index 2978a07620a160..6fde35b56cbe47 100644
--- a/ext/web/01_mimesniff.js
+++ b/ext/web/01_mimesniff.js
@@ -18,9 +18,14 @@ const {
SafeMapIterator,
StringPrototypeReplaceAll,
StringPrototypeToLowerCase,
+ StringPrototypeEndsWith,
+ Uint8Array,
+ TypedArrayPrototypeGetLength,
+ TypedArrayPrototypeIncludes,
} = primordials;
import {
+ assert,
collectHttpQuotedString,
collectSequenceOfCodepoints,
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
@@ -251,4 +256,194 @@ function extractMimeType(headerValues) {
return mimeType;
}
-export { essence, extractMimeType, parseMimeType, serializeMimeType };
+/**
+ * Ref: https://mimesniff.spec.whatwg.org/#xml-mime-type
+ * @param {MimeType} mimeType
+ * @returns {boolean}
+ */
+function isXML(mimeType) {
+ return StringPrototypeEndsWith(mimeType.subtype, "+xml") ||
+ essence(mimeType) === "text/xml" || essence(mimeType) === "application/xml";
+}
+
+/**
+ * Ref: https://mimesniff.spec.whatwg.org/#pattern-matching-algorithm
+ * @param {Uint8Array} input
+ * @param {Uint8Array} pattern
+ * @param {Uint8Array} mask
+ * @param {Uint8Array} ignored
+ * @returns {boolean}
+ */
+function patternMatchingAlgorithm(input, pattern, mask, ignored) {
+ assert(
+ TypedArrayPrototypeGetLength(pattern) ===
+ TypedArrayPrototypeGetLength(mask),
+ );
+
+ if (
+ TypedArrayPrototypeGetLength(input) < TypedArrayPrototypeGetLength(pattern)
+ ) {
+ return false;
+ }
+
+ let s = 0;
+ for (; s < TypedArrayPrototypeGetLength(input); s++) {
+ if (!TypedArrayPrototypeIncludes(ignored, input[s])) {
+ break;
+ }
+ }
+
+ let p = 0;
+ for (; p < TypedArrayPrototypeGetLength(pattern); p++, s++) {
+ const maskedData = input[s] & mask[p];
+ if (maskedData !== pattern[p]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+const ImageTypePatternTable = [
+ // A Windows Icon signature.
+ [
+ new Uint8Array([0x00, 0x00, 0x01, 0x00]),
+ new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/x-icon",
+ ],
+ // A Windows Cursor signature.
+ [
+ new Uint8Array([0x00, 0x00, 0x02, 0x00]),
+ new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/x-icon",
+ ],
+ // The string "BM", a BMP signature.
+ [
+ new Uint8Array([0x42, 0x4D]),
+ new Uint8Array([0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/bmp",
+ ],
+ // The string "GIF87a", a GIF signature.
+ [
+ new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x37, 0x61]),
+ new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/gif",
+ ],
+ // The string "GIF89a", a GIF signature.
+ [
+ new Uint8Array([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]),
+ new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/gif",
+ ],
+ // The string "RIFF" followed by four bytes followed by the string "WEBPVP".
+ [
+ new Uint8Array([
+ 0x52,
+ 0x49,
+ 0x46,
+ 0x46,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x57,
+ 0x45,
+ 0x42,
+ 0x50,
+ 0x56,
+ 0x50,
+ ]),
+ new Uint8Array([
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ 0xFF,
+ ]),
+ new Uint8Array(),
+ "image/webp",
+ ],
+ // An error-checking byte followed by the string "PNG" followed by CR LF SUB LF, the PNG signature.
+ [
+ new Uint8Array([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
+ new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/png",
+ ],
+ // The JPEG Start of Image marker followed by the indicator byte of another marker.
+ [
+ new Uint8Array([0xFF, 0xD8, 0xFF]),
+ new Uint8Array([0xFF, 0xFF, 0xFF]),
+ new Uint8Array(),
+ "image/jpeg",
+ ],
+];
+
+/**
+ * Ref: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm
+ * @param {Uint8Array} input
+ * @returns {string | undefined}
+ */
+function imageTypePatternMatchingAlgorithm(input) {
+ for (let i = 0; i < ImageTypePatternTable.length; i++) {
+ const row = ImageTypePatternTable[i];
+ const patternMatched = patternMatchingAlgorithm(
+ input,
+ row[0],
+ row[1],
+ row[2],
+ );
+ if (patternMatched) {
+ return row[3];
+ }
+ }
+
+ return undefined;
+}
+
+/**
+ * Ref: https://mimesniff.spec.whatwg.org/#rules-for-sniffing-images-specifically
+ * @param {string} mimeTypeString
+ * @returns {string}
+ */
+function sniffImage(mimeTypeString) {
+ const mimeType = parseMimeType(mimeTypeString);
+ if (mimeType === null) {
+ return mimeTypeString;
+ }
+
+ if (isXML(mimeType)) {
+ return mimeTypeString;
+ }
+
+ const imageTypeMatched = imageTypePatternMatchingAlgorithm(
+ new TextEncoder().encode(mimeTypeString),
+ );
+ if (imageTypeMatched !== undefined) {
+ return imageTypeMatched;
+ }
+
+ return mimeTypeString;
+}
+
+export {
+ essence,
+ extractMimeType,
+ parseMimeType,
+ serializeMimeType,
+ sniffImage,
+};
diff --git a/ext/web/16_image_data.js b/ext/web/16_image_data.js
deleted file mode 100644
index 3dc6a46da91dab..00000000000000
--- a/ext/web/16_image_data.js
+++ /dev/null
@@ -1,216 +0,0 @@
-// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-
-import { primordials } from "ext:core/mod.js";
-const {
- ObjectPrototypeIsPrototypeOf,
- SymbolFor,
- TypedArrayPrototypeGetLength,
- TypedArrayPrototypeGetSymbolToStringTag,
- Uint8ClampedArray,
-} = primordials;
-
-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";
-
-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 c980ddceeb0ee9..4af04b07135a8e 100644
--- a/ext/web/internal.d.ts
+++ b/ext/web/internal.d.ts
@@ -111,7 +111,3 @@ 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 67d1d10c9af964..55048e14e5f333 100644
--- a/ext/web/lib.deno_web.d.ts
+++ b/ext/web/lib.deno_web.d.ts
@@ -1237,31 +1237,3 @@ 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 acac78f562e985..2792212ae3d70a 100644
--- a/ext/web/lib.rs
+++ b/ext/web/lib.rs
@@ -117,7 +117,6 @@ 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/Cargo.toml b/runtime/Cargo.toml
index 18bad2d07856ae..0b07839b295223 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -42,6 +42,7 @@ path = "examples/extension_with_ops/main.rs"
deno_ast.workspace = true
deno_broadcast_channel.workspace = true
deno_cache.workspace = true
+deno_canvas.workspace = true
deno_console.workspace = true
deno_core.workspace = true
deno_cron.workspace = true
@@ -73,6 +74,7 @@ winapi.workspace = true
deno_ast.workspace = true
deno_broadcast_channel.workspace = true
deno_cache.workspace = true
+deno_canvas.workspace = true
deno_console.workspace = true
deno_core.workspace = true
deno_cron.workspace = true
diff --git a/runtime/js/98_global_scope_shared.js b/runtime/js/98_global_scope_shared.js
index 04a6e4bd380db4..8ef26953958d3b 100644
--- a/runtime/js/98_global_scope_shared.js
+++ b/runtime/js/98_global_scope_shared.js
@@ -31,11 +31,67 @@ import * as messagePort from "ext:deno_web/13_message_port.js";
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { DOMException } from "ext:deno_web/01_dom_exception.js";
import * as abortSignal from "ext:deno_web/03_abort_signal.js";
-import * as imageData from "ext:deno_web/16_image_data.js";
import { webgpu, webGPUNonEnumerable } from "ext:deno_webgpu/00_init.js";
import * as webgpuSurface from "ext:deno_webgpu/02_surface.js";
import { unstableIds } from "ext:runtime/90_deno_ns.js";
+const { op_lazy_load_esm } = core.ensureFastOps(true);
+let image;
+
+function ImageNonEnumerable(getter) {
+ let valueIsSet = false;
+ let value;
+
+ return {
+ get() {
+ loadImage();
+
+ if (valueIsSet) {
+ return value;
+ } else {
+ return getter();
+ }
+ },
+ set(v) {
+ loadImage();
+
+ valueIsSet = true;
+ value = v;
+ },
+ enumerable: false,
+ configurable: true,
+ };
+}
+function ImageWritable(getter) {
+ let valueIsSet = false;
+ let value;
+
+ return {
+ get() {
+ loadImage();
+
+ if (valueIsSet) {
+ return value;
+ } else {
+ return getter();
+ }
+ },
+ set(v) {
+ loadImage();
+
+ valueIsSet = true;
+ value = v;
+ },
+ enumerable: true,
+ configurable: true,
+ };
+}
+function loadImage() {
+ if (!image) {
+ image = op_lazy_load_esm("ext:deno_canvas/01_image.js");
+ }
+}
+
// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope
const windowOrWorkerGlobalScope = {
AbortController: util.nonEnumerable(abortSignal.AbortController),
@@ -60,7 +116,8 @@ const windowOrWorkerGlobalScope = {
FileReader: util.nonEnumerable(fileReader.FileReader),
FormData: util.nonEnumerable(formData.FormData),
Headers: util.nonEnumerable(headers.Headers),
- ImageData: util.nonEnumerable(imageData.ImageData),
+ ImageData: ImageNonEnumerable(() => image.ImageData),
+ ImageBitmap: ImageNonEnumerable(() => image.ImageBitmap),
MessageEvent: util.nonEnumerable(event.MessageEvent),
Performance: util.nonEnumerable(performance.Performance),
PerformanceEntry: util.nonEnumerable(performance.PerformanceEntry),
@@ -110,6 +167,7 @@ const windowOrWorkerGlobalScope = {
),
atob: util.writable(base64.atob),
btoa: util.writable(base64.btoa),
+ createImageBitmap: ImageWritable(() => image.createImageBitmap),
clearInterval: util.writable(timers.clearInterval),
clearTimeout: util.writable(timers.clearTimeout),
caches: {
diff --git a/runtime/lib.rs b/runtime/lib.rs
index dbdce7850a48e7..5aa4e21a1ee746 100644
--- a/runtime/lib.rs
+++ b/runtime/lib.rs
@@ -2,6 +2,7 @@
pub use deno_broadcast_channel;
pub use deno_cache;
+pub use deno_canvas;
pub use deno_console;
pub use deno_core;
pub use deno_cron;
diff --git a/runtime/snapshot.rs b/runtime/snapshot.rs
index a50f0773abfe72..794de14d9e77b1 100644
--- a/runtime/snapshot.rs
+++ b/runtime/snapshot.rs
@@ -212,6 +212,7 @@ pub fn create_runtime_snapshot(
Default::default(),
),
deno_webgpu::deno_webgpu::init_ops_and_esm(),
+ deno_canvas::deno_canvas::init_ops_and_esm(),
deno_fetch::deno_fetch::init_ops_and_esm::(Default::default()),
deno_cache::deno_cache::init_ops_and_esm::(None),
deno_websocket::deno_websocket::init_ops_and_esm::(
diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs
index de32e39945ed2a..2b6eb19c91ac53 100644
--- a/runtime/web_worker.rs
+++ b/runtime/web_worker.rs
@@ -411,6 +411,7 @@ impl WebWorker {
Some(main_module.clone()),
),
deno_webgpu::deno_webgpu::init_ops_and_esm(),
+ deno_canvas::deno_canvas::init_ops_and_esm(),
deno_fetch::deno_fetch::init_ops_and_esm::(
deno_fetch::Options {
user_agent: options.bootstrap.user_agent.clone(),
diff --git a/runtime/worker.rs b/runtime/worker.rs
index 2cb1ab4915cce0..5dc5db71d6a168 100644
--- a/runtime/worker.rs
+++ b/runtime/worker.rs
@@ -346,6 +346,7 @@ impl MainWorker {
options.bootstrap.location.clone(),
),
deno_webgpu::deno_webgpu::init_ops_and_esm(),
+ deno_canvas::deno_canvas::init_ops_and_esm(),
deno_fetch::deno_fetch::init_ops_and_esm::(
deno_fetch::Options {
user_agent: options.bootstrap.user_agent.clone(),
diff --git a/tools/core_import_map.json b/tools/core_import_map.json
index 8122a2c84d35e1..cbae323edbd722 100644
--- a/tools/core_import_map.json
+++ b/tools/core_import_map.json
@@ -2,6 +2,7 @@
"imports": {
"ext:deno_broadcast_channel/01_broadcast_channel.js": "../ext/broadcast_channel/01_broadcast_channel.js",
"ext:deno_cache/01_cache.js": "../ext/cache/01_cache.js",
+ "ext:deno_canvas/01_image.js": "../ext/canvas/01_image.js",
"ext:deno_console/01_console.js": "../ext/console/01_console.js",
"ext:deno_cron/01_cron.ts": "../ext/cron/01_cron.ts",
"ext:deno_crypto/00_crypto.js": "../ext/crypto/00_crypto.js",
@@ -222,7 +223,6 @@
"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/expectation.json b/tools/wpt/expectation.json
index f473eef2062337..97bb6511383f64 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -8250,7 +8250,6 @@
"interface-objects": {
"001.worker.html": [
"The SharedWorker 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.",
"The CanvasPath interface object should be exposed.",
@@ -10971,4 +10970,4 @@
"eventsource-reconnect.window.html": false,
"request-status-error.window.html": false
}
-}
\ No newline at end of file
+}