diff options
Diffstat (limited to 'ext')
| -rw-r--r-- | ext/canvas/01_image.js | 552 | ||||
| -rw-r--r-- | ext/canvas/Cargo.toml | 21 | ||||
| -rw-r--r-- | ext/canvas/README.md | 3 | ||||
| -rw-r--r-- | ext/canvas/lib.deno_canvas.d.ts | 87 | ||||
| -rw-r--r-- | ext/canvas/lib.rs | 153 | ||||
| -rw-r--r-- | ext/web/01_mimesniff.js | 197 | ||||
| -rw-r--r-- | ext/web/16_image_data.js | 216 | ||||
| -rw-r--r-- | ext/web/internal.d.ts | 4 | ||||
| -rw-r--r-- | ext/web/lib.deno_web.d.ts | 28 | ||||
| -rw-r--r-- | ext/web/lib.rs | 1 |
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>, |
