@ultimat3/core 1.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.
@@ -0,0 +1,267 @@
1
+ // Single responsibility: what a JPEG's descriptive segments DECLARE, and what they are refused for
2
+ // declaring — the DQT tables, the SOF geometry, the SOS component selection, and every coding
3
+ // flavour this decoder names rather than guesses at. No entropy decoding and no pixels: that half
4
+ // is `jpeg-decode.ts`, and keeping the two apart is what keeps either inside one file's worth of job.
5
+
6
+ import { imageDecodeFailed, imageUnsupported } from './errors';
7
+ import type { HuffmanTable } from './jpeg-huffman';
8
+ import { AAN_SCALE, ZIGZAG } from './jpeg-tables';
9
+ import { assertPixelBudget } from './raster';
10
+
11
+ export const BASELINE_FIX =
12
+ 're-encode to baseline JPEG: `convert in.jpg -interlace none baseline.jpg`';
13
+
14
+ export const readU16 = (bytes: Uint8Array, at: number): number =>
15
+ ((bytes[at] ?? 0) << 8) | (bytes[at + 1] ?? 0);
16
+
17
+ export const hex = (value: number | undefined): string =>
18
+ (value ?? 0).toString(16).toUpperCase().padStart(2, '0');
19
+
20
+ /** Every SOF flavour this decoder refuses, named the way a re-encoding tool names it. */
21
+ const REFUSED_SOF: Readonly<Record<number, string>> = {
22
+ 194: 'SOF2 progressive DCT',
23
+ 195: 'SOF3 lossless',
24
+ 197: 'SOF5 differential sequential DCT',
25
+ 198: 'SOF6 differential progressive DCT',
26
+ 199: 'SOF7 differential lossless',
27
+ 201: 'SOF9 extended sequential DCT, arithmetic coding',
28
+ 202: 'SOF10 progressive DCT, arithmetic coding',
29
+ 203: 'SOF11 lossless, arithmetic coding',
30
+ 204: 'DAC arithmetic coding conditioning',
31
+ 205: 'SOF13 differential sequential DCT, arithmetic coding',
32
+ 206: 'SOF14 differential progressive DCT, arithmetic coding',
33
+ 207: 'SOF15 differential lossless, arithmetic coding',
34
+ };
35
+
36
+ /** Refused at the marker, before a single coefficient is read: the name is the whole point. */
37
+ export function assertSupportedCoding(marker: number): void {
38
+ const refused = REFUSED_SOF[marker];
39
+ if (refused === undefined) return;
40
+ throw imageUnsupported(`this JPEG is ${refused}, which is not baseline`, BASELINE_FIX, {
41
+ marker: `FF${hex(marker)}`,
42
+ });
43
+ }
44
+
45
+ /** APP14 payload `Adobe` + version + two flag words, then the colour transform at byte 11. */
46
+ const ADOBE = [0x41, 0x64, 0x6f, 0x62, 0x65] as const;
47
+ export const isAdobe = (seg: Uint8Array): boolean =>
48
+ seg.length >= 12 && ADOBE.every((byte, i) => seg[i] === byte);
49
+
50
+ export interface Component {
51
+ readonly id: number;
52
+ readonly h: number;
53
+ readonly v: number;
54
+ readonly quantId: number;
55
+ /** MCU-padded sample plane; `stride` exceeds the image width whenever `h < maxH`. */
56
+ readonly samples: Uint8ClampedArray;
57
+ readonly stride: number;
58
+ readonly blocksPerLine: number;
59
+ readonly blocksPerColumn: number;
60
+ dequant: Float32Array;
61
+ /** The running DC predictor, reset at every restart interval and at every scan. */
62
+ pred: number;
63
+ }
64
+
65
+ export interface Frame {
66
+ readonly width: number;
67
+ readonly height: number;
68
+ readonly components: readonly Component[];
69
+ readonly maxH: number;
70
+ readonly maxV: number;
71
+ readonly mcusPerLine: number;
72
+ readonly mcusPerColumn: number;
73
+ }
74
+
75
+ /** One component as this scan selects it — resolved once, so no block decode re-checks a table. */
76
+ export interface ScanComponent {
77
+ readonly comp: Component;
78
+ readonly dc: HuffmanTable;
79
+ readonly ac: HuffmanTable;
80
+ }
81
+
82
+ /**
83
+ * The shared AAN scale factors, folded into the quantisation table at DQT time along with the 1/8
84
+ * the two-dimensional inverse owes. That is what lets `idct1d` be 11 multiplies per row instead of
85
+ * the 64 a direct evaluation costs, without a single scaling step in the per-block hot path.
86
+ * `Float32Array` because every value it multiplies is float32 — the widths must match, not the
87
+ * numbers only.
88
+ */
89
+ const AAN = Float32Array.from(AAN_SCALE);
90
+
91
+ /** DQT carries any number of tables; 16-bit precision doubles each entry. */
92
+ export function readQuantTables(seg: Uint8Array, quant: Array<Float32Array | undefined>): void {
93
+ let at = 0;
94
+ while (at < seg.length) {
95
+ const spec = seg[at] ?? 0;
96
+ at += 1;
97
+ const precision = spec >> 4;
98
+ const id = spec & 15;
99
+ if (precision > 1 || id > 3) {
100
+ throw imageDecodeFailed(`DQT declares table ${id} with precision code ${precision}`, {
101
+ id,
102
+ precision,
103
+ });
104
+ }
105
+ const size = precision === 1 ? 2 : 1;
106
+ if (at + 64 * size > seg.length) {
107
+ throw imageDecodeFailed(`DQT table ${id} is truncated: it needs ${64 * size} more bytes`, {
108
+ id,
109
+ });
110
+ }
111
+ const table = new Float32Array(64);
112
+ for (let k = 0; k < 64; k += 1) {
113
+ const value = size === 2 ? readU16(seg, at) : (seg[at] ?? 0);
114
+ at += size;
115
+ const natural = ZIGZAG[k] ?? 0;
116
+ table[natural] = value * (AAN[natural >> 3] ?? 1) * (AAN[natural & 7] ?? 1) * 0.125;
117
+ }
118
+ quant[id] = table;
119
+ }
120
+ }
121
+
122
+ export function readFrame(seg: Uint8Array, marker: number): Frame {
123
+ if (seg.length < 6) throw imageDecodeFailed('the frame header (SOF) is shorter than its fields');
124
+ const precision = seg[0] ?? 0;
125
+ if (precision !== 8) {
126
+ throw imageUnsupported(
127
+ `this JPEG codes ${precision}-bit samples; only 8-bit precision is decoded`,
128
+ BASELINE_FIX,
129
+ { precision, marker: `FF${hex(marker)}` },
130
+ );
131
+ }
132
+ const height = readU16(seg, 1);
133
+ const width = readU16(seg, 3);
134
+ assertPixelBudget(width, height, 'jpeg');
135
+ const count = seg[5] ?? 0;
136
+ if (count !== 1 && count !== 3) {
137
+ const model = count === 4 ? 'CMYK or YCCK' : 'an unknown colour model';
138
+ throw imageUnsupported(
139
+ `this JPEG has ${count} components (${model}); only 1-component greyscale and ` +
140
+ '3-component YCbCr are decoded',
141
+ BASELINE_FIX,
142
+ { components: count },
143
+ );
144
+ }
145
+ if (seg.length < 6 + count * 3) {
146
+ throw imageDecodeFailed(`the frame header ends before its ${count} component descriptors`);
147
+ }
148
+ const specs: Array<readonly [number, number, number, number]> = [];
149
+ let maxH = 1;
150
+ let maxV = 1;
151
+ for (let i = 0; i < count; i += 1) {
152
+ const at = 6 + i * 3;
153
+ const sampling = seg[at + 1] ?? 0;
154
+ const h = sampling >> 4;
155
+ const v = sampling & 15;
156
+ if (h < 1 || h > 4 || v < 1 || v > 4) {
157
+ throw imageDecodeFailed(`component ${i} declares ${h}x${v} sampling factors, outside 1-4`, {
158
+ h,
159
+ v,
160
+ });
161
+ }
162
+ maxH = Math.max(maxH, h);
163
+ maxV = Math.max(maxV, v);
164
+ specs.push([seg[at] ?? 0, h, v, seg[at + 2] ?? 0]);
165
+ }
166
+ const mcusPerLine = Math.ceil(width / (8 * maxH));
167
+ const mcusPerColumn = Math.ceil(height / (8 * maxV));
168
+ const components = specs.map(([id, h, v, quantId]) => {
169
+ const blocksPerLine = mcusPerLine * h;
170
+ const blocksPerColumn = mcusPerColumn * v;
171
+ const stride = blocksPerLine * 8;
172
+ return {
173
+ id,
174
+ h,
175
+ v,
176
+ quantId,
177
+ samples: new Uint8ClampedArray(stride * blocksPerColumn * 8),
178
+ stride,
179
+ blocksPerLine,
180
+ blocksPerColumn,
181
+ dequant: new Float32Array(64),
182
+ pred: 0,
183
+ };
184
+ });
185
+ return { width, height, components, maxH, maxV, mcusPerLine, mcusPerColumn };
186
+ }
187
+
188
+ const pickTable = (
189
+ tables: ReadonlyArray<HuffmanTable | undefined>,
190
+ id: number,
191
+ kind: string,
192
+ component: number,
193
+ ): HuffmanTable => {
194
+ const table = tables[id];
195
+ if (table === undefined) {
196
+ throw imageDecodeFailed(
197
+ `component ${component} selects ${kind} Huffman table ${id}, which no DHT segment defined`,
198
+ { component, kind, table: id },
199
+ );
200
+ }
201
+ return table;
202
+ };
203
+
204
+ /**
205
+ * The last three SOS bytes, which a sequential scan is only allowed one value of: the whole band
206
+ * (Ss=0, Se=63) at full precision (Ah=Al=0). Anything else is a progressive scan's shape wearing a
207
+ * baseline frame header — `decodeBlock` would read its coefficients as complete ones and emit a
208
+ * plausible, wrong image, which is exactly what this decoder refuses to do.
209
+ */
210
+ function assertWholeBand(seg: Uint8Array, count: number): void {
211
+ const ss = seg[1 + count * 2] ?? 0;
212
+ const se = seg[2 + count * 2] ?? 0;
213
+ const approx = seg[3 + count * 2] ?? 0;
214
+ const ah = approx >> 4;
215
+ const al = approx & 15;
216
+ if (ss === 0 && se === 63 && ah === 0 && al === 0) return;
217
+ throw imageUnsupported(
218
+ `this JPEG's scan codes coefficients ${ss}-${se} at successive approximation ${ah}/${al}; a ` +
219
+ 'sequential scan must carry the whole 0-63 band at 0/0',
220
+ BASELINE_FIX,
221
+ { ss, se, ah, al },
222
+ );
223
+ }
224
+
225
+ /**
226
+ * SOS: the components this scan codes, bound to the tables each one selects. Resolving them once,
227
+ * here, is what leaves the per-block hot path with no lookups and no validation of its own.
228
+ */
229
+ export function readScanHeader(
230
+ seg: Uint8Array,
231
+ frame: Frame,
232
+ quant: ReadonlyArray<Float32Array | undefined>,
233
+ dcTables: ReadonlyArray<HuffmanTable | undefined>,
234
+ acTables: ReadonlyArray<HuffmanTable | undefined>,
235
+ ): ScanComponent[] {
236
+ const count = seg[0] ?? 0;
237
+ if (count < 1 || seg.length < 1 + count * 2 + 3) {
238
+ throw imageDecodeFailed(`the scan header (SOS) declares ${count} components but is too short`);
239
+ }
240
+ assertWholeBand(seg, count);
241
+ const scan: ScanComponent[] = [];
242
+ for (let i = 0; i < count; i += 1) {
243
+ const id = seg[1 + i * 2] ?? 0;
244
+ const selector = seg[2 + i * 2] ?? 0;
245
+ const comp = frame.components.find((candidate) => candidate.id === id);
246
+ if (comp === undefined) {
247
+ throw imageDecodeFailed(`the scan names component ${id}, which the frame never declared`, {
248
+ component: id,
249
+ });
250
+ }
251
+ const table = quant[comp.quantId];
252
+ if (table === undefined) {
253
+ throw imageDecodeFailed(
254
+ `component ${id} selects quantisation table ${comp.quantId}, which no DQT segment defined`,
255
+ { component: id, table: comp.quantId },
256
+ );
257
+ }
258
+ comp.dequant = table;
259
+ comp.pred = 0;
260
+ scan.push({
261
+ comp,
262
+ dc: pickTable(dcTables, selector >> 4, 'DC', id),
263
+ ac: pickTable(acTables, selector & 15, 'AC', id),
264
+ });
265
+ }
266
+ return scan;
267
+ }
@@ -0,0 +1,202 @@
1
+ // Single responsibility: JPEG's entropy layer — the canonical Huffman decode table derived from a
2
+ // DHT segment's bits/values, and the MSB-first bit reader that unstuffs `FF 00` and refuses to read
3
+ // through a marker. Split out of the decoder because it knows nothing of blocks, colour or frames.
4
+
5
+ import { imageDecodeFailed } from './errors';
6
+
7
+ /**
8
+ * T.81 Annex F's `MINCODE`/`MAXCODE`/`VALPTR` form rather than a code tree: three flat arrays
9
+ * indexed by code length decode a symbol in one comparison per bit, with no pointer chasing and
10
+ * no allocation per block. `maxcode[l]` is -1 when the table has no code of length `l`.
11
+ */
12
+ export interface HuffmanTable {
13
+ readonly mincode: Int32Array;
14
+ readonly maxcode: Int32Array;
15
+ readonly valptr: Int32Array;
16
+ readonly values: Uint8Array;
17
+ }
18
+
19
+ /** `bits[i]` counts the codes of length `i + 1`; `values` lists the symbols in code order. */
20
+ export function buildHuffmanTable(bits: Uint8Array, values: Uint8Array): HuffmanTable {
21
+ const mincode = new Int32Array(17);
22
+ const maxcode = new Int32Array(17).fill(-1);
23
+ const valptr = new Int32Array(17);
24
+ let code = 0;
25
+ let assigned = 0;
26
+ for (let length = 1; length <= 16; length += 1) {
27
+ const count = bits[length - 1] ?? 0;
28
+ if (count > 0) {
29
+ valptr[length] = assigned;
30
+ mincode[length] = code;
31
+ assigned += count;
32
+ code += count;
33
+ maxcode[length] = code - 1;
34
+ }
35
+ // More codes than the length can hold means the table is over-subscribed: some code would
36
+ // be a prefix of another and the scan would decode into a different image than it encodes.
37
+ if (code > 1 << length) {
38
+ throw imageDecodeFailed(
39
+ `a DHT table is over-subscribed at code length ${length}: it needs more codes than ` +
40
+ `${1 << length} distinct ones`,
41
+ { length, codes: code },
42
+ );
43
+ }
44
+ code <<= 1;
45
+ }
46
+ if (assigned !== values.length) {
47
+ throw imageDecodeFailed(
48
+ `a DHT table declares ${assigned} codes but carries ${values.length} symbols`,
49
+ { codes: assigned, symbols: values.length },
50
+ );
51
+ }
52
+ return { mincode, maxcode, valptr, values };
53
+ }
54
+
55
+ /** A DHT segment packs several tables, each a class/id byte then 16 counts then the symbols. */
56
+ export function readHuffmanTables(
57
+ seg: Uint8Array,
58
+ dc: Array<HuffmanTable | undefined>,
59
+ ac: Array<HuffmanTable | undefined>,
60
+ ): void {
61
+ let at = 0;
62
+ while (at < seg.length) {
63
+ const spec = seg[at] ?? 0;
64
+ at += 1;
65
+ const kind = spec >> 4;
66
+ const id = spec & 15;
67
+ if (kind > 1 || id > 3) {
68
+ throw imageDecodeFailed(`DHT declares class ${kind} table ${id}, outside class 0-1 id 0-3`, {
69
+ kind,
70
+ id,
71
+ });
72
+ }
73
+ if (at + 16 > seg.length) {
74
+ throw imageDecodeFailed(`DHT table ${id} is truncated before its 16 code-length counts`, {
75
+ id,
76
+ });
77
+ }
78
+ const bits = seg.subarray(at, at + 16);
79
+ at += 16;
80
+ let total = 0;
81
+ for (let i = 0; i < 16; i += 1) total += bits[i] ?? 0;
82
+ if (at + total > seg.length) {
83
+ throw imageDecodeFailed(
84
+ `DHT table ${id} declares ${total} symbols but the segment holds ${seg.length - at}`,
85
+ { id, symbols: total },
86
+ );
87
+ }
88
+ (kind === 0 ? dc : ac)[id] = buildHuffmanTable(bits, seg.subarray(at, at + total));
89
+ at += total;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Walks entropy-coded data one bit at a time, most significant first. Every failure is a coded
95
+ * error rather than a zero bit, because padding a truncated scan with zeros yields a plausible
96
+ * grey image and no signal that anything went wrong.
97
+ */
98
+ export class JpegBitReader {
99
+ private readonly bytes: Uint8Array;
100
+ private at: number;
101
+ private buffer = 0;
102
+ private count = 0;
103
+
104
+ constructor(bytes: Uint8Array, start: number) {
105
+ this.bytes = bytes;
106
+ this.at = start;
107
+ }
108
+
109
+ /** Where the next unread byte begins — the marker walk resumes the file from here. */
110
+ get position(): number {
111
+ return this.at;
112
+ }
113
+
114
+ private nextByte(): number {
115
+ if (this.at >= this.bytes.length) {
116
+ throw imageDecodeFailed('the entropy-coded data ends before the scan does (truncated JPEG)', {
117
+ at: this.at,
118
+ });
119
+ }
120
+ const byte = this.bytes[this.at] ?? 0;
121
+ this.at += 1;
122
+ if (byte !== 0xff) return byte;
123
+ let next = this.bytes[this.at];
124
+ while (next === 0xff) {
125
+ this.at += 1; // repeated 0xFF ahead of a marker is fill, and carries no bits
126
+ next = this.bytes[this.at];
127
+ }
128
+ if (next === 0x00) {
129
+ this.at += 1; // `FF 00` is a stuffed literal 0xFF sample byte
130
+ return 0xff;
131
+ }
132
+ this.at -= 1;
133
+ throw imageDecodeFailed(
134
+ next === undefined
135
+ ? 'the entropy-coded data ends inside a marker (truncated JPEG)'
136
+ : `marker FF${next.toString(16).toUpperCase().padStart(2, '0')} interrupts the scan ` +
137
+ 'before its last block was decoded',
138
+ { at: this.at, marker: next ?? null },
139
+ );
140
+ }
141
+
142
+ readBit(): number {
143
+ if (this.count === 0) {
144
+ this.buffer = this.nextByte();
145
+ this.count = 8;
146
+ }
147
+ this.count -= 1;
148
+ return (this.buffer >> this.count) & 1;
149
+ }
150
+
151
+ receive(length: number): number {
152
+ let value = 0;
153
+ for (let i = 0; i < length; i += 1) value = (value << 1) | this.readBit();
154
+ return value;
155
+ }
156
+
157
+ /** T.81's EXTEND: an `length`-bit magnitude whose top bit is 0 is the negative half of the range. */
158
+ receiveAndExtend(length: number): number {
159
+ if (length === 0) return 0;
160
+ const value = this.receive(length);
161
+ return value < 1 << (length - 1) ? value - (1 << length) + 1 : value;
162
+ }
163
+
164
+ decode(table: HuffmanTable): number {
165
+ let code = this.readBit();
166
+ for (let length = 1; length <= 16; length += 1) {
167
+ const max = table.maxcode[length] ?? -1;
168
+ if (max >= 0 && code <= max) {
169
+ const symbol =
170
+ table.values[(table.valptr[length] ?? 0) + code - (table.mincode[length] ?? 0)];
171
+ if (symbol === undefined) {
172
+ throw imageDecodeFailed(
173
+ `a ${length}-bit Huffman code resolves outside the symbols its table defines`,
174
+ { length, code },
175
+ );
176
+ }
177
+ return symbol;
178
+ }
179
+ if (length < 16) code = (code << 1) | this.readBit();
180
+ }
181
+ throw imageDecodeFailed('no Huffman code of 16 bits or fewer matches the entropy data', {
182
+ code,
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Byte-aligns and swallows one `RSTn` marker, reporting whether it was there. The bit buffer
188
+ * dies with it: a restart interval exists precisely so a decoder can resynchronise mid-scan.
189
+ */
190
+ restart(): boolean {
191
+ this.count = 0;
192
+ this.buffer = 0;
193
+ let at = this.at;
194
+ while (this.bytes[at] === 0xff && this.bytes[at + 1] === 0xff) at += 1;
195
+ const marker = this.bytes[at] === 0xff ? this.bytes[at + 1] : undefined;
196
+ if (marker !== undefined && marker >= 0xd0 && marker <= 0xd7) {
197
+ this.at = at + 2;
198
+ return true;
199
+ }
200
+ return false;
201
+ }
202
+ }
@@ -0,0 +1,117 @@
1
+ // Single responsibility: the constant tables JPEG's decoder and encoder must agree on —
2
+ // zig-zag order, the Annex K quantisation tables and the Annex K Huffman code lengths.
3
+ // They live here, once, so the two halves of the codec structurally cannot drift apart.
4
+
5
+ /** Zig-zag index -> natural (row-major) index within an 8x8 block. */
6
+ export const ZIGZAG: readonly number[] = Object.freeze([
7
+ 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20,
8
+ 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52,
9
+ 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63,
10
+ ]);
11
+
12
+ /**
13
+ * cos(k*PI/16) * sqrt(2) — the scaling an AAN butterfly leaves in its output. The encoder folds it
14
+ * into the quantiser and the decoder into the dequantiser, so both halves read the same 8 numbers:
15
+ * a decoder scaled by anything else than the encoder assumed reads every coefficient wrong.
16
+ */
17
+ export const AAN_SCALE: readonly number[] = Object.freeze([
18
+ 1, 1.387039845, 1.306562965, 1.175875602, 1, 0.785694958, 0.5411961, 0.275899379,
19
+ ]);
20
+
21
+ /** ITU T.81 Annex K.1, luminance, in NATURAL order. */
22
+ export const STD_LUMINANCE_QUANT: readonly number[] = Object.freeze([
23
+ 16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56,
24
+ 14, 17, 22, 29, 51, 87, 80, 62, 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113,
25
+ 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99,
26
+ ]);
27
+
28
+ /** ITU T.81 Annex K.1, chrominance, in NATURAL order. */
29
+ export const STD_CHROMINANCE_QUANT: readonly number[] = Object.freeze([
30
+ 17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99,
31
+ 47, 66, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
32
+ 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99,
33
+ ]);
34
+
35
+ /** Annex K.3 code-length counts (index 0 == codes of length 1) and the symbols they map to. */
36
+ export const STD_DC_LUMINANCE_BITS: readonly number[] = Object.freeze([
37
+ 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,
38
+ ]);
39
+ export const STD_DC_LUMINANCE_VALUES: readonly number[] = Object.freeze([
40
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
41
+ ]);
42
+
43
+ export const STD_DC_CHROMINANCE_BITS: readonly number[] = Object.freeze([
44
+ 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,
45
+ ]);
46
+ export const STD_DC_CHROMINANCE_VALUES: readonly number[] = Object.freeze([
47
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
48
+ ]);
49
+
50
+ export const STD_AC_LUMINANCE_BITS: readonly number[] = Object.freeze([
51
+ 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,
52
+ ]);
53
+ export const STD_AC_LUMINANCE_VALUES: readonly number[] = Object.freeze([
54
+ 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,
55
+ 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,
56
+ 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,
57
+ 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
58
+ 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,
59
+ 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,
60
+ 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
61
+ 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,
62
+ 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,
63
+ 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
64
+ 0xf9, 0xfa,
65
+ ]);
66
+
67
+ export const STD_AC_CHROMINANCE_BITS: readonly number[] = Object.freeze([
68
+ 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,
69
+ ]);
70
+ export const STD_AC_CHROMINANCE_VALUES: readonly number[] = Object.freeze([
71
+ 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,
72
+ 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,
73
+ 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,
74
+ 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
75
+ 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
76
+ 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
77
+ 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,
78
+ 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,
79
+ 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,
80
+ 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,
81
+ 0xf9, 0xfa,
82
+ ]);
83
+
84
+ export const DEFAULT_JPEG_QUALITY = 80;
85
+
86
+ /**
87
+ * Annex K's quality scaling, the same curve libjpeg uses, so a `quality: 80` here is the
88
+ * `quality: 80` every other tool means. Returns a table in NATURAL order.
89
+ */
90
+ export function scaleQuantTable(base: readonly number[], quality: number): Uint8Array {
91
+ const clamped = Math.min(100, Math.max(1, Math.round(quality)));
92
+ const scale = clamped < 50 ? Math.floor(5000 / clamped) : 200 - clamped * 2;
93
+ const table = new Uint8Array(64);
94
+ for (let i = 0; i < 64; i += 1) {
95
+ const value = Math.floor((((base[i] ?? 1) * scale + 50) / 100) | 0);
96
+ table[i] = Math.min(255, Math.max(1, value));
97
+ }
98
+ return table;
99
+ }
100
+
101
+ /** BT.601 full-range, the transform every baseline JFIF file is written in. */
102
+ export function rgbToYcbcr(r: number, g: number, b: number): readonly [number, number, number] {
103
+ return [
104
+ 0.299 * r + 0.587 * g + 0.114 * b,
105
+ 128 - 0.168736 * r - 0.331264 * g + 0.5 * b,
106
+ 128 + 0.5 * r - 0.418688 * g - 0.081312 * b,
107
+ ];
108
+ }
109
+
110
+ /** The exact inverse of `rgbToYcbcr`; both halves must round the same way. */
111
+ export function ycbcrToRgb(y: number, cb: number, cr: number): readonly [number, number, number] {
112
+ return [
113
+ y + 1.402 * (cr - 128),
114
+ y - 0.344136 * (cb - 128) - 0.714136 * (cr - 128),
115
+ y + 1.772 * (cb - 128),
116
+ ];
117
+ }