@wasmedia/wenblend 0.1.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.
- package/LICENSE +367 -0
- package/LICENSE-GPL-3.0 +674 -0
- package/README.md +98 -0
- package/dist/cli.d.ts +27 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +167 -0
- package/dist/cli.js.map +1 -0
- package/dist/filesystem.d.ts +25 -0
- package/dist/filesystem.d.ts.map +1 -0
- package/dist/filesystem.js +75 -0
- package/dist/filesystem.js.map +1 -0
- package/dist/handler.d.ts +62 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +91 -0
- package/dist/handler.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +74 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +181 -0
- package/dist/loader.js.map +1 -0
- package/dist/operations.d.ts +45 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -0
- package/dist/staging.d.ts +37 -0
- package/dist/staging.d.ts.map +1 -0
- package/dist/staging.js +157 -0
- package/dist/staging.js.map +1 -0
- package/dist/tiff-header.d.ts +16 -0
- package/dist/tiff-header.d.ts.map +1 -0
- package/dist/tiff-header.js +97 -0
- package/dist/tiff-header.js.map +1 -0
- package/dist/types.d.ts +128 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +47 -0
- package/dist/types.js.map +1 -0
- package/dist/wenblend.js +2 -0
- package/dist/wenblend.wasm +0 -0
- package/dist/wenfuse.js +2 -0
- package/dist/wenfuse.wasm +0 -0
- package/dist/wopfs.d.ts +37 -0
- package/dist/wopfs.d.ts.map +1 -0
- package/dist/wopfs.js +38 -0
- package/dist/wopfs.js.map +1 -0
- package/dist/worker.d.ts +16 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +29 -0
- package/dist/worker.js.map +1 -0
- package/package.json +67 -0
- package/src/cli.ts +201 -0
- package/src/filesystem.ts +85 -0
- package/src/handler.ts +112 -0
- package/src/index.ts +41 -0
- package/src/loader.ts +223 -0
- package/src/operations.ts +147 -0
- package/src/staging.ts +173 -0
- package/src/tiff-header.ts +94 -0
- package/src/types.ts +153 -0
- package/src/wopfs.ts +70 -0
- package/src/worker.ts +31 -0
- package/types/wasm.d.ts +12 -0
package/src/staging.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run's files reach the tool and come back.
|
|
3
|
+
*
|
|
4
|
+
* runTool() has one flow: stage, callMain, collect, release. This interface
|
|
5
|
+
* is what varies. stageBuffers is the MEMFS path every caller used before file
|
|
6
|
+
* mode existed, and its behaviour is unchanged. stageFiles (file mode) mounts
|
|
7
|
+
* files through wopfs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { FileInput, InputImage, LoaderOptions, OutputDir, WasmModule, WrittenFile } from "./types.js";
|
|
11
|
+
import { createWorkDir, stageInputs, collectOutputs, cleanWorkDir, nextWorkDirName, toBytes } from "./filesystem.js";
|
|
12
|
+
import { tiffPixelCount, tiffPixelCountAt, type ReadAt } from "./tiff-header.js";
|
|
13
|
+
import { loadWopfs, type WopfsCommittedFile } from "./wopfs.js";
|
|
14
|
+
|
|
15
|
+
export interface Staging {
|
|
16
|
+
/** The directory the tool runs in (its cwd). */
|
|
17
|
+
readonly workDir: string;
|
|
18
|
+
readonly inputNames: ReadonlySet<string>;
|
|
19
|
+
/** Everything the run created. Buffer mode: bytes. File mode: committed files. */
|
|
20
|
+
collect(): Promise<Map<string, Uint8Array | WrittenFile>>;
|
|
21
|
+
/** Total input pixels, for OutOfMemoryError. Callable after release(). */
|
|
22
|
+
pixelCount(): Promise<number | undefined>;
|
|
23
|
+
/** Idempotent. runTool calls it in a finally on every path. */
|
|
24
|
+
release(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function stageBuffers(module: WasmModule, inputs: InputImage[]): Staging {
|
|
28
|
+
const workDir = createWorkDir(module);
|
|
29
|
+
const inputNames = stageInputs(module, workDir, inputs);
|
|
30
|
+
let released = false;
|
|
31
|
+
return {
|
|
32
|
+
workDir,
|
|
33
|
+
inputNames,
|
|
34
|
+
async collect() {
|
|
35
|
+
return collectOutputs(module, workDir, inputNames);
|
|
36
|
+
},
|
|
37
|
+
async pixelCount() {
|
|
38
|
+
// With no inputs no header states a count, so none is known; 0 would be a claim.
|
|
39
|
+
if (inputs.length === 0) return undefined;
|
|
40
|
+
let total = 0;
|
|
41
|
+
for (const input of inputs) {
|
|
42
|
+
const n = tiffPixelCount(toBytes(input.data));
|
|
43
|
+
if (n === undefined) return undefined;
|
|
44
|
+
total += n;
|
|
45
|
+
}
|
|
46
|
+
return total;
|
|
47
|
+
},
|
|
48
|
+
async release() {
|
|
49
|
+
if (released) return;
|
|
50
|
+
released = true;
|
|
51
|
+
// The instance is discarded after this call (never reused, per
|
|
52
|
+
// createInstance's contract), so there is no previous cwd to restore.
|
|
53
|
+
cleanWorkDir(module, workDir);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** True for `{ name, file }`, the file-mode input shape. */
|
|
59
|
+
export function isFileInput(x: unknown): x is FileInput {
|
|
60
|
+
return typeof x === "object" && x !== null && "file" in x && "name" in x && !("data" in x);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// getopt_long accepts any unambiguous prefix of a long option. Over-counting
|
|
64
|
+
// costs an empty pre-opened file; under-counting fails the run, so a prefix
|
|
65
|
+
// match errs on the generous side.
|
|
66
|
+
const longName = (arg: string) => (arg.startsWith("--") ? arg.slice(2).split("=", 1)[0]! : "");
|
|
67
|
+
const names = (argv: readonly string[], option: string) =>
|
|
68
|
+
argv.some((arg) => { const n = longName(arg); return n.length >= 2 && option.startsWith(n); });
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How many files a run can create, from its argv: the -o output, plus
|
|
72
|
+
* --visualize's image, plus a soft and a hard mask per input for
|
|
73
|
+
* --save-mask(s). enblend and enfuse create no temporary files (spec,
|
|
74
|
+
* finding 2). --load-mask(s) reads, so it adds nothing here.
|
|
75
|
+
*/
|
|
76
|
+
export function scratchFor(argv: readonly string[], inputCount: number): number {
|
|
77
|
+
let n = 1;
|
|
78
|
+
if (names(argv, "visualize")) n += 1;
|
|
79
|
+
if (names(argv, "save-masks")) n += 2 * inputCount;
|
|
80
|
+
return n;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A reader for the pixel count: a byte range of a file, without reading the rest. */
|
|
84
|
+
function readerFor(file: FileSystemFileHandle | string): ReadAt {
|
|
85
|
+
if (typeof file === "string") {
|
|
86
|
+
return async (offset, length) => {
|
|
87
|
+
// A variable specifier: the browser build never resolves node:fs.
|
|
88
|
+
const specifier = "node:fs/promises";
|
|
89
|
+
const { open } = await import(/* @vite-ignore */ specifier);
|
|
90
|
+
const handle = await open(file, "r");
|
|
91
|
+
try {
|
|
92
|
+
const buffer = new Uint8Array(length);
|
|
93
|
+
const { bytesRead } = await handle.read(buffer, 0, length, offset);
|
|
94
|
+
return buffer.subarray(0, bytesRead);
|
|
95
|
+
} finally {
|
|
96
|
+
await handle.close();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return async (offset, length) =>
|
|
101
|
+
new Uint8Array(await (await file.getFile()).slice(offset, offset + length).arrayBuffer());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Turn what wopfs committed into a WrittenFile. On the main thread wopfs
|
|
106
|
+
* falls back to MEMFS, which returns bytes and ignores outputDir. Write those
|
|
107
|
+
* into outputDir here, so file mode hands back a file in every environment.
|
|
108
|
+
*/
|
|
109
|
+
async function toWrittenFile(c: WopfsCommittedFile, outputDir: OutputDir): Promise<WrittenFile> {
|
|
110
|
+
if (c.handle) return { name: c.path, file: c.handle, size: c.size };
|
|
111
|
+
if (c.hostPath) return { name: c.path, file: c.hostPath, size: c.size };
|
|
112
|
+
if (c.bytes && typeof outputDir !== "string") {
|
|
113
|
+
// Check before creating the file, so a failure leaves no empty file behind.
|
|
114
|
+
if (typeof FileSystemFileHandle === "undefined" || !("createWritable" in FileSystemFileHandle.prototype)) {
|
|
115
|
+
throw new Error("file mode on this thread needs FileSystemFileHandle.createWritable; run it in a dedicated worker instead");
|
|
116
|
+
}
|
|
117
|
+
const handle = await outputDir.getFileHandle(c.path, { create: true });
|
|
118
|
+
const writable = await handle.createWritable();
|
|
119
|
+
await writable.write(c.bytes as Uint8Array<ArrayBuffer>);
|
|
120
|
+
await writable.close();
|
|
121
|
+
return { name: c.path, file: handle, size: c.bytes.length };
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`wopfs committed "${c.path}" with neither a file nor bytes`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Mount the run's files through wopfs. Everything is opened here, before
|
|
128
|
+
* callMain, because the tool cannot await. The pool is strict: spilling
|
|
129
|
+
* would put new files back in memory and defeat the point (spec, Decisions).
|
|
130
|
+
*/
|
|
131
|
+
export async function stageFiles(
|
|
132
|
+
module: WasmModule,
|
|
133
|
+
inputs: FileInput[],
|
|
134
|
+
outputDir: OutputDir,
|
|
135
|
+
argv: readonly string[],
|
|
136
|
+
options?: LoaderOptions,
|
|
137
|
+
): Promise<Staging> {
|
|
138
|
+
const wopfs = await loadWopfs(options);
|
|
139
|
+
const workDir = nextWorkDirName();
|
|
140
|
+
const mount = await wopfs.mountFiles(module, workDir, {
|
|
141
|
+
inputs: Object.fromEntries(inputs.map((i) => [i.name, i.file])),
|
|
142
|
+
outputDir,
|
|
143
|
+
scratch: scratchFor(argv, inputs.length),
|
|
144
|
+
onExhausted: "strict",
|
|
145
|
+
});
|
|
146
|
+
let released = false;
|
|
147
|
+
return {
|
|
148
|
+
workDir,
|
|
149
|
+
inputNames: new Set(inputs.map((i) => i.name)),
|
|
150
|
+
async collect() {
|
|
151
|
+
const out = new Map<string, Uint8Array | WrittenFile>();
|
|
152
|
+
for (const c of await mount.commit()) out.set(c.path, await toWrittenFile(c, outputDir));
|
|
153
|
+
return out;
|
|
154
|
+
},
|
|
155
|
+
async pixelCount() {
|
|
156
|
+
if (inputs.length === 0) return undefined;
|
|
157
|
+
let total = 0;
|
|
158
|
+
for (const input of inputs) {
|
|
159
|
+
const n = await tiffPixelCountAt(readerFor(input.file));
|
|
160
|
+
if (n === undefined) return undefined;
|
|
161
|
+
total += n;
|
|
162
|
+
}
|
|
163
|
+
return total;
|
|
164
|
+
},
|
|
165
|
+
async release() {
|
|
166
|
+
if (released) return;
|
|
167
|
+
released = true;
|
|
168
|
+
// runTool ran the tool inside the mount; wopfs >= 0.2.0 moves the cwd
|
|
169
|
+
// out before unmounting (https://gitlab.com/wasmedia/wopfs/-/work_items/2).
|
|
170
|
+
await mount.release();
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pixel count of a TIFF, read from its header and image directories only.
|
|
3
|
+
*
|
|
4
|
+
* One walker serves both callers: buffer mode, which has the whole file's
|
|
5
|
+
* bytes, and file mode, where the file sits in OPFS or on disk and only the
|
|
6
|
+
* few hundred bytes the walk needs should be read. The walker is a generator
|
|
7
|
+
* that yields the byte ranges it needs; tiffPixelCount drives it from a byte
|
|
8
|
+
* array, and tiffPixelCountAt from an async reader.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type Need = { offset: number; length: number };
|
|
12
|
+
|
|
13
|
+
/** A random-access reader. A short or empty result means past the end. */
|
|
14
|
+
export type ReadAt = (offset: number, length: number) => Promise<Uint8Array>;
|
|
15
|
+
|
|
16
|
+
const view = (b: Uint8Array) => new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sum of width x height over every image directory, from the ImageWidth and
|
|
20
|
+
* ImageLength tags. Undefined for anything that cannot be read with certainty:
|
|
21
|
+
* not a classic TIFF (BigTIFF included), truncated, a cycle, or a directory
|
|
22
|
+
* missing either tag. Never a guess.
|
|
23
|
+
*/
|
|
24
|
+
function* walk(): Generator<Need, number | undefined, Uint8Array | undefined> {
|
|
25
|
+
const head = yield { offset: 0, length: 8 };
|
|
26
|
+
if (!head || head.length < 8) return undefined;
|
|
27
|
+
const hv = view(head);
|
|
28
|
+
const order = hv.getUint16(0);
|
|
29
|
+
if (order !== 0x4949 && order !== 0x4d4d) return undefined;
|
|
30
|
+
const le = order === 0x4949;
|
|
31
|
+
if (hv.getUint16(2, le) !== 42) return undefined;
|
|
32
|
+
let offset = hv.getUint32(4, le);
|
|
33
|
+
let total = 0;
|
|
34
|
+
const visited = new Set<number>();
|
|
35
|
+
while (offset !== 0) {
|
|
36
|
+
if (visited.has(offset)) return undefined;
|
|
37
|
+
visited.add(offset);
|
|
38
|
+
const countBytes = yield { offset, length: 2 };
|
|
39
|
+
if (!countBytes || countBytes.length < 2) return undefined;
|
|
40
|
+
const count = view(countBytes).getUint16(0, le);
|
|
41
|
+
const size = count * 12 + 4;
|
|
42
|
+
const dir = yield { offset: offset + 2, length: size };
|
|
43
|
+
if (!dir || dir.length < size) return undefined;
|
|
44
|
+
const dv = view(dir);
|
|
45
|
+
let width: number | undefined;
|
|
46
|
+
let height: number | undefined;
|
|
47
|
+
for (let i = 0; i < count; i++) {
|
|
48
|
+
const entry = i * 12;
|
|
49
|
+
const tag = dv.getUint16(entry, le);
|
|
50
|
+
if (tag !== 256 && tag !== 257) continue;
|
|
51
|
+
const type = dv.getUint16(entry + 2, le);
|
|
52
|
+
// SHORT (3) or LONG (4), exactly one value, stored in the entry itself.
|
|
53
|
+
// Any other count is malformed; reading it as one value would misreport.
|
|
54
|
+
const values = dv.getUint32(entry + 4, le);
|
|
55
|
+
const value = values !== 1 ? undefined
|
|
56
|
+
: type === 3 ? dv.getUint16(entry + 8, le)
|
|
57
|
+
: type === 4 ? dv.getUint32(entry + 8, le)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (tag === 256) width = value;
|
|
60
|
+
else height = value;
|
|
61
|
+
}
|
|
62
|
+
if (width === undefined || height === undefined) return undefined;
|
|
63
|
+
total += width * height;
|
|
64
|
+
offset = dv.getUint32(count * 12, le);
|
|
65
|
+
}
|
|
66
|
+
return total;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The pixel count of a TIFF held in memory. */
|
|
70
|
+
export function tiffPixelCount(bytes: Uint8Array): number | undefined {
|
|
71
|
+
const it = walk();
|
|
72
|
+
let step = it.next();
|
|
73
|
+
while (!step.done) {
|
|
74
|
+
const { offset, length } = step.value;
|
|
75
|
+
step = it.next(bytes.subarray(offset, offset + length));
|
|
76
|
+
}
|
|
77
|
+
return step.value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The pixel count of a TIFF read through `readAt`, fetching only what the walk needs. */
|
|
81
|
+
export async function tiffPixelCountAt(readAt: ReadAt): Promise<number | undefined> {
|
|
82
|
+
const it = walk();
|
|
83
|
+
let step = it.next();
|
|
84
|
+
while (!step.done) {
|
|
85
|
+
let chunk: Uint8Array | undefined;
|
|
86
|
+
try {
|
|
87
|
+
chunk = await readAt(step.value.offset, step.value.length);
|
|
88
|
+
} catch {
|
|
89
|
+
chunk = undefined;
|
|
90
|
+
}
|
|
91
|
+
step = it.next(chunk);
|
|
92
|
+
}
|
|
93
|
+
return step.value;
|
|
94
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for wenblend.
|
|
3
|
+
*
|
|
4
|
+
* Only three C symbols are exported by the wasm modules (`_malloc`,
|
|
5
|
+
* `_free`, `_main`), so `WasmModule` describes the Emscripten runtime
|
|
6
|
+
* surface rather than a bound C API.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Emscripten MEMFS node stat. */
|
|
10
|
+
export interface FSStat {
|
|
11
|
+
size: number;
|
|
12
|
+
mode: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** The subset of Emscripten's FS we use. */
|
|
16
|
+
export interface EmscriptenFS {
|
|
17
|
+
mkdir(path: string): void;
|
|
18
|
+
chdir(path: string): void;
|
|
19
|
+
cwd(): string;
|
|
20
|
+
writeFile(path: string, data: Uint8Array): void;
|
|
21
|
+
readFile(path: string, opts?: { encoding?: "binary" | "utf8" }): Uint8Array;
|
|
22
|
+
readdir(path: string): string[];
|
|
23
|
+
unlink(path: string): void;
|
|
24
|
+
rmdir(path: string): void;
|
|
25
|
+
stat(path: string): FSStat;
|
|
26
|
+
analyzePath(path: string): { exists: boolean };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The Emscripten module surface this wrapper relies on. */
|
|
30
|
+
export interface WasmModule {
|
|
31
|
+
FS: EmscriptenFS;
|
|
32
|
+
callMain(argv: string[]): number | undefined;
|
|
33
|
+
print?: (text: string) => void;
|
|
34
|
+
printErr?: (text: string) => void;
|
|
35
|
+
_malloc(size: number): number;
|
|
36
|
+
_free(ptr: number): void;
|
|
37
|
+
HEAPU8: Uint8Array;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A named input image. Callers control the names their argv references. */
|
|
41
|
+
export interface InputImage {
|
|
42
|
+
name: string;
|
|
43
|
+
data: Uint8Array | ArrayBuffer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The result of one CLI invocation. */
|
|
47
|
+
export interface CLIResult {
|
|
48
|
+
exitCode: number;
|
|
49
|
+
stdout: string;
|
|
50
|
+
stderr: string;
|
|
51
|
+
/** Files present in the work directory that were not inputs. */
|
|
52
|
+
outputFiles: Map<string, Uint8Array>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A file a file-mode run committed to its output directory. */
|
|
56
|
+
export interface WrittenFile {
|
|
57
|
+
name: string;
|
|
58
|
+
/** The committed file: an OPFS handle in a browser, a host path under Node. */
|
|
59
|
+
file: FileSystemFileHandle | string;
|
|
60
|
+
size: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** An input that stays in a file: an OPFS handle in a browser, a host path under Node. */
|
|
64
|
+
export interface FileInput {
|
|
65
|
+
name: string;
|
|
66
|
+
file: FileSystemFileHandle | string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Where file mode commits outputs: an OPFS directory, or a host directory under Node. */
|
|
70
|
+
export type OutputDir = FileSystemDirectoryHandle | string;
|
|
71
|
+
|
|
72
|
+
/** A file-mode CLI result: outputs are committed files, not bytes. */
|
|
73
|
+
export interface CLIFileResult {
|
|
74
|
+
exitCode: number;
|
|
75
|
+
stdout: string;
|
|
76
|
+
stderr: string;
|
|
77
|
+
/** Committed outputs by name. Empty unless the tool exited 0. */
|
|
78
|
+
written: Map<string, WrittenFile>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type FileModeOptions = LoaderOptions & { outputDir: OutputDir };
|
|
82
|
+
|
|
83
|
+
/** Options common to both tools. */
|
|
84
|
+
export interface CommonOptions {
|
|
85
|
+
/** Output filename inside MEMFS. Defaults to `output.tif`. */
|
|
86
|
+
output?: string;
|
|
87
|
+
/** Extra argv appended verbatim, exactly as typed on a command line. */
|
|
88
|
+
args?: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type BlendOptions = CommonOptions;
|
|
92
|
+
export type FuseOptions = CommonOptions;
|
|
93
|
+
|
|
94
|
+
/** Progress phases reported by the loader. */
|
|
95
|
+
export type LoadPhase = "loading" | "initializing" | "ready";
|
|
96
|
+
|
|
97
|
+
export interface LoaderOptions {
|
|
98
|
+
/** Override the module URL (for bundlers or custom hosting). */
|
|
99
|
+
wasmPath?: string;
|
|
100
|
+
/**
|
|
101
|
+
* Where file mode loads @wasmedia/wopfs from. The default imports
|
|
102
|
+
* "@wasmedia/wopfs" itself, which works under Node and with a bundler. A
|
|
103
|
+
* browser without a bundler (plain ES modules, workers) passes the URL of
|
|
104
|
+
* wopfs's dist/index.js.
|
|
105
|
+
*/
|
|
106
|
+
wopfsPath?: string;
|
|
107
|
+
onProgress?: (phase: LoadPhase) => void;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** A non-zero exit from enblend or enfuse. Carries the tool's own diagnostics. */
|
|
111
|
+
export class EnblendError extends Error {
|
|
112
|
+
readonly exitCode: number;
|
|
113
|
+
readonly stderr: string;
|
|
114
|
+
|
|
115
|
+
constructor(message: string, exitCode: number, stderr: string) {
|
|
116
|
+
super(message);
|
|
117
|
+
this.name = "EnblendError";
|
|
118
|
+
this.exitCode = exitCode;
|
|
119
|
+
this.stderr = stderr;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Allocation failure.
|
|
125
|
+
*
|
|
126
|
+
* Nothing bounds memory in this port: enblend's image cache was removed
|
|
127
|
+
* upstream in 4.2.1, so there is no spill-to-disk and no graceful
|
|
128
|
+
* degradation. `ALLOW_MEMORY_GROWTH=1` with `MAXIMUM_MEMORY=4GB` is the
|
|
129
|
+
* only ceiling and browsers allocate less in practice. This is the
|
|
130
|
+
* runtime signal for the documented ceiling in `docs/building.md`.
|
|
131
|
+
*/
|
|
132
|
+
export class OutOfMemoryError extends Error {
|
|
133
|
+
/**
|
|
134
|
+
* Total input pixels, read from the inputs' TIFF headers (every image
|
|
135
|
+
* directory of every input). Undefined when any input's header could not
|
|
136
|
+
* be read with certainty; it is never estimated from byte counts, which
|
|
137
|
+
* for compressed TIFFs can be off several-fold.
|
|
138
|
+
*/
|
|
139
|
+
readonly pixels: number | undefined;
|
|
140
|
+
/** The tool's stderr, including its own `out of memory` line. */
|
|
141
|
+
readonly stderr: string;
|
|
142
|
+
|
|
143
|
+
constructor(pixels: number | undefined, stderr = "") {
|
|
144
|
+
super(
|
|
145
|
+
`Out of memory${pixels === undefined ? "" : ` blending ${pixels} input pixels`}. ` +
|
|
146
|
+
`This port has no image cache, so memory scales with total input area. ` +
|
|
147
|
+
`See the measured ceiling in docs/building.md.`,
|
|
148
|
+
);
|
|
149
|
+
this.name = "OutOfMemoryError";
|
|
150
|
+
this.pixels = pixels;
|
|
151
|
+
this.stderr = stderr;
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/wopfs.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of @wasmedia/wopfs that file mode uses, and its lazy loader.
|
|
3
|
+
*
|
|
4
|
+
* The types are declared here structurally rather than imported, so a
|
|
5
|
+
* consumer who never uses file mode needs neither the package nor its type
|
|
6
|
+
* declarations. Nothing else in src/ imports wopfs; the package is loaded
|
|
7
|
+
* on the first file-mode call, the way each tool's module is loaded on its
|
|
8
|
+
* first use.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { LoaderOptions } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export interface WopfsCommittedFile {
|
|
14
|
+
path: string;
|
|
15
|
+
size: number;
|
|
16
|
+
handle?: FileSystemFileHandle;
|
|
17
|
+
hostPath?: string;
|
|
18
|
+
bytes?: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface WopfsMount {
|
|
22
|
+
readonly backend: "opfs" | "node" | "memfs";
|
|
23
|
+
commit(): Promise<WopfsCommittedFile[]>;
|
|
24
|
+
release(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WopfsMountOptions {
|
|
28
|
+
inputs?: Record<string, FileSystemFileHandle | Uint8Array | Blob | string>;
|
|
29
|
+
outputDir?: FileSystemDirectoryHandle | string;
|
|
30
|
+
scratch?: number;
|
|
31
|
+
onExhausted?: "spill" | "strict";
|
|
32
|
+
backend?: "opfs" | "node" | "memfs" | "auto";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WopfsModule {
|
|
36
|
+
mountFiles(module: unknown, mountpoint: string, options?: WopfsMountOptions): Promise<WopfsMount>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const DEFAULT_SPECIFIER = "@wasmedia/wopfs";
|
|
40
|
+
|
|
41
|
+
// The default is a LITERAL import so a bundler resolves and code-splits it (it
|
|
42
|
+
// stays lazy: nothing evaluates it until the first file-mode call) and Node
|
|
43
|
+
// resolves it from node_modules. An explicit wopfsPath is a URL the bundler
|
|
44
|
+
// cannot know, so it goes through a variable specifier.
|
|
45
|
+
let importer: (specifier: string) => Promise<WopfsModule> = (specifier) =>
|
|
46
|
+
(specifier === DEFAULT_SPECIFIER
|
|
47
|
+
? import("@wasmedia/wopfs")
|
|
48
|
+
: import(/* @vite-ignore */ specifier)) as Promise<WopfsModule>;
|
|
49
|
+
const cache = new Map<string, Promise<WopfsModule>>();
|
|
50
|
+
|
|
51
|
+
/** Load wopfs once per specifier. A failed load is not cached, so a retry can succeed. */
|
|
52
|
+
export function loadWopfs(options?: LoaderOptions): Promise<WopfsModule> {
|
|
53
|
+
const specifier = options?.wopfsPath ?? DEFAULT_SPECIFIER;
|
|
54
|
+
let loading = cache.get(specifier);
|
|
55
|
+
if (!loading) {
|
|
56
|
+
loading = importer(specifier).catch((e: unknown) => {
|
|
57
|
+
cache.delete(specifier);
|
|
58
|
+
const why = e instanceof Error ? e.message : String(e);
|
|
59
|
+
throw new Error(`file mode needs @wasmedia/wopfs, which could not be loaded from "${specifier}": ${why}`);
|
|
60
|
+
});
|
|
61
|
+
cache.set(specifier, loading);
|
|
62
|
+
}
|
|
63
|
+
return loading;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Test seam: replace the importer. Clears the cache. */
|
|
67
|
+
export function __setWopfsLoader(fn: (specifier: string) => Promise<WopfsModule>): void {
|
|
68
|
+
importer = fn;
|
|
69
|
+
cache.clear();
|
|
70
|
+
}
|
package/src/worker.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dedicated Web Worker entry — the package's `./worker` export.
|
|
3
|
+
*
|
|
4
|
+
* Blending is CPU-bound and single-threaded (OpenMP is off), so it
|
|
5
|
+
* blocks whatever thread it runs on. Running it in a worker keeps the
|
|
6
|
+
* page responsive.
|
|
7
|
+
*
|
|
8
|
+
* This is the ONLY module in the package with an import-time side
|
|
9
|
+
* effect: loaded inside a worker, it registers a `message` listener.
|
|
10
|
+
* The main entry (`src/index.ts`) re-exports the handler from
|
|
11
|
+
* `./handler.js`, never from here, so importing the library inside a
|
|
12
|
+
* consumer's own worker attaches nothing. package.json's `sideEffects`
|
|
13
|
+
* field names this file alone so bundlers know the difference.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { handleMessage, isWorkerScope, type WorkerRequest } from "./handler.js";
|
|
17
|
+
|
|
18
|
+
export { handleMessage, isWorkerScope, type WorkerRequest, type WorkerResponse } from "./handler.js";
|
|
19
|
+
|
|
20
|
+
// Only register when actually running inside a worker, so importing this
|
|
21
|
+
// entry in a test or on a main thread is harmless.
|
|
22
|
+
if (isWorkerScope()) {
|
|
23
|
+
self.addEventListener("message", (event: MessageEvent<WorkerRequest>) => {
|
|
24
|
+
void handleMessage(event.data).then((response) => {
|
|
25
|
+
// Transfer the output buffer rather than copying it (buffer mode only).
|
|
26
|
+
const transfer = response.result && response.result instanceof Uint8Array ? [response.result.buffer] : [];
|
|
27
|
+
(self as unknown as { postMessage: (m: unknown, t: Transferable[]) => void })
|
|
28
|
+
.postMessage(response, transfer as Transferable[]);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
package/types/wasm.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the raw Emscripten glue, exported as `@wasmedia/wenblend/wasm/enblend`
|
|
3
|
+
* and `@wasmedia/wenblend/wasm/enfuse`. Each default-exports its tool's module
|
|
4
|
+
* factory. Prefer `createInstance()`, which also supplies the cached compiled
|
|
5
|
+
* module; calling the factory directly compiles the .wasm on every call, and
|
|
6
|
+
* `print`/`printErr` must be passed in the config object (assigning them after
|
|
7
|
+
* construction has no effect).
|
|
8
|
+
*/
|
|
9
|
+
import type { ModuleFactory } from "../dist/loader.js";
|
|
10
|
+
|
|
11
|
+
declare const factory: ModuleFactory;
|
|
12
|
+
export default factory;
|