diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig
index de9c4849..ca4e83d4 100644
--- a/src/browser/js/Context.zig
+++ b/src/browser/js/Context.zig
@@ -1045,6 +1045,12 @@ pub const FinalizerCallback = struct {
}
};
+/// Creates a new typed array. Memory is owned by JS context.
+/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Typed_arrays
+pub fn createTypedArray(self: *Context, comptime array_type: js.ArrayType, size: usize) js.ArrayBufferRef(array_type) {
+ return .init(self.isolate, size);
+}
+
// == Profiler ==
pub fn startCpuProfiler(self: *Context) void {
if (comptime !IS_DEBUG) {
diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig
index 6ce1166e..7beab5c9 100644
--- a/src/browser/js/Local.zig
+++ b/src/browser/js/Local.zig
@@ -306,6 +306,14 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
js.Value => return value,
js.Exception => return .{ .local = self, .handle = isolate.throwException(value.handle) },
+ js.ArrayBufferRef(.int8), js.ArrayBufferRef(.uint8), js.ArrayBufferRef(.uint8_clamped),
+ js.ArrayBufferRef(.int16), js.ArrayBufferRef(.uint16),
+ js.ArrayBufferRef(.int32), js.ArrayBufferRef(.uint32),
+ js.ArrayBufferRef(.float16), js.ArrayBufferRef(.float32), js.ArrayBufferRef(.float64),
+ => {
+ return .{ .local = self, .handle = value.handle };
+ },
+
inline
js.Function,
js.Object,
diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 623d898e..4a6c298e 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -864,4 +864,5 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/canvas/WebGLRenderingContext.zig"),
@import("../webapi/SubtleCrypto.zig"),
@import("../webapi/Selection.zig"),
+ @import("../webapi/ImageData.zig"),
});
diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig
index 0dc6609d..acb58a27 100644
--- a/src/browser/js/js.zig
+++ b/src/browser/js/js.zig
@@ -77,14 +77,69 @@ pub const ArrayBuffer = struct {
}
};
-/// `ref` indicates bytes are not copied by `simpleZigValueToJs`;
-/// instead, `values` references an already allocated memory. Note that
-/// this variant assumes memory is (de)allocated by an arena allocator.
-///
-/// `copy` behaves the same as `TypedArray(T)`.
-pub fn Uint8ClampedArray(comptime state: enum(u1) { ref, copy }) type {
+pub const ArrayType = enum(u8) {
+ int8,
+ uint8,
+ uint8_clamped,
+ int16,
+ uint16,
+ int32,
+ uint32,
+ float16,
+ float32,
+ float64,
+};
+
+pub fn ArrayBufferRef(comptime kind: ArrayType) type {
return struct {
- values: if (state == .ref) []u8 else []const u8,
+ const Self = @This();
+
+ const BackingInt = switch (kind) {
+ .int8 => i8,
+ .uint8, .uint8_clamped => u8,
+ .int16 => i16,
+ .uint16 => u16,
+ .int32 => i32,
+ .uint32 => u32,
+ .float16 => f16,
+ .float32 => f32,
+ .float64 => f64,
+ };
+
+ handle: *const v8.Value,
+
+ pub fn init(isolate: Isolate, size: usize) Self {
+ const bits = switch (@typeInfo(BackingInt)) {
+ .int => |n| n.bits,
+ .float => |f| f.bits,
+ else => unreachable,
+ };
+
+ var array_buffer: *const v8.ArrayBuffer = undefined;
+ if (size == 0) {
+ array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
+ } else {
+ const buffer_len = size * bits / 8;
+ const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, buffer_len).?;
+ const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
+ array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
+ }
+
+ const handle: *const v8.Value = switch (comptime kind) {
+ .int8 => @ptrCast(v8.v8__Int8Array__New(array_buffer, 0, size).?),
+ .uint8 => @ptrCast(v8.v8__Uint8Array__New(array_buffer, 0, size).?),
+ .uint8_clamped => @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, size).?),
+ .int16 => @ptrCast(v8.v8__Int16Array__New(array_buffer, 0, size).?),
+ .uint16 => @ptrCast(v8.v8__Uint16Array__New(array_buffer, 0, size).?),
+ .int32 => @ptrCast(v8.v8__Int32Array__New(array_buffer, 0, size).?),
+ .uint32 => @ptrCast(v8.v8__Uint32Array__New(array_buffer, 0, size).?),
+ .float16 => @ptrCast(v8.v8__Float16Array__New(array_buffer, 0, size).?),
+ .float32 => @ptrCast(v8.v8__Float32Array__New(array_buffer, 0, size).?),
+ .float64 => @ptrCast(v8.v8__Float64Array__New(array_buffer, 0, size).?),
+ };
+
+ return .{ .handle = handle };
+ }
};
}
@@ -207,40 +262,6 @@ pub fn simpleZigValueToJs(isolate: Isolate, value: anytype, comptime fail: bool,
// but this can never be valid.
@compileError("Invalid TypeArray type: " ++ @typeName(value_type));
},
- Uint8ClampedArray(.ref) => {
- const values = value.values;
- const len = values.len;
- var array_buffer: *const v8.ArrayBuffer = undefined;
- if (len == 0) {
- array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
- } else {
- // `deleter` cannot be null.
- const empty_deleter = struct {
- fn deleter(_: ?*anyopaque, _: usize, _: ?*anyopaque) callconv(.c) void {}
- }.deleter;
- const backing_store = v8.v8__ArrayBuffer__NewBackingStore2(values.ptr, len, empty_deleter, null);
- const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
- // Attach store to array buffer.
- array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
- }
- return @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, len));
- },
- Uint8ClampedArray(.copy) => {
- const values = value.values;
- const len = values.len;
- var array_buffer: *const v8.ArrayBuffer = undefined;
- if (len == 0) {
- array_buffer = v8.v8__ArrayBuffer__New(isolate.handle, 0).?;
- } else {
- const backing_store = v8.v8__ArrayBuffer__NewBackingStore(isolate.handle, len);
- const data: [*]u8 = @ptrCast(@alignCast(v8.v8__BackingStore__Data(backing_store)));
- @memcpy(data[0..len], @as([]const u8, @ptrCast(values))[0..len]);
- const backing_store_ptr = v8.v8__BackingStore__TO_SHARED_PTR(backing_store);
- // Attach store to array buffer.
- array_buffer = v8.v8__ArrayBuffer__New2(isolate.handle, &backing_store_ptr).?;
- }
- return @ptrCast(v8.v8__Uint8ClampedArray__New(array_buffer, 0, len));
- },
inline String, BigInt, Integer, Number, Value, Object => return value.handle,
else => {},
}
diff --git a/src/browser/tests/image_data.html b/src/browser/tests/image_data.html
new file mode 100644
index 00000000..ccaef668
--- /dev/null
+++ b/src/browser/tests/image_data.html
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/ImageData.zig b/src/browser/webapi/ImageData.zig
new file mode 100644
index 00000000..eed265fc
--- /dev/null
+++ b/src/browser/webapi/ImageData.zig
@@ -0,0 +1,122 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+
+const String = @import("../../string.zig").String;
+const log = @import("../../log.zig");
+
+const js = @import("../js/js.zig");
+const color = @import("../color.zig");
+const Page = @import("../Page.zig");
+
+/// https://developer.mozilla.org/en-US/docs/Web/API/ImageData/ImageData
+const ImageData = @This();
+_width: u32,
+_height: u32,
+_data: js.ArrayBufferRef(.uint8_clamped),
+
+pub const ConstructorSettings = struct {
+ /// Specifies the color space of the image data.
+ /// Can be set to "srgb" for the sRGB color space or "display-p3" for the display-p3 color space.
+ colorSpace: String = .wrap("srgb"),
+ /// Specifies the pixel format.
+ /// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/createImageData#pixelformat
+ pixelFormat: String = .wrap("rgba-unorm8"),
+};
+
+/// This has many constructors:
+///
+/// ```js
+/// new ImageData(width, height)
+/// new ImageData(width, height, settings)
+///
+/// new ImageData(dataArray, width)
+/// new ImageData(dataArray, width, height)
+/// new ImageData(dataArray, width, height, settings)
+/// ```
+///
+/// We currently support only the first 2.
+pub fn constructor(
+ width: u32,
+ height: u32,
+ maybe_settings: ?ConstructorSettings,
+ page: *Page,
+) !*ImageData {
+ if (width == 0 or height == 0) {
+ return error.IndexSizeError;
+ }
+
+ const settings: ConstructorSettings = maybe_settings orelse .{};
+ if (settings.colorSpace.eql(comptime .wrap("srgb")) == false) {
+ return error.TypeError;
+ }
+ if (settings.pixelFormat.eql(comptime .wrap("rgba-unorm8")) == false) {
+ return error.TypeError;
+ }
+
+ const size = width * height * 4;
+ return page._factory.create(ImageData{
+ ._width = width,
+ ._height = height,
+ ._data = page.js.createTypedArray(.uint8_clamped, size),
+ });
+}
+
+pub fn getWidth(self: *const ImageData) u32 {
+ return self._width;
+}
+
+pub fn getHeight(self: *const ImageData) u32 {
+ return self._height;
+}
+
+pub fn getPixelFormat(_: *const ImageData) String {
+ return comptime .wrap("rgba-unorm8");
+}
+
+pub fn getColorSpace(_: *const ImageData) String {
+ return comptime .wrap("srgb");
+}
+
+pub fn getData(self: *const ImageData) js.ArrayBufferRef(.uint8_clamped) {
+ return self._data;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(ImageData);
+
+ pub const Meta = struct {
+ pub const name = "ImageData";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(ImageData.constructor, .{ .dom_exception = true });
+
+ pub const width = bridge.accessor(ImageData.getWidth, null, .{});
+ pub const height = bridge.accessor(ImageData.getHeight, null, .{});
+ pub const pixelFormat = bridge.accessor(ImageData.getPixelFormat, null, .{});
+ pub const colorSpace = bridge.accessor(ImageData.getColorSpace, null, .{});
+ pub const data = bridge.accessor(ImageData.getData, null, .{});
+};
+
+const testing = @import("../../testing.zig");
+test "WebApi: ImageData" {
+ try testing.htmlRunner("image_data.html", .{});
+}