1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

feat(web): ImageBitmap (#21898)

This commit is contained in:
Leo Kettmeir 2024-01-22 12:08:01 +01:00 committed by GitHub
parent b4990d1aa2
commit 8f76762793
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 1258 additions and 256 deletions

78
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

@ -34,6 +34,7 @@ let knownGlobals = [
closed,
confirm,
console,
createImageBitmap,
crypto,
Deno,
dispatchEvent,

View file

@ -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,
]));
});

View file

@ -8,6 +8,8 @@
/// <reference lib="deno.console" />
/// <reference lib="deno.url" />
/// <reference lib="deno.web" />
/// <reference lib="deno.webgpu" />
/// <reference lib="deno.canvas" />
/// <reference lib="deno.fetch" />
/// <reference lib="deno.websocket" />
/// <reference lib="deno.crypto" />

View file

@ -3,7 +3,6 @@
/// <reference no-default-lib="true" />
/// <reference lib="deno.ns" />
/// <reference lib="deno.shared_globals" />
/// <reference lib="deno.webgpu" />
/// <reference lib="deno.webstorage" />
/// <reference lib="esnext" />
/// <reference lib="deno.cache" />

View file

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

552
ext/canvas/01_image.js Normal file
View file

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

21
ext/canvas/Cargo.toml Normal file
View file

@ -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"] }

3
ext/canvas/README.md Normal file
View file

@ -0,0 +1,3 @@
# deno_canvas
Extension that implements various OffscreenCanvas related APIs.

87
ext/canvas/lib.deno_canvas.d.ts vendored Normal file
View file

@ -0,0 +1,87 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-var
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/** @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<ImageBitmap>;
/** @category Web APIs */
declare function createImageBitmap(
image: ImageBitmapSource,
sx: number,
sy: number,
sw: number,
sh: number,
options?: ImageBitmapOptions,
): Promise<ImageBitmap>;
/** @category Web APIs */
interface ImageBitmap {
readonly height: number;
readonly width: number;
close(): void;
}
/** @category Web APIs */
declare var ImageBitmap: {
prototype: ImageBitmap;
new (): ImageBitmap;
};

153
ext/canvas/lib.rs Normal file
View file

@ -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<bool>,
}
#[op2]
#[serde]
fn op_image_process(
#[buffer] buf: &[u8],
#[serde] args: ImageProcessArgs,
) -> Result<ToJsBuffer, AnyError> {
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<DecodedPng, AnyError> {
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")
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: {

View file

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

View file

@ -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::<Permissions>(Default::default()),
deno_cache::deno_cache::init_ops_and_esm::<SqliteBackedCache>(None),
deno_websocket::deno_websocket::init_ops_and_esm::<Permissions>(

View file

@ -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::<PermissionsContainer>(
deno_fetch::Options {
user_agent: options.bootstrap.user_agent.clone(),

View file

@ -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::<PermissionsContainer>(
deno_fetch::Options {
user_agent: options.bootstrap.user_agent.clone(),

View file

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

View file

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