@workglow/util 0.2.16 → 0.2.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/browser.js +54 -23
  2. package/dist/browser.js.map +7 -6
  3. package/dist/bun.js +54 -23
  4. package/dist/bun.js.map +7 -6
  5. package/dist/di/Container.d.ts +0 -3
  6. package/dist/di/Container.d.ts.map +1 -1
  7. package/dist/di/PortCodecRegistry.d.ts +14 -0
  8. package/dist/di/PortCodecRegistry.d.ts.map +1 -0
  9. package/dist/di/index.d.ts +1 -0
  10. package/dist/di/index.d.ts.map +1 -1
  11. package/dist/media/color.d.ts +3 -3
  12. package/dist/media/color.d.ts.map +1 -1
  13. package/dist/media/cpuImage.d.ts +29 -0
  14. package/dist/media/cpuImage.d.ts.map +1 -0
  15. package/dist/media/encode.d.ts +11 -0
  16. package/dist/media/encode.d.ts.map +1 -0
  17. package/dist/media/filterRegistry.d.ts +13 -0
  18. package/dist/media/filterRegistry.d.ts.map +1 -0
  19. package/dist/media/gpuDevice.browser.d.ts +8 -0
  20. package/dist/media/gpuDevice.browser.d.ts.map +1 -0
  21. package/dist/media/gpuImage.d.ts +50 -0
  22. package/dist/media/gpuImage.d.ts.map +1 -0
  23. package/dist/media/gpuImageSchema.d.ts +8 -0
  24. package/dist/media/gpuImageSchema.d.ts.map +1 -0
  25. package/dist/media/imageCacheCodec.d.ts +9 -0
  26. package/dist/media/imageCacheCodec.d.ts.map +1 -0
  27. package/dist/media/imageHydrationResolver.d.ts +2 -0
  28. package/dist/media/imageHydrationResolver.d.ts.map +1 -0
  29. package/dist/media/imageRasterCodecRegistry.d.ts.map +1 -1
  30. package/dist/media/previewBudget.d.ts +23 -0
  31. package/dist/media/previewBudget.d.ts.map +1 -0
  32. package/dist/media/shaderRegistry.browser.d.ts +13 -0
  33. package/dist/media/shaderRegistry.browser.d.ts.map +1 -0
  34. package/dist/media/sharpImage.bun.d.ts +7 -0
  35. package/dist/media/sharpImage.bun.d.ts.map +1 -0
  36. package/dist/media/sharpImage.node.d.ts +93 -0
  37. package/dist/media/sharpImage.node.d.ts.map +1 -0
  38. package/dist/media/texturePool.browser.d.ts +17 -0
  39. package/dist/media/texturePool.browser.d.ts.map +1 -0
  40. package/dist/media/webGpuImage.browser.d.ts +40 -0
  41. package/dist/media/webGpuImage.browser.d.ts.map +1 -0
  42. package/dist/media-browser.d.ts +18 -3
  43. package/dist/media-browser.d.ts.map +1 -1
  44. package/dist/media-browser.js +850 -404
  45. package/dist/media-browser.js.map +21 -7
  46. package/dist/media-node.d.ts +21 -2
  47. package/dist/media-node.d.ts.map +1 -1
  48. package/dist/media-node.js +695 -312
  49. package/dist/media-node.js.map +20 -6
  50. package/dist/node.js +54 -23
  51. package/dist/node.js.map +7 -6
  52. package/dist/worker/WorkerManager.d.ts +6 -6
  53. package/dist/worker/WorkerManager.d.ts.map +1 -1
  54. package/dist/worker/WorkerServerBase.d.ts +8 -8
  55. package/dist/worker/WorkerServerBase.d.ts.map +1 -1
  56. package/dist/worker-browser.js +54 -23
  57. package/dist/worker-browser.js.map +7 -6
  58. package/dist/worker-bun.js +54 -23
  59. package/dist/worker-bun.js.map +7 -6
  60. package/dist/worker-node.js +54 -23
  61. package/dist/worker-node.js.map +7 -6
  62. package/package.json +4 -1
  63. package/dist/media/Image.browser.d.ts +0 -24
  64. package/dist/media/Image.browser.d.ts.map +0 -1
  65. package/dist/media/Image.d.ts +0 -100
  66. package/dist/media/Image.d.ts.map +0 -1
@@ -1,16 +1,342 @@
1
- // src/media/imageTypes.ts
2
- function parseDataUri(dataUri) {
3
- const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
4
- if (!match) {
5
- throw new Error("Invalid base64 data URI");
1
+ // src/media/imageCacheCodec.ts
2
+ import { registerPortCodec } from "@workglow/util";
3
+
4
+ // src/media/gpuImage.ts
5
+ var GLOBAL_FACTORY_KEY = Symbol.for("@workglow/util/media/gpuImageFactory");
6
+ var _g = globalThis;
7
+ if (!_g[GLOBAL_FACTORY_KEY]) {
8
+ _g[GLOBAL_FACTORY_KEY] = {};
9
+ }
10
+ var factory = _g[GLOBAL_FACTORY_KEY];
11
+ function registerGpuImageFactory(key, fn) {
12
+ factory[key] = fn;
13
+ }
14
+ function getGpuImageFactory(key) {
15
+ const fn = factory[key];
16
+ return typeof fn === "function" ? fn : undefined;
17
+ }
18
+ var GpuImage = new Proxy({}, {
19
+ get(_t, prop) {
20
+ if (typeof prop !== "string" || prop === "then")
21
+ return;
22
+ const fn = factory[prop];
23
+ if (typeof fn !== "function") {
24
+ throw new Error(`GpuImage.${prop} is not registered. Import the platform entry point.`);
25
+ }
26
+ return fn;
6
27
  }
7
- return {
8
- mimeType: match[1],
9
- base64: match[2]
10
- };
28
+ });
29
+
30
+ // src/media/imageRasterCodecRegistry.ts
31
+ var GLOBAL_CODEC_KEY = Symbol.for("@workglow/util/media/imageRasterCodec");
32
+ var _g2 = globalThis;
33
+ if (!_g2[GLOBAL_CODEC_KEY]) {
34
+ _g2[GLOBAL_CODEC_KEY] = { value: null };
35
+ }
36
+ var slot = _g2[GLOBAL_CODEC_KEY];
37
+ function registerImageRasterCodec(next) {
38
+ slot.value = next;
39
+ }
40
+ function getImageRasterCodec() {
41
+ if (!slot.value) {
42
+ throw new Error("Image raster codec is not registered. Ensure you import @workglow/tasks from the browser or Node entry (dist/browser.js or dist/node.js), or call registerImageRasterCodec() during startup.");
43
+ }
44
+ return slot.value;
45
+ }
46
+
47
+ // src/media/cpuImage.ts
48
+ var FORMAT_TO_MIME = {
49
+ png: "image/png",
50
+ jpeg: "image/jpeg",
51
+ webp: "image/webp"
52
+ };
53
+ function dataUriToBytes(dataUri) {
54
+ const comma = dataUri.indexOf(",");
55
+ const b64 = dataUri.slice(comma + 1);
56
+ const bin = atob(b64);
57
+ const bytes = new Uint8Array(bin.length);
58
+ for (let i = 0;i < bin.length; i++)
59
+ bytes[i] = bin.charCodeAt(i);
60
+ return bytes;
61
+ }
62
+ function expandToRgba(bin) {
63
+ if (bin.channels === 4)
64
+ return bin.data;
65
+ const px = bin.width * bin.height;
66
+ const out = new Uint8ClampedArray(px * 4);
67
+ if (bin.channels === 3) {
68
+ for (let i = 0;i < px; i++) {
69
+ out[i * 4 + 0] = bin.data[i * 3 + 0] ?? 0;
70
+ out[i * 4 + 1] = bin.data[i * 3 + 1] ?? 0;
71
+ out[i * 4 + 2] = bin.data[i * 3 + 2] ?? 0;
72
+ out[i * 4 + 3] = 255;
73
+ }
74
+ } else if (bin.channels === 1) {
75
+ for (let i = 0;i < px; i++) {
76
+ const g = bin.data[i] ?? 0;
77
+ out[i * 4 + 0] = g;
78
+ out[i * 4 + 1] = g;
79
+ out[i * 4 + 2] = g;
80
+ out[i * 4 + 3] = 255;
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+
86
+ class CpuImage {
87
+ bin;
88
+ backend = "cpu";
89
+ _previewScale;
90
+ constructor(bin, previewScale = 1) {
91
+ this.bin = bin;
92
+ this._previewScale = previewScale;
93
+ }
94
+ get width() {
95
+ return this.bin.width;
96
+ }
97
+ get height() {
98
+ return this.bin.height;
99
+ }
100
+ get channels() {
101
+ return this.bin.channels;
102
+ }
103
+ get previewScale() {
104
+ return this._previewScale;
105
+ }
106
+ _setPreviewScale(scale) {
107
+ this._previewScale = scale;
108
+ return this;
109
+ }
110
+ async materialize() {
111
+ return this.bin;
112
+ }
113
+ getBinary() {
114
+ return this.bin;
115
+ }
116
+ async toCanvas(canvas) {
117
+ if (typeof ImageData === "undefined") {
118
+ throw new Error("CpuImage.toCanvas requires a browser environment with ImageData");
119
+ }
120
+ const rgba = expandToRgba(this.bin);
121
+ const id = new ImageData(new Uint8ClampedArray(rgba.buffer, rgba.byteOffset, rgba.byteLength), this.bin.width, this.bin.height);
122
+ if (canvas.width !== this.bin.width)
123
+ canvas.width = this.bin.width;
124
+ if (canvas.height !== this.bin.height)
125
+ canvas.height = this.bin.height;
126
+ const ctx = canvas.getContext("2d");
127
+ if (!ctx)
128
+ throw new Error("CpuImage.toCanvas could not acquire a 2D context");
129
+ ctx.putImageData(id, 0, 0);
130
+ }
131
+ async encode(format, _quality) {
132
+ const codec = getImageRasterCodec();
133
+ const dataUri = await codec.encodeDataUri(this.bin, FORMAT_TO_MIME[format]);
134
+ return dataUriToBytes(dataUri);
135
+ }
136
+ retain(_n = 1) {
137
+ return this;
138
+ }
139
+ release() {}
140
+ static fromImageBinary(bin, previewScale = 1) {
141
+ return new CpuImage(bin, previewScale);
142
+ }
143
+ }
144
+ registerGpuImageFactory("fromImageBinary", CpuImage.fromImageBinary);
145
+
146
+ // src/media/imageCacheCodec.ts
147
+ registerPortCodec("image", {
148
+ async serialize(value) {
149
+ if (typeof value.materialize !== "function") {
150
+ return value;
151
+ }
152
+ const bin = await value.materialize();
153
+ return {
154
+ kind: "image-binary",
155
+ width: bin.width,
156
+ height: bin.height,
157
+ channels: bin.channels,
158
+ data: bin.data
159
+ };
160
+ },
161
+ async deserialize(cached) {
162
+ if (cached.kind !== "image-binary") {
163
+ return cached;
164
+ }
165
+ return CpuImage.fromImageBinary({
166
+ data: cached.data,
167
+ width: cached.width,
168
+ height: cached.height,
169
+ channels: cached.channels
170
+ });
171
+ }
172
+ });
173
+
174
+ // src/di/Container.ts
175
+ class Container {
176
+ services = new Map;
177
+ factories = new Map;
178
+ singletons = new Set;
179
+ resolving = [];
180
+ register(token, factory2, singleton = true) {
181
+ this.factories.set(token, factory2);
182
+ if (singleton) {
183
+ this.singletons.add(token);
184
+ }
185
+ }
186
+ registerIfAbsent(token, factory2, singleton = true) {
187
+ if (this.factories.has(token) || this.services.has(token)) {
188
+ return;
189
+ }
190
+ this.register(token, factory2, singleton);
191
+ }
192
+ registerInstance(token, instance) {
193
+ this.services.set(token, instance);
194
+ this.singletons.add(token);
195
+ }
196
+ get(token) {
197
+ if (this.services.has(token)) {
198
+ return this.services.get(token);
199
+ }
200
+ const factory2 = this.factories.get(token);
201
+ if (!factory2) {
202
+ throw new Error(`Service not registered: ${String(token)}`);
203
+ }
204
+ if (this.resolving.includes(token)) {
205
+ const cycle = [...this.resolving.slice(this.resolving.indexOf(token)), token];
206
+ throw new Error(`Circular dependency detected: ${cycle.join(" -> ")}`);
207
+ }
208
+ this.resolving.push(token);
209
+ try {
210
+ const instance = factory2();
211
+ if (this.singletons.has(token)) {
212
+ this.services.set(token, instance);
213
+ }
214
+ return instance;
215
+ } finally {
216
+ this.resolving.pop();
217
+ }
218
+ }
219
+ has(token) {
220
+ return this.services.has(token) || this.factories.has(token);
221
+ }
222
+ remove(token) {
223
+ this.services.delete(token);
224
+ this.factories.delete(token);
225
+ this.singletons.delete(token);
226
+ }
227
+ async dispose() {
228
+ const errors = [];
229
+ try {
230
+ for (const service of this.services.values()) {
231
+ if (service == null)
232
+ continue;
233
+ try {
234
+ if (typeof service[Symbol.asyncDispose] === "function") {
235
+ await service[Symbol.asyncDispose]();
236
+ } else if (typeof service[Symbol.dispose] === "function") {
237
+ service[Symbol.dispose]();
238
+ } else if (typeof service.dispose === "function") {
239
+ await service.dispose();
240
+ }
241
+ } catch (err) {
242
+ errors.push(err);
243
+ }
244
+ }
245
+ } finally {
246
+ this.services.clear();
247
+ this.factories.clear();
248
+ this.singletons.clear();
249
+ }
250
+ if (errors.length > 0) {
251
+ throw new AggregateError(errors, "One or more services failed to dispose");
252
+ }
253
+ }
254
+ async[Symbol.asyncDispose]() {
255
+ await this.dispose();
256
+ }
257
+ createChildContainer() {
258
+ const child = new Container;
259
+ this.factories.forEach((factory2, token) => {
260
+ child.factories.set(token, factory2);
261
+ if (this.singletons.has(token)) {
262
+ child.singletons.add(token);
263
+ }
264
+ });
265
+ this.services.forEach((service, token) => {
266
+ if (this.singletons.has(token)) {
267
+ child.services.set(token, service);
268
+ child.singletons.add(token);
269
+ }
270
+ });
271
+ return child;
272
+ }
273
+ }
274
+ var GLOBAL_CONTAINER_KEY = Symbol.for("@workglow/util/di/globalContainer");
275
+ var _g3 = globalThis;
276
+ if (!_g3[GLOBAL_CONTAINER_KEY]) {
277
+ _g3[GLOBAL_CONTAINER_KEY] = new Container;
11
278
  }
279
+ var globalContainer = _g3[GLOBAL_CONTAINER_KEY];
280
+
281
+ // src/di/ServiceRegistry.ts
282
+ function createServiceToken(id) {
283
+ return { id, _type: null };
284
+ }
285
+
286
+ class ServiceRegistry {
287
+ container;
288
+ constructor(container = globalContainer) {
289
+ this.container = container;
290
+ }
291
+ register(token, factory2, singleton = true) {
292
+ this.container.register(token.id, factory2, singleton);
293
+ }
294
+ registerIfAbsent(token, factory2, singleton = true) {
295
+ this.container.registerIfAbsent(token.id, factory2, singleton);
296
+ }
297
+ registerInstance(token, instance) {
298
+ this.container.registerInstance(token.id, instance);
299
+ }
300
+ get(token) {
301
+ return this.container.get(token.id);
302
+ }
303
+ has(token) {
304
+ return this.container.has(token.id);
305
+ }
306
+ async dispose() {
307
+ await this.container.dispose();
308
+ }
309
+ }
310
+ var globalServiceRegistry = new ServiceRegistry(globalContainer);
311
+
312
+ // src/di/InputResolverRegistry.ts
313
+ var INPUT_RESOLVERS = createServiceToken("task.input.resolvers");
314
+ globalServiceRegistry.registerIfAbsent(INPUT_RESOLVERS, () => new Map, true);
315
+ function getInputResolvers() {
316
+ return globalServiceRegistry.get(INPUT_RESOLVERS);
317
+ }
318
+ function registerInputResolver(formatPrefix, resolver) {
319
+ const resolvers = getInputResolvers();
320
+ resolvers.set(formatPrefix, resolver);
321
+ }
322
+
323
+ // src/media/imageHydrationResolver.ts
324
+ async function resolveImageString(id, _format, _registry) {
325
+ if (typeof id !== "string")
326
+ return id;
327
+ if (id.startsWith("data:")) {
328
+ return GpuImage.fromDataUri(id);
329
+ }
330
+ const preview = id.length > 32 ? `${id.slice(0, 32)}...` : id;
331
+ throw new Error(`format:"image" resolver received an unsupported string "${preview}". ` + `Only data: URIs are handled. For other schemes register a sub-resolver, ` + `e.g. registerInputResolver("image:http", fn).`);
332
+ }
333
+ registerInputResolver("image", resolveImageString);
334
+
12
335
  // src/media/color.ts
13
336
  var HEX_PATTERN = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
337
+ var CSS_RGB_CHANNEL = "(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)";
338
+ var CSS_RGB_ALPHA = "(?:0(?:\\.\\d+)?|1(?:\\.0+)?)";
339
+ var CSS_RGB_PATTERN = new RegExp(`^rgba?\\(\\s*(${CSS_RGB_CHANNEL})\\s*,\\s*(${CSS_RGB_CHANNEL})\\s*,\\s*(${CSS_RGB_CHANNEL})\\s*(?:,\\s*(${CSS_RGB_ALPHA}))?\\s*\\)$`);
14
340
  function parseHexColor(hex) {
15
341
  if (typeof hex !== "string" || !HEX_PATTERN.test(hex)) {
16
342
  throw new Error(`Invalid hex color: ${String(hex)}`);
@@ -81,9 +407,29 @@ function isColorObject(value) {
81
407
  function isHexColor(value) {
82
408
  return typeof value === "string" && HEX_PATTERN.test(value);
83
409
  }
410
+ function parseCssRgbColor(value) {
411
+ const match = CSS_RGB_PATTERN.exec(value);
412
+ if (!match) {
413
+ throw new Error(`Invalid CSS rgb color: ${String(value)}`);
414
+ }
415
+ const r = Number.parseInt(match[1] ?? "", 10);
416
+ const g = Number.parseInt(match[2] ?? "", 10);
417
+ const b = Number.parseInt(match[3] ?? "", 10);
418
+ const alpha = match[4] === undefined ? 1 : Number.parseFloat(match[4]);
419
+ assertChannel("r", r);
420
+ assertChannel("g", g);
421
+ assertChannel("b", b);
422
+ if (!Number.isFinite(alpha) || alpha < 0 || alpha > 1) {
423
+ throw new Error(`Color alpha out of range (0-1 number): ${match[4]}`);
424
+ }
425
+ return { r, g, b, a: Math.round(alpha * 255) };
426
+ }
84
427
  function resolveColor(value) {
85
- if (typeof value === "string")
86
- return parseHexColor(value);
428
+ if (typeof value === "string") {
429
+ if (isHexColor(value))
430
+ return parseHexColor(value);
431
+ return parseCssRgbColor(value);
432
+ }
87
433
  if (!isColorObject(value)) {
88
434
  throw new Error(`Invalid color value: ${JSON.stringify(value)}`);
89
435
  }
@@ -94,16 +440,101 @@ function resolveColor(value) {
94
440
  a: value.a ?? 255
95
441
  };
96
442
  }
97
- // src/media/imageRasterCodecRegistry.ts
98
- var codec = null;
99
- function registerImageRasterCodec(next) {
100
- codec = next;
443
+ // src/media/encode.ts
444
+ async function encodeImageBinaryBytes(bin, mimeType) {
445
+ const dataUri = await getImageRasterCodec().encodeDataUri(bin, mimeType);
446
+ const b64 = dataUri.slice(dataUri.indexOf(",") + 1);
447
+ const decoded = atob(b64);
448
+ const bytes = new Uint8Array(decoded.length);
449
+ for (let i = 0;i < decoded.length; i++)
450
+ bytes[i] = decoded.charCodeAt(i);
451
+ return bytes;
101
452
  }
102
- function getImageRasterCodec() {
103
- if (!codec) {
104
- throw new Error("Image raster codec is not registered. Ensure you import @workglow/tasks from the browser or Node entry (dist/browser.js or dist/node.js), or call registerImageRasterCodec() during startup.");
453
+ async function encodeImageBinaryToPng(bin) {
454
+ return encodeImageBinaryBytes(bin, "image/png");
455
+ }
456
+ async function imageBinaryToBase64Png(bin) {
457
+ const dataUri = await getImageRasterCodec().encodeDataUri(bin, "image/png");
458
+ return dataUri.slice(dataUri.indexOf(",") + 1);
459
+ }
460
+ async function imageBinaryToDataUri(bin, mimeType = "image/png") {
461
+ return getImageRasterCodec().encodeDataUri(bin, mimeType);
462
+ }
463
+ async function imageBinaryToBlob(bin, mimeType = "image/png") {
464
+ const bytes = await encodeImageBinaryBytes(bin, mimeType);
465
+ return new Blob([bytes.buffer], { type: mimeType });
466
+ }
467
+ // src/media/filterRegistry.ts
468
+ var GLOBAL_REGISTRY_KEY = Symbol.for("@workglow/util/media/filterRegistry");
469
+ var _g4 = globalThis;
470
+ function getRegistry() {
471
+ let reg = _g4[GLOBAL_REGISTRY_KEY];
472
+ if (!reg) {
473
+ reg = new Map;
474
+ _g4[GLOBAL_REGISTRY_KEY] = reg;
475
+ }
476
+ return reg;
477
+ }
478
+ var key = (backend, filter) => `${backend}:${filter}`;
479
+ function registerFilterOp(backend, filter, fn) {
480
+ getRegistry().set(key(backend, filter), fn);
481
+ }
482
+ function applyFilter(image, filter, params) {
483
+ const fn = getRegistry().get(key(image.backend, filter));
484
+ if (!fn) {
485
+ throw new Error(`applyFilter("${filter}") on backend "${image.backend}": no implementation registered. ` + `Task-layer fallback should have routed this to "cpu" first; this means even the cpu arm is missing. ` + `Ensure @workglow/tasks has been imported so its filter-arm side effects run.`);
105
486
  }
106
- return codec;
487
+ return fn(image, params);
488
+ }
489
+ function hasFilterOp(backend, filter) {
490
+ return getRegistry().has(key(backend, filter));
491
+ }
492
+ function _resetFilterRegistryForTests() {
493
+ getRegistry().clear();
494
+ }
495
+ // src/media/gpuDevice.browser.ts
496
+ var cached = null;
497
+ async function getGpuDevice() {
498
+ if (cached)
499
+ return cached;
500
+ cached = (async () => {
501
+ if (typeof navigator === "undefined" || !("gpu" in navigator))
502
+ return null;
503
+ const adapter = await navigator.gpu.requestAdapter();
504
+ if (!adapter)
505
+ return null;
506
+ const device = await adapter.requestDevice();
507
+ device.lost.then(() => {
508
+ cached = null;
509
+ });
510
+ return device;
511
+ })();
512
+ return cached;
513
+ }
514
+ function resetGpuDeviceForTests() {
515
+ cached = null;
516
+ }
517
+ // src/media/gpuImageSchema.ts
518
+ function GpuImageSchema(annotations = {}) {
519
+ return {
520
+ type: ["string", "object"],
521
+ properties: {},
522
+ title: "Image",
523
+ description: "Image (hydrated to GpuImage by the runner)",
524
+ ...annotations,
525
+ format: "image"
526
+ };
527
+ }
528
+ // src/media/imageTypes.ts
529
+ function parseDataUri(dataUri) {
530
+ const match = dataUri.match(/^data:([^;]+);base64,(.+)$/);
531
+ if (!match) {
532
+ throw new Error("Invalid base64 data URI");
533
+ }
534
+ return {
535
+ mimeType: match[1],
536
+ base64: match[2]
537
+ };
107
538
  }
108
539
  // src/media/MediaRawImage.ts
109
540
  class MediaRawImage {
@@ -124,430 +555,445 @@ function isMediaRawImageShape(value) {
124
555
  const v = value;
125
556
  return v.data instanceof Uint8ClampedArray && typeof v.width === "number" && typeof v.height === "number" && typeof v.channels === "number";
126
557
  }
127
- // src/media/Image.ts
128
- var IMAGE_BRAND = Symbol.for("@workglow/util/media/Image");
129
- function parseDataUriMimeType(dataUri) {
130
- const match = dataUri.match(/^data:([^;,]+)/);
131
- const raw = match?.[1]?.trim();
132
- return raw ? raw.toLowerCase() : "image/png";
558
+ // src/media/previewBudget.ts
559
+ var GLOBAL_RESIZE_KEY = Symbol.for("@workglow/util/media/previewResizeFn");
560
+ var GLOBAL_BUDGET_KEY = Symbol.for("@workglow/util/media/previewBudget");
561
+ var _g5 = globalThis;
562
+ var DEFAULT_BUDGET = 512;
563
+ if (typeof _g5[GLOBAL_BUDGET_KEY] !== "number") {
564
+ _g5[GLOBAL_BUDGET_KEY] = DEFAULT_BUDGET;
133
565
  }
134
- function dataUriToBlob(dataUri) {
135
- const { mimeType, base64 } = parseDataUri(dataUri);
136
- const binary = atob(base64);
137
- const bytes = new Uint8Array(binary.length);
138
- for (let i = 0;i < binary.length; i++) {
139
- bytes[i] = binary.charCodeAt(i);
140
- }
141
- return new Blob([bytes], { type: mimeType });
566
+ function registerPreviewResizeFn(fn) {
567
+ _g5[GLOBAL_RESIZE_KEY] = fn;
568
+ }
569
+ function getPreviewResizeFn() {
570
+ return _g5[GLOBAL_RESIZE_KEY];
571
+ }
572
+ function getPreviewBudget() {
573
+ return _g5[GLOBAL_BUDGET_KEY];
142
574
  }
143
- function toImageBinary(value) {
144
- const ch = value.channels;
145
- if (ch !== 1 && ch !== 3 && ch !== 4) {
146
- throw new Error(`Image: unsupported channel count ${ch}`);
575
+ function setPreviewBudget(px) {
576
+ if (!Number.isFinite(px) || px <= 0) {
577
+ throw new Error(`setPreviewBudget: invalid value ${px}; expected a positive finite number`);
147
578
  }
148
- const data = coerceToUint8ClampedArray(value.data);
579
+ _g5[GLOBAL_BUDGET_KEY] = Math.floor(px);
580
+ }
581
+ function previewSource(image) {
582
+ if (image.backend !== "webgpu")
583
+ return image;
584
+ const budget = getPreviewBudget();
585
+ const long = Math.max(image.width, image.height);
586
+ if (long <= budget)
587
+ return image;
588
+ const ratio = budget / long;
589
+ const resize = getPreviewResizeFn();
590
+ if (!resize)
591
+ return image;
592
+ const result = resize(image, Math.round(image.width * ratio), Math.round(image.height * ratio));
593
+ const composed = image.previewScale * ratio;
594
+ return result._setPreviewScale(composed);
595
+ }
596
+ // src/media/shaderRegistry.browser.ts
597
+ var VERTEX_PRELUDE = `
598
+ @group(0) @binding(0) var src: texture_2d<f32>;
599
+ @group(0) @binding(1) var src_sampler: sampler;
600
+
601
+ struct VsOut {
602
+ @builtin(position) pos: vec4f,
603
+ @location(0) uv: vec2f,
604
+ };
605
+
606
+ @vertex
607
+ fn vs(@builtin(vertex_index) vid: u32) -> VsOut {
608
+ let xy = vec2f(f32((vid << 1u) & 2u), f32(vid & 2u));
609
+ var out: VsOut;
610
+ out.pos = vec4f(xy * 2.0 - 1.0, 0.0, 1.0);
611
+ out.uv = vec2f(xy.x, 1.0 - xy.y);
612
+ return out;
613
+ }`;
614
+ var PASSTHROUGH_SHADER_SRC = `${VERTEX_PRELUDE}
615
+ @fragment
616
+ fn fs(in: VsOut) -> @location(0) vec4f {
617
+ return textureSample(src, src_sampler, in.uv);
618
+ }
619
+ `;
620
+ function createShaderCache(device) {
621
+ const map = new Map;
149
622
  return {
150
- data,
151
- width: value.width,
152
- height: value.height,
153
- channels: ch,
154
- rawChannels: value.rawChannels
623
+ get(source) {
624
+ let mod = map.get(source);
625
+ if (!mod) {
626
+ mod = device.createShaderModule({ code: source });
627
+ map.set(source, mod);
628
+ }
629
+ return mod;
630
+ }
155
631
  };
156
632
  }
157
- function coerceToUint8ClampedArray(data) {
158
- if (data instanceof Uint8ClampedArray) {
159
- return data;
160
- }
161
- if (ArrayBuffer.isView(data)) {
162
- return new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength);
633
+ var singleton = null;
634
+ function getShaderCache(device) {
635
+ if (!singleton || singleton.device !== device) {
636
+ singleton = { device, cache: createShaderCache(device) };
163
637
  }
164
- if (Array.isArray(data)) {
165
- return Uint8ClampedArray.from(data);
166
- }
167
- if (data && typeof data === "object") {
168
- const obj = data;
169
- const keys = Object.keys(obj);
170
- if (keys.length > 0 && keys.every((k) => /^\d+$/.test(k))) {
171
- const arr = new Uint8ClampedArray(keys.length);
172
- for (let i = 0;i < keys.length; i++) {
173
- arr[i] = Number(obj[String(i)]);
638
+ return singleton.cache;
639
+ }
640
+ // src/media/texturePool.browser.ts
641
+ var DEFAULT_CAPACITY_PER_SIZE = 8;
642
+ var TEXTURE_USAGE = 4 | 16 | 1 | 2;
643
+ function createTexturePool(device, opts = {}) {
644
+ const capacity = opts.capacityPerSize ?? DEFAULT_CAPACITY_PER_SIZE;
645
+ const buckets = new Map;
646
+ const owners = new WeakMap;
647
+ const sizeClassKey = (w, h, f) => `${w}x${h}:${f}`;
648
+ return {
649
+ acquire(width, height, format) {
650
+ const k = sizeClassKey(width, height, format);
651
+ const bucket = buckets.get(k);
652
+ if (bucket && bucket.length > 0) {
653
+ const reused = bucket.pop();
654
+ reused.inPool = false;
655
+ return reused.texture;
656
+ }
657
+ const texture = device.createTexture({
658
+ size: [width, height, 1],
659
+ format,
660
+ usage: TEXTURE_USAGE
661
+ });
662
+ const entry = { texture, width, height, format, inPool: false };
663
+ owners.set(texture, entry);
664
+ return texture;
665
+ },
666
+ release(texture) {
667
+ const entry = owners.get(texture);
668
+ if (!entry)
669
+ return;
670
+ if (entry.inPool)
671
+ return;
672
+ const k = sizeClassKey(entry.width, entry.height, entry.format);
673
+ let bucket = buckets.get(k);
674
+ if (!bucket) {
675
+ bucket = [];
676
+ buckets.set(k, bucket);
677
+ }
678
+ if (bucket.length >= capacity) {
679
+ owners.delete(texture);
680
+ texture.destroy();
681
+ return;
174
682
  }
175
- return arr;
683
+ entry.inPool = true;
684
+ bucket.push(entry);
685
+ },
686
+ drain() {
687
+ for (const bucket of buckets.values()) {
688
+ for (const entry of bucket) {
689
+ owners.delete(entry.texture);
690
+ entry.texture.destroy();
691
+ }
692
+ }
693
+ buckets.clear();
176
694
  }
177
- }
178
- throw new Error("Image: pixel data is not array-like");
695
+ };
179
696
  }
180
-
181
- class Image {
182
- [IMAGE_BRAND] = true;
183
- source;
184
- pixelsCache;
185
- dataUriCache = new Map;
186
- blobCache = new Map;
187
- constructor(source) {
188
- this.source = source;
189
- }
190
- static fromDataUri(dataUri) {
191
- if (!dataUri.startsWith("data:")) {
192
- throw new Error("Image.fromDataUri: input must start with 'data:'");
193
- }
194
- return new Image({ kind: "dataUri", dataUri, mimeType: parseDataUriMimeType(dataUri) });
195
- }
196
- static fromPixels(pixels) {
197
- return new Image({ kind: "pixels", pixels });
697
+ var singleton2 = null;
698
+ function getTexturePool(device) {
699
+ if (!singleton2 || singleton2.device !== device) {
700
+ singleton2?.pool.drain();
701
+ singleton2 = { device, pool: createTexturePool(device) };
198
702
  }
199
- static fromBlob(blob) {
200
- return new Image({ kind: "blob", blob });
201
- }
202
- static from(value) {
203
- if (Image.is(value)) {
204
- return value;
205
- }
206
- if (typeof value === "string" && value.startsWith("data:")) {
207
- return Image.fromDataUri(value);
208
- }
209
- if (typeof Blob !== "undefined" && value instanceof Blob) {
210
- return Image.fromBlob(value);
211
- }
212
- if (typeof ImageBitmap !== "undefined" && value instanceof ImageBitmap) {
213
- return new Image({ kind: "bitmap", bitmap: value });
214
- }
215
- if (typeof VideoFrame !== "undefined" && value instanceof VideoFrame) {
216
- return new Image({ kind: "videoFrame", frame: value });
217
- }
218
- if (typeof OffscreenCanvas !== "undefined" && value instanceof OffscreenCanvas) {
219
- return new Image({ kind: "offscreenCanvas", canvas: value });
703
+ return singleton2.pool;
704
+ }
705
+ function resetTexturePoolForTests() {
706
+ singleton2?.pool.drain();
707
+ singleton2 = null;
708
+ }
709
+ // src/media/webGpuImage.browser.ts
710
+ var TEX_FORMAT = "rgba8unorm";
711
+ var finalizers = typeof FinalizationRegistry !== "undefined" ? new FinalizationRegistry((fn) => {
712
+ try {
713
+ fn();
714
+ } catch {}
715
+ }) : undefined;
716
+ function expandToRgba2(bin) {
717
+ if (bin.channels === 4)
718
+ return bin.data;
719
+ const px = bin.width * bin.height;
720
+ const out = new Uint8ClampedArray(px * 4);
721
+ if (bin.channels === 3) {
722
+ for (let i = 0;i < px; i++) {
723
+ out[i * 4 + 0] = bin.data[i * 3 + 0] ?? 0;
724
+ out[i * 4 + 1] = bin.data[i * 3 + 1] ?? 0;
725
+ out[i * 4 + 2] = bin.data[i * 3 + 2] ?? 0;
726
+ out[i * 4 + 3] = 255;
220
727
  }
221
- if (value && typeof value === "object" && "data" in value && "width" in value && "height" in value && "channels" in value) {
222
- return Image.fromPixels(toImageBinary(value));
728
+ } else if (bin.channels === 1) {
729
+ for (let i = 0;i < px; i++) {
730
+ const g = bin.data[i] ?? 0;
731
+ out[i * 4 + 0] = g;
732
+ out[i * 4 + 1] = g;
733
+ out[i * 4 + 2] = g;
734
+ out[i * 4 + 3] = 255;
223
735
  }
224
- throw new Error(`Image.from: unsupported image value of type ${typeof value}`);
225
- }
226
- static is(value) {
227
- return typeof value === "object" && value !== null && value[IMAGE_BRAND] === true;
228
736
  }
229
- static fromJSON(value) {
230
- if (Image.is(value)) {
231
- return value;
232
- }
233
- if (typeof value === "string" && value.startsWith("data:")) {
234
- return Image.fromDataUri(value);
235
- }
236
- if (value && typeof value === "object" && value.unsynced === true) {
237
- const kind = value.kind;
238
- throw new Error(`Image.fromJSON: cannot reconstruct image from "unsynced" sentinel (kind=${String(kind)}); pixels were not materialized before serialization. Call await image.getPixels() before JSON.stringify.`);
239
- }
240
- if (value && typeof value === "object" && "data" in value && typeof value.width === "number" && typeof value.height === "number" && typeof value.channels === "number") {
241
- const v = value;
242
- return Image.fromPixels(toImageBinary({
243
- data: v.data,
244
- width: v.width,
245
- height: v.height,
246
- channels: v.channels
247
- }));
737
+ return out;
738
+ }
739
+ function align(n, m) {
740
+ return Math.ceil(n / m) * m;
741
+ }
742
+
743
+ class WebGpuImage {
744
+ device;
745
+ texture;
746
+ width;
747
+ height;
748
+ backend = "webgpu";
749
+ channels = 4;
750
+ refcount = 1;
751
+ _previewScale;
752
+ constructor(device, texture, width, height, previewScale = 1) {
753
+ this.device = device;
754
+ this.texture = texture;
755
+ this.width = width;
756
+ this.height = height;
757
+ this._previewScale = previewScale;
758
+ if (finalizers && texture) {
759
+ const dev = device;
760
+ const tex = texture;
761
+ finalizers.register(this, () => {
762
+ try {
763
+ getTexturePool(dev).release(tex);
764
+ } catch {}
765
+ }, this);
248
766
  }
249
- throw new Error("Image.fromJSON: value does not match any known Image shape");
250
767
  }
251
- get kind() {
252
- return this.source.kind;
253
- }
254
- get mimeType() {
255
- if (this.source.kind === "dataUri")
256
- return this.source.mimeType;
257
- if (this.source.kind === "blob")
258
- return this.source.blob.type || undefined;
259
- return;
260
- }
261
- get width() {
262
- if (this.source.kind === "pixels")
263
- return this.source.pixels.width;
264
- if (this.source.kind === "bitmap")
265
- return this.source.bitmap.width;
266
- if (this.source.kind === "offscreenCanvas")
267
- return this.source.canvas.width;
268
- if (this.source.kind === "videoFrame")
269
- return this.source.frame.displayWidth;
270
- return this.pixelsCache?.width;
271
- }
272
- get height() {
273
- if (this.source.kind === "pixels")
274
- return this.source.pixels.height;
275
- if (this.source.kind === "bitmap")
276
- return this.source.bitmap.height;
277
- if (this.source.kind === "offscreenCanvas")
278
- return this.source.canvas.height;
279
- if (this.source.kind === "videoFrame")
280
- return this.source.frame.displayHeight;
281
- return this.pixelsCache?.height;
282
- }
283
- get channels() {
284
- if (this.source.kind === "pixels")
285
- return this.source.pixels.channels;
286
- return this.pixelsCache?.channels;
287
- }
288
- async getPixels() {
289
- if (this.pixelsCache)
290
- return this.pixelsCache;
291
- if (this.source.kind === "pixels") {
292
- this.pixelsCache = this.source.pixels;
293
- return this.pixelsCache;
768
+ get previewScale() {
769
+ return this._previewScale;
770
+ }
771
+ _setPreviewScale(scale) {
772
+ this._previewScale = scale;
773
+ return this;
774
+ }
775
+ static async fromImageBinary(bin) {
776
+ const dev = await getGpuDevice();
777
+ if (!dev)
778
+ throw new Error("WebGPU device unavailable; use CpuImage.fromImageBinary instead");
779
+ const tex = getTexturePool(dev).acquire(bin.width, bin.height, TEX_FORMAT);
780
+ const rgba = bin.channels === 4 ? bin.data : expandToRgba2(bin);
781
+ dev.queue.writeTexture({ texture: tex }, rgba, { bytesPerRow: bin.width * 4, rowsPerImage: bin.height }, [bin.width, bin.height, 1]);
782
+ return new WebGpuImage(dev, tex, bin.width, bin.height);
783
+ }
784
+ apply(params) {
785
+ if (!this.texture)
786
+ throw new Error("WebGpuImage.apply called on a released image");
787
+ const outW = params.outSize?.width ?? this.width;
788
+ const outH = params.outSize?.height ?? this.height;
789
+ const out = getTexturePool(this.device).acquire(outW, outH, TEX_FORMAT);
790
+ const shaderModule = getShaderCache(this.device).get(params.shader);
791
+ const pipeline = this.device.createRenderPipeline({
792
+ layout: "auto",
793
+ vertex: { module: shaderModule, entryPoint: "vs" },
794
+ fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format: TEX_FORMAT }] },
795
+ primitive: { topology: "triangle-list" }
796
+ });
797
+ const sampler = this.device.createSampler({ magFilter: "linear", minFilter: "linear" });
798
+ const bindEntries = [
799
+ { binding: 0, resource: this.texture.createView() },
800
+ { binding: 1, resource: sampler }
801
+ ];
802
+ if (params.uniforms) {
803
+ const ubo = this.device.createBuffer({
804
+ size: params.uniforms.byteLength,
805
+ usage: 64 | 8
806
+ });
807
+ this.device.queue.writeBuffer(ubo, 0, params.uniforms);
808
+ bindEntries.push({ binding: 2, resource: { buffer: ubo } });
294
809
  }
295
- if (this.source.kind === "dataUri") {
296
- this.pixelsCache = await getImageRasterCodec().decodeDataUri(this.source.dataUri);
297
- return this.pixelsCache;
298
- }
299
- if (this.source.kind === "blob") {
300
- const dataUri = await blobToDataUri(this.source.blob);
301
- this.pixelsCache = await getImageRasterCodec().decodeDataUri(dataUri);
302
- return this.pixelsCache;
303
- }
304
- throw new Error(`Image.getPixels: browser-only source '${this.source.kind}' requires Image.browser augmentation`);
305
- }
306
- async getDataUri(mimeType = "image/png") {
307
- if (this.source.kind === "dataUri") {
308
- if (mimeType === this.source.mimeType) {
309
- return this.source.dataUri;
810
+ const bindGroup = this.device.createBindGroup({
811
+ layout: pipeline.getBindGroupLayout(0),
812
+ entries: bindEntries
813
+ });
814
+ const enc = this.device.createCommandEncoder();
815
+ const pass = enc.beginRenderPass({
816
+ colorAttachments: [{
817
+ view: out.createView(),
818
+ loadOp: "clear",
819
+ storeOp: "store",
820
+ clearValue: [0, 0, 0, 0]
821
+ }]
822
+ });
823
+ pass.setPipeline(pipeline);
824
+ pass.setBindGroup(0, bindGroup);
825
+ pass.draw(3);
826
+ pass.end();
827
+ this.device.queue.submit([enc.finish()]);
828
+ return new WebGpuImage(this.device, out, outW, outH, this._previewScale);
829
+ }
830
+ async materialize() {
831
+ if (!this.texture)
832
+ throw new Error("WebGpuImage.materialize called on a released image");
833
+ const bytesPerRow = align(this.width * 4, 256);
834
+ const buffer = this.device.createBuffer({
835
+ size: bytesPerRow * this.height,
836
+ usage: 1 | 8
837
+ });
838
+ const enc = this.device.createCommandEncoder();
839
+ enc.copyTextureToBuffer({ texture: this.texture }, { buffer, bytesPerRow, rowsPerImage: this.height }, [this.width, this.height, 1]);
840
+ this.device.queue.submit([enc.finish()]);
841
+ await buffer.mapAsync(1);
842
+ const mapped = new Uint8Array(buffer.getMappedRange());
843
+ const tightStride = this.width * 4;
844
+ const tight = new Uint8ClampedArray(this.width * this.height * 4);
845
+ if (bytesPerRow === tightStride) {
846
+ tight.set(mapped);
847
+ } else {
848
+ for (let y = 0;y < this.height; y++) {
849
+ tight.set(mapped.subarray(y * bytesPerRow, y * bytesPerRow + tightStride), y * tightStride);
310
850
  }
311
851
  }
312
- const cached = this.dataUriCache.get(mimeType);
313
- if (cached)
314
- return cached;
315
- const pixels = await this.getPixels();
316
- const dataUri = await getImageRasterCodec().encodeDataUri(pixels, mimeType);
317
- this.dataUriCache.set(mimeType, dataUri);
318
- return dataUri;
319
- }
320
- async getBlob(mimeType = "image/png") {
321
- if (this.source.kind === "blob" && (!mimeType || this.source.blob.type === mimeType)) {
322
- return this.source.blob;
323
- }
324
- const cached = this.blobCache.get(mimeType);
325
- if (cached)
326
- return cached;
327
- if (this.source.kind === "dataUri" && this.source.mimeType === mimeType) {
328
- const blob2 = dataUriToBlob(this.source.dataUri);
329
- this.blobCache.set(mimeType, blob2);
330
- return blob2;
331
- }
332
- const dataUri = await this.getDataUri(mimeType);
333
- const blob = dataUriToBlob(dataUri);
334
- this.blobCache.set(mimeType, blob);
335
- return blob;
336
- }
337
- async toFirstSupported(supports) {
338
- const canonical = this.canonicalSupport();
339
- if (canonical && supports.includes(canonical)) {
340
- return this.currentSourceValue();
852
+ buffer.unmap();
853
+ buffer.destroy();
854
+ return { data: tight, width: this.width, height: this.height, channels: 4 };
855
+ }
856
+ async toCanvas(canvas) {
857
+ if (!this.texture)
858
+ throw new Error("WebGpuImage.toCanvas called on a released image");
859
+ if (canvas.width !== this.width)
860
+ canvas.width = this.width;
861
+ if (canvas.height !== this.height)
862
+ canvas.height = this.height;
863
+ const ctx = canvas.getContext("webgpu");
864
+ if (!ctx)
865
+ throw new Error("WebGpuImage.toCanvas requires a webgpu context");
866
+ const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
867
+ ctx.configure({ device: this.device, format: presentationFormat, alphaMode: "premultiplied" });
868
+ const view = ctx.getCurrentTexture().createView();
869
+ const shaderModule = getShaderCache(this.device).get(PASSTHROUGH_SHADER_SRC);
870
+ const pipeline = this.device.createRenderPipeline({
871
+ layout: "auto",
872
+ vertex: { module: shaderModule, entryPoint: "vs" },
873
+ fragment: { module: shaderModule, entryPoint: "fs", targets: [{ format: presentationFormat }] },
874
+ primitive: { topology: "triangle-list" }
875
+ });
876
+ const sampler = this.device.createSampler({ magFilter: "linear", minFilter: "linear" });
877
+ const bindGroup = this.device.createBindGroup({
878
+ layout: pipeline.getBindGroupLayout(0),
879
+ entries: [
880
+ { binding: 0, resource: this.texture.createView() },
881
+ { binding: 1, resource: sampler }
882
+ ]
883
+ });
884
+ const enc = this.device.createCommandEncoder();
885
+ const pass = enc.beginRenderPass({
886
+ colorAttachments: [{ view, loadOp: "clear", storeOp: "store", clearValue: [0, 0, 0, 0] }]
887
+ });
888
+ pass.setPipeline(pipeline);
889
+ pass.setBindGroup(0, bindGroup);
890
+ pass.draw(3);
891
+ pass.end();
892
+ this.device.queue.submit([enc.finish()]);
893
+ }
894
+ async encode(format, quality) {
895
+ const bin = await this.materialize();
896
+ if (typeof OffscreenCanvas === "undefined") {
897
+ throw new Error("WebGpuImage.encode requires an OffscreenCanvas environment");
341
898
  }
342
- for (const want of supports) {
343
- switch (want) {
344
- case "ImageBinary":
345
- return this.getPixels();
346
- case "Blob":
347
- return this.getBlob();
348
- case "DataUri":
349
- return this.getDataUri();
350
- case "RawImage": {
351
- const p = await this.getPixels();
352
- return new MediaRawImage(p.data, p.width, p.height, p.channels);
353
- }
354
- case "ImageBitmap":
355
- case "VideoFrame":
356
- case "OffscreenCanvas": {
357
- const asBrowser = this;
358
- if (asBrowser.toFirstSupportedBrowser) {
359
- const produced = await asBrowser.toFirstSupportedBrowser(want);
360
- if (produced !== undefined)
361
- return produced;
362
- }
363
- continue;
364
- }
365
- case "Sharp":
366
- continue;
367
- }
899
+ const off = new OffscreenCanvas(this.width, this.height);
900
+ const ctx = off.getContext("2d");
901
+ if (!ctx)
902
+ throw new Error("WebGpuImage.encode could not acquire a 2D context");
903
+ ctx.putImageData(new ImageData(bin.data, this.width, this.height), 0, 0);
904
+ const blob = await off.convertToBlob({ type: `image/${format}`, quality });
905
+ return new Uint8Array(await blob.arrayBuffer());
906
+ }
907
+ retain(n = 1) {
908
+ if (this.refcount <= 0) {
909
+ throw new Error("WebGpuImage.retain called on a released image");
368
910
  }
369
- throw new Error(`Image.toFirstSupported: none of [${supports.join(", ")}] can be produced on this platform`);
911
+ this.refcount += n;
912
+ return this;
370
913
  }
371
- toJSON() {
372
- if (this.source.kind === "dataUri") {
373
- return this.source.dataUri;
374
- }
375
- const pixels = this.source.kind === "pixels" ? this.source.pixels : this.pixelsCache;
376
- if (pixels) {
377
- return {
378
- data: Array.from(pixels.data),
379
- width: pixels.width,
380
- height: pixels.height,
381
- channels: pixels.channels
382
- };
914
+ release() {
915
+ if (this.refcount <= 0) {
916
+ throw new Error("WebGpuImage.release called on a released image");
383
917
  }
384
- return { unsynced: true, kind: this.source.kind };
385
- }
386
- canonicalSupport() {
387
- switch (this.source.kind) {
388
- case "dataUri":
389
- return "DataUri";
390
- case "pixels":
391
- return "ImageBinary";
392
- case "blob":
393
- return "Blob";
394
- case "bitmap":
395
- return "ImageBitmap";
396
- case "videoFrame":
397
- return "VideoFrame";
398
- case "offscreenCanvas":
399
- return "OffscreenCanvas";
400
- }
401
- }
402
- getSource() {
403
- return this.source;
404
- }
405
- setPixelsCache(pixels) {
406
- this.pixelsCache = pixels;
407
- }
408
- currentSourceValue() {
409
- switch (this.source.kind) {
410
- case "dataUri":
411
- return this.source.dataUri;
412
- case "pixels":
413
- return this.source.pixels;
414
- case "blob":
415
- return this.source.blob;
416
- case "bitmap":
417
- return this.source.bitmap;
418
- case "videoFrame":
419
- return this.source.frame;
420
- case "offscreenCanvas":
421
- return this.source.canvas;
918
+ this.refcount -= 1;
919
+ if (this.refcount > 0)
920
+ return;
921
+ if (this.texture) {
922
+ const tex = this.texture;
923
+ this.texture = null;
924
+ if (finalizers)
925
+ finalizers.unregister(this);
926
+ getTexturePool(this.device).release(tex);
422
927
  }
423
928
  }
424
929
  }
425
- async function blobToDataUri(blob) {
426
- const buffer = await blob.arrayBuffer();
427
- const bytes = new Uint8Array(buffer);
428
- let binary = "";
429
- const CHUNK = 8192;
430
- for (let i = 0;i < bytes.byteLength; i += CHUNK) {
431
- binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK));
432
- }
433
- const mime = blob.type || "image/png";
434
- return `data:${mime};base64,${btoa(binary)}`;
435
- }
436
- // src/media/Image.browser.ts
437
- Image.fromBitmap = function fromBitmap(bitmap) {
438
- return Image.from(bitmap);
439
- };
440
- Image.fromVideoFrame = function fromVideoFrame(frame) {
441
- return Image.from(frame);
442
- };
443
- Image.fromOffscreenCanvas = function fromOffscreenCanvas(canvas) {
444
- return Image.from(canvas);
445
- };
446
- function rasterToImageData(image) {
447
- const { width, height, channels, data } = image;
448
- const id = new ImageData(width, height);
449
- if (channels === 4) {
450
- id.data.set(data);
451
- return id;
452
- }
453
- if (channels === 3) {
454
- for (let i = 0;i < width * height; i++) {
455
- id.data[i * 4] = data[i * 3];
456
- id.data[i * 4 + 1] = data[i * 3 + 1];
457
- id.data[i * 4 + 2] = data[i * 3 + 2];
458
- id.data[i * 4 + 3] = 255;
459
- }
460
- return id;
461
- }
462
- if (channels === 1) {
463
- for (let i = 0;i < width * height; i++) {
464
- const v = data[i];
465
- id.data[i * 4] = v;
466
- id.data[i * 4 + 1] = v;
467
- id.data[i * 4 + 2] = v;
468
- id.data[i * 4 + 3] = 255;
469
- }
470
- return id;
471
- }
472
- throw new Error(`Image.getImageData: unsupported channel count ${channels}`);
930
+ // src/media-browser.ts
931
+ async function _preferGpu(bin) {
932
+ const dev = await getGpuDevice();
933
+ return dev ? WebGpuImage.fromImageBinary(bin) : CpuImage.fromImageBinary(bin);
473
934
  }
474
- async function blobToOffscreenCanvas(blob) {
935
+ registerGpuImageFactory("fromImageBinaryAsync", _preferGpu);
936
+ registerGpuImageFactory("fromDataUri", async (dataUri) => {
937
+ const bin = await getImageRasterCodec().decodeDataUri(dataUri);
938
+ return _preferGpu(bin);
939
+ });
940
+ registerGpuImageFactory("fromBlob", async (blob) => {
475
941
  const bitmap = await createImageBitmap(blob);
476
- try {
477
- const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
478
- const ctx = canvas.getContext("2d");
479
- if (!ctx)
480
- throw new Error("Image: failed to get 2D context on OffscreenCanvas");
481
- ctx.drawImage(bitmap, 0, 0);
482
- return canvas;
483
- } finally {
484
- bitmap.close();
485
- }
486
- }
487
- Image.prototype.getImageData = async function getImageData() {
488
- const pixels = await this.getPixels();
489
- return rasterToImageData(pixels);
490
- };
491
- Image.prototype.getImageBitmap = async function getImageBitmap() {
492
- const source = this.getSource();
493
- if (source.kind === "bitmap")
494
- return source.bitmap;
495
- if (source.kind === "blob")
496
- return createImageBitmap(source.blob);
497
- if (source.kind === "offscreenCanvas") {
498
- return source.canvas.transferToImageBitmap();
499
- }
500
- if (source.kind === "dataUri") {
501
- return createImageBitmap(dataUriToBlob(source.dataUri));
502
- }
503
- const id = await this.getImageData();
504
- return createImageBitmap(id);
505
- };
506
- Image.prototype.getVideoFrame = async function getVideoFrame() {
507
- const source = this.getSource();
508
- if (source.kind === "videoFrame")
509
- return source.frame;
510
- const bitmap = await this.getImageBitmap();
511
- return new VideoFrame(bitmap, { timestamp: 0 });
512
- };
513
- Image.prototype.getOffscreenCanvas = async function getOffscreenCanvas() {
514
- const source = this.getSource();
515
- if (source.kind === "offscreenCanvas")
516
- return source.canvas;
517
- if (source.kind === "blob")
518
- return blobToOffscreenCanvas(source.blob);
519
- if (source.kind === "dataUri")
520
- return blobToOffscreenCanvas(dataUriToBlob(source.dataUri));
521
- const id = await this.getImageData();
522
- const canvas = new OffscreenCanvas(id.width, id.height);
523
- const ctx = canvas.getContext("2d");
942
+ const off = new OffscreenCanvas(bitmap.width, bitmap.height);
943
+ const ctx = off.getContext("2d");
524
944
  if (!ctx)
525
- throw new Error("Image.getOffscreenCanvas: failed to get 2D context");
526
- ctx.putImageData(id, 0, 0);
527
- return canvas;
528
- };
529
- Image.prototype.toFirstSupportedBrowser = async function toFirstSupportedBrowser(want) {
530
- if (want === "ImageBitmap")
531
- return this.getImageBitmap();
532
- if (want === "VideoFrame")
533
- return this.getVideoFrame();
534
- if (want === "OffscreenCanvas")
535
- return this.getOffscreenCanvas();
536
- return;
537
- };
945
+ throw new Error("fromBlob: could not acquire 2D context");
946
+ ctx.drawImage(bitmap, 0, 0);
947
+ const id = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
948
+ return _preferGpu({ data: id.data, width: bitmap.width, height: bitmap.height, channels: 4 });
949
+ });
950
+ registerGpuImageFactory("fromImageBitmap", async (bitmap) => {
951
+ const off = new OffscreenCanvas(bitmap.width, bitmap.height);
952
+ const ctx = off.getContext("2d");
953
+ if (!ctx)
954
+ throw new Error("fromImageBitmap: could not acquire 2D context");
955
+ ctx.drawImage(bitmap, 0, 0);
956
+ const id = ctx.getImageData(0, 0, bitmap.width, bitmap.height);
957
+ return _preferGpu({ data: id.data, width: bitmap.width, height: bitmap.height, channels: 4 });
958
+ });
538
959
  export {
539
960
  toHexColor,
961
+ setPreviewBudget,
540
962
  resolveColor,
963
+ resetTexturePoolForTests,
964
+ resetGpuDeviceForTests,
965
+ registerPreviewResizeFn,
541
966
  registerImageRasterCodec,
967
+ registerGpuImageFactory,
968
+ registerFilterOp,
969
+ previewSource,
542
970
  parseHexColor,
543
971
  parseDataUri,
544
972
  isMediaRawImageShape,
545
973
  isHexColor,
546
974
  isColorObject,
975
+ imageBinaryToDataUri,
976
+ imageBinaryToBlob,
977
+ imageBinaryToBase64Png,
978
+ hasFilterOp,
979
+ getTexturePool,
980
+ getShaderCache,
981
+ getPreviewBudget,
547
982
  getImageRasterCodec,
548
- dataUriToBlob,
983
+ getGpuImageFactory,
984
+ getGpuDevice,
985
+ encodeImageBinaryToPng,
986
+ createTexturePool,
987
+ createShaderCache,
988
+ applyFilter,
989
+ _resetFilterRegistryForTests,
990
+ WebGpuImage,
991
+ VERTEX_PRELUDE,
992
+ PASSTHROUGH_SHADER_SRC,
549
993
  MediaRawImage,
550
- Image
994
+ GpuImageSchema,
995
+ GpuImage as GpuImageFactory,
996
+ CpuImage
551
997
  };
552
998
 
553
- //# debugId=9670F06EEC7694A064756E2164756E21
999
+ //# debugId=006D8DBD8329F1E964756E2164756E21