1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-27 01:29:14 -05:00
denoland-deno/ext/canvas/01_image.js

552 lines
14 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { internals, primordials } from "ext:core/mod.js";
import { op_image_decode_png, op_image_process } from "ext: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 } = 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 = 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 };