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.
@@ -0,0 +1,198 @@
1
+ import { t as createImg2NumModule } from "./wasmWorker-CpNpexYK.js";
2
+ //#region src/workers/wasmWorker.js
3
+ /**
4
+ * @file wasmWorker.js
5
+ * @description
6
+ * Worker for calling WASM functions (Img2Num module) with proper memory handling.
7
+ *
8
+ * Notes:
9
+ * - WASM exposes its linear memory via typed array views such as HEAP32 (Int32) or HEAPU8 (byte).
10
+ * - When adding a new TypedArray type:
11
+ * 1. Add the corresponding HEAP view to EXPORTED_RUNTIME_METHODS in the Emscripten CMakeLists.txt.
12
+ * 2. Allocate memory with `_malloc`.
13
+ * 3. Copy data into the appropriate HEAP view. HEAP views are important because they allow the raw data to be read correctly.
14
+ * 4. After the call, read data back from the same HEAP view.
15
+ * 5. Free the allocated memory. This is important! We are using C-style memory since the code interfaces with the C ABI, so there is no GC.
16
+ * - This ensures JavaScript arrays correctly map to WASM memory.
17
+ *
18
+ * Important:
19
+ * - The `args` object passed to all convenience wrapper functions must match the order
20
+ * of the C++ function parameters exactly. For example:
21
+ * ```js
22
+ * args = { a: 1, b: 2 };
23
+ * ```
24
+ * corresponds to
25
+ * ```cpp
26
+ * int add(int a, int b);
27
+ * ```
28
+ *
29
+ * - Async functions:
30
+ * WebGPU operations inside WASM can be asynchronous. Use Emscripten Asyncify
31
+ * (via `ccall` with `{ async: true }`) to properly pause and resume execution.
32
+ *
33
+ * @example
34
+ * self.postMessage({
35
+ * id: 1,
36
+ * funcName: "bilateral_filter_gpu",
37
+ * args: { input: myArray },
38
+ * bufferKeys: [{ key: "input", type: "Uint8Array" }],
39
+ * returnType: "void"
40
+ * });
41
+ */
42
+ var wasmModule;
43
+ /**
44
+ * Promise that resolves when WASM module is ready.
45
+ * @type {Promise<void>}
46
+ */
47
+ var readyPromise = createImg2NumModule().then((mod) => {
48
+ wasmModule = mod;
49
+ });
50
+ /**
51
+ * Handlers for allocating, reading, and freeing different WASM types.
52
+ * @type {Record<string, {alloc: Function, read: Function}>}
53
+ */
54
+ var WASM_TYPES = {
55
+ void: {
56
+ alloc: () => null,
57
+ read: () => void 0
58
+ },
59
+ Int32Array: {
60
+ /**
61
+ * Allocate an Int32Array in WASM memory.
62
+ * @param {Int32Array} arr
63
+ * @returns {number} Pointer to allocated memory
64
+ */
65
+ alloc: (arr) => {
66
+ const ptr = wasmModule._malloc(arr.byteLength);
67
+ wasmModule.HEAP32.set(arr, ptr >> 2);
68
+ return ptr;
69
+ },
70
+ /**
71
+ * Read an Int32Array from WASM memory.
72
+ * @param {number} ptr
73
+ * @param {number} len
74
+ * @returns {Int32Array}
75
+ */
76
+ read: (ptr, len) => new Int32Array(wasmModule.HEAP32.buffer, ptr, len).slice()
77
+ },
78
+ Uint8Array: {
79
+ alloc: (arr) => {
80
+ const ptr = wasmModule._malloc(arr.byteLength);
81
+ wasmModule.HEAPU8.set(arr, ptr);
82
+ return ptr;
83
+ },
84
+ read: (ptr, len) => wasmModule.HEAPU8.slice(ptr, ptr + len)
85
+ },
86
+ Uint8ClampedArray: {
87
+ alloc: (arr) => {
88
+ const ptr = wasmModule._malloc(arr.byteLength);
89
+ wasmModule.HEAPU8.set(arr, ptr);
90
+ return ptr;
91
+ },
92
+ read: (ptr, len) => new Uint8ClampedArray(wasmModule.HEAPU8.slice(ptr, ptr + len))
93
+ },
94
+ string: {
95
+ /**
96
+ * Allocate a string in WASM memory.
97
+ * @param {string} str
98
+ * @returns {number} Pointer to allocated memory
99
+ */
100
+ alloc: (str) => {
101
+ const len = wasmModule.lengthBytesUTF8(str) + 1;
102
+ const ptr = wasmModule._malloc(len);
103
+ wasmModule.stringToUTF8(str, ptr, len);
104
+ return ptr;
105
+ },
106
+ /**
107
+ * Read a string from WASM memory.
108
+ * @param {number} ptr
109
+ * @returns {string|null}
110
+ */
111
+ read: (ptr) => ptr ? wasmModule.UTF8ToString(ptr) : null
112
+ }
113
+ };
114
+ /**
115
+ * Call a WASM function via ccall, handling asyncify automatically.
116
+ * @param {string} funcName
117
+ * @param {Map<string, number>} argsMap - Map of argument names to WASM pointers or numbers
118
+ * @param {string} returnType - WASM return type (e.g., 'void', 'Int32Array', 'string')
119
+ * @returns {Promise<number>} Result pointer or numeric return value
120
+ */
121
+ async function callWasm(funcName, argsMap, returnType) {
122
+ const argTypes = Array(argsMap.size).fill("number");
123
+ const retType = returnType !== "void" ? "number" : null;
124
+ return await wasmModule.ccall(funcName, retType, argTypes, [...argsMap.values()], { async: true });
125
+ }
126
+ /**
127
+ * Handle messages from main thread.
128
+ * Expects `data` to contain:
129
+ * - id: unique message ID
130
+ * - funcName: WASM export to call
131
+ * - args: object of input arguments to WASM export
132
+ * - bufferKeys: array of {key, type} defining memory buffers for WASM export args - JS doesn't have pointers, so we must do this
133
+ * - returnType: expected return type of the WASM export
134
+ * @param {MessageEvent} event
135
+ */
136
+ async function handleMessage(data) {
137
+ await readyPromise;
138
+ const { id, funcName, args, bufferKeys, returnType } = data;
139
+ const pointers = /* @__PURE__ */ new Map();
140
+ try {
141
+ if (!funcName) throw new Error("Missing funcName");
142
+ if (!args) throw new Error("Missing args");
143
+ if (!bufferKeys) throw new Error("Missing bufferKeys");
144
+ if (!returnType) throw new Error("Missing returnType");
145
+ const argsMap = new Map(Object.entries(args));
146
+ for (const { key, type } of bufferKeys) {
147
+ const handler = WASM_TYPES[type];
148
+ if (!handler) throw new Error(`Unsupported type: ${type}`);
149
+ const val = argsMap.get(key);
150
+ const ptr = handler.alloc(val);
151
+ pointers.set(key, {
152
+ ptr,
153
+ type,
154
+ length: val?.length
155
+ });
156
+ argsMap.set(key, ptr);
157
+ }
158
+ let result = await callWasm(funcName, argsMap, returnType);
159
+ /** @type {Record<string, any>} */
160
+ const output = Object.create(null);
161
+ for (const { key, type } of bufferKeys) {
162
+ const { ptr, length } = pointers.get(key);
163
+ output[key] = WASM_TYPES[type].read(ptr, length);
164
+ }
165
+ let returnValue = result;
166
+ if (returnType !== "void") returnValue = WASM_TYPES[returnType].read(result);
167
+ if (returnType === "string" && result) wasmModule._free(result);
168
+ globalThis.postMessage({
169
+ id,
170
+ output,
171
+ returnValue
172
+ });
173
+ } catch (error) {
174
+ globalThis.postMessage({
175
+ id,
176
+ error: error.message
177
+ });
178
+ } finally {
179
+ for (const { ptr } of pointers.values()) wasmModule._free(ptr);
180
+ }
181
+ }
182
+ {
183
+ const { parentPort } = await import("node:worker_threads");
184
+ const { initWebGPU, destroyWebGPU } = await import("./webgpu-BjVEVfI9.js");
185
+ try {
186
+ await initWebGPU();
187
+ } catch (err) {
188
+ console.error(`[Img2Num node/worker.js] Error: ${err}`);
189
+ }
190
+ globalThis.postMessage = (data) => parentPort.postMessage(data);
191
+ parentPort.on("message", async (data) => {
192
+ await handleMessage(data);
193
+ await destroyWebGPU();
194
+ parentPort.close();
195
+ });
196
+ }
197
+ //#endregion
198
+ export {};
@@ -0,0 +1,23 @@
1
+ import { create } from "webgpu";
2
+ //#region src/target/node/webgpu.js
3
+ var gpuInitPromise;
4
+ async function initWebGPU() {
5
+ if (globalThis.navigator?.gpu) return globalThis.navigator.gpu;
6
+ if (!gpuInitPromise) gpuInitPromise = Promise.resolve().then(() => {
7
+ const nativeGpu = create(["backend=vulkan"]);
8
+ globalThis.navigator ??= {};
9
+ globalThis.navigator.gpu = nativeGpu;
10
+ return nativeGpu;
11
+ });
12
+ return gpuInitPromise;
13
+ }
14
+ async function destroyWebGPU() {
15
+ if (globalThis.navigator?.gpu) {
16
+ delete globalThis.navigator.gpu;
17
+ if (Object.keys(globalThis.navigator).length === 0) delete globalThis.navigator;
18
+ }
19
+ gpuInitPromise = null;
20
+ await new Promise((resolve) => setTimeout(resolve, 50));
21
+ }
22
+ //#endregion
23
+ export { destroyWebGPU, initWebGPU };
package/package.json CHANGED
@@ -1,7 +1,10 @@
1
1
  {
2
2
  "name": "img2num",
3
- "version": "0.0.0",
3
+ "version": "0.2.0",
4
4
  "description": "Img2Num is a raster vectorization library - it converts images to SVGs",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
5
8
  "keywords": [
6
9
  "img2num",
7
10
  "computer-vision",
@@ -20,21 +23,28 @@
20
23
  "license": "MIT",
21
24
  "author": "Ryan-Millard",
22
25
  "type": "module",
23
- "main": "./index.js",
26
+ "main": "./dist/browser/img2num.js",
24
27
  "engines": {
25
28
  "node": ">=14"
26
29
  },
27
- "files": [
28
- "build-wasm/index.js",
29
- "build-wasm/index.wasm",
30
- "index.js",
31
- "safeWasmWrappers.js",
32
- "wasmClient.js",
33
- "wasmWorker.js",
34
- "imageToUint8ClampedArray.js"
35
- ],
36
- "dependencies": {},
30
+ "exports": {
31
+ ".": {
32
+ "node": "./dist/node/img2num.js",
33
+ "browser": "./dist/browser/img2num.js",
34
+ "import": "./dist/browser/img2num.js",
35
+ "default": "./dist/browser/img2num.js"
36
+ }
37
+ },
38
+ "optionalDependencies": {
39
+ "webgpu": "^0.4.0"
40
+ },
41
+ "devDependencies": {
42
+ "cross-env": "^10.1.0",
43
+ "vite": "^8.0.14"
44
+ },
37
45
  "scripts": {
38
- "test": "echo \"Error: no test specified\" && exit 1"
46
+ "build:browser": "cross-env TARGET=browser vite build",
47
+ "build:node": "cross-env TARGET=node vite build",
48
+ "build": "pnpm build:browser && pnpm build:node"
39
49
  }
40
50
  }
@@ -16,10 +16,10 @@
16
16
  * Each function handles memory management and exposes a JavaScript-friendly API.
17
17
  */
18
18
 
19
- import { initWasmWorker, callWasm } from "./wasmClient.js";
19
+ import { callWasm, initWasmWorker } from "./wasmClient.js";
20
20
 
21
21
  // Ensure worker is ready as soon as this module is imported
22
- initWasmWorker();
22
+ await initWasmWorker(); //it's an async function as of #433
23
23
 
24
24
  /**
25
25
  * @summary Apply a Gaussian blur to an image using FFT in WASM.
@@ -184,17 +184,18 @@ export const kmeans = async ({
184
184
  * @param {number} options.width - Image width.
185
185
  * @param {number} options.height - Image height.
186
186
  * @param {number} [options.min_area=100] - Minimum area of a region to be considered a contour.
187
- * @returns {Promise<{svg: string>} Generated SVG.
187
+ * @param {number} [options.min_thickness=10] - Minimum thickness of a region to be considered a contour.
188
+ * @returns {Promise<{svg: string}>} Generated SVG.
188
189
  * @throws {Error} If the WASM function fails or input labels are invalid.
189
190
  * @example
190
191
  * const { svg } = await findContours({ pixels, labels, width, height });
191
192
  * @variation Converts labeled (from a clustering algorithm, e.g. K-Means) image into an SVG.
192
193
  * @since 0.0.0
193
194
  */
194
- export const findContours = async ({ pixels, labels, width, height, min_area = 100 }) => {
195
+ export const findContours = async ({ pixels, labels, width, height, min_area = 100, min_thickness = 10 }) => {
195
196
  const result = await callWasm({
196
197
  funcName: "labels_to_svg",
197
- args: { pixels, labels, width, height, min_area },
198
+ args: { pixels, labels, width, height, min_area, min_thickness },
198
199
  bufferKeys: [
199
200
  { key: "pixels", type: "Uint8ClampedArray" },
200
201
  { key: "labels", type: "Int32Array" },
@@ -221,6 +222,7 @@ export const findContours = async ({ pixels, labels, width, height, min_area = 1
221
222
  * @param {number} [options.num_colors=16] - Number of color clusters.
222
223
  * @param {number} [options.max_iter=100] - Maximum number of iterations.
223
224
  * @param {number} [options.min_area=100] - Minimum area of a region to be considered a contour.
225
+ * @param {number} [options.min_thickness=10] - Minimum thickness of a region to be considered a contour.
224
226
  * @param {number} [options.color_space=0] - Color space mode.
225
227
  * @returns {Promise<{svg: string}>} Generated SVG.
226
228
  * @throws {Error} If the WASM function fails or input labels are invalid.
@@ -229,10 +231,10 @@ export const findContours = async ({ pixels, labels, width, height, min_area = 1
229
231
  * @variation Convert a raster image (e.g., PNG, JPG) into an SVG.
230
232
  * @since 0.0.0
231
233
  */
232
- export const imageToSvg = async ({ pixels, width, height, sigma_spatial = 3, sigma_range = 50, num_colors = 16, max_iter = 100, min_area = 100, color_space = 0 }) => {
234
+ export const 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 }) => {
233
235
  const result = await callWasm({
234
236
  funcName: "image_to_svg",
235
- args: { pixels, width, height, sigma_spatial, sigma_range, num_colors, max_iter, min_area, color_space },
237
+ args: { pixels, width, height, sigma_spatial, sigma_range, num_colors, max_iter, min_area, min_thickness, color_space },
236
238
  bufferKeys: [{ key: "pixels", type: "Uint8ClampedArray" }],
237
239
  returnType: "string",
238
240
  });
@@ -0,0 +1,10 @@
1
+ export async function createWorker() {
2
+ const worker = new Worker(new URL("@workers/wasmWorker.js", import.meta.url), { type: "module" });
3
+
4
+ return {
5
+ postMessage: (msg) => worker.postMessage(msg),
6
+ onMessage: (fn) => (worker.onmessage = (e) => fn(e.data)),
7
+ onError: (fn) => (worker.onerror = fn),
8
+ terminate: () => worker.terminate(),
9
+ };
10
+ }
@@ -0,0 +1,35 @@
1
+ import { create } from "webgpu";
2
+
3
+ let gpuInitPromise;
4
+
5
+ export async function initWebGPU() {
6
+ if (globalThis.navigator?.gpu) return globalThis.navigator.gpu;
7
+ if (!gpuInitPromise) {
8
+ gpuInitPromise = Promise.resolve().then(() => {
9
+ const nativeGpu = create(["backend=vulkan"]);
10
+ globalThis.navigator ??= {};
11
+ globalThis.navigator.gpu = nativeGpu;
12
+ return nativeGpu;
13
+ });
14
+ }
15
+ return gpuInitPromise;
16
+ }
17
+
18
+ // Drop-in companion deallocation handler
19
+ export async function destroyWebGPU() {
20
+ // 2. Sever the native reference links so Dawn can drop its ref counts
21
+ if (globalThis.navigator?.gpu) {
22
+ delete globalThis.navigator.gpu;
23
+ if (Object.keys(globalThis.navigator).length === 0) {
24
+ delete globalThis.navigator;
25
+ }
26
+ }
27
+
28
+ // 3. Reset our internal module-level promise cache
29
+ gpuInitPromise = null;
30
+
31
+ // 4. CRITICAL: Yield to the event loop. This gives Dawn's native engine
32
+ // a small time window to notice the reference count hit 0, dismantle its
33
+ // background threads, and flush outstanding callbacks BEFORE the process terminates.
34
+ await new Promise((resolve) => setTimeout(resolve, 50));
35
+ }
@@ -0,0 +1,20 @@
1
+ import { Worker } from "node:worker_threads";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, resolve } from "node:path";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export async function createWorker() {
8
+ // Reference the worker by path relative to this file at runtime,
9
+ // not via import.meta.url which Vite will try to bundle/inline.
10
+ const workerPath = resolve(__dirname, "./wasmWorker.js");
11
+
12
+ const worker = new Worker(workerPath);
13
+
14
+ return {
15
+ postMessage: (msg) => worker.postMessage(msg),
16
+ onMessage: (fn) => worker.on("message", fn),
17
+ onError: (fn) => worker.on("error", fn),
18
+ terminate: () => worker.terminate(),
19
+ };
20
+ }
@@ -17,6 +17,8 @@
17
17
  * This allows more granular control.
18
18
  */
19
19
 
20
+ import { createWorker } from "@__TARGET__/worker.js";
21
+
20
22
  /**
21
23
  * Worker instance that handles communication with the WASM module.
22
24
  * @type {Worker | null}
@@ -42,6 +44,22 @@ const callbacks = new Map();
42
44
  */
43
45
  let initialized = false;
44
46
 
47
+ /**
48
+ * Handle response messages from the WASM worker.
49
+ * @param {{ id: number, output?: any, returnValue?: any, error?: string }} data
50
+ */
51
+ function handleMessage(data) {
52
+ const cb = callbacks.get(data.id);
53
+ if (!cb) return;
54
+ callbacks.delete(data.id);
55
+
56
+ if (data.error) {
57
+ cb.reject(new Error(data.error));
58
+ } else {
59
+ cb.resolve({ output: data.output, returnValue: data.returnValue });
60
+ }
61
+ }
62
+
45
63
  /**
46
64
  * @summary Initialize the WASM worker.
47
65
  *
@@ -59,27 +77,21 @@ let initialized = false;
59
77
  *
60
78
  * @since 0.0.0
61
79
  */
62
- export function initWasmWorker() {
63
- if (initialized) return;
64
-
65
- worker = new Worker(new URL("./wasmWorker.js", import.meta.url), { type: "module" });
66
80
 
67
- worker.onmessage = ({ data }) => {
68
- const { id, error, output, returnValue } = data;
69
- const cb = callbacks.get(id);
70
- if (!cb) return;
81
+ export async function initWasmWorker() {
82
+ if (initialized) return;
71
83
 
72
- error ? cb.reject(new Error(error)) : cb.resolve({ output, returnValue });
73
- callbacks.delete(id);
74
- };
84
+ worker = await createWorker();
75
85
 
76
- worker.onerror = (event) => {
77
- const err = new Error(event.message || "WASM worker error");
86
+ worker.onMessage(handleMessage);
87
+ worker.onError((event) => {
88
+ const output = event.message || "WASM worker error";
89
+ const err = new Error(`[Img2Num wasmClient] Error: ${output}`);
78
90
  for (const [_id, cb] of callbacks) {
79
91
  cb.reject(err);
80
92
  }
81
93
  callbacks.clear();
82
- };
94
+ });
83
95
 
84
96
  initialized = true;
85
97
  }
@@ -120,10 +132,13 @@ export function initWasmWorker() {
120
132
  */
121
133
  export async function callWasm({ funcName, args = {}, bufferKeys = [], returnType = "void" }) {
122
134
  if (!initialized) throw new Error("WASM worker not initialized. Call initWasmWorker() first.");
135
+
123
136
  const id = idCounter++;
137
+
124
138
  return new Promise((resolve, reject) => {
125
139
  callbacks.set(id, { resolve, reject });
126
- worker.postMessage({ id, funcName, args, bufferKeys, returnType }); // <-- send it
140
+
141
+ worker.postMessage({ id, funcName, args, bufferKeys, returnType });
127
142
  });
128
143
  }
129
144