Skip to content

Commit 3b2cbfe

Browse files
crowlbotcrowlKats
andauthored
refactor(ext/web): port ImageData to Rust with CPPGC/objectwrap (#33777)
## Summary - Moves the `ImageData` class from a JS IIFE (`ext/web/16_image_data.js`) into a CPPGC-backed Rust struct in `ext/web/image_data.rs`, registered via `op2`'s `objects = [...]`. - Mirrors the established pattern from the geometry interfaces (`DOMRect`/`DOMPoint`/etc.): WebIDL-derived enums and dictionary, manual `trace` over `v8::TracedReference`, and an ESM (`image_data.js`) that wires up the custom inspect proxy and `webidl.configureInterface`. - `runtime/js/98_global_scope_shared.js` now lazy-loads `ImageData` (consistent with how `DOMRect` and friends are exposed). - `ext/image/01_image.js` switches off the private `_data` / `_width` / `_height` symbols and uses the public `width` / `height` / `data` getters instead. - Registers a new `DOMExceptionIndexSizeError` error builder so the Rust constructor can throw the same `IndexSizeError` cases the JS implementation did. The two `ImageData` constructor overloads (`(sw, sh [, settings])` and `(data, sw [, sh [, settings]])`) and every existing error path are preserved. `Uint8ClampedArray` and `Float16Array` data are kept by reference (no copy) via `v8::TracedReference<v8::Object>`. ## Test plan - [x] `cargo build --bin deno` - [x] `tools/format.js --check` - [x] `tools/lint.js` - [x] Smoke test the WPT-style cases from `tests/unit/image_data_test.ts` (both overloads, default `srgb` / `rgba-unorm8`, custom `display-p3` / `rgba-float16`, and every `DOMException` error path). - [x] `createImageBitmap(imageData)` (basic + crop) round-trips through the new public `image.data` accessor in `ext/image/01_image.js`. - [x] `tests/testdata/workers/image_data_worker.ts` (ImageData inside a Worker). --------- Co-authored-by: Leo Kettmeir <crowlkats@toaxl.com>
1 parent d513f20 commit 3b2cbfe

6 files changed

Lines changed: 422 additions & 263 deletions

File tree

ext/image/01_image.js

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,10 @@ const {
2323
RangeError,
2424
ArrayPrototypeJoin,
2525
} = primordials;
26-
const {
27-
_data,
28-
_height,
29-
_width,
30-
ImageDataPrototype,
31-
} = core.loadExtScript("ext:deno_web/16_image_data.js");
26+
let _imageDataMod;
27+
const loadImageData = () =>
28+
_imageDataMod ??
29+
(_imageDataMod = core.loadExtScript("ext:deno_web/16_image_data.js"));
3230

3331
webidl.converters["ImageOrientation"] = webidl.createEnumConverter(
3432
"ImageOrientation",
@@ -200,6 +198,7 @@ function createImageBitmap(
200198

201199
// 3.
202200
const isBlob = ObjectPrototypeIsPrototypeOf(BlobPrototype, image);
201+
const { ImageDataPrototype } = loadImageData();
203202
const isImageData = ObjectPrototypeIsPrototypeOf(ImageDataPrototype, image);
204203
const isImageBitmap = ObjectPrototypeIsPrototypeOf(
205204
ImageBitmapPrototype,
@@ -257,10 +256,10 @@ docs: https://mimesniff.spec.whatwg.org/#image-type-pattern-matching-algorithm\n
257256
);
258257
}
259258
} else if (isImageData) {
260-
width = image[_width];
261-
height = image[_height];
259+
width = image.width;
260+
height = image.height;
262261
imageBitmapSource = 1;
263-
let data = image[_data];
262+
let data = image.data;
264263
switch (TypedArrayPrototypeGetSymbolToStringTag(data)) {
265264
case "Float16Array":
266265
data = float16ToUnorm8(data);

ext/web/16_image_data.js

Lines changed: 32 additions & 252 deletions
Original file line numberDiff line numberDiff line change
@@ -2,267 +2,47 @@
22

33
(function () {
44
const { core, primordials } = __bootstrap;
5-
const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
6-
const { DOMException } = core.loadExtScript("ext:deno_web/01_dom_exception.js");
7-
const { createFilteredInspectProxy } = core.loadExtScript(
8-
"ext:deno_web/01_console.js",
9-
);
5+
const { ImageData } = core.ops;
106
const {
7+
ObjectDefineProperty,
118
ObjectPrototypeIsPrototypeOf,
12-
Symbol,
139
SymbolFor,
14-
TypedArrayPrototypeGetLength,
15-
TypedArrayPrototypeGetSymbolToStringTag,
16-
Uint8ClampedArray,
1710
} = primordials;
1811

19-
webidl.converters["PredefinedColorSpace"] = webidl.createEnumConverter(
20-
"PredefinedColorSpace",
21-
[
22-
"srgb",
23-
"display-p3",
24-
],
12+
const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
13+
const { createFilteredInspectProxy } = core.loadExtScript(
14+
"ext:deno_web/01_console.js",
2515
);
2616

27-
webidl.converters["ImageDataPixelFormat"] = webidl.createEnumConverter(
28-
"ImageDataPixelFormat",
29-
["rgba-unorm8", "rgba-float16"],
30-
);
17+
const ImageDataPrototype = ImageData.prototype;
3118

32-
webidl.converters["ImageDataSettings"] = webidl.createDictionaryConverter(
33-
"ImageDataSettings",
34-
[
35-
{ key: "colorSpace", converter: webidl.converters["PredefinedColorSpace"] },
36-
{
37-
key: "pixelFormat",
38-
converter: webidl.converters["ImageDataPixelFormat"],
39-
defaultValue: "rgba-unorm8",
19+
ObjectDefineProperty(
20+
ImageDataPrototype,
21+
SymbolFor("Deno.privateCustomInspect"),
22+
{
23+
__proto__: null,
24+
value: function customInspect(inspect, inspectOptions) {
25+
return inspect(
26+
createFilteredInspectProxy({
27+
object: this,
28+
evaluate: ObjectPrototypeIsPrototypeOf(ImageDataPrototype, this),
29+
keys: [
30+
"data",
31+
"width",
32+
"height",
33+
"pixelFormat",
34+
"colorSpace",
35+
],
36+
}),
37+
inspectOptions,
38+
);
4039
},
41-
],
40+
enumerable: false,
41+
writable: true,
42+
configurable: true,
43+
},
4244
);
45+
webidl.configureInterface(ImageData);
4346

44-
const _data = Symbol("[[data]]");
45-
const _width = Symbol("[[width]]");
46-
const _height = Symbol("[[height]]");
47-
class ImageData {
48-
/** @type {number} */
49-
[_width];
50-
/** @type {number} */
51-
[_height];
52-
/** @type {ImageDataArray} */
53-
[_data];
54-
/** @type {ImageDataPixelFormat} */
55-
#pixelFormat;
56-
/** @type {PredefinedColorSpace} */
57-
#colorSpace;
58-
59-
constructor(arg0, arg1, arg2 = undefined, arg3 = undefined) {
60-
webidl.requiredArguments(
61-
arguments.length,
62-
2,
63-
'Failed to construct "ImageData"',
64-
);
65-
this[webidl.brand] = webidl.brand;
66-
67-
let sourceWidth;
68-
let sourceHeight;
69-
let data;
70-
let settings;
71-
const prefix = "Failed to construct 'ImageData'";
72-
const tag = TypedArrayPrototypeGetSymbolToStringTag(arg0);
73-
74-
// Overload: new ImageData(data, sw [, sh [, settings ] ])
75-
if (
76-
arguments.length > 3 ||
77-
tag === "Uint8ClampedArray" || tag === "Float16Array"
78-
) {
79-
data = webidl.converters.ArrayBufferView(arg0, prefix, "Argument 1");
80-
sourceWidth = webidl.converters["unsigned long"](
81-
arg1,
82-
prefix,
83-
"Argument 2",
84-
);
85-
const dataLength = TypedArrayPrototypeGetLength(data);
86-
87-
if (arg2 !== undefined) {
88-
sourceHeight = webidl.converters["unsigned long"](
89-
arg2,
90-
prefix,
91-
"Argument 3",
92-
);
93-
}
94-
95-
settings = webidl.converters["ImageDataSettings"](
96-
arg3,
97-
prefix,
98-
"Argument 4",
99-
);
100-
101-
if (dataLength === 0) {
102-
throw new DOMException(
103-
"Failed to construct 'ImageData': the input data has zero elements",
104-
"InvalidStateError",
105-
);
106-
}
107-
108-
if (dataLength % 4 !== 0) {
109-
throw new DOMException(
110-
`Failed to construct 'ImageData': the input data length is not a multiple of 4, received ${dataLength}`,
111-
"InvalidStateError",
112-
);
113-
}
114-
115-
if (sourceWidth < 1) {
116-
throw new DOMException(
117-
"Failed to construct 'ImageData': the source width is zero or not a number",
118-
"IndexSizeError",
119-
);
120-
}
121-
122-
if (sourceHeight !== undefined && sourceHeight < 1) {
123-
throw new DOMException(
124-
"Failed to construct 'ImageData': the source height is zero or not a number",
125-
"IndexSizeError",
126-
);
127-
}
128-
129-
if (dataLength / 4 % sourceWidth !== 0) {
130-
throw new DOMException(
131-
"Failed to construct 'ImageData': the input data length is not a multiple of (4 * width)",
132-
"IndexSizeError",
133-
);
134-
}
135-
136-
if (
137-
sourceHeight !== undefined &&
138-
dataLength / 4 / sourceWidth !== sourceHeight
139-
) {
140-
throw new DOMException(
141-
"Failed to construct 'ImageData': the input data length is not equal to (4 * width * height)",
142-
"IndexSizeError",
143-
);
144-
}
145-
146-
switch (tag) {
147-
case "Uint8ClampedArray":
148-
if (settings.pixelFormat !== "rgba-unorm8") {
149-
throw new DOMException(
150-
"Failed to construct 'ImageData': Uint8ClampedArray must use rgba-unorm8 pixelFormat.",
151-
"InvalidStateError",
152-
);
153-
}
154-
break;
155-
case "Float16Array":
156-
if (settings.pixelFormat !== "rgba-float16") {
157-
throw new DOMException(
158-
"Failed to construct 'ImageData': Float16Array must use rgba-float16 pixelFormat.",
159-
"InvalidStateError",
160-
);
161-
}
162-
break;
163-
}
164-
165-
this.#pixelFormat = settings.pixelFormat;
166-
this.#colorSpace = settings.colorSpace ?? "srgb";
167-
this[_width] = sourceWidth;
168-
this[_height] = dataLength / 4 / sourceWidth;
169-
this[_data] = data;
170-
return;
171-
}
172-
173-
// Overload: new ImageData(sw, sh [, settings])
174-
sourceWidth = webidl.converters["unsigned long"](
175-
arg0,
176-
prefix,
177-
"Argument 1",
178-
);
179-
sourceHeight = webidl.converters["unsigned long"](
180-
arg1,
181-
prefix,
182-
"Argument 2",
183-
);
184-
185-
settings = webidl.converters["ImageDataSettings"](
186-
arg2,
187-
prefix,
188-
"Argument 3",
189-
);
190-
191-
if (sourceWidth < 1) {
192-
throw new DOMException(
193-
"Failed to construct 'ImageData': the source width is zero or not a number",
194-
"IndexSizeError",
195-
);
196-
}
197-
198-
if (sourceHeight < 1) {
199-
throw new DOMException(
200-
"Failed to construct 'ImageData': the source height is zero or not a number",
201-
"IndexSizeError",
202-
);
203-
}
204-
205-
switch (settings.pixelFormat) {
206-
case "rgba-unorm8":
207-
data = new Uint8ClampedArray(sourceWidth * sourceHeight * 4);
208-
break;
209-
case "rgba-float16":
210-
// TODO(0f-0b): add Float16Array to primordials
211-
data = new Float16Array(sourceWidth * sourceHeight * 4);
212-
break;
213-
}
214-
215-
this.#pixelFormat = settings.pixelFormat;
216-
this.#colorSpace = settings.colorSpace ?? "srgb";
217-
this[_width] = sourceWidth;
218-
this[_height] = sourceHeight;
219-
this[_data] = data;
220-
}
221-
222-
get width() {
223-
webidl.assertBranded(this, ImageDataPrototype);
224-
return this[_width];
225-
}
226-
227-
get height() {
228-
webidl.assertBranded(this, ImageDataPrototype);
229-
return this[_height];
230-
}
231-
232-
get data() {
233-
webidl.assertBranded(this, ImageDataPrototype);
234-
return this[_data];
235-
}
236-
237-
get pixelFormat() {
238-
webidl.assertBranded(this, ImageDataPrototype);
239-
return this.#pixelFormat;
240-
}
241-
242-
get colorSpace() {
243-
webidl.assertBranded(this, ImageDataPrototype);
244-
return this.#colorSpace;
245-
}
246-
247-
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
248-
return inspect(
249-
createFilteredInspectProxy({
250-
object: this,
251-
evaluate: ObjectPrototypeIsPrototypeOf(ImageDataPrototype, this),
252-
keys: [
253-
"data",
254-
"width",
255-
"height",
256-
"pixelFormat",
257-
"colorSpace",
258-
],
259-
}),
260-
inspectOptions,
261-
);
262-
}
263-
}
264-
265-
const ImageDataPrototype = ImageData.prototype;
266-
267-
return { _data, _height, _width, ImageData, ImageDataPrototype };
47+
return { ImageData, ImageDataPrototype };
26848
})();

0 commit comments

Comments
 (0)