format-png 0.1.2 → 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/core.js ADDED
@@ -0,0 +1,405 @@
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), { cause: 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
+ options.threads ?? "single",
245
+ ];
246
+ }
247
+ function toEncodeImage(image) {
248
+ const { header, data, palette, transparency, metadata, chunks = [] } = image;
249
+ const target = new wasm.EncodeImage(header.width, header.height, header.bitDepth, header.colorType, header.interlaced, data);
250
+ try {
251
+ if (palette)
252
+ target.setPalette(Uint8Array.from(palette.flat()));
253
+ if (transparency) {
254
+ const values = transparency.kind === "palette" ? transparency.alpha : transparency.kind === "gray" ? [transparency.value] : transparency.value;
255
+ target.setTransparency(transparency.kind, Uint16Array.from(values));
256
+ }
257
+ if (metadata) {
258
+ const { gamma, chromaticities: c, srgb, physicalDimensions: p, time: t, text = [], iccProfile, cicp, exif } = metadata;
259
+ if (cicp)
260
+ target.setCicp(cicp.colorPrimaries, cicp.transferFunction, cicp.matrixCoefficients, cicp.fullRange);
261
+ if (iccProfile)
262
+ target.setIccProfile(iccProfile.name, iccProfile.profile);
263
+ if (exif)
264
+ target.setExif(exif.data);
265
+ if (gamma !== undefined)
266
+ target.setGamma(gamma);
267
+ if (c)
268
+ 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]));
269
+ if (srgb)
270
+ target.setSrgb(srgb);
271
+ if (p)
272
+ target.setPhysicalDimensions(p.x, p.y, p.unit);
273
+ if (t)
274
+ target.setTime(t.year, t.month, t.day, t.hour, t.minute, t.second);
275
+ for (const entry of text) {
276
+ target.addText(entry.chunkType ?? "tEXt", entry.keyword, entry.text, entry.languageTag ?? "", entry.translatedKeyword ?? "", entry.compressed ?? false);
277
+ }
278
+ }
279
+ for (const chunk of chunks)
280
+ target.addChunk(chunk.type, chunk.data, chunk.position);
281
+ return target;
282
+ }
283
+ catch (error) {
284
+ target.free();
285
+ throw error;
286
+ }
287
+ }
288
+ /** `image` as `encode` takes it. Internal: for the worker pool. */
289
+ export function rgbaToPngImage(image) {
290
+ const { width, height, data, metadata, chunks, interlaced = false } = image;
291
+ return {
292
+ header: { width, height, bitDepth: 8, colorType: "rgba", interlaced },
293
+ // A view of the same bytes, so a Uint8ClampedArray isn't copied here.
294
+ data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
295
+ metadata,
296
+ chunks,
297
+ };
298
+ }
299
+ /** Encodes `image`, then frees the wasm copy of it. */
300
+ function encodeWith(image, encode) {
301
+ const target = toEncodeImage(image);
302
+ try {
303
+ return encode(target);
304
+ }
305
+ finally {
306
+ target.free();
307
+ }
308
+ }
309
+ /**
310
+ * Encodes an image in any PNG pixel format, with its palette, transparency,
311
+ * metadata and raw chunks.
312
+ */
313
+ export function encode(image, options = {}) {
314
+ return call(() => encodeWith(image, (target) => wasm.encode(target, ...encoderArgs(options))));
315
+ }
316
+ /**
317
+ * Encodes 8-bit RGBA pixels: the reverse of `decodeRgba8`. Pass a decoded
318
+ * `RgbaImage` to re-encode it with its metadata and chunks, or the `ImageData`
319
+ * of a canvas.
320
+ */
321
+ export function encodeRgba8(image, options = {}) {
322
+ return encode(rgbaToPngImage(image), options);
323
+ }
324
+ /** Wraps an image for `CanvasRenderingContext2D.putImageData`. Browser only. */
325
+ export function toImageData(image) {
326
+ return new ImageData(image.data, image.width, image.height);
327
+ }
328
+ /**
329
+ * A decoder for many images: it keeps its buffers in wasm memory between calls.
330
+ * Call `free()` when you're done with it.
331
+ */
332
+ export class PngDecoder {
333
+ #inner;
334
+ constructor(options = {}) {
335
+ this.#inner = call(() => new wasm.Decoder(...decoderArgs(options)));
336
+ }
337
+ decodeRgba8(bytes) {
338
+ return call(() => toImage(this.#inner.decodeRgba8(bytes)));
339
+ }
340
+ /** Like the `decode` function. */
341
+ decode(bytes) {
342
+ return call(() => toRawImage(this.#inner.decode(bytes)));
343
+ }
344
+ /** Like the `readChunks` function. Only the `validateCrc` and `strictAncillary` options apply. */
345
+ readChunks(bytes) {
346
+ return call(() => toChunks(this.#inner.readChunks(bytes), bytes));
347
+ }
348
+ free() {
349
+ this.#inner.free();
350
+ }
351
+ }
352
+ /**
353
+ * An encoder for many images: it keeps its compressor and buffers in wasm
354
+ * memory between calls. Call `free()` when you're done with it.
355
+ */
356
+ export class PngEncoder {
357
+ #inner;
358
+ constructor(options = {}) {
359
+ this.#inner = call(() => new wasm.Encoder(...encoderArgs(options)));
360
+ wasmEncoders.set(this, this.#inner);
361
+ }
362
+ /** Like the `encode` function, with this encoder's options. */
363
+ encode(image) {
364
+ return call(() => encodeWith(image, (target) => this.#inner.encode(target)));
365
+ }
366
+ /** Like the `encodeRgba8` function, with this encoder's options. */
367
+ encodeRgba8(image) {
368
+ return this.encode(rgbaToPngImage(image));
369
+ }
370
+ free() {
371
+ this.#inner.free();
372
+ }
373
+ }
374
+ // Internal: for the worker pool, which splits one image's compression across
375
+ // its workers. Not exported from index.ts.
376
+ /** Each `PngEncoder`'s wasm encoder, for `prepare`. */
377
+ const wasmEncoders = new WeakMap();
378
+ /**
379
+ * Everything `encoder.encode(image)` does except compress the image data,
380
+ * which is left in segments. Call `free()` on the result when done with it.
381
+ */
382
+ export function prepare(encoder, image) {
383
+ const inner = wasmEncoders.get(encoder);
384
+ return call(() => {
385
+ const target = toEncodeImage(image);
386
+ try {
387
+ return inner.prepare(target);
388
+ }
389
+ finally {
390
+ target.free();
391
+ }
392
+ });
393
+ }
394
+ /** Compresses one segment of a `Prepared`, with its `compressionLevel` and `compressionStrategy`. */
395
+ export function compressSegment(segment, level, strategy) {
396
+ return call(() => wasm.compressSegment(segment, level, strategy));
397
+ }
398
+ /** The PNG, from `prepared` and its segments compressed in order. `prepared` still needs `free()`. */
399
+ export function finish(prepared, compressed) {
400
+ return call(() => {
401
+ for (const segment of compressed)
402
+ prepared.pushCompressed(segment);
403
+ return prepared.finish();
404
+ });
405
+ }