img2num 0.0.0 → 0.2.0

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.
@@ -38,7 +38,7 @@
38
38
  * });
39
39
  */
40
40
 
41
- import createImg2NumModule from "./build-wasm/index.js";
41
+ import createImg2NumModule from "@wasm/index.js";
42
42
 
43
43
  let wasmModule;
44
44
 
@@ -154,7 +154,8 @@ async function callWasm(funcName, argsMap, returnType) {
154
154
  * - returnType: expected return type of the WASM export
155
155
  * @param {MessageEvent} event
156
156
  */
157
- self.onmessage = async ({ data }) => {
157
+
158
+ async function handleMessage(data) {
158
159
  await readyPromise;
159
160
 
160
161
  const { id, funcName, args, bufferKeys, returnType } = data;
@@ -205,13 +206,38 @@ self.onmessage = async ({ data }) => {
205
206
  wasmModule._free(result);
206
207
  }
207
208
 
208
- self.postMessage({ id, output, returnValue });
209
+ globalThis.postMessage({ id, output, returnValue });
209
210
  } catch (error) {
210
- self.postMessage({ id, error: error.message });
211
+ globalThis.postMessage({ id, error: error.message });
211
212
  } finally {
212
213
  // -------- Cleanup --------
213
214
  for (const { ptr } of pointers.values()) {
214
215
  wasmModule._free(ptr);
215
216
  }
216
217
  }
217
- };
218
+ }
219
+
220
+ if (__TARGET__ === "node") {
221
+ const { parentPort } = await import("node:worker_threads");
222
+ const { initWebGPU, destroyWebGPU } = await import("../target/node/webgpu.js");
223
+
224
+ try {
225
+ await initWebGPU();
226
+ } catch (err) {
227
+ console.error(`[Img2Num node/worker.js] Error: ${err}`);
228
+ }
229
+
230
+ // 2. FIX THE TYPO: Polyfill globalThis.postMessage so handleMessage can call it natively!
231
+ globalThis.postMessage = (data) => parentPort.postMessage(data);
232
+
233
+ // 3. Listen for incoming messages from the console app main thread
234
+ // (Node passes the raw payload directly, no nested event wrapper needed)
235
+ parentPort.on("message", async (data) => {
236
+ await handleMessage(data);
237
+ await destroyWebGPU();
238
+ parentPort.close();
239
+ });
240
+ } else {
241
+ // Browser Worker setup: Standard event-unwrapping listener
242
+ globalThis.onmessage = ({ data }) => handleMessage(data);
243
+ }
@@ -0,0 +1,514 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, resolve } from "node:path";
4
+ //#region src/imageToUint8ClampedArray.js
5
+ /**
6
+ * @packageDocumentation
7
+ * Convenience image conversion utility to ensure type compatibility with the library.
8
+ *
9
+ * @file Convenience utility function.
10
+ *
11
+ * @module image-utils
12
+ * @license MIT
13
+ * @copyright Ryan Millard 2026
14
+ * @author Ryan Millard
15
+ * @since 0.0.0
16
+ *
17
+ * @exports imageToUint8ClampedArray
18
+ */
19
+ /**
20
+ * @summary Convert an image file into a `Uint8ClampedArray` of pixel data (RGBA).
21
+ *
22
+ * @function imageToUint8ClampedArray
23
+ * @async
24
+ * @description
25
+ * Reads an image file (PNG, JPEG, etc.) and returns its pixel data as a `Uint8ClampedArray`.
26
+ * Each pixel consists of four consecutive values: red, green, blue, and alpha (RGBA).
27
+ * Also returns the image's original width and height. Useful for canvas operations,
28
+ * image processing, WebGL textures, or computer vision tasks.
29
+ *
30
+ *
31
+ * @param {File} file - The image file to process. Must be a valid `File` object, e.g., from an `<input type="file">` element.
32
+ *
33
+ * @returns {Promise<{pixels: Uint8ClampedArray, width: number, height: number}>}
34
+ * A Promise resolving to an object containing:
35
+ * - `pixels`: A `Uint8ClampedArray` of RGBA pixel values.
36
+ * - `width`: Width of the image in pixels.
37
+ * - `height`: Height of the image in pixels.
38
+ *
39
+ * @throws {Error} Will not throw in current implementation, but could reject if the image fails to load.
40
+ *
41
+ * @example
42
+ * const fileInput = document.querySelector("#fileInput");
43
+ * fileInput.addEventListener("change", async (event) => {
44
+ * const file = event.target.files[0];
45
+ * const { pixels, width, height } = await imageToUint8ClampedArray(file);
46
+ * console.log("Width:", width, "Height:", height);
47
+ * console.log("Pixels:", pixels);
48
+ * });
49
+ *
50
+ * @todo Add error handling for invalid or corrupt image files.
51
+ * @variation Standard image file input
52
+ */
53
+ function imageToUint8ClampedArray(file) {
54
+ return new Promise((resolve) => {
55
+ const img = new Image();
56
+ img.onload = () => {
57
+ const canvas = document.createElement("canvas");
58
+ canvas.width = img.width;
59
+ canvas.height = img.height;
60
+ const ctx = canvas.getContext("2d");
61
+ ctx.drawImage(img, 0, 0);
62
+ const { data } = ctx.getImageData(0, 0, img.width, img.height);
63
+ resolve({
64
+ pixels: data,
65
+ width: img.width,
66
+ height: img.height
67
+ });
68
+ };
69
+ img.src = URL.createObjectURL(file);
70
+ });
71
+ }
72
+ //#endregion
73
+ //#region src/target/node/worker.js
74
+ var __dirname = dirname(fileURLToPath(import.meta.url));
75
+ async function createWorker() {
76
+ const worker = new Worker(resolve(__dirname, "./wasmWorker.js"));
77
+ return {
78
+ postMessage: (msg) => worker.postMessage(msg),
79
+ onMessage: (fn) => worker.on("message", fn),
80
+ onError: (fn) => worker.on("error", fn),
81
+ terminate: () => worker.terminate()
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/wasmClient.js
86
+ /**
87
+ * @packageDocumentation
88
+ * Advanced low-level interface for communicating with the WASM worker.
89
+ * Provides granular control over calling WASM functions asynchronously,
90
+ * handling memory transfers, and managing worker lifecycle.
91
+ *
92
+ * @file Manages function call requests to the WASM worker.
93
+ * @internal
94
+ *
95
+ * @module wasm-client
96
+ * @license MIT
97
+ * @copyright Ryan Millard 2026
98
+ * @author Ryan Millard
99
+ * @since 0.0.0
100
+ * @description This module provides low-level image processing functions using WASM.
101
+ * You must specify exact C++ function signatures and manage the input data carefully.
102
+ * This allows more granular control.
103
+ */
104
+ /**
105
+ * Worker instance that handles communication with the WASM module.
106
+ * @type {Worker | null}
107
+ * @private
108
+ */
109
+ var worker;
110
+ /**
111
+ * Incremental ID counter for correlating requests and responses.
112
+ * @type {number}
113
+ * @private
114
+ */
115
+ var idCounter = 0;
116
+ /**
117
+ * Maps request IDs to their corresponding promise callbacks.
118
+ * @type {Map<number, {resolve: Function, reject: Function}>}
119
+ * @private
120
+ */
121
+ var callbacks = /* @__PURE__ */ new Map();
122
+ /**
123
+ * Flag to ensure the WASM worker is initialized only once.
124
+ * @type {boolean}
125
+ * @private
126
+ */
127
+ var initialized = false;
128
+ /**
129
+ * Handle response messages from the WASM worker.
130
+ * @param {{ id: number, output?: any, returnValue?: any, error?: string }} data
131
+ */
132
+ function handleMessage(data) {
133
+ const cb = callbacks.get(data.id);
134
+ if (!cb) return;
135
+ callbacks.delete(data.id);
136
+ if (data.error) cb.reject(new Error(data.error));
137
+ else cb.resolve({
138
+ output: data.output,
139
+ returnValue: data.returnValue
140
+ });
141
+ }
142
+ /**
143
+ * @summary Initialize the WASM worker.
144
+ *
145
+ * @description
146
+ * Sets up message and error handlers. Safe to call multiple times;
147
+ * subsequent calls are no-ops. After initialization, functions can be called
148
+ * via {@link callWasm}.
149
+ *
150
+ * @function initWasmWorker
151
+ *
152
+ * @example
153
+ * import { initWasmWorker } from "./wasmClient.js";
154
+ *
155
+ * initWasmWorker();
156
+ *
157
+ * @since 0.0.0
158
+ */
159
+ async function initWasmWorker() {
160
+ if (initialized) return;
161
+ worker = await createWorker();
162
+ worker.onMessage(handleMessage);
163
+ worker.onError((event) => {
164
+ const output = event.message || "WASM worker error";
165
+ const err = /* @__PURE__ */ new Error(`[Img2Num wasmClient] Error: ${output}`);
166
+ for (const [_id, cb] of callbacks) cb.reject(err);
167
+ callbacks.clear();
168
+ });
169
+ initialized = true;
170
+ }
171
+ /**
172
+ * @summary Call a function in the WASM worker.
173
+ *
174
+ * @description
175
+ * Directly send a request to the WASM worker to call the specified function,
176
+ * passing specific buffers and and arguments.
177
+ *
178
+ * @async
179
+ * @function callWasm
180
+ * @param {Object} __named_parameters - Options for the WASM call.
181
+ * @property {string} __named_parameters.funcName - The name of the WASM function to invoke.
182
+ * @property {Object} [__named_parameters.args={}] - Named arguments to pass to the WASM function.
183
+ * @property {string[]} [__named_parameters.bufferKeys=[]] - Keys of arguments that should be transferred as ArrayBuffers.
184
+ * @property {string} [__named_parameters.returnType="void"] - Expected return type.
185
+ *
186
+ * @returns {Promise<{output: any, returnValue: any}>} Resolves with the result of the WASM function call.
187
+ *
188
+ * @throws {Error} If the worker has not been initialized.
189
+ *
190
+ * @example
191
+ * import { callWasm, initWasmWorker } from "./wasmClient.js";
192
+ *
193
+ * initWasmWorker();
194
+ *
195
+ * const result = await callWasm({
196
+ * funcName: "gaussian_blur_fft",
197
+ * args: { pixels, width, height, sigma_pixels: 5 },
198
+ * bufferKeys: ["pixels"],
199
+ * returnType: "Uint8ClampedArray"
200
+ * });
201
+ * console.log(result.output);
202
+ *
203
+ * @since 0.0.0
204
+ */
205
+ async function callWasm({ funcName, args = {}, bufferKeys = [], returnType = "void" }) {
206
+ if (!initialized) throw new Error("WASM worker not initialized. Call initWasmWorker() first.");
207
+ const id = idCounter++;
208
+ return new Promise((resolve, reject) => {
209
+ callbacks.set(id, {
210
+ resolve,
211
+ reject
212
+ });
213
+ worker.postMessage({
214
+ id,
215
+ funcName,
216
+ args,
217
+ bufferKeys,
218
+ returnType
219
+ });
220
+ });
221
+ }
222
+ //#endregion
223
+ //#region src/safeWasmWrappers.js
224
+ /**
225
+ * @packageDocumentation
226
+ * High-level image operations exposed via WASM.
227
+ *
228
+ * The exports defined here abstract away the manual memory management required
229
+ * when importing raw WASM functions, making them more JavaScript-friendly.
230
+ *
231
+ * @file Safely wraps unsafe WASM (C++) function calls.
232
+ *
233
+ * @module image-wasm
234
+ * @license MIT
235
+ * @copyright Ryan Millard 2026
236
+ * @author Ryan Millard
237
+ * @since 0.0.0
238
+ * @description This module provides high-level image processing functions using WASM.
239
+ * Each function handles memory management and exposes a JavaScript-friendly API.
240
+ */
241
+ await initWasmWorker();
242
+ /**
243
+ * @summary Apply a Gaussian blur to an image using FFT in WASM.
244
+ *
245
+ * @description
246
+ * Takes a Uint8ClampedArray and its dimensions and applies a Gaussian blur on the Uint8ClampedArray image.
247
+ * The `sigma_pixels` parameter determines the blur radius and has a dynamic default value equal to 5% of the image's width.
248
+ * Useful for denoising images by applying a low-pass filter. Sped up by a 2-D FFT.
249
+ *
250
+ * @async
251
+ * @function gaussianBlur
252
+ * @param {Object} options - The input options.
253
+ * @param {Uint8ClampedArray} options.pixels - The image pixel data (flat RGBA array).
254
+ * @param {number} options.width - The width of the image.
255
+ * @param {number} options.height - The height of the image.
256
+ * @param {number} [options.sigma_pixels=width*0.005] - Standard deviation of the Gaussian blur (default=width*0.005; 5% of width).
257
+ * @returns {Promise<Uint8ClampedArray>} The blurred image pixels.
258
+ * @throws {Error} If the WASM function fails or memory allocation fails.
259
+ * @example
260
+ * const blurred = await gaussianBlur({ pixels, width, height });
261
+ * @todo Fix FFT zero-padding bug around edges of the image.
262
+ * @variation Standard Gaussian blur using FFT
263
+ * @since 0.0.0
264
+ */
265
+ var gaussianBlur = async ({ pixels, width, height, sigma_pixels = width * .005 }) => {
266
+ return (await callWasm({
267
+ funcName: "gaussian_blur_fft",
268
+ args: {
269
+ pixels,
270
+ width,
271
+ height,
272
+ sigma_pixels
273
+ },
274
+ bufferKeys: [{
275
+ key: "pixels",
276
+ type: "Uint8ClampedArray"
277
+ }]
278
+ })).output.pixels;
279
+ };
280
+ /**
281
+ * @summary Apply a bilateral filter to an image using WASM.
282
+ *
283
+ * @description
284
+ * Takes a Uint8ClampedArray and its dimensions and applies a bilateral filter on the Uint8ClampedArray image.
285
+ * The `sigma_spatial` and `sigma_range` set weights to the respective Gaussian kernels applied to spatial (x, y) and range (color) data -
286
+ * they both have recommended default values applied.
287
+ * The default `color_space` is 0, which is CIE LAB, but sRGB can be chosen by setting `color_space` = 1. CIE LAB is more
288
+ * accurate, but sRGB is slightly faster.
289
+ *
290
+ * @async
291
+ * @function bilateralFilter
292
+ * @param {Object} options - The input options.
293
+ * @param {Uint8ClampedArray} options.pixels - The image pixel data (flat RGBA array).
294
+ * @param {number} options.width - The width of the image.
295
+ * @param {number} options.height - The height of the image.
296
+ * @param {number} [options.sigma_spatial=3] - Spatial standard deviation.
297
+ * @param {number} [options.sigma_range=50] - Range (color) standard deviation.
298
+ * @param {number} [options.color_space=0] - Color space mode (0: CIE LAB; 1: sRGB).
299
+ * @returns {Promise<Uint8ClampedArray>} The filtered image pixels.
300
+ * @throws {Error} If the WASM function fails.
301
+ * @example
302
+ * const filtered = await bilateralFilter({ pixels, width, height });
303
+ * @variation Standard bilateral filter with default parameters
304
+ * @since 0.0.0
305
+ */
306
+ var bilateralFilter = async ({ pixels, width, height, sigma_spatial = 3, sigma_range = 50, color_space = 0 }) => {
307
+ return (await callWasm({
308
+ funcName: "bilateral_filter",
309
+ args: {
310
+ pixels,
311
+ width,
312
+ height,
313
+ sigma_spatial,
314
+ sigma_range,
315
+ color_space
316
+ },
317
+ bufferKeys: [{
318
+ key: "pixels",
319
+ type: "Uint8ClampedArray"
320
+ }]
321
+ })).output.pixels;
322
+ };
323
+ /**
324
+ * @summary Apply a black-biased threshold filter to reduce colors in an image.
325
+ *
326
+ * @description
327
+ * Apply a simple sRGB bin-based threshold on the Uint8ClampedArray image.
328
+ * The bins in this function are determined by the `num_colors` parameter.
329
+ *
330
+ * @async
331
+ * @function blackThreshold
332
+ * @param {Object} options - The input options.
333
+ * @param {Uint8ClampedArray} options.pixels - The image pixel data (flat RGBA array).
334
+ * @param {number} options.width - The width of the image.
335
+ * @param {number} options.height - The height of the image.
336
+ * @param {number} options.num_colors - Number of colors to reduce the image to.
337
+ * @returns {Promise<Uint8ClampedArray>} The thresholded image pixels.
338
+ * @throws {Error} If the WASM function fails.
339
+ * @example
340
+ * const thresholded = await blackThreshold({ pixels, width, height, num_colors: 16 });
341
+ * @see {@link https://en.wikipedia.org/wiki/Color_quantization|Color Quantization Wiki}
342
+ * @todo Support different bias levels for black/white thresholds.
343
+ * @variation Black-biased threshold with customizable number of colors
344
+ * @since 0.0.0
345
+ */
346
+ var blackThreshold = async ({ pixels, width, height, num_colors }) => {
347
+ return (await callWasm({
348
+ funcName: "black_threshold_image",
349
+ args: {
350
+ pixels,
351
+ width,
352
+ height,
353
+ num_colors
354
+ },
355
+ bufferKeys: [{
356
+ key: "pixels",
357
+ type: "Uint8ClampedArray"
358
+ }]
359
+ })).output.pixels;
360
+ };
361
+ /**
362
+ * @summary Cluster pixels using the K-Means algorithm in WASM.
363
+ *
364
+ * @description
365
+ * Apply a standard K-Means clustering algorithm to the input image in the specified `color_space`
366
+ * (default is 0: CIE LAB, but 1: sRGB can be use) using pre-specified maximum color and iteration counts.
367
+ * You can provide the `out_pixels` and `out_labels` arrays,
368
+ * however this is atypical in JavaScript (since it is modified in-place and you will need to allocate a sufficiently large array),
369
+ * so it is recommended to use the default arguments and returns.
370
+ *
371
+ * @async
372
+ * @function kmeans
373
+ * @param {Object} options - The input options.
374
+ * @param {Uint8ClampedArray} options.pixels - Original image pixels.
375
+ * @param {Uint8ClampedArray} [options.out_pixels=new Uint8ClampedArray(pixels.length)] - Output pixels array.
376
+ * @param {Int32Array} [options.out_labels=new Int32Array(pixels.length/4)] - Output labels array.
377
+ * @param {number} options.width - Image width.
378
+ * @param {number} options.height - Image height.
379
+ * @param {number} options.num_colors - Number of color clusters.
380
+ * @param {number} [options.max_iter=100] - Maximum number of iterations.
381
+ * @param {number} [options.color_space=0] - Color space mode.
382
+ * @returns {Promise<{pixels: Uint8ClampedArray, labels: Int32Array}>} Clustered pixels and labels.
383
+ * @throws {Error} If the WASM function fails or iterations do not converge.
384
+ * @example
385
+ * const { pixels: clusteredPixels, labels } = await kmeans({ pixels, width, height, num_colors: 8 });
386
+ * @variation K-means clustering with default color space
387
+ * @since 0.0.0
388
+ */
389
+ var kmeans = async ({ pixels, out_pixels = new Uint8ClampedArray(pixels.length), out_labels = new Int32Array(pixels.length / 4), width, height, num_colors, max_iter = 100, color_space = 0 }) => {
390
+ const result = await callWasm({
391
+ funcName: "kmeans",
392
+ args: {
393
+ pixels,
394
+ out_pixels,
395
+ out_labels,
396
+ width,
397
+ height,
398
+ num_colors,
399
+ max_iter,
400
+ color_space
401
+ },
402
+ bufferKeys: [
403
+ {
404
+ key: "pixels",
405
+ type: "Uint8ClampedArray"
406
+ },
407
+ {
408
+ key: "out_pixels",
409
+ type: "Uint8ClampedArray"
410
+ },
411
+ {
412
+ key: "out_labels",
413
+ type: "Int32Array"
414
+ }
415
+ ]
416
+ });
417
+ return {
418
+ pixels: result.output.out_pixels,
419
+ labels: result.output.out_labels
420
+ };
421
+ };
422
+ /**
423
+ * @summary Convert labeled regions to SVG contours.
424
+ *
425
+ * @description
426
+ * Convert an input image and its labeled regions into an SVG.
427
+ *
428
+ * @async
429
+ * @function findContours
430
+ * @param {Object} options - The input options.
431
+ * @param {Uint8ClampedArray} options.pixels - Original image pixels.
432
+ * @param {Int32Array} options.labels - Label array from clustering (e.g., K-Means) or segmentation.
433
+ * @param {number} options.width - Image width.
434
+ * @param {number} options.height - Image height.
435
+ * @param {number} [options.min_area=100] - Minimum area of a region to be considered a contour.
436
+ * @param {number} [options.min_thickness=10] - Minimum thickness of a region to be considered a contour.
437
+ * @returns {Promise<{svg: string}>} Generated SVG.
438
+ * @throws {Error} If the WASM function fails or input labels are invalid.
439
+ * @example
440
+ * const { svg } = await findContours({ pixels, labels, width, height });
441
+ * @variation Converts labeled (from a clustering algorithm, e.g. K-Means) image into an SVG.
442
+ * @since 0.0.0
443
+ */
444
+ var findContours = async ({ pixels, labels, width, height, min_area = 100, min_thickness = 10 }) => {
445
+ return { svg: (await callWasm({
446
+ funcName: "labels_to_svg",
447
+ args: {
448
+ pixels,
449
+ labels,
450
+ width,
451
+ height,
452
+ min_area,
453
+ min_thickness
454
+ },
455
+ bufferKeys: [{
456
+ key: "pixels",
457
+ type: "Uint8ClampedArray"
458
+ }, {
459
+ key: "labels",
460
+ type: "Int32Array"
461
+ }],
462
+ returnType: "string"
463
+ })).returnValue };
464
+ };
465
+ /**
466
+ * @summary Convert raster images (e.g., JPEG, PNG) to SVGs.
467
+ *
468
+ * @description
469
+ * Convert an input raster image into an SVG. A unification of `bilateralFilter`, `kmeans`, and `findContours`.
470
+ *
471
+ * @async
472
+ * @function imageToSvg
473
+ * @param {Object} options - The input options.
474
+ * @param {Uint8ClampedArray} options.pixels - Original image pixels.
475
+ * @param {number} options.width - Image width.
476
+ * @param {number} options.height - Image height.
477
+ * @param {number} [options.sigma_spatial=3] - Spatial standard deviation.
478
+ * @param {number} [options.sigma_range=50] - Range (color) standard deviation.
479
+ * @param {number} [options.num_colors=16] - Number of color clusters.
480
+ * @param {number} [options.max_iter=100] - Maximum number of iterations.
481
+ * @param {number} [options.min_area=100] - Minimum area of a region to be considered a contour.
482
+ * @param {number} [options.min_thickness=10] - Minimum thickness of a region to be considered a contour.
483
+ * @param {number} [options.color_space=0] - Color space mode.
484
+ * @returns {Promise<{svg: string}>} Generated SVG.
485
+ * @throws {Error} If the WASM function fails or input labels are invalid.
486
+ * @example
487
+ * const { svg } = await findContours({ pixels, labels, width, height });
488
+ * @variation Convert a raster image (e.g., PNG, JPG) into an SVG.
489
+ * @since 0.0.0
490
+ */
491
+ var imageToSvg = async ({ pixels, width, height, sigma_spatial = 3, sigma_range = 50, num_colors = 16, max_iter = 100, min_area = 100, min_thickness = 10, color_space = 0 }) => {
492
+ return { svg: (await callWasm({
493
+ funcName: "image_to_svg",
494
+ args: {
495
+ pixels,
496
+ width,
497
+ height,
498
+ sigma_spatial,
499
+ sigma_range,
500
+ num_colors,
501
+ max_iter,
502
+ min_area,
503
+ min_thickness,
504
+ color_space
505
+ },
506
+ bufferKeys: [{
507
+ key: "pixels",
508
+ type: "Uint8ClampedArray"
509
+ }],
510
+ returnType: "string"
511
+ })).returnValue };
512
+ };
513
+ //#endregion
514
+ export { bilateralFilter, blackThreshold, findContours, gaussianBlur, imageToSvg, imageToUint8ClampedArray, kmeans };