format-png 0.2.0 → 0.3.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 @@
1
+ export {};
package/dist/worker.js ADDED
@@ -0,0 +1,137 @@
1
+ // A WorkerPool worker: runs one task at a time with the sync API. It keeps its
2
+ // decoders and encoders between tasks, so a batch keeps the crate's buffer
3
+ // reuse, and the images it prepared until they're finished or freed. See
4
+ // protocol.ts for the messages.
5
+ import { listen, post } from "#port";
6
+ import { PngDecoder, PngEncoder, PngError, compressSegment, finish, init, parseText, prepare, readHeader, rgbaToPngImage, } from "./core.js";
7
+ import { buffersIn } from "./protocol.js";
8
+ /** The most recently used decoders or encoders, one per set of options. */
9
+ class Cache {
10
+ #entries = new Map();
11
+ get(key, create) {
12
+ let entry = this.#entries.get(key);
13
+ if (entry) {
14
+ this.#entries.delete(key); // moved to the end: most recently used
15
+ }
16
+ else {
17
+ entry = create();
18
+ if (this.#entries.size === 4) {
19
+ const [oldest, value] = this.#entries.entries().next().value;
20
+ this.#entries.delete(oldest);
21
+ value.free();
22
+ }
23
+ }
24
+ this.#entries.set(key, entry);
25
+ return entry;
26
+ }
27
+ }
28
+ const decoders = new Cache();
29
+ const encoders = new Cache();
30
+ let ready;
31
+ let loaded = false;
32
+ function decoder(options) {
33
+ const { validateCrc = true, preserveMetadata = false, preserveChunks = false, strictAncillary = false } = options;
34
+ const key = [validateCrc, preserveMetadata, preserveChunks, strictAncillary].join();
35
+ return decoders.get(key, () => new PngDecoder({ validateCrc, preserveMetadata, preserveChunks, strictAncillary }));
36
+ }
37
+ function encoder(options) {
38
+ const { compression, compressionStrategy, filter, keepUnsafeChunks, palette, strip, threads } = options;
39
+ const normalized = { compression, compressionStrategy, filter, keepUnsafeChunks, palette, strip, threads };
40
+ return encoders.get(JSON.stringify(normalized), () => new PngEncoder(normalized));
41
+ }
42
+ /** The images this worker prepared, by handle, until they're finished or freed. */
43
+ const handles = new Map();
44
+ /** Prepares `image`, keeping it as `handle` if it has segments to compress. */
45
+ function prepareImage(image, options, handle) {
46
+ const prepared = prepare(encoder(options), image);
47
+ try {
48
+ if (prepared.segmentCount === 0)
49
+ return { png: finish(prepared, []) };
50
+ const segments = Array.from({ length: prepared.segmentCount }, (_, i) => prepared.segment(i));
51
+ const result = { segments, level: prepared.compressionLevel, strategy: prepared.compressionStrategy };
52
+ handles.set(handle, prepared);
53
+ return result;
54
+ }
55
+ finally {
56
+ if (!handles.has(handle))
57
+ prepared.free();
58
+ }
59
+ }
60
+ /** Takes the prepared image `handle` out of `handles`, so it's freed whatever happens next. */
61
+ function takeHandle(handle) {
62
+ const prepared = handles.get(handle);
63
+ if (!prepared)
64
+ throw new Error(`format-png: no prepared image ${handle} in this worker`);
65
+ handles.delete(handle);
66
+ return prepared;
67
+ }
68
+ /** The chunks' data are views into the input; they're sent as lengths and rebuilt on the other side. */
69
+ function withoutChunkData(chunks) {
70
+ return { ...chunks, chunks: chunks.chunks.map((chunk) => ({ ...chunk, data: chunk.data.byteLength })) };
71
+ }
72
+ function run(request) {
73
+ const { op, args } = request;
74
+ switch (op) {
75
+ case "decode":
76
+ return decoder(args[1]).decode(args[0]);
77
+ case "decodeRgba8":
78
+ return decoder(args[1]).decodeRgba8(args[0]);
79
+ case "readChunks": {
80
+ const { validateCrc, strictAncillary } = args[1];
81
+ const chunks = withoutChunkData(decoder({ validateCrc, strictAncillary }).readChunks(args[0]));
82
+ return request.returnInput ? { chunks, input: args[0] } : { chunks };
83
+ }
84
+ case "readHeader":
85
+ return readHeader(args[0]);
86
+ case "parseText":
87
+ return parseText(args[0], args[1]);
88
+ case "encode":
89
+ case "encodeRgba8": {
90
+ const image = op === "encode" ? args[0] : rgbaToPngImage(args[0]);
91
+ const options = args[1];
92
+ if (request.split)
93
+ return prepareImage(image, options, request.handle);
94
+ return encoder(options).encode(image);
95
+ }
96
+ case "compressSegment":
97
+ return compressSegment(args[0], args[1], args[2]);
98
+ case "finish": {
99
+ const prepared = takeHandle(args[0]);
100
+ try {
101
+ return finish(prepared, args[1]);
102
+ }
103
+ finally {
104
+ prepared.free();
105
+ }
106
+ }
107
+ case "free":
108
+ takeHandle(args[0]).free();
109
+ return null;
110
+ case "handles":
111
+ return handles.size;
112
+ }
113
+ }
114
+ listen(async (request) => {
115
+ let response;
116
+ try {
117
+ await (ready ??= init());
118
+ loaded = true;
119
+ const value = run(request);
120
+ post({ id: request.id, ok: true, value }, buffersIn(value));
121
+ return;
122
+ }
123
+ catch (error) {
124
+ // A trap (a Rust panic, or running out of memory) can leave the wasm
125
+ // instance broken, and a worker whose module didn't load is no use, so
126
+ // the pool replaces this worker.
127
+ const cause = error instanceof PngError ? error.cause : error;
128
+ const retire = cause instanceof WebAssembly.RuntimeError || !loaded;
129
+ response = {
130
+ id: request.id,
131
+ ok: false,
132
+ error: error instanceof Error ? { name: error.name, message: error.message } : { name: "Error", message: String(error) },
133
+ retire,
134
+ };
135
+ }
136
+ post(response, []);
137
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "format-png",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A fast, lossless PNG decoder and encoder for the browser and Node, compiled from Rust to WebAssembly: every color type and bit depth, metadata, chunk inspection, palette optimization and chunk stripping.",
5
5
  "keywords": [
6
6
  "png",
@@ -33,15 +33,28 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "default": "./dist/index.js"
35
35
  },
36
+ "./worker.js": "./dist/worker.js",
36
37
  "./package.json": "./package.json"
37
38
  },
39
+ "imports": {
40
+ "#spawn": {
41
+ "node": "./dist/spawn-node.js",
42
+ "default": "./dist/spawn-web.js"
43
+ },
44
+ "#port": {
45
+ "node": "./dist/port-node.js",
46
+ "default": "./dist/port-web.js"
47
+ }
48
+ },
38
49
  "files": [
39
50
  "dist",
40
51
  "README.md",
41
52
  "CHANGELOG.md",
42
53
  "LICENSE"
43
54
  ],
44
- "sideEffects": false,
55
+ "sideEffects": [
56
+ "./dist/worker.js"
57
+ ],
45
58
  "engines": {
46
59
  "node": ">=20.6"
47
60
  },
@@ -50,11 +63,18 @@
50
63
  "build:wasm": "cargo build -p format-png-wasm --release --target wasm32-unknown-unknown && wasm-bindgen --target web --out-dir src/wasm ../../target/wasm32-unknown-unknown/release/format_png_wasm.wasm && node scripts/inline-wasm.mjs",
51
64
  "build:ts": "tsc -p tsconfig.json",
52
65
  "build": "npm run clean && npm run build:wasm && npm run build:ts && node scripts/copy-wasm.mjs",
53
- "test": "vitest run",
66
+ "test": "vitest run --project node",
67
+ "test:browser": "vitest run --project browser",
54
68
  "test:package": "node scripts/test-package.mjs",
55
- "prepublishOnly": "npm run build && npm test && npm run test:package"
69
+ "bench": "node scripts/bench.mjs",
70
+ "bench:pool": "node scripts/bench-pool.mjs",
71
+ "prepublishOnly": "npm run build && npm test && npm run test:browser && npm run test:package"
56
72
  },
57
73
  "devDependencies": {
74
+ "@types/node": "^20.19.43",
75
+ "@vitest/browser-playwright": "^4.1.11",
76
+ "esbuild": "^0.25.12",
77
+ "playwright": "^1.63.0",
58
78
  "typescript": "^7.0.2",
59
79
  "vitest": "^4.1.11"
60
80
  }