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 +}