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.
package/dist/index.js CHANGED
@@ -1,370 +1,3 @@
1
- import initWasm, * as wasm from "./wasm/format_png_wasm.js";
2
- import wasmBase64 from "./wasm/format_png_wasm_bg.wasm.base64.js";
3
- /** Thrown for malformed PNGs; `message` says what's wrong. */
4
- export class PngError extends Error {
5
- name = "PngError";
6
- }
7
- let ready;
8
- let initialized = false;
9
- /**
10
- * Loads the WebAssembly module. Call it once before anything else; later calls
11
- * return the same promise.
12
- *
13
- * With no argument it uses the copy embedded in this package, in browsers,
14
- * bundlers and Node alike: nothing is fetched. You can instead pass a
15
- * `WebAssembly.Module` compiled from it, for example one a page compiled once
16
- * and posted to its workers.
17
- */
18
- export function init(source) {
19
- ready ??= initWasm({ module_or_path: source ?? decodeBase64(wasmBase64) })
20
- .then(() => {
21
- initialized = true;
22
- })
23
- .catch((error) => {
24
- ready = undefined; // allow a retry
25
- throw error;
26
- });
27
- return ready;
28
- }
29
- function decodeBase64(text) {
30
- const binary = atob(text);
31
- const bytes = new Uint8Array(binary.length);
32
- for (let i = 0; i < binary.length; i++)
33
- bytes[i] = binary.charCodeAt(i);
34
- return bytes;
35
- }
36
- function call(fn) {
37
- if (!initialized)
38
- throw new PngError("format-png: call init() first");
39
- try {
40
- return fn();
41
- }
42
- catch (error) {
43
- throw new PngError(error instanceof Error ? error.message : String(error));
44
- }
45
- }
46
- function decoderArgs(options) {
47
- return [
48
- options.validateCrc ?? true,
49
- options.preserveMetadata ?? false,
50
- options.preserveChunks ?? false,
51
- options.strictAncillary ?? false,
52
- ];
53
- }
54
- function toText(source) {
55
- try {
56
- return {
57
- keyword: source.keyword,
58
- text: source.text,
59
- languageTag: source.languageTag,
60
- translatedKeyword: source.translatedKeyword,
61
- chunkType: source.chunkType,
62
- compressed: source.compressed,
63
- };
64
- }
65
- finally {
66
- source.free();
67
- }
68
- }
69
- function toMetadata(source) {
70
- const metadata = { text: source.text().map(toText) };
71
- if (source.gamma !== undefined)
72
- metadata.gamma = source.gamma;
73
- if (source.srgb !== undefined)
74
- metadata.srgb = source.srgb;
75
- const c = source.chromaticities;
76
- if (c) {
77
- metadata.chromaticities = {
78
- white: { x: c[0], y: c[1] },
79
- red: { x: c[2], y: c[3] },
80
- green: { x: c[4], y: c[5] },
81
- blue: { x: c[6], y: c[7] },
82
- };
83
- }
84
- const physical = source.physicalPixelsPerUnit;
85
- if (physical) {
86
- metadata.physicalDimensions = {
87
- x: physical[0],
88
- y: physical[1],
89
- unit: source.physicalUnit,
90
- };
91
- }
92
- const t = source.time;
93
- if (t)
94
- metadata.time = { year: t[0], month: t[1], day: t[2], hour: t[3], minute: t[4], second: t[5] };
95
- const profile = source.iccProfile;
96
- if (profile)
97
- metadata.iccProfile = { name: source.iccProfileName, profile };
98
- const cicp = source.cicp;
99
- if (cicp) {
100
- metadata.cicp = { colorPrimaries: cicp[0], transferFunction: cicp[1], matrixCoefficients: cicp[2], fullRange: cicp[3] === 1 };
101
- }
102
- const exif = source.exif;
103
- if (exif)
104
- metadata.exif = { data: exif, byteOrder: source.exifByteOrder };
105
- return metadata;
106
- }
107
- function toChunk(chunk) {
108
- const { chunkType: type, position } = chunk;
109
- return { type, position: position, data: chunk.intoData() }; // `intoData` frees `chunk`
110
- }
111
- /** Reads `source.metadata()` and frees the wasm copy. */
112
- function takeMetadata(source) {
113
- const metadata = source.metadata();
114
- try {
115
- return toMetadata(metadata);
116
- }
117
- finally {
118
- metadata.free();
119
- }
120
- }
121
- function toHeader(header) {
122
- try {
123
- return {
124
- width: header.width,
125
- height: header.height,
126
- bitDepth: header.bitDepth,
127
- colorType: header.colorType,
128
- interlaced: header.interlaced,
129
- };
130
- }
131
- finally {
132
- header.free();
133
- }
134
- }
135
- function toTransparency(kind, values) {
136
- switch (kind) {
137
- case "gray":
138
- return { kind, value: values[0] };
139
- case "rgb":
140
- return { kind, value: [values[0], values[1], values[2]] };
141
- default:
142
- return { kind: "palette", alpha: Uint8Array.from(values) };
143
- }
144
- }
145
- const isUpper = (char) => char >= "A" && char <= "Z";
146
- function toChunks(list, bytes) {
147
- try {
148
- const { types, offsets, lengths, crcs, known, palette, transparencyKind, transparencyValues } = list;
149
- const result = {
150
- header: toHeader(list.header()),
151
- metadata: takeMetadata(list),
152
- chunks: Array.from(offsets, (offset, i) => {
153
- const type = types.slice(i * 4, i * 4 + 4);
154
- return {
155
- type,
156
- offset,
157
- data: bytes.subarray(offset + 8, offset + 8 + lengths[i]),
158
- crc: crcs[i],
159
- known: known[i] === 1,
160
- critical: isUpper(type[0]),
161
- public: isUpper(type[1]),
162
- safeToCopy: !isUpper(type[3]),
163
- };
164
- }),
165
- };
166
- if (palette)
167
- result.palette = toPalette(palette);
168
- if (transparencyKind && transparencyValues)
169
- result.transparency = toTransparency(transparencyKind, transparencyValues);
170
- return result;
171
- }
172
- finally {
173
- list.free();
174
- }
175
- }
176
- function toImage(decoded) {
177
- const { width, height } = decoded;
178
- const metadata = takeMetadata(decoded);
179
- const chunks = decoded.takeChunks().map(toChunk);
180
- const pixels = decoded.intoPixels(); // copies out of wasm memory and frees `decoded`
181
- // A fresh ArrayBuffer, so viewing it as clamped bytes copies nothing.
182
- const data = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, pixels.byteLength);
183
- return { width, height, data, metadata, chunks };
184
- }
185
- /** Turns a flat `r, g, b, …` array into `[r, g, b]` entries. */
186
- function toPalette(flat) {
187
- return Array.from({ length: flat.length / 3 }, (_, i) => [flat[i * 3], flat[i * 3 + 1], flat[i * 3 + 2]]);
188
- }
189
- function toRawImage(decoded) {
190
- const header = toHeader(decoded.header());
191
- const { palette, transparencyKind, transparencyValues } = decoded;
192
- const metadata = takeMetadata(decoded);
193
- const chunks = decoded.takeChunks().map(toChunk);
194
- const data = decoded.intoData(); // copies out of wasm memory and frees `decoded`
195
- const image = { header, data, metadata, chunks };
196
- if (palette)
197
- image.palette = toPalette(palette);
198
- if (transparencyKind && transparencyValues)
199
- image.transparency = toTransparency(transparencyKind, transparencyValues);
200
- return image;
201
- }
202
- /**
203
- * Decodes a PNG in its own format, without conversion: every bit depth and
204
- * color type as stored, with its palette and transparency. Pass the result to
205
- * `encode` to re-encode it losslessly. Use `decodeRgba8` to display it.
206
- */
207
- export function decode(bytes, options = {}) {
208
- return call(() => toRawImage(wasm.decode(bytes, ...decoderArgs(options))));
209
- }
210
- /** Decodes a PNG to 8-bit RGBA. Pass options to also read metadata or raw chunks. */
211
- export function decodeRgba8(bytes, options = {}) {
212
- return call(() => toImage(wasm.decodeRgba8(bytes, ...decoderArgs(options))));
213
- }
214
- /**
215
- * Reads and parses every chunk without decompressing the image data, which is
216
- * much faster than decoding. Known chunks are parsed; every chunk, including
217
- * private and unknown ones, is also returned raw in file order.
218
- */
219
- export function readChunks(bytes, options = {}) {
220
- return call(() => toChunks(wasm.readChunks(bytes, options.validateCrc ?? true, options.strictAncillary ?? false), bytes));
221
- }
222
- /** Parses the data of a `tEXt`, `zTXt` or `iTXt` chunk on its own, for example one from `readChunks`. */
223
- export function parseText(type, data) {
224
- return call(() => toText(wasm.parseText(type, data)));
225
- }
226
- /** Pixels per inch from `pHYs`, or undefined if its unit isn't meters. */
227
- export function pixelsPerInch(dimensions) {
228
- if (dimensions.unit !== "meter")
229
- return undefined;
230
- return { x: dimensions.x * 0.0254, y: dimensions.y * 0.0254 };
231
- }
232
- /** Reads the image header without decoding pixels. */
233
- export function readHeader(bytes) {
234
- return call(() => toHeader(wasm.readHeader(bytes)));
235
- }
236
- function encoderArgs(options) {
237
- return [
238
- options.compression ?? 6,
239
- options.compressionStrategy ?? "dynamic",
240
- options.filter ?? "adaptive",
241
- options.keepUnsafeChunks ?? false,
242
- options.palette ?? "keep",
243
- options.strip ?? "keep",
244
- ];
245
- }
246
- function toEncodeImage(image) {
247
- const { header, data, palette, transparency, metadata, chunks = [] } = image;
248
- const target = new wasm.EncodeImage(header.width, header.height, header.bitDepth, header.colorType, header.interlaced, data);
249
- try {
250
- if (palette)
251
- target.setPalette(Uint8Array.from(palette.flat()));
252
- if (transparency) {
253
- const values = transparency.kind === "palette" ? transparency.alpha : transparency.kind === "gray" ? [transparency.value] : transparency.value;
254
- target.setTransparency(transparency.kind, Uint16Array.from(values));
255
- }
256
- if (metadata) {
257
- const { gamma, chromaticities: c, srgb, physicalDimensions: p, time: t, text = [], iccProfile, cicp, exif } = metadata;
258
- if (cicp)
259
- target.setCicp(cicp.colorPrimaries, cicp.transferFunction, cicp.matrixCoefficients, cicp.fullRange);
260
- if (iccProfile)
261
- target.setIccProfile(iccProfile.name, iccProfile.profile);
262
- if (exif)
263
- target.setExif(exif.data);
264
- if (gamma !== undefined)
265
- target.setGamma(gamma);
266
- if (c)
267
- target.setChromaticities(new Float64Array([c.white.x, c.white.y, c.red.x, c.red.y, c.green.x, c.green.y, c.blue.x, c.blue.y]));
268
- if (srgb)
269
- target.setSrgb(srgb);
270
- if (p)
271
- target.setPhysicalDimensions(p.x, p.y, p.unit);
272
- if (t)
273
- target.setTime(t.year, t.month, t.day, t.hour, t.minute, t.second);
274
- for (const entry of text) {
275
- target.addText(entry.chunkType ?? "tEXt", entry.keyword, entry.text, entry.languageTag ?? "", entry.translatedKeyword ?? "", entry.compressed ?? false);
276
- }
277
- }
278
- for (const chunk of chunks)
279
- target.addChunk(chunk.type, chunk.data, chunk.position);
280
- return target;
281
- }
282
- catch (error) {
283
- target.free();
284
- throw error;
285
- }
286
- }
287
- function rgbaToPngImage(image) {
288
- const { width, height, data, metadata, chunks, interlaced = false } = image;
289
- return {
290
- header: { width, height, bitDepth: 8, colorType: "rgba", interlaced },
291
- // A view of the same bytes, so a Uint8ClampedArray isn't copied here.
292
- data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
293
- metadata,
294
- chunks,
295
- };
296
- }
297
- /** Encodes `image`, then frees the wasm copy of it. */
298
- function encodeWith(image, encode) {
299
- const target = toEncodeImage(image);
300
- try {
301
- return encode(target);
302
- }
303
- finally {
304
- target.free();
305
- }
306
- }
307
- /**
308
- * Encodes an image in any PNG pixel format, with its palette, transparency,
309
- * metadata and raw chunks.
310
- */
311
- export function encode(image, options = {}) {
312
- return call(() => encodeWith(image, (target) => wasm.encode(target, ...encoderArgs(options))));
313
- }
314
- /**
315
- * Encodes 8-bit RGBA pixels: the reverse of `decodeRgba8`. Pass a decoded
316
- * `RgbaImage` to re-encode it with its metadata and chunks, or the `ImageData`
317
- * of a canvas.
318
- */
319
- export function encodeRgba8(image, options = {}) {
320
- return encode(rgbaToPngImage(image), options);
321
- }
322
- /** Wraps an image for `CanvasRenderingContext2D.putImageData`. Browser only. */
323
- export function toImageData(image) {
324
- return new ImageData(image.data, image.width, image.height);
325
- }
326
- /**
327
- * A decoder for many images: it keeps its buffers in wasm memory between calls.
328
- * Call `free()` when you're done with it.
329
- */
330
- export class PngDecoder {
331
- #inner;
332
- constructor(options = {}) {
333
- this.#inner = call(() => new wasm.Decoder(...decoderArgs(options)));
334
- }
335
- decodeRgba8(bytes) {
336
- return call(() => toImage(this.#inner.decodeRgba8(bytes)));
337
- }
338
- /** Like the `decode` function. */
339
- decode(bytes) {
340
- return call(() => toRawImage(this.#inner.decode(bytes)));
341
- }
342
- /** Like the `readChunks` function. Only the `validateCrc` and `strictAncillary` options apply. */
343
- readChunks(bytes) {
344
- return call(() => toChunks(this.#inner.readChunks(bytes), bytes));
345
- }
346
- free() {
347
- this.#inner.free();
348
- }
349
- }
350
- /**
351
- * An encoder for many images: it keeps its compressor and buffers in wasm
352
- * memory between calls. Call `free()` when you're done with it.
353
- */
354
- export class PngEncoder {
355
- #inner;
356
- constructor(options = {}) {
357
- this.#inner = call(() => new wasm.Encoder(...encoderArgs(options)));
358
- }
359
- /** Like the `encode` function, with this encoder's options. */
360
- encode(image) {
361
- return call(() => encodeWith(image, (target) => this.#inner.encode(target)));
362
- }
363
- /** Like the `encodeRgba8` function, with this encoder's options. */
364
- encodeRgba8(image) {
365
- return this.encode(rgbaToPngImage(image));
366
- }
367
- free() {
368
- this.#inner.free();
369
- }
370
- }
1
+ export { PngDecoder, PngEncoder, PngError, decode, decodeRgba8, encode, encodeRgba8, init, parseText, pixelsPerInch, readChunks, readHeader, toImageData, } from "./core.js";
2
+ export { createWorkerPool, WorkerPoolError } from "./pool.js";
3
+ export { decodeAsync, decodeRgba8Async, defaultWorkerPool, encodeAsync, encodeRgba8Async, parseTextAsync, readChunksAsync, readHeaderAsync, terminateDefaultWorkerPool, } from "./async.js";
package/dist/pool.d.ts ADDED
@@ -0,0 +1,82 @@
1
+ import { type DecodeOptions, type EncodeOptions, type PngChunks, type PngHeader, type PngImage, type PngText, type RawImage, type ReadChunksOptions, type RgbaImage, type RgbaImageInput } from "./core.js";
2
+ /** The parts of a Node `worker_threads` `Worker` a pool uses. */
3
+ export interface NodeWorkerLike {
4
+ postMessage(value: unknown, transferList?: readonly ArrayBuffer[]): void;
5
+ on(event: "message" | "error" | "messageerror" | "exit", listener: (value: any) => void): unknown;
6
+ terminate(): unknown;
7
+ ref?(): void;
8
+ unref?(): void;
9
+ }
10
+ export interface WorkerPoolOptions {
11
+ /**
12
+ * The most workers to run at once. Default: the number of CPU cores
13
+ * (`navigator.hardwareConcurrency`, or `os.availableParallelism()` in Node),
14
+ * at most 8.
15
+ */
16
+ size?: number;
17
+ /**
18
+ * Starts a worker running format-png's worker script, instead of the pool
19
+ * starting one itself. Only needed with bundlers that don't bundle
20
+ * `new Worker(new URL("./worker.js", import.meta.url))`, such as esbuild, or
21
+ * to start workers your own way.
22
+ */
23
+ createWorker?: () => Worker | NodeWorkerLike;
24
+ }
25
+ /** Options for one job, taken by every `WorkerPool` method alongside its usual options. */
26
+ export interface JobOptions {
27
+ /**
28
+ * Move the input's `ArrayBuffer` to the worker instead of copying it.
29
+ * Default false. The buffer is detached as soon as the method is called:
30
+ * the input, and every other view of the same buffer, becomes empty. It
31
+ * must be a plain `ArrayBuffer`, not a `SharedArrayBuffer`. For `encode`
32
+ * and `encodeRgba8` this applies to `image.data`; the rest is copied.
33
+ */
34
+ transfer?: boolean;
35
+ /**
36
+ * Cancels the job, rejecting it with `signal.reason`. A queued job is taken
37
+ * off the queue; a running one has its worker stopped and replaced.
38
+ */
39
+ signal?: AbortSignal;
40
+ }
41
+ /**
42
+ * Runs format-png in Web Workers (or worker_threads in Node), so decoding and
43
+ * encoding don't block the calling thread. Each method takes the same
44
+ * arguments as the function of the same name and resolves to the same result,
45
+ * or rejects with the same `PngError`.
46
+ *
47
+ * Workers start when jobs need them, at most `size`, and stay alive between
48
+ * jobs until `terminate()`. Jobs queue when every worker is busy and start in
49
+ * the order they were submitted.
50
+ */
51
+ export interface WorkerPool {
52
+ /** The most workers this pool runs at once. */
53
+ readonly size: number;
54
+ decode(bytes: Uint8Array, options?: DecodeOptions & JobOptions): Promise<RawImage>;
55
+ decodeRgba8(bytes: Uint8Array, options?: DecodeOptions & JobOptions): Promise<RgbaImage>;
56
+ /**
57
+ * As with the `readChunks` function, each chunk's `data` is a view into
58
+ * `bytes`; with `transfer: true`, into a buffer moved back from the worker.
59
+ */
60
+ readChunks(bytes: Uint8Array, options?: ReadChunksOptions & JobOptions): Promise<PngChunks>;
61
+ readHeader(bytes: Uint8Array, options?: JobOptions): Promise<PngHeader>;
62
+ parseText(type: PngText["chunkType"], data: Uint8Array, options?: JobOptions): Promise<PngText>;
63
+ encode(image: PngImage, options?: EncodeOptions & JobOptions): Promise<Uint8Array>;
64
+ encodeRgba8(image: RgbaImageInput, options?: EncodeOptions & JobOptions): Promise<Uint8Array>;
65
+ /**
66
+ * Stops every worker. Queued and running jobs, and every later call, reject
67
+ * with a `WorkerPoolError` whose `code` is "terminated".
68
+ */
69
+ terminate(): Promise<void>;
70
+ }
71
+ /**
72
+ * A job that failed because of the pool rather than the PNG: "terminated" if
73
+ * the pool was terminated, "worker-crashed" if its worker stopped unexpectedly
74
+ * (the pool replaces it).
75
+ */
76
+ export declare class WorkerPoolError extends Error {
77
+ readonly code: "terminated" | "worker-crashed";
78
+ name: string;
79
+ constructor(code: "terminated" | "worker-crashed", message: string);
80
+ }
81
+ /** Creates a pool of workers. Nothing starts until the first job. */
82
+ export declare function createWorkerPool(options?: WorkerPoolOptions): WorkerPool;