diff options
Diffstat (limited to 'ext/canvas/01_image.js')
-rw-r--r-- | ext/canvas/01_image.js | 552 |
1 files changed, 552 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 }; |