summaryrefslogtreecommitdiff
path: root/ext/canvas
diff options
context:
space:
mode:
authorLeo Kettmeir <crowlkats@toaxl.com>2024-01-22 12:08:01 +0100
committerGitHub <noreply@github.com>2024-01-22 12:08:01 +0100
commit8f767627938ef10802864419061e58a8a75db567 (patch)
tree81f61ba0f8c14fb820a72500840eb0c619d54362 /ext/canvas
parentb4990d1aa233db662cf22d7f872d45b3a947e0f6 (diff)
feat(web): ImageBitmap (#21898)
Diffstat (limited to 'ext/canvas')
-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
5 files changed, 816 insertions, 0 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")
+}