byte-codec 1.0.0 → 1.0.2
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-DCdK98vS.d.cts +81 -0
- package/dist/index-DCdK98vS.d.ts +81 -0
- package/dist/index.cjs +563 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +549 -0
- package/package.json +7 -3
- package/.github/workflows/ci.yml +0 -254
- package/.github/workflows/dependabot-auto-merge.yml +0 -64
- package/.github/workflows/sibling-dependency-update.yml +0 -174
- package/.husky/commit-msg +0 -1
- package/.husky/pre-commit +0 -2
- package/.husky/pre-push +0 -2
- package/CHANGELOG.md +0 -12
- package/commitlint.config.ts +0 -8
- package/eslint.config.ts +0 -19
- package/release.config.ts +0 -65
- package/src/bytes/crc32.ts +0 -24
- package/src/bytes/flate.ts +0 -69
- package/src/bytes/reader.ts +0 -73
- package/src/bytes/writer.ts +0 -45
- package/src/image/jpeg-info.ts +0 -90
- package/src/image/png-decode.ts +0 -263
- package/src/image/png-encode.ts +0 -74
- package/src/image/png-filter.ts +0 -142
- package/src/index.test.ts +0 -33
- package/src/index.ts +0 -9
- package/tsconfig.json +0 -18
- package/tsdown.config.ts +0 -10
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let fflate = require("fflate");
|
|
3
|
+
//#region src/bytes/writer.ts
|
|
4
|
+
var ByteWriter = class {
|
|
5
|
+
chunks = [];
|
|
6
|
+
byteLength = 0;
|
|
7
|
+
get length() {
|
|
8
|
+
return this.byteLength;
|
|
9
|
+
}
|
|
10
|
+
writeBytes(bytes) {
|
|
11
|
+
if (bytes.length === 0) return;
|
|
12
|
+
this.chunks.push(bytes);
|
|
13
|
+
this.byteLength += bytes.length;
|
|
14
|
+
}
|
|
15
|
+
writeByte(byte) {
|
|
16
|
+
this.writeBytes(new Uint8Array([byte]));
|
|
17
|
+
}
|
|
18
|
+
writeAscii(text) {
|
|
19
|
+
this.writeBytes(new TextEncoder().encode(text));
|
|
20
|
+
}
|
|
21
|
+
toBytes() {
|
|
22
|
+
const out = new Uint8Array(this.byteLength);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
for (const chunk of this.chunks) {
|
|
25
|
+
out.set(chunk, offset);
|
|
26
|
+
offset += chunk.length;
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
function concatBytes(chunks) {
|
|
32
|
+
const writer = new ByteWriter();
|
|
33
|
+
for (const chunk of chunks) writer.writeBytes(chunk);
|
|
34
|
+
return writer.toBytes();
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/bytes/reader.ts
|
|
38
|
+
const ASCII_WHITESPACE_BYTES = /* @__PURE__ */ new Set([
|
|
39
|
+
0,
|
|
40
|
+
9,
|
|
41
|
+
10,
|
|
42
|
+
12,
|
|
43
|
+
13,
|
|
44
|
+
32
|
|
45
|
+
]);
|
|
46
|
+
function isAsciiWhitespace(byte) {
|
|
47
|
+
return byte !== void 0 && ASCII_WHITESPACE_BYTES.has(byte);
|
|
48
|
+
}
|
|
49
|
+
var ByteReader = class {
|
|
50
|
+
bytes;
|
|
51
|
+
position = 0;
|
|
52
|
+
constructor(bytes) {
|
|
53
|
+
this.bytes = bytes;
|
|
54
|
+
}
|
|
55
|
+
get offset() {
|
|
56
|
+
return this.position;
|
|
57
|
+
}
|
|
58
|
+
get length() {
|
|
59
|
+
return this.bytes.length;
|
|
60
|
+
}
|
|
61
|
+
atEnd() {
|
|
62
|
+
return this.position >= this.bytes.length;
|
|
63
|
+
}
|
|
64
|
+
peek(aheadBy = 0) {
|
|
65
|
+
return this.bytes[this.position + aheadBy];
|
|
66
|
+
}
|
|
67
|
+
next() {
|
|
68
|
+
const byte = this.bytes[this.position];
|
|
69
|
+
if (byte !== void 0) this.position++;
|
|
70
|
+
return byte;
|
|
71
|
+
}
|
|
72
|
+
mark() {
|
|
73
|
+
return this.position;
|
|
74
|
+
}
|
|
75
|
+
reset(mark) {
|
|
76
|
+
this.position = mark;
|
|
77
|
+
}
|
|
78
|
+
seek(offset) {
|
|
79
|
+
this.position = offset;
|
|
80
|
+
}
|
|
81
|
+
slice(start, end) {
|
|
82
|
+
return this.bytes.subarray(start, end);
|
|
83
|
+
}
|
|
84
|
+
skipWhitespace() {
|
|
85
|
+
while (isAsciiWhitespace(this.peek())) this.position++;
|
|
86
|
+
}
|
|
87
|
+
matchKeyword(keyword) {
|
|
88
|
+
for (let i = 0; i < keyword.length; i++) if (this.peek(i) !== keyword.charCodeAt(i)) return false;
|
|
89
|
+
this.position += keyword.length;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/bytes/crc32.ts
|
|
95
|
+
const CRC32_POLYNOMIAL = 3988292384;
|
|
96
|
+
const BYTE_VALUES = 256;
|
|
97
|
+
const BITS_PER_BYTE = 8;
|
|
98
|
+
const CRC32_TABLE = (() => {
|
|
99
|
+
const table = new Uint32Array(BYTE_VALUES);
|
|
100
|
+
for (let n = 0; n < BYTE_VALUES; n++) {
|
|
101
|
+
let c = n;
|
|
102
|
+
for (let k = 0; k < BITS_PER_BYTE; k++) c = (c & 1) === 1 ? CRC32_POLYNOMIAL ^ c >>> 1 : c >>> 1;
|
|
103
|
+
table[n] = c >>> 0;
|
|
104
|
+
}
|
|
105
|
+
return table;
|
|
106
|
+
})();
|
|
107
|
+
function crc32(bytes) {
|
|
108
|
+
let crc = 4294967295;
|
|
109
|
+
for (const byte of bytes) crc = CRC32_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
110
|
+
return (crc ^ 4294967295) >>> 0;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/bytes/flate.ts
|
|
114
|
+
const MAX_INFLATE_OUTPUT_BYTES = 536870912;
|
|
115
|
+
function deflate(data, level) {
|
|
116
|
+
return (0, fflate.zlibSync)(data, level === void 0 ? void 0 : { level });
|
|
117
|
+
}
|
|
118
|
+
function inflate(data) {
|
|
119
|
+
const out = (0, fflate.unzlibSync)(data);
|
|
120
|
+
if (out.length > 536870912) throw new Error(`inflated output exceeds the ${MAX_INFLATE_OUTPUT_BYTES}-byte limit`);
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
function inflateTolerant(data) {
|
|
124
|
+
try {
|
|
125
|
+
return {
|
|
126
|
+
bytes: inflate(data),
|
|
127
|
+
recovered: false
|
|
128
|
+
};
|
|
129
|
+
} catch {}
|
|
130
|
+
let offset = 0;
|
|
131
|
+
while (offset < data.length && isAsciiWhitespace(data[offset])) offset++;
|
|
132
|
+
if (offset > 0) try {
|
|
133
|
+
return {
|
|
134
|
+
bytes: inflate(data.subarray(offset)),
|
|
135
|
+
recovered: true
|
|
136
|
+
};
|
|
137
|
+
} catch {}
|
|
138
|
+
try {
|
|
139
|
+
return {
|
|
140
|
+
bytes: (0, fflate.inflateSync)(data),
|
|
141
|
+
recovered: true
|
|
142
|
+
};
|
|
143
|
+
} catch {}
|
|
144
|
+
const chunks = [];
|
|
145
|
+
const unzlib = new fflate.Unzlib((chunk) => {
|
|
146
|
+
chunks.push(chunk);
|
|
147
|
+
});
|
|
148
|
+
try {
|
|
149
|
+
unzlib.push(data, false);
|
|
150
|
+
} catch {}
|
|
151
|
+
if (chunks.length === 0) throw new Error("unable to inflate stream: no data could be recovered");
|
|
152
|
+
return {
|
|
153
|
+
bytes: concatBytes(chunks),
|
|
154
|
+
recovered: true
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/image/png-filter.ts
|
|
159
|
+
function paethPredictor(a, b, c) {
|
|
160
|
+
const p = a + b - c;
|
|
161
|
+
const pa = Math.abs(p - a);
|
|
162
|
+
const pb = Math.abs(p - b);
|
|
163
|
+
const pc = Math.abs(p - c);
|
|
164
|
+
if (pa <= pb && pa <= pc) return a;
|
|
165
|
+
if (pb <= pc) return b;
|
|
166
|
+
return c;
|
|
167
|
+
}
|
|
168
|
+
function isPngFilterType(value) {
|
|
169
|
+
return value === 0 || value === 1 || value === 2 || value === 3 || value === 4;
|
|
170
|
+
}
|
|
171
|
+
function predictorValue(filterType, a, b, c) {
|
|
172
|
+
if (filterType === 1) return a;
|
|
173
|
+
if (filterType === 2) return b;
|
|
174
|
+
if (filterType === 3) return Math.floor((a + b) / 2);
|
|
175
|
+
if (filterType === 4) return paethPredictor(a, b, c);
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
|
178
|
+
function unfilterScanlines(data, height, bytesPerRow, bpp) {
|
|
179
|
+
const stride = bytesPerRow + 1;
|
|
180
|
+
if (data.length < height * stride) throw new Error(`PNG scanline data too short: expected at least ${height * stride} bytes, got ${data.length}`);
|
|
181
|
+
const out = new Uint8Array(height * bytesPerRow);
|
|
182
|
+
for (let y = 0; y < height; y++) {
|
|
183
|
+
const filterByte = data[y * stride];
|
|
184
|
+
if (filterByte === void 0 || !isPngFilterType(filterByte)) throw new Error(`unknown PNG filter type: ${String(filterByte)}`);
|
|
185
|
+
const rowStart = y * stride + 1;
|
|
186
|
+
const outRowStart = y * bytesPerRow;
|
|
187
|
+
const prevOutRowStart = y > 0 ? outRowStart - bytesPerRow : void 0;
|
|
188
|
+
for (let x = 0; x < bytesPerRow; x++) {
|
|
189
|
+
const raw = data[rowStart + x];
|
|
190
|
+
const a = x >= bpp ? out[outRowStart + x - bpp] : 0;
|
|
191
|
+
const b = prevOutRowStart === void 0 ? 0 : out[prevOutRowStart + x];
|
|
192
|
+
const c = x >= bpp && prevOutRowStart !== void 0 ? out[prevOutRowStart + x - bpp] : 0;
|
|
193
|
+
out[outRowStart + x] = raw + predictorValue(filterByte, a, b, c) & 255;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function sumOfAbsSigned(bytes) {
|
|
199
|
+
let sum = 0;
|
|
200
|
+
for (const byte of bytes) sum += byte < 128 ? byte : 256 - byte;
|
|
201
|
+
return sum;
|
|
202
|
+
}
|
|
203
|
+
function filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, filterType, out, outOffset) {
|
|
204
|
+
for (let x = 0; x < bytesPerRow; x++) {
|
|
205
|
+
const rawByte = raw[rowStart + x];
|
|
206
|
+
const a = x >= bpp ? raw[rowStart + x - bpp] : 0;
|
|
207
|
+
const b = prevRowStart === void 0 ? 0 : raw[prevRowStart + x];
|
|
208
|
+
const c = x >= bpp && prevRowStart !== void 0 ? raw[prevRowStart + x - bpp] : 0;
|
|
209
|
+
out[outOffset + x] = rawByte - predictorValue(filterType, a, b, c) & 255;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const ALL_FILTER_TYPES = [
|
|
213
|
+
0,
|
|
214
|
+
1,
|
|
215
|
+
2,
|
|
216
|
+
3,
|
|
217
|
+
4
|
|
218
|
+
];
|
|
219
|
+
function filterScanlines(raw, height, bytesPerRow, bpp, strategy = "adaptive") {
|
|
220
|
+
const stride = bytesPerRow + 1;
|
|
221
|
+
const out = new Uint8Array(height * stride);
|
|
222
|
+
const candidate = new Uint8Array(bytesPerRow);
|
|
223
|
+
for (let y = 0; y < height; y++) {
|
|
224
|
+
const rowStart = y * bytesPerRow;
|
|
225
|
+
const prevRowStart = y > 0 ? rowStart - bytesPerRow : void 0;
|
|
226
|
+
const outRowStart = y * stride;
|
|
227
|
+
if (strategy === "none") {
|
|
228
|
+
out[outRowStart] = 0;
|
|
229
|
+
filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, 0, out, outRowStart + 1);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
let bestType = 0;
|
|
233
|
+
let bestSum = Number.POSITIVE_INFINITY;
|
|
234
|
+
let best;
|
|
235
|
+
for (const filterType of ALL_FILTER_TYPES) {
|
|
236
|
+
filterRowInto(raw, rowStart, prevRowStart, bytesPerRow, bpp, filterType, candidate, 0);
|
|
237
|
+
const sum = sumOfAbsSigned(candidate);
|
|
238
|
+
if (sum < bestSum) {
|
|
239
|
+
bestSum = sum;
|
|
240
|
+
bestType = filterType;
|
|
241
|
+
best = candidate.slice();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
out[outRowStart] = bestType;
|
|
245
|
+
if (best !== void 0) out.set(best, outRowStart + 1);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/image/png-encode.ts
|
|
251
|
+
const PNG_SIGNATURE$1 = new Uint8Array([
|
|
252
|
+
137,
|
|
253
|
+
80,
|
|
254
|
+
78,
|
|
255
|
+
71,
|
|
256
|
+
13,
|
|
257
|
+
10,
|
|
258
|
+
26,
|
|
259
|
+
10
|
|
260
|
+
]);
|
|
261
|
+
function u32be(value) {
|
|
262
|
+
const bytes = /* @__PURE__ */ new Uint8Array(4);
|
|
263
|
+
new DataView(bytes.buffer).setUint32(0, value);
|
|
264
|
+
return bytes;
|
|
265
|
+
}
|
|
266
|
+
function writeChunk(writer, type, data) {
|
|
267
|
+
const typeBytes = new TextEncoder().encode(type);
|
|
268
|
+
writer.writeBytes(u32be(data.length));
|
|
269
|
+
writer.writeBytes(typeBytes);
|
|
270
|
+
writer.writeBytes(data);
|
|
271
|
+
writer.writeBytes(u32be(crc32(concatBytes([typeBytes, data]))));
|
|
272
|
+
}
|
|
273
|
+
function colorTypeFor(image) {
|
|
274
|
+
if (image.channels === 1) return image.alpha === void 0 ? 0 : 4;
|
|
275
|
+
return image.alpha === void 0 ? 2 : 6;
|
|
276
|
+
}
|
|
277
|
+
function encodePng(image, options = {}) {
|
|
278
|
+
const { width, height, channels, data, alpha } = image;
|
|
279
|
+
const outChannels = alpha === void 0 ? channels : channels + 1;
|
|
280
|
+
const bytesPerRow = width * outChannels;
|
|
281
|
+
const pixelCount = width * height;
|
|
282
|
+
const interleaved = new Uint8Array(pixelCount * outChannels);
|
|
283
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
284
|
+
const srcBase = i * channels;
|
|
285
|
+
const dstBase = i * outChannels;
|
|
286
|
+
for (let c = 0; c < channels; c++) interleaved[dstBase + c] = data[srcBase + c];
|
|
287
|
+
if (alpha !== void 0) interleaved[dstBase + channels] = alpha[i];
|
|
288
|
+
}
|
|
289
|
+
const compressed = deflate(filterScanlines(interleaved, height, bytesPerRow, outChannels, options.filter ?? "adaptive"));
|
|
290
|
+
const ihdr = /* @__PURE__ */ new Uint8Array(13);
|
|
291
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
292
|
+
ihdrView.setUint32(0, width);
|
|
293
|
+
ihdrView.setUint32(4, height);
|
|
294
|
+
ihdr[8] = 8;
|
|
295
|
+
ihdr[9] = colorTypeFor(image);
|
|
296
|
+
ihdr[10] = 0;
|
|
297
|
+
ihdr[11] = 0;
|
|
298
|
+
ihdr[12] = 0;
|
|
299
|
+
const writer = new ByteWriter();
|
|
300
|
+
writer.writeBytes(PNG_SIGNATURE$1);
|
|
301
|
+
writeChunk(writer, "IHDR", ihdr);
|
|
302
|
+
writeChunk(writer, "IDAT", compressed);
|
|
303
|
+
writeChunk(writer, "IEND", /* @__PURE__ */ new Uint8Array(0));
|
|
304
|
+
return writer.toBytes();
|
|
305
|
+
}
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/image/png-decode.ts
|
|
308
|
+
const PNG_SIGNATURE = [
|
|
309
|
+
137,
|
|
310
|
+
80,
|
|
311
|
+
78,
|
|
312
|
+
71,
|
|
313
|
+
13,
|
|
314
|
+
10,
|
|
315
|
+
26,
|
|
316
|
+
10
|
|
317
|
+
];
|
|
318
|
+
function requireDataView(bytes) {
|
|
319
|
+
return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
320
|
+
}
|
|
321
|
+
function readChunks(bytes, onWarning) {
|
|
322
|
+
const chunks = [];
|
|
323
|
+
const view = requireDataView(bytes);
|
|
324
|
+
let offset = PNG_SIGNATURE.length;
|
|
325
|
+
while (offset + 8 <= bytes.length) {
|
|
326
|
+
const length = view.getUint32(offset);
|
|
327
|
+
const typeBytes = bytes.subarray(offset + 4, offset + 8);
|
|
328
|
+
const type = new TextDecoder("latin1").decode(typeBytes);
|
|
329
|
+
const dataStart = offset + 8;
|
|
330
|
+
const dataEnd = dataStart + length;
|
|
331
|
+
if (dataEnd + 4 > bytes.length) throw new Error(`PNG chunk '${type}' declares a length that runs past the end of the file`);
|
|
332
|
+
const data = bytes.subarray(dataStart, dataEnd);
|
|
333
|
+
if (onWarning !== void 0) {
|
|
334
|
+
if (view.getUint32(dataEnd) !== crc32(concatBytes([typeBytes, data]))) onWarning(`PNG chunk '${type}' failed its CRC32 check`);
|
|
335
|
+
}
|
|
336
|
+
chunks.push({
|
|
337
|
+
type,
|
|
338
|
+
data
|
|
339
|
+
});
|
|
340
|
+
offset = dataEnd + 4;
|
|
341
|
+
if (type === "IEND") break;
|
|
342
|
+
}
|
|
343
|
+
return chunks;
|
|
344
|
+
}
|
|
345
|
+
function parseIhdr(data) {
|
|
346
|
+
const view = requireDataView(data);
|
|
347
|
+
return {
|
|
348
|
+
width: view.getUint32(0),
|
|
349
|
+
height: view.getUint32(4),
|
|
350
|
+
bitDepth: data[8],
|
|
351
|
+
colorType: data[9],
|
|
352
|
+
interlace: data[12]
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function channelsForColorType(colorType) {
|
|
356
|
+
if (colorType === 0) return 1;
|
|
357
|
+
if (colorType === 2) return 3;
|
|
358
|
+
if (colorType === 3) return 1;
|
|
359
|
+
if (colorType === 4) return 2;
|
|
360
|
+
if (colorType === 6) return 4;
|
|
361
|
+
throw new Error(`unsupported PNG colour type: ${colorType}`);
|
|
362
|
+
}
|
|
363
|
+
function filterBpp(bitDepth, channels) {
|
|
364
|
+
return Math.max(1, Math.ceil(bitDepth * channels / 8));
|
|
365
|
+
}
|
|
366
|
+
function unpackRow(rowBytes, width, channels, bitDepth) {
|
|
367
|
+
const sampleCount = width * channels;
|
|
368
|
+
const samples = new Array(sampleCount);
|
|
369
|
+
if (bitDepth === 8) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i];
|
|
370
|
+
else if (bitDepth === 16) for (let i = 0; i < sampleCount; i++) samples[i] = rowBytes[i * 2];
|
|
371
|
+
else {
|
|
372
|
+
const mask = (1 << bitDepth) - 1;
|
|
373
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
374
|
+
const bitOffset = i * bitDepth;
|
|
375
|
+
const byteIndex = bitOffset >> 3;
|
|
376
|
+
const shift = 8 - bitDepth - (bitOffset & 7);
|
|
377
|
+
samples[i] = rowBytes[byteIndex] >> shift & mask;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return samples;
|
|
381
|
+
}
|
|
382
|
+
function readTrnsGrayValue(trns) {
|
|
383
|
+
return requireDataView(trns).getUint16(0);
|
|
384
|
+
}
|
|
385
|
+
function readTrnsRgbKey(trns) {
|
|
386
|
+
const view = requireDataView(trns);
|
|
387
|
+
return [
|
|
388
|
+
view.getUint16(0),
|
|
389
|
+
view.getUint16(2),
|
|
390
|
+
view.getUint16(4)
|
|
391
|
+
];
|
|
392
|
+
}
|
|
393
|
+
function scaleToByte(sample, bitDepth) {
|
|
394
|
+
if (bitDepth === 16) return sample;
|
|
395
|
+
const maxSample = (1 << bitDepth) - 1;
|
|
396
|
+
return Math.round(sample * 255 / maxSample);
|
|
397
|
+
}
|
|
398
|
+
function decodePng(bytes, options = {}) {
|
|
399
|
+
for (let i = 0; i < PNG_SIGNATURE.length; i++) if (bytes[i] !== PNG_SIGNATURE[i]) throw new Error("not a valid PNG file: bad signature");
|
|
400
|
+
const chunks = readChunks(bytes, options.onWarning);
|
|
401
|
+
const ihdrChunk = chunks[0];
|
|
402
|
+
if (ihdrChunk?.type !== "IHDR") throw new Error("PNG file does not begin with an IHDR chunk");
|
|
403
|
+
const ihdr = parseIhdr(ihdrChunk.data);
|
|
404
|
+
if (ihdr.interlace !== 0) throw new Error("Adam7-interlaced PNG images are not supported");
|
|
405
|
+
const channels = channelsForColorType(ihdr.colorType);
|
|
406
|
+
const bpp = filterBpp(ihdr.bitDepth, channels);
|
|
407
|
+
const bytesPerRow = Math.ceil(ihdr.width * channels * ihdr.bitDepth / 8);
|
|
408
|
+
const idatChunks = chunks.filter((c) => c.type === "IDAT").map((c) => c.data);
|
|
409
|
+
if (idatChunks.length === 0) throw new Error("PNG file has no IDAT chunks");
|
|
410
|
+
const { bytes: inflated, recovered } = inflateTolerant(concatBytes(idatChunks));
|
|
411
|
+
if (recovered && options.onWarning !== void 0) options.onWarning("PNG IDAT stream required tolerant recovery (truncated or malformed)");
|
|
412
|
+
const unfiltered = unfilterScanlines(inflated, ihdr.height, bytesPerRow, bpp);
|
|
413
|
+
const palette = ihdr.colorType === 3 ? chunks.find((c) => c.type === "PLTE")?.data : void 0;
|
|
414
|
+
if (ihdr.colorType === 3 && palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
415
|
+
const trns = chunks.find((c) => c.type === "tRNS")?.data;
|
|
416
|
+
return buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns);
|
|
417
|
+
}
|
|
418
|
+
function buildRawImage(ihdr, channels, bytesPerRow, unfiltered, palette, trns) {
|
|
419
|
+
const { width, height, bitDepth, colorType } = ihdr;
|
|
420
|
+
const outChannels = colorType === 0 || colorType === 4 ? 1 : 3;
|
|
421
|
+
const data = new Uint8Array(width * height * outChannels);
|
|
422
|
+
const alpha = colorType === 4 || colorType === 6 || trns !== void 0 ? new Uint8Array(width * height).fill(255) : void 0;
|
|
423
|
+
const trnsGray = colorType === 0 && trns !== void 0 ? readTrnsGrayValue(trns) : void 0;
|
|
424
|
+
const trnsRgb = colorType === 2 && trns !== void 0 ? readTrnsRgbKey(trns) : void 0;
|
|
425
|
+
for (let y = 0; y < height; y++) {
|
|
426
|
+
const rowStart = y * bytesPerRow;
|
|
427
|
+
const samples = unpackRow(unfiltered.subarray(rowStart, rowStart + bytesPerRow), width, channels, bitDepth);
|
|
428
|
+
for (let x = 0; x < width; x++) {
|
|
429
|
+
const pixelBase = x * channels;
|
|
430
|
+
const outBase = (y * width + x) * outChannels;
|
|
431
|
+
const alphaIndex = y * width + x;
|
|
432
|
+
if (colorType === 0) {
|
|
433
|
+
const g = samples[pixelBase];
|
|
434
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
435
|
+
if (alpha !== void 0 && trnsGray !== void 0) alpha[alphaIndex] = g === trnsGray ? 0 : 255;
|
|
436
|
+
} else if (colorType === 2) {
|
|
437
|
+
const r = samples[pixelBase];
|
|
438
|
+
const g = samples[pixelBase + 1];
|
|
439
|
+
const b = samples[pixelBase + 2];
|
|
440
|
+
data[outBase] = r;
|
|
441
|
+
data[outBase + 1] = g;
|
|
442
|
+
data[outBase + 2] = b;
|
|
443
|
+
if (alpha !== void 0 && trnsRgb !== void 0) {
|
|
444
|
+
const [kr, kg, kb] = trnsRgb;
|
|
445
|
+
alpha[alphaIndex] = r === kr && g === kg && b === kb ? 0 : 255;
|
|
446
|
+
}
|
|
447
|
+
} else if (colorType === 3) {
|
|
448
|
+
const index = samples[pixelBase];
|
|
449
|
+
if (palette === void 0) throw new Error("indexed-colour PNG has no PLTE chunk");
|
|
450
|
+
data[outBase] = palette[index * 3];
|
|
451
|
+
data[outBase + 1] = palette[index * 3 + 1];
|
|
452
|
+
data[outBase + 2] = palette[index * 3 + 2];
|
|
453
|
+
if (alpha !== void 0 && trns !== void 0) alpha[alphaIndex] = index < trns.length ? trns[index] : 255;
|
|
454
|
+
} else if (colorType === 4) {
|
|
455
|
+
const g = samples[pixelBase];
|
|
456
|
+
const a = samples[pixelBase + 1];
|
|
457
|
+
data[outBase] = scaleToByte(g, bitDepth);
|
|
458
|
+
if (alpha !== void 0) alpha[alphaIndex] = scaleToByte(a, bitDepth);
|
|
459
|
+
} else {
|
|
460
|
+
data[outBase] = samples[pixelBase];
|
|
461
|
+
data[outBase + 1] = samples[pixelBase + 1];
|
|
462
|
+
data[outBase + 2] = samples[pixelBase + 2];
|
|
463
|
+
if (alpha !== void 0) alpha[alphaIndex] = samples[pixelBase + 3];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return alpha === void 0 ? {
|
|
468
|
+
width,
|
|
469
|
+
height,
|
|
470
|
+
channels: outChannels,
|
|
471
|
+
data
|
|
472
|
+
} : {
|
|
473
|
+
width,
|
|
474
|
+
height,
|
|
475
|
+
channels: outChannels,
|
|
476
|
+
data,
|
|
477
|
+
alpha
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/image/jpeg-info.ts
|
|
482
|
+
const SOI = 216;
|
|
483
|
+
const EOI = 217;
|
|
484
|
+
const APP14 = 238;
|
|
485
|
+
const SOF_MARKERS = /* @__PURE__ */ new Set([
|
|
486
|
+
192,
|
|
487
|
+
193,
|
|
488
|
+
194,
|
|
489
|
+
201,
|
|
490
|
+
202
|
|
491
|
+
]);
|
|
492
|
+
const PROGRESSIVE_SOF_MARKERS = /* @__PURE__ */ new Set([194, 202]);
|
|
493
|
+
const NO_PAYLOAD_MARKERS = /* @__PURE__ */ new Set([
|
|
494
|
+
1,
|
|
495
|
+
216,
|
|
496
|
+
217,
|
|
497
|
+
208,
|
|
498
|
+
209,
|
|
499
|
+
210,
|
|
500
|
+
211,
|
|
501
|
+
212,
|
|
502
|
+
213,
|
|
503
|
+
214,
|
|
504
|
+
215
|
|
505
|
+
]);
|
|
506
|
+
function requireByte(bytes, index) {
|
|
507
|
+
const value = bytes[index];
|
|
508
|
+
if (value === void 0) throw new Error("unexpected end of JPEG data");
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
function readUint16BE(bytes, offset) {
|
|
512
|
+
return requireByte(bytes, offset) << 8 | requireByte(bytes, offset + 1);
|
|
513
|
+
}
|
|
514
|
+
function readJpegInfo(bytes) {
|
|
515
|
+
if (requireByte(bytes, 0) !== 255 || requireByte(bytes, 1) !== SOI) throw new Error("not a valid JPEG file: missing SOI marker");
|
|
516
|
+
let offset = 2;
|
|
517
|
+
let adobeTransform;
|
|
518
|
+
while (offset < bytes.length) {
|
|
519
|
+
if (bytes[offset] !== 255) {
|
|
520
|
+
offset++;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
let markerOffset = offset + 1;
|
|
524
|
+
while (bytes[markerOffset] === 255) markerOffset++;
|
|
525
|
+
const marker = bytes[markerOffset];
|
|
526
|
+
if (marker === void 0) break;
|
|
527
|
+
offset = markerOffset + 1;
|
|
528
|
+
if (marker === EOI) break;
|
|
529
|
+
if (NO_PAYLOAD_MARKERS.has(marker)) continue;
|
|
530
|
+
const segmentLength = readUint16BE(bytes, offset);
|
|
531
|
+
if (marker === APP14 && segmentLength >= 14) adobeTransform = requireByte(bytes, offset + 2 + 11);
|
|
532
|
+
if (SOF_MARKERS.has(marker)) {
|
|
533
|
+
const p = offset + 2;
|
|
534
|
+
const precision = requireByte(bytes, p);
|
|
535
|
+
const height = readUint16BE(bytes, p + 1);
|
|
536
|
+
return {
|
|
537
|
+
width: readUint16BE(bytes, p + 3),
|
|
538
|
+
height,
|
|
539
|
+
components: requireByte(bytes, p + 5),
|
|
540
|
+
precision,
|
|
541
|
+
progressive: PROGRESSIVE_SOF_MARKERS.has(marker),
|
|
542
|
+
adobeTransform
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
offset += segmentLength;
|
|
546
|
+
}
|
|
547
|
+
throw new Error("no SOF marker found in JPEG file");
|
|
548
|
+
}
|
|
549
|
+
//#endregion
|
|
550
|
+
exports.ByteReader = ByteReader;
|
|
551
|
+
exports.ByteWriter = ByteWriter;
|
|
552
|
+
exports.MAX_INFLATE_OUTPUT_BYTES = MAX_INFLATE_OUTPUT_BYTES;
|
|
553
|
+
exports.concatBytes = concatBytes;
|
|
554
|
+
exports.crc32 = crc32;
|
|
555
|
+
exports.decodePng = decodePng;
|
|
556
|
+
exports.deflate = deflate;
|
|
557
|
+
exports.encodePng = encodePng;
|
|
558
|
+
exports.filterScanlines = filterScanlines;
|
|
559
|
+
exports.inflate = inflate;
|
|
560
|
+
exports.inflateTolerant = inflateTolerant;
|
|
561
|
+
exports.isAsciiWhitespace = isAsciiWhitespace;
|
|
562
|
+
exports.readJpegInfo = readJpegInfo;
|
|
563
|
+
exports.unfilterScanlines = unfilterScanlines;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region src/bytes/writer.d.ts
|
|
2
|
+
declare class ByteWriter {
|
|
3
|
+
private readonly chunks;
|
|
4
|
+
private byteLength;
|
|
5
|
+
get length(): number;
|
|
6
|
+
writeBytes(bytes: Uint8Array<ArrayBuffer>): void;
|
|
7
|
+
writeByte(byte: number): void;
|
|
8
|
+
writeAscii(text: string): void;
|
|
9
|
+
toBytes(): Uint8Array<ArrayBuffer>;
|
|
10
|
+
}
|
|
11
|
+
declare function concatBytes(chunks: readonly Uint8Array<ArrayBuffer>[]): Uint8Array<ArrayBuffer>;
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/bytes/reader.d.ts
|
|
14
|
+
declare function isAsciiWhitespace(byte: number | undefined): boolean;
|
|
15
|
+
declare class ByteReader {
|
|
16
|
+
private readonly bytes;
|
|
17
|
+
private position;
|
|
18
|
+
constructor(bytes: Uint8Array<ArrayBuffer>);
|
|
19
|
+
get offset(): number;
|
|
20
|
+
get length(): number;
|
|
21
|
+
atEnd(): boolean;
|
|
22
|
+
peek(aheadBy?: number): number | undefined;
|
|
23
|
+
next(): number | undefined;
|
|
24
|
+
mark(): number;
|
|
25
|
+
reset(mark: number): void;
|
|
26
|
+
seek(offset: number): void;
|
|
27
|
+
slice(start: number, end: number): Uint8Array<ArrayBuffer>;
|
|
28
|
+
skipWhitespace(): void;
|
|
29
|
+
matchKeyword(keyword: string): boolean;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/bytes/crc32.d.ts
|
|
33
|
+
declare function crc32(bytes: Uint8Array<ArrayBuffer>): number;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/bytes/flate.d.ts
|
|
36
|
+
declare const MAX_INFLATE_OUTPUT_BYTES: number;
|
|
37
|
+
type DeflateLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
|
38
|
+
declare function deflate(data: Uint8Array<ArrayBuffer>, level?: DeflateLevel): Uint8Array<ArrayBuffer>;
|
|
39
|
+
declare function inflate(data: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBuffer>;
|
|
40
|
+
interface InflateResult {
|
|
41
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
42
|
+
readonly recovered: boolean;
|
|
43
|
+
}
|
|
44
|
+
declare function inflateTolerant(data: Uint8Array<ArrayBuffer>): InflateResult;
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/image/png-decode.d.ts
|
|
47
|
+
interface RawImage {
|
|
48
|
+
readonly width: number;
|
|
49
|
+
readonly height: number;
|
|
50
|
+
readonly channels: 1 | 3;
|
|
51
|
+
readonly data: Uint8Array<ArrayBuffer>;
|
|
52
|
+
readonly alpha?: Uint8Array<ArrayBuffer>;
|
|
53
|
+
}
|
|
54
|
+
interface PngDecodeOptions {
|
|
55
|
+
readonly onWarning?: (message: string) => void;
|
|
56
|
+
}
|
|
57
|
+
declare function decodePng(bytes: Uint8Array<ArrayBuffer>, options?: PngDecodeOptions): RawImage;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/image/png-encode.d.ts
|
|
60
|
+
interface PngEncodeOptions {
|
|
61
|
+
readonly filter?: 'none' | 'adaptive';
|
|
62
|
+
}
|
|
63
|
+
declare function encodePng(image: RawImage, options?: PngEncodeOptions): Uint8Array<ArrayBuffer>;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/image/png-filter.d.ts
|
|
66
|
+
type PngFilterType = 0 | 1 | 2 | 3 | 4;
|
|
67
|
+
declare function unfilterScanlines(data: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number): Uint8Array<ArrayBuffer>;
|
|
68
|
+
declare function filterScanlines(raw: Uint8Array<ArrayBuffer>, height: number, bytesPerRow: number, bpp: number, strategy?: 'none' | 'adaptive'): Uint8Array<ArrayBuffer>;
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/image/jpeg-info.d.ts
|
|
71
|
+
interface JpegInfo {
|
|
72
|
+
readonly width: number;
|
|
73
|
+
readonly height: number;
|
|
74
|
+
readonly components: number;
|
|
75
|
+
readonly precision: number;
|
|
76
|
+
readonly progressive: boolean;
|
|
77
|
+
readonly adobeTransform: number | undefined;
|
|
78
|
+
}
|
|
79
|
+
declare function readJpegInfo(bytes: Uint8Array<ArrayBuffer>): JpegInfo;
|
|
80
|
+
//#endregion
|
|
81
|
+
export { ByteReader, ByteWriter, DeflateLevel, InflateResult, JpegInfo, MAX_INFLATE_OUTPUT_BYTES, PngDecodeOptions, PngEncodeOptions, PngFilterType, RawImage, concatBytes, crc32, decodePng, deflate, encodePng, filterScanlines, inflate, inflateTolerant, isAsciiWhitespace, readJpegInfo, unfilterScanlines };
|