Repository: denoland/deno
Pull request: feat(ext/web): add `ImageData` `Float16Array` support

Implements whatwg/html#11143.


Changed files:
filename: cli/tsc/dts/lib.deno_web.d.ts
patch: @@ -1457,25 +1457,35 @@ declare function reportError(
 /** @category Platform */
 type PredefinedColorSpace = "srgb" | "display-p3";
 
+/** @category Platform */
+type ImageDataArray =
+  | Uint8ClampedArray<ArrayBuffer>
+  | Float16Array<ArrayBuffer>;
+
+/** @category Platform */
+type ImageDataPixelFormat = "rgba-unorm8" | "rgba-float16";
+
 /** @category Platform */
 interface ImageDataSettings {
   readonly colorSpace?: PredefinedColorSpace;
+  readonly pixelFormat?: ImageDataPixelFormat;
 }
 
 /** @category Platform */
 interface ImageData {
-  readonly colorSpace: PredefinedColorSpace;
-  readonly data: Uint8ClampedArray<ArrayBuffer>;
-  readonly height: number;
   readonly width: number;
+  readonly height: number;
+  readonly data: ImageDataArray;
+  readonly pixelFormat: ImageDataPixelFormat;
+  readonly colorSpace: PredefinedColorSpace;
 }
 
 /** @category Platform */
 declare var ImageData: {
   readonly prototype: ImageData;
   new (sw: number, sh: number, settings?: ImageDataSettings): ImageData;
   new (
-    data: Uint8ClampedArray<ArrayBuffer>,
+    data: ImageDataArray,
     sw: number,
     sh?: number,
     settings?: ImageDataSettings,

filename: ext/canvas/01_image.js
patch: @@ -12,7 +12,11 @@ const {
   Symbol,
   SymbolFor,
   TypedArrayPrototypeGetBuffer,
+  TypedArrayPrototypeGetByteOffset,
+  TypedArrayPrototypeGetLength,
+  TypedArrayPrototypeGetSymbolToStringTag,
   Uint8Array,
+  Uint8ClampedArray,
   PromiseReject,
   RangeError,
   ArrayPrototypeJoin,
@@ -153,6 +157,15 @@ class ImageBitmap {
 }
 const ImageBitmapPrototype = ImageBitmap.prototype;
 
+function float16ToUnorm8(data) {
+  const length = TypedArrayPrototypeGetLength(data);
+  const result = new Uint8ClampedArray(length);
+  for (let i = 0; i < length; i++) {
+    result[i] = data[i] * 255;
+  }
+  return result;
+}
+
 function createImageBitmap(
   image,
   sxOrOptions = undefined,
@@ -296,7 +309,17 @@ docs: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm\n
       width = image[_width];
       height = image[_height];
       imageBitmapSource = 1;
-      buf = new Uint8Array(TypedArrayPrototypeGetBuffer(image[_data]));
+      let data = image[_data];
+      switch (TypedArrayPrototypeGetSymbolToStringTag(data)) {
+        case "Float16Array":
+          data = float16ToUnorm8(data);
+          break;
+      }
+      buf = new Uint8Array(
+        TypedArrayPrototypeGetBuffer(data),
+        TypedArrayPrototypeGetByteOffset(data),
+        TypedArrayPrototypeGetLength(data),
+      );
     } else if (isImageBitmap) {
       width = image[_width];
       height = image[_height];

filename: ext/web/16_image_data.js
patch: @@ -21,10 +21,20 @@ webidl.converters["PredefinedColorSpace"] = webidl.createEnumConverter(
   ],
 );
 
+webidl.converters["ImageDataPixelFormat"] = webidl.createEnumConverter(
+  "ImageDataPixelFormat",
+  ["rgba-unorm8", "rgba-float16"],
+);
+
 webidl.converters["ImageDataSettings"] = webidl.createDictionaryConverter(
   "ImageDataSettings",
   [
     { key: "colorSpace", converter: webidl.converters["PredefinedColorSpace"] },
+    {
+      key: "pixelFormat",
+      converter: webidl.converters["ImageDataPixelFormat"],
+      defaultValue: "rgba-unorm8",
+    },
   ],
 );
 
@@ -34,11 +44,13 @@ const _height = Symbol("[[height]]");
 class ImageData {
   /** @type {number} */
   [_width];
-  /** @type {height} */
+  /** @type {number} */
   [_height];
-  /** @type {Uint8Array} */
+  /** @type {ImageDataArray} */
   [_data];
-  /** @type {'srgb' | 'display-p3'} */
+  /** @type {ImageDataPixelFormat} */
+  #pixelFormat;
+  /** @type {PredefinedColorSpace} */
   #colorSpace;
 
   constructor(arg0, arg1, arg2 = undefined, arg3 = undefined) {
@@ -54,21 +66,22 @@ class ImageData {
     let data;
     let settings;
     const prefix = "Failed to construct 'ImageData'";
+    const tag = TypedArrayPrototypeGetSymbolToStringTag(arg0);
 
     // Overload: new ImageData(data, sw [, sh [, settings ] ])
     if (
       arguments.length > 3 ||
-      TypedArrayPrototypeGetSymbolToStringTag(arg0) === "Uint8ClampedArray"
+      tag === "Uint8ClampedArray" || tag === "Float16Array"
     ) {
-      data = webidl.converters.Uint8ClampedArray(arg0, prefix, "Argument 1");
+      data = webidl.converters.ArrayBufferView(arg0, prefix, "Argument 1");
       sourceWidth = webidl.converters["unsigned long"](
         arg1,
         prefix,
         "Argument 2",
       );
       const dataLength = TypedArrayPrototypeGetLength(data);
 
-      if (webidl.type(arg2) !== "Undefined") {
+      if (arg2 !== undefined) {
         sourceHeight = webidl.converters["unsigned long"](
           arg2,
           prefix,
@@ -103,7 +116,7 @@ class ImageData {
         );
       }
 
-      if (webidl.type(sourceHeight) !== "Undefined" && sourceHeight < 1) {
+      if (sourceHeight !== undefined && sourceHeight < 1) {
         throw new DOMException(
           "Failed to construct 'ImageData': the source height is zero or not a number",
           "IndexSizeError",
@@ -118,23 +131,38 @@ class ImageData {
       }
 
       if (
-        webidl.type(sourceHeight) !== "Undefined" &&
-        (sourceWidth * sourceHeight * 4 !== dataLength)
+        sourceHeight !== undefined &&
+        dataLength / 4 / sourceWidth !== sourceHeight
       ) {
         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;
+      switch (tag) {
+        case "Uint8ClampedArray":
+          if (settings.pixelFormat !== "rgba-unorm8") {
+            throw new DOMException(
+              "Failed to construct 'ImageData': Uint8ClampedArray must use rgba-unorm8 pixelFormat.",
+              "InvalidStateError",
+            );
+          }
+          break;
+        case "Float16Array":
+          if (settings.pixelFormat !== "rgba-float16") {
+            throw new DOMException(
+              "Failed to construct 'ImageData': Float16Array must use rgba-float16 pixelFormat.",
+              "InvalidStateError",
+            );
+          }
+          break;
       }
 
+      this.#pixelFormat = settings.pixelFormat;
       this.#colorSpace = settings.colorSpace ?? "srgb";
       this[_width] = sourceWidth;
+      this[_height] = dataLength / 4 / sourceWidth;
       this[_data] = data;
       return;
     }
@@ -171,10 +199,21 @@ class ImageData {
       );
     }
 
+    switch (settings.pixelFormat) {
+      case "rgba-unorm8":
+        data = new Uint8ClampedArray(sourceWidth * sourceHeight * 4);
+        break;
+      case "rgba-float16":
+        // TODO(0f-0b): add Float16Array to primordials
+        data = new Float16Array(sourceWidth * sourceHeight * 4);
+        break;
+    }
+
+    this.#pixelFormat = settings.pixelFormat;
     this.#colorSpace = settings.colorSpace ?? "srgb";
     this[_width] = sourceWidth;
     this[_height] = sourceHeight;
-    this[_data] = new Uint8ClampedArray(sourceWidth * sourceHeight * 4);
+    this[_data] = data;
   }
 
   get width() {
@@ -192,6 +231,11 @@ class ImageData {
     return this[_data];
   }
 
+  get pixelFormat() {
+    webidl.assertBranded(this, ImageDataPrototype);
+    return this.#pixelFormat;
+  }
+
   get colorSpace() {
     webidl.assertBranded(this, ImageDataPrototype);
     return this.#colorSpace;
@@ -206,6 +250,7 @@ class ImageData {
           "data",
           "width",
           "height",
+          "pixelFormat",
           "colorSpace",
         ],
       }),

filename: tests/unit/image_data_test.ts
patch: @@ -1,38 +1,75 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
-import { assertEquals } from "./test_util.ts";
+import { assertEquals, assertStrictEquals } from "./test_util.ts";
 
 Deno.test(function imageDataInitializedWithSourceWidthAndHeight() {
   const imageData = new ImageData(16, 9);
 
+  assertStrictEquals(imageData.data.constructor, Uint8ClampedArray);
+  assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
   assertEquals(imageData.width, 16);
   assertEquals(imageData.height, 9);
-  assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+  assertEquals(imageData.pixelFormat, "rgba-unorm8");
   assertEquals(imageData.colorSpace, "srgb");
 });
 
 Deno.test(function imageDataInitializedWithImageDataAndWidth() {
-  const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16);
+  const data = new Uint8ClampedArray(16 * 9 * 4);
+  const imageData = new ImageData(data, 16);
 
+  assertStrictEquals(imageData.data, data);
   assertEquals(imageData.width, 16);
   assertEquals(imageData.height, 9);
-  assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+  assertEquals(imageData.pixelFormat, "rgba-unorm8");
   assertEquals(imageData.colorSpace, "srgb");
 });
 
 Deno.test(
   function imageDataInitializedWithImageDataAndWidthAndHeightAndColorSpace() {
-    const imageData = new ImageData(new Uint8ClampedArray(16 * 9 * 4), 16, 9, {
+    const data = new Uint8ClampedArray(16 * 9 * 4);
+    const imageData = new ImageData(data, 16, 9, {
       colorSpace: "display-p3",
     });
 
+    assertStrictEquals(imageData.data, data);
     assertEquals(imageData.width, 16);
     assertEquals(imageData.height, 9);
+    assertEquals(imageData.pixelFormat, "rgba-unorm8");
+    assertEquals(imageData.colorSpace, "display-p3");
+  },
+);
+
+Deno.test(
+  function imageDataInitializedWithWidthAndHeightAndPixelFormatAndColorSpace() {
+    const imageData = new ImageData(16, 9, {
+      pixelFormat: "rgba-float16",
+      colorSpace: "display-p3",
+    });
+
+    assertStrictEquals(imageData.data.constructor, Float16Array);
     assertEquals(imageData.data.length, 16 * 9 * 4); // width * height * 4 (RGBA pixels)
+    assertEquals(imageData.width, 16);
+    assertEquals(imageData.height, 9);
+    assertEquals(imageData.pixelFormat, "rgba-float16");
     assertEquals(imageData.colorSpace, "display-p3");
   },
 );
 
+Deno.test(
+  function imageDataInitializedWithImageDataAndWidthAndPixelFormat() {
+    const data = new Float16Array(16 * 9 * 4);
+    const imageData = new ImageData(data, 16, undefined, {
+      pixelFormat: "rgba-float16",
+    });
+
+    assertStrictEquals(imageData.data, data);
+    assertEquals(imageData.width, 16);
+    assertEquals(imageData.height, 9);
+    assertEquals(imageData.pixelFormat, "rgba-float16");
+    assertEquals(imageData.colorSpace, "srgb");
+  },
+);
+
 Deno.test(
   async function imageDataUsedInWorker() {
     const { promise, resolve } = Promise.withResolvers<void>();