summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/canvas/01_image.js552
-rw-r--r--ext/canvas/Cargo.toml21
-rw-r--r--ext/canvas/README.md3
-rw-r--r--ext/canvas/lib.deno_canvas.d.ts87
-rw-r--r--ext/canvas/lib.rs153
-rw-r--r--ext/web/01_mimesniff.js197
-rw-r--r--ext/web/16_image_data.js216
-rw-r--r--ext/web/internal.d.ts4
-rw-r--r--ext/web/lib.deno_web.d.ts28
-rw-r--r--ext/web/lib.rs1
10 files changed, 1012 insertions, 250 deletions
diff --git a/ext/canvas/01_image.js b/ext/canvas/01_image.js
new file mode 100644
index 000000000..f87b227b3
--- /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 000000000..de5372d0c
--- /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 000000000..cf013677e
--- /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 000000000..28d57d583
--- /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
+
+/// <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;
+};
diff --git a/ext/canvas/lib.rs b/ext/canvas/lib.rs
new file mode 100644
index 000000000..b05332c3f
--- /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<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")
+}
diff --git a/ext/web/01_mimesniff.js b/ext/web/01_mimesniff.js
index 2978a0762..6fde35b56 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 3dc6a46da..000000000
--- 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 c980ddcee..4af04b071 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 67d1d10c9..55048e14e 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 acac78f56..2792212ae 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<BlobStore>,