@ultimat3/core 5.0.1 → 7.0.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/CLAUDE.md +20 -0
- package/README.md +45 -19
- package/package.json +7 -1
- package/src/async-context.ts +79 -0
- package/src/config.ts +30 -13
- package/src/context.ts +13 -8
- package/src/error-codes.ts +1 -0
- package/src/error-reporter-sentry.ts +1 -1
- package/src/image/canvas.ts +170 -0
- package/src/image/errors.ts +35 -3
- package/src/image/pipeline.ts +91 -70
- package/src/image/png-pixels.ts +183 -0
- package/src/impersonate.ts +8 -5
- package/src/index.ts +7 -10
- package/src/logger.ts +1 -1
- package/src/otlp-span-exporter.ts +2 -2
- package/src/roles.ts +1 -1
- package/src/route-vocabulary.ts +23 -0
- package/src/runtime-metrics.ts +1 -1
- package/src/telemetry.ts +7 -4
- package/src/time-zone-name.ts +43 -0
- package/src/type-pins.ts +30 -5
- package/src/image/jpeg-decode.ts +0 -283
- package/src/image/jpeg-encode.ts +0 -463
- package/src/image/jpeg-headers.ts +0 -267
- package/src/image/jpeg-huffman.ts +0 -202
- package/src/image/jpeg-tables.ts +0 -117
- package/src/image/png.ts +0 -433
- package/src/image/resize.ts +0 -320
package/src/image/png.ts
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
// Single responsibility: the PNG codec. Every colour type, bit depth and row filter the format
|
|
2
|
-
// allows decodes into the one 8-bit RGBA raster; encoding has exactly one output shape (colour
|
|
3
|
-
// type 6, adaptive filters). Chunk CRCs are verified on the way in because a decoder that
|
|
4
|
-
// tolerates a corrupt chunk hands the app wrong pixels instead of a coded error.
|
|
5
|
-
|
|
6
|
-
import { imageDecodeFailed, imageUnsupported } from './errors';
|
|
7
|
-
import {
|
|
8
|
-
adler32,
|
|
9
|
-
chunk,
|
|
10
|
-
crc32,
|
|
11
|
-
EMPTY_CHUNK_DATA,
|
|
12
|
-
joinBytes,
|
|
13
|
-
PNG_SIGNATURE,
|
|
14
|
-
paeth,
|
|
15
|
-
readU32,
|
|
16
|
-
unshared,
|
|
17
|
-
writeU32,
|
|
18
|
-
} from './png-bytes';
|
|
19
|
-
import { assertPixelBudget, type Raster, rasterFrom } from './raster';
|
|
20
|
-
|
|
21
|
-
/** Bytes per pixel of the encoder's one output shape, and its filter offset. */
|
|
22
|
-
const RGBA_BPP = 4;
|
|
23
|
-
|
|
24
|
-
/** Samples per pixel, by colour type. A palette row carries one index, not one colour. */
|
|
25
|
-
const CHANNELS = Uint8Array.of(1, 0, 3, 1, 2, 0, 4);
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* PNG spec table 11.1, keyed by colour type. Every legal depth is a power of two, so the set of
|
|
29
|
-
* them masks directly — which is also why a depth like 3 has to be rejected as not one at all.
|
|
30
|
-
*/
|
|
31
|
-
const LEGAL_DEPTHS: Readonly<Record<number, number>> = {
|
|
32
|
-
0: 1 | 2 | 4 | 8 | 16,
|
|
33
|
-
2: 8 | 16,
|
|
34
|
-
3: 1 | 2 | 4 | 8,
|
|
35
|
-
4: 8 | 16,
|
|
36
|
-
6: 8 | 16,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const channelsOf = (colourType: number): number => CHANNELS[colourType] ?? 1;
|
|
40
|
-
|
|
41
|
-
const isLegalShape = (colourType: number, bitDepth: number): boolean =>
|
|
42
|
-
bitDepth !== 0 &&
|
|
43
|
-
(bitDepth & (bitDepth - 1)) === 0 &&
|
|
44
|
-
((LEGAL_DEPTHS[colourType] ?? 0) & bitDepth) === bitDepth;
|
|
45
|
-
|
|
46
|
-
/** Stretches a sub-byte sample across the full range, so depth 1 reads 0/255 rather than 0/1. */
|
|
47
|
-
const UPSCALE: Readonly<Record<number, number>> = { 1: 255, 2: 85, 4: 17 };
|
|
48
|
-
|
|
49
|
-
interface PngHeader {
|
|
50
|
-
readonly width: number;
|
|
51
|
-
readonly height: number;
|
|
52
|
-
readonly bitDepth: number;
|
|
53
|
-
readonly colourType: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function assertSignature(bytes: Uint8Array): void {
|
|
57
|
-
for (let i = 0; i < PNG_SIGNATURE.length; i += 1) {
|
|
58
|
-
if (bytes[i] === PNG_SIGNATURE[i]) continue;
|
|
59
|
-
const found = Array.from(bytes.subarray(0, 8));
|
|
60
|
-
throw imageDecodeFailed(`the bytes are not a PNG: signature reads ${found}`, {
|
|
61
|
-
signature: found,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function readHeader(bytes: Uint8Array, at: number, length: number): PngHeader {
|
|
67
|
-
if (length !== 13) {
|
|
68
|
-
throw imageDecodeFailed(`the PNG IHDR chunk carries ${length} bytes, not the required 13`, {
|
|
69
|
-
length,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
const width = readU32(bytes, at);
|
|
73
|
-
const height = readU32(bytes, at + 4);
|
|
74
|
-
const bitDepth = bytes[at + 8] ?? 0;
|
|
75
|
-
const colourType = bytes[at + 9] ?? 0;
|
|
76
|
-
const compression = bytes[at + 10] ?? 0;
|
|
77
|
-
const filter = bytes[at + 11] ?? 0;
|
|
78
|
-
const interlace = bytes[at + 12] ?? 0;
|
|
79
|
-
if (!isLegalShape(colourType, bitDepth)) {
|
|
80
|
-
throw imageDecodeFailed(
|
|
81
|
-
`the PNG declares colour type ${colourType} at ${bitDepth} bits, a pair the format omits`,
|
|
82
|
-
{ colourType, bitDepth },
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
if (compression !== 0 || filter !== 0 || interlace > 1) {
|
|
86
|
-
throw imageDecodeFailed(
|
|
87
|
-
`the PNG declares compression ${compression}, filter method ${filter}, interlace ` +
|
|
88
|
-
`${interlace}; only 0, 0 and 0-or-1 have ever been defined`,
|
|
89
|
-
{ compression, filter, interlace },
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
// Before a single byte is allocated: the declared size is the only bomb guard that is cheap.
|
|
93
|
-
assertPixelBudget(width, height, 'png');
|
|
94
|
-
if (interlace === 1) {
|
|
95
|
-
throw imageUnsupported(
|
|
96
|
-
'the file is an Adam7 interlaced PNG, which the built-in decoder does not implement',
|
|
97
|
-
'convert the file to a non-interlaced PNG: `convert in.png -interlace none out.png`',
|
|
98
|
-
{ width, height },
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return { width, height, bitDepth, colourType };
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* PNG wraps its deflate stream in zlib, so the 2-byte header and 4-byte Adler-32 trailer are
|
|
106
|
-
* validated and stripped here and the payload is inflated as RAW deflate. `windowBits: -15` states
|
|
107
|
-
* that: Bun's documented default is 15, meaning zlib-wrapped, and a version that starts honouring
|
|
108
|
-
* it would otherwise reject every PNG we read.
|
|
109
|
-
*/
|
|
110
|
-
function inflateIdat(stream: Uint8Array): Uint8Array {
|
|
111
|
-
const cmf = stream[0] ?? 0;
|
|
112
|
-
const flg = stream[1] ?? 0;
|
|
113
|
-
const check = ((cmf << 8) | flg) >>> 0;
|
|
114
|
-
if (stream.length < 6 || (cmf & 0x0f) !== 8 || check % 31 !== 0) {
|
|
115
|
-
throw imageDecodeFailed(
|
|
116
|
-
`the PNG IDAT stream is ${stream.length} bytes opening 0x${check.toString(16)}, not zlib`,
|
|
117
|
-
{ header: check, compressed: stream.length },
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
if ((flg & 0x20) !== 0) {
|
|
121
|
-
throw imageUnsupported(
|
|
122
|
-
'the PNG IDAT stream sets a zlib preset dictionary, which the PNG format forbids',
|
|
123
|
-
're-export the file with a conformant encoder: `convert in.png out.png`',
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
try {
|
|
127
|
-
return Bun.inflateSync(unshared(stream.subarray(2, stream.length - 4)), { windowBits: -15 });
|
|
128
|
-
} catch (error) {
|
|
129
|
-
const why = error instanceof Error ? error.message : String(error);
|
|
130
|
-
throw imageDecodeFailed(`the PNG IDAT stream could not be inflated: ${why}`, {
|
|
131
|
-
compressed: stream.length,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Reverses the per-scanline filter in place, leaving each row's raw bytes where they lay. */
|
|
137
|
-
function unfilter(raw: Uint8Array, height: number, rowBytes: number, bpp: number): void {
|
|
138
|
-
const stride = rowBytes + 1;
|
|
139
|
-
for (let y = 0; y < height; y += 1) {
|
|
140
|
-
const at = y * stride;
|
|
141
|
-
const filter = raw[at] ?? 0;
|
|
142
|
-
const row = at + 1;
|
|
143
|
-
const prev = row - stride;
|
|
144
|
-
const hasPrev = y > 0;
|
|
145
|
-
switch (filter) {
|
|
146
|
-
case 0:
|
|
147
|
-
break;
|
|
148
|
-
case 1: {
|
|
149
|
-
for (let i = bpp; i < rowBytes; i += 1) {
|
|
150
|
-
raw[row + i] = ((raw[row + i] ?? 0) + (raw[row + i - bpp] ?? 0)) & 0xff;
|
|
151
|
-
}
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
case 2: {
|
|
155
|
-
if (!hasPrev) break;
|
|
156
|
-
for (let i = 0; i < rowBytes; i += 1) {
|
|
157
|
-
raw[row + i] = ((raw[row + i] ?? 0) + (raw[prev + i] ?? 0)) & 0xff;
|
|
158
|
-
}
|
|
159
|
-
break;
|
|
160
|
-
}
|
|
161
|
-
// Average and Paeth read the same three neighbours; only the predictor differs.
|
|
162
|
-
case 3:
|
|
163
|
-
case 4: {
|
|
164
|
-
for (let i = 0; i < rowBytes; i += 1) {
|
|
165
|
-
const left = i >= bpp ? (raw[row + i - bpp] ?? 0) : 0;
|
|
166
|
-
const up = hasPrev ? (raw[prev + i] ?? 0) : 0;
|
|
167
|
-
const upLeft = hasPrev && i >= bpp ? (raw[prev + i - bpp] ?? 0) : 0;
|
|
168
|
-
const guess = filter === 3 ? (left + up) >> 1 : paeth(left, up, upLeft);
|
|
169
|
-
raw[row + i] = ((raw[row + i] ?? 0) + guess) & 0xff;
|
|
170
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
default:
|
|
174
|
-
throw imageDecodeFailed(
|
|
175
|
-
`PNG scanline ${y} declares filter type ${filter}, which is not one of 0-4`,
|
|
176
|
-
{ row: y, filter },
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** One scanline of unfiltered bytes into whole samples, at whatever precision the file uses. */
|
|
183
|
-
function readSamples(raw: Uint8Array, at: number, out: Uint16Array, bitDepth: number): void {
|
|
184
|
-
const count = out.length;
|
|
185
|
-
if (bitDepth === 8) {
|
|
186
|
-
for (let i = 0; i < count; i += 1) out[i] = raw[at + i] ?? 0;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (bitDepth === 16) {
|
|
190
|
-
for (let i = 0; i < count; i += 1) {
|
|
191
|
-
out[i] = ((raw[at + i * 2] ?? 0) << 8) | (raw[at + i * 2 + 1] ?? 0);
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const perByte = 8 / bitDepth;
|
|
196
|
-
const mask = (1 << bitDepth) - 1;
|
|
197
|
-
for (let i = 0; i < count; i += 1) {
|
|
198
|
-
const byte = raw[at + ((i / perByte) | 0)] ?? 0;
|
|
199
|
-
out[i] = (byte >> (8 - bitDepth * ((i % perByte) + 1))) & mask;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* `tRNS` on a greyscale or truecolour image is not a table: it names ONE sample value that is
|
|
205
|
-
* fully transparent, at the image's own bit depth. Ignoring it drops a logo's cut-out.
|
|
206
|
-
*/
|
|
207
|
-
function transparentKey(colourType: number, trns: Uint8Array | undefined): Uint16Array | undefined {
|
|
208
|
-
if (trns === undefined || (colourType !== 0 && colourType !== 2)) return undefined;
|
|
209
|
-
const samples = colourType === 2 ? 3 : 1;
|
|
210
|
-
if (trns.length < samples * 2) return undefined;
|
|
211
|
-
const key = new Uint16Array(samples);
|
|
212
|
-
for (let i = 0; i < samples; i += 1) {
|
|
213
|
-
key[i] = ((trns[i * 2] ?? 0) << 8) | (trns[i * 2 + 1] ?? 0);
|
|
214
|
-
}
|
|
215
|
-
return key;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Which sample of a pixel feeds R, G, B and A, by colour type; `-1` is "no alpha sample". Stating
|
|
220
|
-
* the mapping once stops four colour types from becoming four loops that can each be wrong.
|
|
221
|
-
*/
|
|
222
|
-
const RGBA_SOURCE: Readonly<Record<number, readonly [number, number, number, number]>> = {
|
|
223
|
-
0: [0, 0, 0, -1],
|
|
224
|
-
2: [0, 1, 2, -1],
|
|
225
|
-
4: [0, 0, 0, 1],
|
|
226
|
-
6: [0, 1, 2, 3],
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
/** Opaque unless a `tRNS` key colour matches this pixel's samples exactly. */
|
|
230
|
-
function opacityFor(samples: Uint16Array, at: number, key: Uint16Array | undefined): number {
|
|
231
|
-
if (key === undefined) return 255;
|
|
232
|
-
for (let i = 0; i < key.length; i += 1) {
|
|
233
|
-
if (samples[at + i] !== key[i]) return 255;
|
|
234
|
-
}
|
|
235
|
-
return 0;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function expand(
|
|
239
|
-
raw: Uint8Array,
|
|
240
|
-
header: PngHeader,
|
|
241
|
-
palette: Uint8Array | undefined,
|
|
242
|
-
trns: Uint8Array | undefined,
|
|
243
|
-
): Uint8ClampedArray {
|
|
244
|
-
const { width, height, bitDepth, colourType } = header;
|
|
245
|
-
if (colourType === 3 && palette === undefined) {
|
|
246
|
-
throw imageDecodeFailed('the PNG is indexed colour but carries no PLTE chunk');
|
|
247
|
-
}
|
|
248
|
-
const plte = palette ?? EMPTY_CHUNK_DATA;
|
|
249
|
-
const entries = (plte.length / 3) | 0;
|
|
250
|
-
const channels = channelsOf(colourType);
|
|
251
|
-
const stride = Math.ceil((width * channels * bitDepth) / 8) + 1;
|
|
252
|
-
const upscale = UPSCALE[bitDepth] ?? 1;
|
|
253
|
-
const key = transparentKey(colourType, trns);
|
|
254
|
-
const samples = new Uint16Array(width * channels);
|
|
255
|
-
const pixels = new Uint8ClampedArray(width * height * 4);
|
|
256
|
-
const [sr, sg, sb, sa] = RGBA_SOURCE[colourType] ?? [0, 0, 0, -1];
|
|
257
|
-
const byteOf = (sample: number): number => (bitDepth === 16 ? sample >>> 8 : sample * upscale);
|
|
258
|
-
|
|
259
|
-
for (let y = 0; y < height; y += 1) {
|
|
260
|
-
readSamples(raw, y * stride + 1, samples, bitDepth);
|
|
261
|
-
let p = y * width * 4;
|
|
262
|
-
for (let x = 0; x < width; x += 1) {
|
|
263
|
-
const s = x * channels;
|
|
264
|
-
if (colourType === 3) {
|
|
265
|
-
const index = samples[s] ?? 0;
|
|
266
|
-
if (index >= entries) {
|
|
267
|
-
throw imageDecodeFailed(
|
|
268
|
-
`PNG pixel ${x},${y} uses palette index ${index} but PLTE holds ${entries} entries`,
|
|
269
|
-
{ x, y, index, entries },
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
pixels[p] = plte[index * 3] ?? 0;
|
|
273
|
-
pixels[p + 1] = plte[index * 3 + 1] ?? 0;
|
|
274
|
-
pixels[p + 2] = plte[index * 3 + 2] ?? 0;
|
|
275
|
-
pixels[p + 3] = trns !== undefined && index < trns.length ? (trns[index] ?? 255) : 255;
|
|
276
|
-
} else {
|
|
277
|
-
pixels[p] = byteOf(samples[s + sr] ?? 0);
|
|
278
|
-
pixels[p + 1] = byteOf(samples[s + sg] ?? 0);
|
|
279
|
-
pixels[p + 2] = byteOf(samples[s + sb] ?? 0);
|
|
280
|
-
pixels[p + 3] = sa >= 0 ? byteOf(samples[s + sa] ?? 0) : opacityFor(samples, s, key);
|
|
281
|
-
}
|
|
282
|
-
p += 4;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
return pixels;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** PNG bytes in, RGBA out. Every chunk is CRC-checked before a single pixel is believed. */
|
|
289
|
-
export function decodePng(bytes: Uint8Array): Raster {
|
|
290
|
-
assertSignature(bytes);
|
|
291
|
-
let header: PngHeader | undefined;
|
|
292
|
-
let palette: Uint8Array | undefined;
|
|
293
|
-
let trns: Uint8Array | undefined;
|
|
294
|
-
const idat: Uint8Array[] = [];
|
|
295
|
-
let ended = false;
|
|
296
|
-
let offset = 8;
|
|
297
|
-
|
|
298
|
-
while (offset + 8 <= bytes.length) {
|
|
299
|
-
const length = readU32(bytes, offset);
|
|
300
|
-
const type = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
|
|
301
|
-
const dataAt = offset + 8;
|
|
302
|
-
const crcAt = dataAt + length;
|
|
303
|
-
if (crcAt + 4 > bytes.length) {
|
|
304
|
-
throw imageDecodeFailed(
|
|
305
|
-
`the PNG ends after ${bytes.length} bytes, inside chunk ${type} at offset ${offset}`,
|
|
306
|
-
{ chunk: type, offset, declared: length },
|
|
307
|
-
);
|
|
308
|
-
}
|
|
309
|
-
const declared = readU32(bytes, crcAt);
|
|
310
|
-
const actual = crc32(bytes, offset + 4, crcAt);
|
|
311
|
-
if (declared !== actual) {
|
|
312
|
-
throw imageDecodeFailed(
|
|
313
|
-
`PNG chunk ${type} fails its CRC-32: the file says ${declared}, the bytes hash to ${actual}`,
|
|
314
|
-
{ chunk: type, declared, actual },
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
if (header === undefined && type !== 'IHDR') {
|
|
318
|
-
throw imageDecodeFailed(`the first PNG chunk is ${type}, not IHDR`, { chunk: type });
|
|
319
|
-
}
|
|
320
|
-
if (type === 'IHDR') {
|
|
321
|
-
if (header !== undefined) throw imageDecodeFailed('the PNG carries more than one IHDR chunk');
|
|
322
|
-
header = readHeader(bytes, dataAt, length);
|
|
323
|
-
} else if (type === 'PLTE') {
|
|
324
|
-
palette = bytes.subarray(dataAt, crcAt);
|
|
325
|
-
} else if (type === 'tRNS') {
|
|
326
|
-
trns = bytes.subarray(dataAt, crcAt);
|
|
327
|
-
} else if (type === 'IDAT') {
|
|
328
|
-
idat.push(bytes.subarray(dataAt, crcAt));
|
|
329
|
-
} else if (type === 'IEND') {
|
|
330
|
-
ended = true;
|
|
331
|
-
break;
|
|
332
|
-
}
|
|
333
|
-
offset = crcAt + 4;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (!ended) throw imageDecodeFailed('the PNG never reaches IEND, so the file is truncated');
|
|
337
|
-
if (header === undefined) throw imageDecodeFailed('the PNG carries no IHDR chunk');
|
|
338
|
-
if (idat.length === 0)
|
|
339
|
-
throw imageDecodeFailed('the PNG carries no IDAT chunk, so it has no rows');
|
|
340
|
-
|
|
341
|
-
const { width, height, bitDepth, colourType } = header;
|
|
342
|
-
const channels = channelsOf(colourType);
|
|
343
|
-
const rowBytes = Math.ceil((width * channels * bitDepth) / 8);
|
|
344
|
-
const expected = height * (rowBytes + 1);
|
|
345
|
-
const raw = inflateIdat(joinBytes(idat));
|
|
346
|
-
if (raw.length !== expected) {
|
|
347
|
-
throw imageDecodeFailed(
|
|
348
|
-
`the PNG inflates to ${raw.length} bytes but ${width}x${height} at ${bitDepth} bits over ` +
|
|
349
|
-
`${channels} channels needs exactly ${expected}`,
|
|
350
|
-
{ inflated: raw.length, expected },
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
unfilter(raw, height, rowBytes, Math.max(1, Math.ceil((bitDepth * channels) / 8)));
|
|
354
|
-
return rasterFrom(width, height, expand(raw, header, palette, trns));
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Filters one scanline into `out`, scored by the libpng heuristic: the sum of its bytes read as
|
|
359
|
-
* signed. The lowest sum is the row deflate compresses best, which is why an encoder that always
|
|
360
|
-
* wrote filter 0 would ship files roughly twice this size. A negative `prev` is "no row above".
|
|
361
|
-
*/
|
|
362
|
-
function filterScanline(
|
|
363
|
-
pixels: Uint8ClampedArray,
|
|
364
|
-
row: number,
|
|
365
|
-
prev: number,
|
|
366
|
-
stride: number,
|
|
367
|
-
filter: number,
|
|
368
|
-
out: Uint8Array,
|
|
369
|
-
): number {
|
|
370
|
-
let score = 0;
|
|
371
|
-
const hasPrev = prev >= 0;
|
|
372
|
-
for (let i = 0; i < stride; i += 1) {
|
|
373
|
-
const raw = pixels[row + i] ?? 0;
|
|
374
|
-
const left = i >= RGBA_BPP ? (pixels[row + i - RGBA_BPP] ?? 0) : 0;
|
|
375
|
-
const up = hasPrev ? (pixels[prev + i] ?? 0) : 0;
|
|
376
|
-
let value = raw;
|
|
377
|
-
if (filter === 1) value = raw - left;
|
|
378
|
-
else if (filter === 2) value = raw - up;
|
|
379
|
-
else if (filter === 3) value = raw - ((left + up) >> 1);
|
|
380
|
-
else if (filter === 4) {
|
|
381
|
-
const upLeft = hasPrev && i >= RGBA_BPP ? (pixels[prev + i - RGBA_BPP] ?? 0) : 0;
|
|
382
|
-
value = raw - paeth(left, up, upLeft);
|
|
383
|
-
}
|
|
384
|
-
value &= 0xff;
|
|
385
|
-
out[i] = value;
|
|
386
|
-
score += value < 128 ? value : 256 - value;
|
|
387
|
-
}
|
|
388
|
-
return score;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* RGBA in, PNG bytes out — always 8-bit colour type 6, non-interlaced. Branching on opacity would
|
|
393
|
-
* give the framework two encoders to keep correct, and alpha on an opaque image is nearly free
|
|
394
|
-
* after deflate, so there is one path.
|
|
395
|
-
*/
|
|
396
|
-
export function encodePng(raster: Raster): Uint8Array {
|
|
397
|
-
const { width, height, pixels } = raster;
|
|
398
|
-
const stride = width * 4;
|
|
399
|
-
const filtered = new Uint8Array(height * (stride + 1));
|
|
400
|
-
const scratch = new Uint8Array(stride);
|
|
401
|
-
for (let y = 0; y < height; y += 1) {
|
|
402
|
-
const row = y * stride;
|
|
403
|
-
const at = y * (stride + 1);
|
|
404
|
-
let best = Number.POSITIVE_INFINITY;
|
|
405
|
-
for (let filter = 0; filter <= 4; filter += 1) {
|
|
406
|
-
const score = filterScanline(pixels, row, y > 0 ? row - stride : -1, stride, filter, scratch);
|
|
407
|
-
if (score >= best) continue;
|
|
408
|
-
best = score;
|
|
409
|
-
filtered[at] = filter;
|
|
410
|
-
filtered.set(scratch, at + 1);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// `windowBits: -15` asks for RAW deflate explicitly, because the zlib envelope PNG requires is
|
|
415
|
-
// written by hand below — leaving it to the default would double-wrap the stream the day Bun's
|
|
416
|
-
// documented default (15, zlib-wrapped) is the one that actually applies.
|
|
417
|
-
const deflated = Bun.deflateSync(filtered, { windowBits: -15 });
|
|
418
|
-
const idat = new Uint8Array(deflated.length + 6);
|
|
419
|
-
idat.set([0x78, 0x01]);
|
|
420
|
-
idat.set(deflated, 2);
|
|
421
|
-
writeU32(idat, deflated.length + 2, adler32(filtered));
|
|
422
|
-
|
|
423
|
-
const ihdr = new Uint8Array(13);
|
|
424
|
-
writeU32(ihdr, 0, width);
|
|
425
|
-
writeU32(ihdr, 4, height);
|
|
426
|
-
ihdr.set([8, 6], 8);
|
|
427
|
-
return joinBytes([
|
|
428
|
-
PNG_SIGNATURE,
|
|
429
|
-
chunk('IHDR', ihdr),
|
|
430
|
-
chunk('IDAT', idat),
|
|
431
|
-
chunk('IEND', EMPTY_CHUNK_DATA),
|
|
432
|
-
]);
|
|
433
|
-
}
|