format-png 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 +21 -0
- package/README.md +216 -0
- package/dist/index.d.ts +376 -0
- package/dist/index.js +367 -0
- package/dist/wasm/format_png_wasm.d.ts +473 -0
- package/dist/wasm/format_png_wasm.js +1517 -0
- package/dist/wasm/format_png_wasm_bg.wasm +0 -0
- package/dist/wasm/format_png_wasm_bg.wasm.d.ts +93 -0
- package/package.json +61 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import initWasm, * as wasm from "./wasm/format_png_wasm.js";
|
|
2
|
+
/** Thrown for malformed PNGs; `message` says what's wrong. */
|
|
3
|
+
export class PngError extends Error {
|
|
4
|
+
name = "PngError";
|
|
5
|
+
}
|
|
6
|
+
let ready;
|
|
7
|
+
let initialized = false;
|
|
8
|
+
/**
|
|
9
|
+
* Loads the WebAssembly module. Call it once before anything else; later calls
|
|
10
|
+
* return the same promise.
|
|
11
|
+
*
|
|
12
|
+
* In browsers and bundlers, call it with no argument: the `.wasm` file is
|
|
13
|
+
* fetched from next to this module. In Node, pass its bytes:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* import { readFile } from "node:fs/promises";
|
|
17
|
+
* await init(await readFile(new URL(import.meta.resolve("format-png/format_png_wasm_bg.wasm"))));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* You can also pass a URL to fetch it from, or a compiled `WebAssembly.Module`.
|
|
21
|
+
*/
|
|
22
|
+
export function init(source) {
|
|
23
|
+
ready ??= initWasm(source === undefined ? undefined : { module_or_path: source })
|
|
24
|
+
.then(() => {
|
|
25
|
+
initialized = true;
|
|
26
|
+
})
|
|
27
|
+
.catch((error) => {
|
|
28
|
+
ready = undefined; // allow a retry
|
|
29
|
+
throw error;
|
|
30
|
+
});
|
|
31
|
+
return ready;
|
|
32
|
+
}
|
|
33
|
+
function call(fn) {
|
|
34
|
+
if (!initialized)
|
|
35
|
+
throw new PngError("format-png: call init() first");
|
|
36
|
+
try {
|
|
37
|
+
return fn();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new PngError(error instanceof Error ? error.message : String(error));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function decoderArgs(options) {
|
|
44
|
+
return [
|
|
45
|
+
options.validateCrc ?? true,
|
|
46
|
+
options.preserveMetadata ?? false,
|
|
47
|
+
options.preserveChunks ?? false,
|
|
48
|
+
options.strictAncillary ?? false,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
function toText(source) {
|
|
52
|
+
try {
|
|
53
|
+
return {
|
|
54
|
+
keyword: source.keyword,
|
|
55
|
+
text: source.text,
|
|
56
|
+
languageTag: source.languageTag,
|
|
57
|
+
translatedKeyword: source.translatedKeyword,
|
|
58
|
+
chunkType: source.chunkType,
|
|
59
|
+
compressed: source.compressed,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
source.free();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function toMetadata(source) {
|
|
67
|
+
const metadata = { text: source.text().map(toText) };
|
|
68
|
+
if (source.gamma !== undefined)
|
|
69
|
+
metadata.gamma = source.gamma;
|
|
70
|
+
if (source.srgb !== undefined)
|
|
71
|
+
metadata.srgb = source.srgb;
|
|
72
|
+
const c = source.chromaticities;
|
|
73
|
+
if (c) {
|
|
74
|
+
metadata.chromaticities = {
|
|
75
|
+
white: { x: c[0], y: c[1] },
|
|
76
|
+
red: { x: c[2], y: c[3] },
|
|
77
|
+
green: { x: c[4], y: c[5] },
|
|
78
|
+
blue: { x: c[6], y: c[7] },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const physical = source.physicalPixelsPerUnit;
|
|
82
|
+
if (physical) {
|
|
83
|
+
metadata.physicalDimensions = {
|
|
84
|
+
x: physical[0],
|
|
85
|
+
y: physical[1],
|
|
86
|
+
unit: source.physicalUnit,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const t = source.time;
|
|
90
|
+
if (t)
|
|
91
|
+
metadata.time = { year: t[0], month: t[1], day: t[2], hour: t[3], minute: t[4], second: t[5] };
|
|
92
|
+
const profile = source.iccProfile;
|
|
93
|
+
if (profile)
|
|
94
|
+
metadata.iccProfile = { name: source.iccProfileName, profile };
|
|
95
|
+
const cicp = source.cicp;
|
|
96
|
+
if (cicp) {
|
|
97
|
+
metadata.cicp = { colorPrimaries: cicp[0], transferFunction: cicp[1], matrixCoefficients: cicp[2], fullRange: cicp[3] === 1 };
|
|
98
|
+
}
|
|
99
|
+
const exif = source.exif;
|
|
100
|
+
if (exif)
|
|
101
|
+
metadata.exif = { data: exif, byteOrder: source.exifByteOrder };
|
|
102
|
+
return metadata;
|
|
103
|
+
}
|
|
104
|
+
function toChunk(chunk) {
|
|
105
|
+
const { chunkType: type, position } = chunk;
|
|
106
|
+
return { type, position: position, data: chunk.intoData() }; // `intoData` frees `chunk`
|
|
107
|
+
}
|
|
108
|
+
/** Reads `source.metadata()` and frees the wasm copy. */
|
|
109
|
+
function takeMetadata(source) {
|
|
110
|
+
const metadata = source.metadata();
|
|
111
|
+
try {
|
|
112
|
+
return toMetadata(metadata);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
metadata.free();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function toHeader(header) {
|
|
119
|
+
try {
|
|
120
|
+
return {
|
|
121
|
+
width: header.width,
|
|
122
|
+
height: header.height,
|
|
123
|
+
bitDepth: header.bitDepth,
|
|
124
|
+
colorType: header.colorType,
|
|
125
|
+
interlaced: header.interlaced,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
header.free();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function toTransparency(kind, values) {
|
|
133
|
+
switch (kind) {
|
|
134
|
+
case "gray":
|
|
135
|
+
return { kind, value: values[0] };
|
|
136
|
+
case "rgb":
|
|
137
|
+
return { kind, value: [values[0], values[1], values[2]] };
|
|
138
|
+
default:
|
|
139
|
+
return { kind: "palette", alpha: Uint8Array.from(values) };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const isUpper = (char) => char >= "A" && char <= "Z";
|
|
143
|
+
function toChunks(list, bytes) {
|
|
144
|
+
try {
|
|
145
|
+
const { types, offsets, lengths, crcs, known, palette, transparencyKind, transparencyValues } = list;
|
|
146
|
+
const result = {
|
|
147
|
+
header: toHeader(list.header()),
|
|
148
|
+
metadata: takeMetadata(list),
|
|
149
|
+
chunks: Array.from(offsets, (offset, i) => {
|
|
150
|
+
const type = types.slice(i * 4, i * 4 + 4);
|
|
151
|
+
return {
|
|
152
|
+
type,
|
|
153
|
+
offset,
|
|
154
|
+
data: bytes.subarray(offset + 8, offset + 8 + lengths[i]),
|
|
155
|
+
crc: crcs[i],
|
|
156
|
+
known: known[i] === 1,
|
|
157
|
+
critical: isUpper(type[0]),
|
|
158
|
+
public: isUpper(type[1]),
|
|
159
|
+
safeToCopy: !isUpper(type[3]),
|
|
160
|
+
};
|
|
161
|
+
}),
|
|
162
|
+
};
|
|
163
|
+
if (palette)
|
|
164
|
+
result.palette = toPalette(palette);
|
|
165
|
+
if (transparencyKind && transparencyValues)
|
|
166
|
+
result.transparency = toTransparency(transparencyKind, transparencyValues);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
list.free();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function toImage(decoded) {
|
|
174
|
+
const { width, height } = decoded;
|
|
175
|
+
const metadata = takeMetadata(decoded);
|
|
176
|
+
const chunks = decoded.takeChunks().map(toChunk);
|
|
177
|
+
const pixels = decoded.intoPixels(); // copies out of wasm memory and frees `decoded`
|
|
178
|
+
// A fresh ArrayBuffer, so viewing it as clamped bytes copies nothing.
|
|
179
|
+
const data = new Uint8ClampedArray(pixels.buffer, pixels.byteOffset, pixels.byteLength);
|
|
180
|
+
return { width, height, data, metadata, chunks };
|
|
181
|
+
}
|
|
182
|
+
/** Turns a flat `r, g, b, …` array into `[r, g, b]` entries. */
|
|
183
|
+
function toPalette(flat) {
|
|
184
|
+
return Array.from({ length: flat.length / 3 }, (_, i) => [flat[i * 3], flat[i * 3 + 1], flat[i * 3 + 2]]);
|
|
185
|
+
}
|
|
186
|
+
function toRawImage(decoded) {
|
|
187
|
+
const header = toHeader(decoded.header());
|
|
188
|
+
const { palette, transparencyKind, transparencyValues } = decoded;
|
|
189
|
+
const metadata = takeMetadata(decoded);
|
|
190
|
+
const chunks = decoded.takeChunks().map(toChunk);
|
|
191
|
+
const data = decoded.intoData(); // copies out of wasm memory and frees `decoded`
|
|
192
|
+
const image = { header, data, metadata, chunks };
|
|
193
|
+
if (palette)
|
|
194
|
+
image.palette = toPalette(palette);
|
|
195
|
+
if (transparencyKind && transparencyValues)
|
|
196
|
+
image.transparency = toTransparency(transparencyKind, transparencyValues);
|
|
197
|
+
return image;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Decodes a PNG in its own format, without conversion: every bit depth and
|
|
201
|
+
* color type as stored, with its palette and transparency. Pass the result to
|
|
202
|
+
* `encode` to re-encode it losslessly. Use `decodeRgba8` to display it.
|
|
203
|
+
*/
|
|
204
|
+
export function decode(bytes, options = {}) {
|
|
205
|
+
return call(() => toRawImage(wasm.decode(bytes, ...decoderArgs(options))));
|
|
206
|
+
}
|
|
207
|
+
/** Decodes a PNG to 8-bit RGBA. Pass options to also read metadata or raw chunks. */
|
|
208
|
+
export function decodeRgba8(bytes, options = {}) {
|
|
209
|
+
return call(() => toImage(wasm.decodeRgba8(bytes, ...decoderArgs(options))));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Reads and parses every chunk without decompressing the image data, which is
|
|
213
|
+
* much faster than decoding. Known chunks are parsed; every chunk, including
|
|
214
|
+
* private and unknown ones, is also returned raw in file order.
|
|
215
|
+
*/
|
|
216
|
+
export function readChunks(bytes, options = {}) {
|
|
217
|
+
return call(() => toChunks(wasm.readChunks(bytes, options.validateCrc ?? true, options.strictAncillary ?? false), bytes));
|
|
218
|
+
}
|
|
219
|
+
/** Parses the data of a `tEXt`, `zTXt` or `iTXt` chunk on its own, for example one from `readChunks`. */
|
|
220
|
+
export function parseText(type, data) {
|
|
221
|
+
return call(() => toText(wasm.parseText(type, data)));
|
|
222
|
+
}
|
|
223
|
+
/** Pixels per inch from `pHYs`, or undefined if its unit isn't meters. */
|
|
224
|
+
export function pixelsPerInch(dimensions) {
|
|
225
|
+
if (dimensions.unit !== "meter")
|
|
226
|
+
return undefined;
|
|
227
|
+
return { x: dimensions.x * 0.0254, y: dimensions.y * 0.0254 };
|
|
228
|
+
}
|
|
229
|
+
/** Reads the image header without decoding pixels. */
|
|
230
|
+
export function readHeader(bytes) {
|
|
231
|
+
return call(() => toHeader(wasm.readHeader(bytes)));
|
|
232
|
+
}
|
|
233
|
+
function encoderArgs(options) {
|
|
234
|
+
return [
|
|
235
|
+
options.compression ?? 6,
|
|
236
|
+
options.compressionStrategy ?? "dynamic",
|
|
237
|
+
options.filter ?? "adaptive",
|
|
238
|
+
options.keepUnsafeChunks ?? false,
|
|
239
|
+
options.palette ?? "keep",
|
|
240
|
+
options.strip ?? "keep",
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
function toEncodeImage(image) {
|
|
244
|
+
const { header, data, palette, transparency, metadata, chunks = [] } = image;
|
|
245
|
+
const target = new wasm.EncodeImage(header.width, header.height, header.bitDepth, header.colorType, header.interlaced, data);
|
|
246
|
+
try {
|
|
247
|
+
if (palette)
|
|
248
|
+
target.setPalette(Uint8Array.from(palette.flat()));
|
|
249
|
+
if (transparency) {
|
|
250
|
+
const values = transparency.kind === "palette" ? transparency.alpha : transparency.kind === "gray" ? [transparency.value] : transparency.value;
|
|
251
|
+
target.setTransparency(transparency.kind, Uint16Array.from(values));
|
|
252
|
+
}
|
|
253
|
+
if (metadata) {
|
|
254
|
+
const { gamma, chromaticities: c, srgb, physicalDimensions: p, time: t, text = [], iccProfile, cicp, exif } = metadata;
|
|
255
|
+
if (cicp)
|
|
256
|
+
target.setCicp(cicp.colorPrimaries, cicp.transferFunction, cicp.matrixCoefficients, cicp.fullRange);
|
|
257
|
+
if (iccProfile)
|
|
258
|
+
target.setIccProfile(iccProfile.name, iccProfile.profile);
|
|
259
|
+
if (exif)
|
|
260
|
+
target.setExif(exif.data);
|
|
261
|
+
if (gamma !== undefined)
|
|
262
|
+
target.setGamma(gamma);
|
|
263
|
+
if (c)
|
|
264
|
+
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]));
|
|
265
|
+
if (srgb)
|
|
266
|
+
target.setSrgb(srgb);
|
|
267
|
+
if (p)
|
|
268
|
+
target.setPhysicalDimensions(p.x, p.y, p.unit);
|
|
269
|
+
if (t)
|
|
270
|
+
target.setTime(t.year, t.month, t.day, t.hour, t.minute, t.second);
|
|
271
|
+
for (const entry of text) {
|
|
272
|
+
target.addText(entry.chunkType ?? "tEXt", entry.keyword, entry.text, entry.languageTag ?? "", entry.translatedKeyword ?? "", entry.compressed ?? false);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const chunk of chunks)
|
|
276
|
+
target.addChunk(chunk.type, chunk.data, chunk.position);
|
|
277
|
+
return target;
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
target.free();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function rgbaToPngImage(image) {
|
|
285
|
+
const { width, height, data, metadata, chunks, interlaced = false } = image;
|
|
286
|
+
return {
|
|
287
|
+
header: { width, height, bitDepth: 8, colorType: "rgba", interlaced },
|
|
288
|
+
// A view of the same bytes, so a Uint8ClampedArray isn't copied here.
|
|
289
|
+
data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
|
|
290
|
+
metadata,
|
|
291
|
+
chunks,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
/** Encodes `image`, then frees the wasm copy of it. */
|
|
295
|
+
function encodeWith(image, encode) {
|
|
296
|
+
const target = toEncodeImage(image);
|
|
297
|
+
try {
|
|
298
|
+
return encode(target);
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
target.free();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Encodes an image in any PNG pixel format, with its palette, transparency,
|
|
306
|
+
* metadata and raw chunks.
|
|
307
|
+
*/
|
|
308
|
+
export function encode(image, options = {}) {
|
|
309
|
+
return call(() => encodeWith(image, (target) => wasm.encode(target, ...encoderArgs(options))));
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Encodes 8-bit RGBA pixels: the reverse of `decodeRgba8`. Pass a decoded
|
|
313
|
+
* `RgbaImage` to re-encode it with its metadata and chunks, or the `ImageData`
|
|
314
|
+
* of a canvas.
|
|
315
|
+
*/
|
|
316
|
+
export function encodeRgba8(image, options = {}) {
|
|
317
|
+
return encode(rgbaToPngImage(image), options);
|
|
318
|
+
}
|
|
319
|
+
/** Wraps an image for `CanvasRenderingContext2D.putImageData`. Browser only. */
|
|
320
|
+
export function toImageData(image) {
|
|
321
|
+
return new ImageData(image.data, image.width, image.height);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* A decoder for many images: it keeps its buffers in wasm memory between calls.
|
|
325
|
+
* Call `free()` when you're done with it.
|
|
326
|
+
*/
|
|
327
|
+
export class PngDecoder {
|
|
328
|
+
#inner;
|
|
329
|
+
constructor(options = {}) {
|
|
330
|
+
this.#inner = call(() => new wasm.Decoder(...decoderArgs(options)));
|
|
331
|
+
}
|
|
332
|
+
decodeRgba8(bytes) {
|
|
333
|
+
return call(() => toImage(this.#inner.decodeRgba8(bytes)));
|
|
334
|
+
}
|
|
335
|
+
/** Like the `decode` function. */
|
|
336
|
+
decode(bytes) {
|
|
337
|
+
return call(() => toRawImage(this.#inner.decode(bytes)));
|
|
338
|
+
}
|
|
339
|
+
/** Like the `readChunks` function. Only the `validateCrc` and `strictAncillary` options apply. */
|
|
340
|
+
readChunks(bytes) {
|
|
341
|
+
return call(() => toChunks(this.#inner.readChunks(bytes), bytes));
|
|
342
|
+
}
|
|
343
|
+
free() {
|
|
344
|
+
this.#inner.free();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* An encoder for many images: it keeps its compressor and buffers in wasm
|
|
349
|
+
* memory between calls. Call `free()` when you're done with it.
|
|
350
|
+
*/
|
|
351
|
+
export class PngEncoder {
|
|
352
|
+
#inner;
|
|
353
|
+
constructor(options = {}) {
|
|
354
|
+
this.#inner = call(() => new wasm.Encoder(...encoderArgs(options)));
|
|
355
|
+
}
|
|
356
|
+
/** Like the `encode` function, with this encoder's options. */
|
|
357
|
+
encode(image) {
|
|
358
|
+
return call(() => encodeWith(image, (target) => this.#inner.encode(target)));
|
|
359
|
+
}
|
|
360
|
+
/** Like the `encodeRgba8` function, with this encoder's options. */
|
|
361
|
+
encodeRgba8(image) {
|
|
362
|
+
return this.encode(rgbaToPngImage(image));
|
|
363
|
+
}
|
|
364
|
+
free() {
|
|
365
|
+
this.#inner.free();
|
|
366
|
+
}
|
|
367
|
+
}
|