@ultimat3/core 5.0.0 → 6.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.
@@ -1,283 +0,0 @@
1
- // Single responsibility: turning a baseline or extended sequential Huffman JPEG (SOF0/SOF1) into
2
- // RGBA — the marker walk, the entropy-coded scan, the inverse DCT and the sample planes. What each
3
- // segment DECLARES, and which codings are refused by name, is `jpeg-headers.ts`; this file is the
4
- // algorithm those declarations describe.
5
-
6
- import { imageDecodeFailed } from './errors';
7
- import {
8
- assertSupportedCoding,
9
- type Component,
10
- type Frame,
11
- hex,
12
- isAdobe,
13
- readFrame,
14
- readQuantTables,
15
- readScanHeader,
16
- readU16,
17
- type ScanComponent,
18
- } from './jpeg-headers';
19
- import { type HuffmanTable, JpegBitReader, readHuffmanTables } from './jpeg-huffman';
20
- import { ycbcrToRgb, ZIGZAG } from './jpeg-tables';
21
- import { type Raster, rasterFrom } from './raster';
22
-
23
- const SQRT2 = Math.SQRT2;
24
-
25
- /** Reused across every block of every image: the decoder is synchronous and single-threaded. */
26
- const COEF = new Int32Array(64);
27
- const WORK = new Float32Array(64);
28
-
29
- /** libjpeg's AAN float butterfly, in place over the 8 samples `step` apart from `base`. */
30
- function idct1d(v: Float32Array, base: number, step: number): void {
31
- const s0 = v[base] ?? 0;
32
- const s1 = v[base + step] ?? 0;
33
- const s2 = v[base + step * 2] ?? 0;
34
- const s3 = v[base + step * 3] ?? 0;
35
- const s4 = v[base + step * 4] ?? 0;
36
- const s5 = v[base + step * 5] ?? 0;
37
- const s6 = v[base + step * 6] ?? 0;
38
- const s7 = v[base + step * 7] ?? 0;
39
- const e10 = s0 + s4;
40
- const e11 = s0 - s4;
41
- const e13 = s2 + s6;
42
- const e12 = (s2 - s6) * SQRT2 - e13;
43
- const t0 = e10 + e13;
44
- const t3 = e10 - e13;
45
- const t1 = e11 + e12;
46
- const t2 = e11 - e12;
47
- const z13 = s5 + s3;
48
- const z10 = s5 - s3;
49
- const z11 = s1 + s7;
50
- const z12 = s1 - s7;
51
- const t7 = z11 + z13;
52
- const t11 = (z11 - z13) * SQRT2;
53
- const z5 = (z10 + z12) * 1.847759065;
54
- const t10 = 1.0823922 * z12 - z5;
55
- const t12 = -2.61312593 * z10 + z5;
56
- const t6 = t12 - t7;
57
- const t5 = t11 - t6;
58
- const t4 = t10 + t5;
59
- v[base] = t0 + t7;
60
- v[base + step * 7] = t0 - t7;
61
- v[base + step] = t1 + t6;
62
- v[base + step * 6] = t1 - t6;
63
- v[base + step * 2] = t2 + t5;
64
- v[base + step * 5] = t2 - t5;
65
- v[base + step * 4] = t3 + t4;
66
- v[base + step * 3] = t3 - t4;
67
- }
68
-
69
- /** `Uint8ClampedArray` is the level shift: it rounds and clamps to 0-255 on every store. */
70
- function writeBlock(comp: Component, at: number, flat: boolean): void {
71
- const { samples, stride } = comp;
72
- if (flat) {
73
- const value = (COEF[0] ?? 0) * (comp.dequant[0] ?? 0) + 128;
74
- for (let r = 0; r < 8; r += 1) samples.fill(value, at + r * stride, at + r * stride + 8);
75
- return;
76
- }
77
- for (let i = 0; i < 64; i += 1) WORK[i] = (COEF[i] ?? 0) * (comp.dequant[i] ?? 0);
78
- for (let c = 0; c < 8; c += 1) idct1d(WORK, c, 8);
79
- for (let r = 0; r < 8; r += 1) idct1d(WORK, r * 8, 1);
80
- for (let r = 0; r < 8; r += 1) {
81
- const row = at + r * stride;
82
- for (let c = 0; c < 8; c += 1) samples[row + c] = (WORK[r * 8 + c] ?? 0) + 128;
83
- }
84
- }
85
-
86
- function decodeBlock(reader: JpegBitReader, scan: ScanComponent, row: number, col: number): void {
87
- const { comp } = scan;
88
- if (row >= comp.blocksPerColumn || col >= comp.blocksPerLine) {
89
- throw imageDecodeFailed(`block ${col},${row} falls outside component ${comp.id}`, { row, col });
90
- }
91
- COEF.fill(0);
92
- const size = reader.decode(scan.dc);
93
- if (size > 15) {
94
- throw imageDecodeFailed(`a DC coefficient claims ${size} magnitude bits, over the 15 allowed`);
95
- }
96
- comp.pred += reader.receiveAndExtend(size);
97
- COEF[0] = comp.pred;
98
- let k = 1;
99
- let last = 0;
100
- while (k < 64) {
101
- const rs = reader.decode(scan.ac);
102
- const bits = rs & 15;
103
- const run = rs >> 4;
104
- if (bits === 0) {
105
- if (run !== 15) break; // 0x00 is end-of-block; 0xF0 is a run of 16 zeros
106
- k += 16;
107
- continue;
108
- }
109
- k += run;
110
- if (k > 63) {
111
- throw imageDecodeFailed(`an AC run overruns block ${col},${row} of component ${comp.id}`, {
112
- row,
113
- col,
114
- });
115
- }
116
- COEF[ZIGZAG[k] ?? 0] = reader.receiveAndExtend(bits);
117
- last = k;
118
- k += 1;
119
- }
120
- writeBlock(comp, row * 8 * comp.stride + col * 8, last === 0);
121
- }
122
-
123
- /** Leaves the walk at the next marker, or at the end when the file carries no EOI. */
124
- function skipToMarker(bytes: Uint8Array, from: number): number {
125
- for (let at = from; at + 1 < bytes.length; at += 1) {
126
- if ((bytes[at] ?? 0) === 0xff && (bytes[at + 1] ?? 0) !== 0x00) return at;
127
- }
128
- return bytes.length;
129
- }
130
-
131
- /** One scan's entropy-coded data, from the byte after its header to the marker that ends it. */
132
- function decodeScan(
133
- bytes: Uint8Array,
134
- start: number,
135
- seg: Uint8Array,
136
- frame: Frame,
137
- quant: ReadonlyArray<Float32Array | undefined>,
138
- dcTables: ReadonlyArray<HuffmanTable | undefined>,
139
- acTables: ReadonlyArray<HuffmanTable | undefined>,
140
- restartInterval: number,
141
- ): number {
142
- const scan: readonly ScanComponent[] = readScanHeader(seg, frame, quant, dcTables, acTables);
143
- const reader = new JpegBitReader(bytes, start);
144
- const single = scan.length === 1 ? scan[0] : undefined;
145
- // A non-interleaved scan walks the component's own blocks, which for a subsampled component is
146
- // fewer than its MCU-padded plane holds; an interleaved one walks whole MCUs.
147
- const perLine =
148
- single === undefined
149
- ? frame.mcusPerLine
150
- : Math.ceil(Math.ceil((frame.width * single.comp.h) / frame.maxH) / 8);
151
- const perColumn =
152
- single === undefined
153
- ? frame.mcusPerColumn
154
- : Math.ceil(Math.ceil((frame.height * single.comp.v) / frame.maxV) / 8);
155
- for (let n = 0; n < perLine * perColumn; n += 1) {
156
- if (restartInterval > 0 && n > 0 && n % restartInterval === 0) {
157
- if (!reader.restart()) {
158
- throw imageDecodeFailed(`the scan omits the restart marker due after ${n} units`, { n });
159
- }
160
- for (const entry of scan) entry.comp.pred = 0;
161
- }
162
- const row = (n / perLine) | 0;
163
- const col = n % perLine;
164
- if (single !== undefined) {
165
- decodeBlock(reader, single, row, col);
166
- continue;
167
- }
168
- for (const entry of scan) {
169
- for (let v = 0; v < entry.comp.v; v += 1) {
170
- for (let h = 0; h < entry.comp.h; h += 1) {
171
- decodeBlock(reader, entry, row * entry.comp.v + v, col * entry.comp.h + h);
172
- }
173
- }
174
- }
175
- }
176
- return skipToMarker(bytes, reader.position);
177
- }
178
-
179
- /**
180
- * Sample planes to RGBA, cropped to the declared size: the MCU-padded edge columns and rows exist
181
- * only so the last block is whole, and a decoder that returns them reports the wrong dimensions.
182
- * Chroma is upsampled by replication, which is what `h`/`v` below the maxima mean.
183
- */
184
- function toRaster(frame: Frame, adobeTransform: number): Raster {
185
- const { width, height, components, maxH, maxV } = frame;
186
- const pixels = new Uint8ClampedArray(width * height * 4);
187
- const luma = components[0];
188
- if (luma === undefined) throw imageDecodeFailed('the frame declares no components');
189
- const cb = components[1];
190
- const cr = components[2];
191
- // Adobe transform 0 over three components means the samples already ARE R, G and B.
192
- const alreadyRgb = adobeTransform === 0;
193
- for (let y = 0; y < height; y += 1) {
194
- const lumaRow = (((y * luma.v) / maxV) | 0) * luma.stride;
195
- let out = y * width * 4;
196
- if (cb === undefined || cr === undefined) {
197
- for (let x = 0; x < width; x += 1) {
198
- const grey = luma.samples[lumaRow + (((x * luma.h) / maxH) | 0)] ?? 0;
199
- pixels[out] = grey;
200
- pixels[out + 1] = grey;
201
- pixels[out + 2] = grey;
202
- pixels[out + 3] = 255;
203
- out += 4;
204
- }
205
- continue;
206
- }
207
- const cbRow = (((y * cb.v) / maxV) | 0) * cb.stride;
208
- const crRow = (((y * cr.v) / maxV) | 0) * cr.stride;
209
- for (let x = 0; x < width; x += 1) {
210
- const a = luma.samples[lumaRow + (((x * luma.h) / maxH) | 0)] ?? 0;
211
- const b = cb.samples[cbRow + (((x * cb.h) / maxH) | 0)] ?? 0;
212
- const c = cr.samples[crRow + (((x * cr.h) / maxH) | 0)] ?? 0;
213
- if (alreadyRgb) {
214
- pixels[out] = a;
215
- pixels[out + 1] = b;
216
- pixels[out + 2] = c;
217
- } else {
218
- const [r, g, blue] = ycbcrToRgb(a, b, c);
219
- pixels[out] = r;
220
- pixels[out + 1] = g;
221
- pixels[out + 2] = blue;
222
- }
223
- pixels[out + 3] = 255;
224
- out += 4;
225
- }
226
- }
227
- return rasterFrom(width, height, pixels);
228
- }
229
-
230
- /** JPEG bytes to RGBA. Baseline and extended sequential only; everything else is named and refused. */
231
- export function decodeJpeg(bytes: Uint8Array): Raster {
232
- if ((bytes[0] ?? 0) !== 0xff || (bytes[1] ?? 0) !== 0xd8) {
233
- throw imageDecodeFailed('the bytes do not open with a JPEG SOI marker (FF D8)', {
234
- first: `${hex(bytes[0])} ${hex(bytes[1])}`,
235
- });
236
- }
237
- const quant: Array<Float32Array | undefined> = [];
238
- const dcTables: Array<HuffmanTable | undefined> = [];
239
- const acTables: Array<HuffmanTable | undefined> = [];
240
- let frame: Frame | undefined;
241
- let restartInterval = 0;
242
- let adobeTransform = -1;
243
- let offset = 2;
244
- while (offset + 1 < bytes.length) {
245
- if ((bytes[offset] ?? 0) !== 0xff) {
246
- throw imageDecodeFailed(
247
- `expected a marker at byte ${offset}, found 0x${hex(bytes[offset])}`,
248
- {
249
- offset,
250
- },
251
- );
252
- }
253
- while (bytes[offset + 1] === 0xff) offset += 1; // fill bytes between segments
254
- const marker = bytes[offset + 1];
255
- if (marker === undefined) throw imageDecodeFailed('the file ends inside a marker');
256
- offset += 2;
257
- if (marker === 0xd9) break; // EOI
258
- if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) continue; // no payload
259
- assertSupportedCoding(marker);
260
- const length = readU16(bytes, offset);
261
- if (length < 2 || offset + length > bytes.length) {
262
- throw imageDecodeFailed(
263
- `segment FF${hex(marker)} declares ${length} bytes but ${bytes.length - offset} remain`,
264
- { marker: `FF${hex(marker)}`, length },
265
- );
266
- }
267
- const seg = bytes.subarray(offset + 2, offset + length);
268
- offset += length;
269
- if (marker === 0xdb) readQuantTables(seg, quant);
270
- else if (marker === 0xc4) readHuffmanTables(seg, dcTables, acTables);
271
- else if (marker === 0xc0 || marker === 0xc1) frame = readFrame(seg, marker);
272
- else if (marker === 0xdd) restartInterval = readU16(seg, 0);
273
- else if (marker === 0xee && isAdobe(seg)) adobeTransform = seg[11] ?? adobeTransform;
274
- else if (marker === 0xda) {
275
- if (frame === undefined) {
276
- throw imageDecodeFailed('a scan (SOS) arrives before any frame header (SOF)');
277
- }
278
- offset = decodeScan(bytes, offset, seg, frame, quant, dcTables, acTables, restartInterval);
279
- }
280
- }
281
- if (frame === undefined) throw imageDecodeFailed('the file carries no frame header (SOF)');
282
- return toRaster(frame, adobeTransform);
283
- }