cross-image 0.2.4 → 0.4.1
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 -21
- package/README.md +615 -333
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +7 -5
- package/esm/src/formats/apng.js +15 -10
- package/esm/src/formats/ascii.d.ts +3 -3
- package/esm/src/formats/ascii.js +1 -1
- package/esm/src/formats/avif.d.ts +3 -3
- package/esm/src/formats/avif.js +17 -7
- package/esm/src/formats/bmp.d.ts +3 -3
- package/esm/src/formats/bmp.js +2 -2
- package/esm/src/formats/dng.d.ts +1 -1
- package/esm/src/formats/dng.js +1 -1
- package/esm/src/formats/gif.d.ts +5 -5
- package/esm/src/formats/gif.js +17 -13
- package/esm/src/formats/heic.d.ts +3 -3
- package/esm/src/formats/heic.js +17 -7
- package/esm/src/formats/ico.d.ts +3 -3
- package/esm/src/formats/ico.js +4 -4
- package/esm/src/formats/jpeg.d.ts +3 -3
- package/esm/src/formats/jpeg.js +23 -11
- package/esm/src/formats/pam.d.ts +3 -3
- package/esm/src/formats/pam.js +2 -2
- package/esm/src/formats/pcx.d.ts +3 -3
- package/esm/src/formats/pcx.js +2 -2
- package/esm/src/formats/png.d.ts +4 -3
- package/esm/src/formats/png.js +9 -3
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +200 -10
- package/esm/src/formats/ppm.d.ts +3 -3
- package/esm/src/formats/ppm.js +2 -2
- package/esm/src/formats/tiff.d.ts +7 -18
- package/esm/src/formats/tiff.js +162 -27
- package/esm/src/formats/webp.d.ts +3 -3
- package/esm/src/formats/webp.js +11 -8
- package/esm/src/image.d.ts +26 -3
- package/esm/src/image.js +66 -22
- package/esm/src/types.d.ts +122 -4
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +232 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +1 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +13 -3
- package/script/src/formats/apng.d.ts +7 -5
- package/script/src/formats/apng.js +15 -10
- package/script/src/formats/ascii.d.ts +3 -3
- package/script/src/formats/ascii.js +1 -1
- package/script/src/formats/avif.d.ts +3 -3
- package/script/src/formats/avif.js +17 -7
- package/script/src/formats/bmp.d.ts +3 -3
- package/script/src/formats/bmp.js +2 -2
- package/script/src/formats/dng.d.ts +1 -1
- package/script/src/formats/dng.js +1 -1
- package/script/src/formats/gif.d.ts +5 -5
- package/script/src/formats/gif.js +17 -13
- package/script/src/formats/heic.d.ts +3 -3
- package/script/src/formats/heic.js +17 -7
- package/script/src/formats/ico.d.ts +3 -3
- package/script/src/formats/ico.js +4 -4
- package/script/src/formats/jpeg.d.ts +3 -3
- package/script/src/formats/jpeg.js +23 -11
- package/script/src/formats/pam.d.ts +3 -3
- package/script/src/formats/pam.js +2 -2
- package/script/src/formats/pcx.d.ts +3 -3
- package/script/src/formats/pcx.js +2 -2
- package/script/src/formats/png.d.ts +4 -3
- package/script/src/formats/png.js +9 -3
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +200 -10
- package/script/src/formats/ppm.d.ts +3 -3
- package/script/src/formats/ppm.js +2 -2
- package/script/src/formats/tiff.d.ts +7 -18
- package/script/src/formats/tiff.js +162 -27
- package/script/src/formats/webp.d.ts +3 -3
- package/script/src/formats/webp.js +11 -8
- package/script/src/image.d.ts +26 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +122 -4
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +236 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Supports baseline DCT JPEG images (the most common format)
|
|
3
|
+
* JPEG decoder implementation supporting both baseline and progressive DCT
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Baseline DCT (SOF0) - Sequential encoding
|
|
7
|
+
* - Progressive DCT (SOF2) - Multi-scan encoding with spectral selection and successive approximation
|
|
8
|
+
*
|
|
9
|
+
* This is a pure JavaScript implementation that handles common JPEG files.
|
|
7
10
|
* For complex or non-standard JPEGs, the ImageDecoder API fallback is preferred.
|
|
8
11
|
*/
|
|
9
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
13
|
exports.JPEGDecoder = void 0;
|
|
14
|
+
/**
|
|
15
|
+
* Custom error class for end-of-scan marker detection
|
|
16
|
+
* Thrown when a marker is encountered in scan data, indicating the scan has ended
|
|
17
|
+
*/
|
|
18
|
+
class EndOfScanError extends Error {
|
|
19
|
+
constructor(message = "End of scan marker detected") {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "EndOfScanError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
11
24
|
// JPEG markers
|
|
12
25
|
const EOI = 0xFFD9; // End of Image
|
|
13
26
|
const SOS = 0xFFDA; // Start of Scan
|
|
@@ -16,7 +29,8 @@ const DHT = 0xFFC4; // Define Huffman Table
|
|
|
16
29
|
const SOF0 = 0xFFC0; // Start of Frame (Baseline DCT)
|
|
17
30
|
const SOF2 = 0xFFC2; // Start of Frame (Progressive DCT)
|
|
18
31
|
const DRI = 0xFFDD; // Define Restart Interval
|
|
19
|
-
// Zigzag order for DCT coefficients
|
|
32
|
+
// Zigzag order for DCT coefficients (JPEG standard)
|
|
33
|
+
// Maps coefficient index in zigzag order to position in 8x8 block
|
|
20
34
|
const ZIGZAG = [
|
|
21
35
|
0,
|
|
22
36
|
1,
|
|
@@ -84,7 +98,7 @@ const ZIGZAG = [
|
|
|
84
98
|
63,
|
|
85
99
|
];
|
|
86
100
|
class JPEGDecoder {
|
|
87
|
-
constructor(data) {
|
|
101
|
+
constructor(data, settings = {}) {
|
|
88
102
|
Object.defineProperty(this, "data", {
|
|
89
103
|
enumerable: true,
|
|
90
104
|
configurable: true,
|
|
@@ -151,7 +165,60 @@ class JPEGDecoder {
|
|
|
151
165
|
writable: true,
|
|
152
166
|
value: 0
|
|
153
167
|
});
|
|
168
|
+
Object.defineProperty(this, "options", {
|
|
169
|
+
enumerable: true,
|
|
170
|
+
configurable: true,
|
|
171
|
+
writable: true,
|
|
172
|
+
value: void 0
|
|
173
|
+
});
|
|
174
|
+
Object.defineProperty(this, "isProgressive", {
|
|
175
|
+
enumerable: true,
|
|
176
|
+
configurable: true,
|
|
177
|
+
writable: true,
|
|
178
|
+
value: false
|
|
179
|
+
});
|
|
180
|
+
// Progressive JPEG scan parameters
|
|
181
|
+
Object.defineProperty(this, "spectralStart", {
|
|
182
|
+
enumerable: true,
|
|
183
|
+
configurable: true,
|
|
184
|
+
writable: true,
|
|
185
|
+
value: 0
|
|
186
|
+
}); // Start of spectral selection (Ss)
|
|
187
|
+
Object.defineProperty(this, "spectralEnd", {
|
|
188
|
+
enumerable: true,
|
|
189
|
+
configurable: true,
|
|
190
|
+
writable: true,
|
|
191
|
+
value: 63
|
|
192
|
+
}); // End of spectral selection (Se)
|
|
193
|
+
Object.defineProperty(this, "successiveHigh", {
|
|
194
|
+
enumerable: true,
|
|
195
|
+
configurable: true,
|
|
196
|
+
writable: true,
|
|
197
|
+
value: 0
|
|
198
|
+
}); // Successive approximation bit high (Ah)
|
|
199
|
+
Object.defineProperty(this, "successiveLow", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
configurable: true,
|
|
202
|
+
writable: true,
|
|
203
|
+
value: 0
|
|
204
|
+
}); // Successive approximation bit low (Al)
|
|
205
|
+
Object.defineProperty(this, "scanComponentIds", {
|
|
206
|
+
enumerable: true,
|
|
207
|
+
configurable: true,
|
|
208
|
+
writable: true,
|
|
209
|
+
value: []
|
|
210
|
+
}); // Component IDs included in current scan
|
|
211
|
+
Object.defineProperty(this, "eobRun", {
|
|
212
|
+
enumerable: true,
|
|
213
|
+
configurable: true,
|
|
214
|
+
writable: true,
|
|
215
|
+
value: 0
|
|
216
|
+
}); // Remaining blocks to skip due to EOBn
|
|
154
217
|
this.data = data;
|
|
218
|
+
this.options = {
|
|
219
|
+
tolerantDecoding: settings.tolerantDecoding ?? true,
|
|
220
|
+
onWarning: settings.onWarning,
|
|
221
|
+
};
|
|
155
222
|
}
|
|
156
223
|
decode() {
|
|
157
224
|
// Verify JPEG signature
|
|
@@ -168,7 +235,11 @@ class JPEGDecoder {
|
|
|
168
235
|
else if (marker === SOS) {
|
|
169
236
|
this.parseSOS();
|
|
170
237
|
this.decodeScan();
|
|
171
|
-
|
|
238
|
+
// For progressive JPEG, continue to process more scans
|
|
239
|
+
// For baseline JPEG, stop after first scan
|
|
240
|
+
if (!this.isProgressive) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
172
243
|
}
|
|
173
244
|
else if (marker === DQT) {
|
|
174
245
|
this.parseDQT();
|
|
@@ -176,11 +247,11 @@ class JPEGDecoder {
|
|
|
176
247
|
else if (marker === DHT) {
|
|
177
248
|
this.parseDHT();
|
|
178
249
|
}
|
|
179
|
-
else if (marker === SOF0) {
|
|
250
|
+
else if (marker === SOF0 || marker === SOF2) {
|
|
251
|
+
// Parse SOF for both baseline (SOF0) and progressive (SOF2)
|
|
252
|
+
// Progressive JPEGs have the same frame header structure
|
|
180
253
|
this.parseSOF();
|
|
181
|
-
|
|
182
|
-
else if (marker === SOF2) {
|
|
183
|
-
throw new Error("Progressive JPEG not supported in pure JS decoder");
|
|
254
|
+
this.isProgressive = marker === SOF2;
|
|
184
255
|
}
|
|
185
256
|
else if (marker === DRI) {
|
|
186
257
|
this.parseDRI();
|
|
@@ -205,6 +276,20 @@ class JPEGDecoder {
|
|
|
205
276
|
if (this.width === 0 || this.height === 0) {
|
|
206
277
|
throw new Error("Failed to decode JPEG: invalid dimensions");
|
|
207
278
|
}
|
|
279
|
+
// For progressive JPEGs, perform IDCT on all blocks after all scans are complete
|
|
280
|
+
// This ensures that frequency-domain coefficients from multiple scans are properly
|
|
281
|
+
// accumulated before transformation to spatial domain
|
|
282
|
+
if (this.isProgressive) {
|
|
283
|
+
for (const component of this.components) {
|
|
284
|
+
if (component.blocks) {
|
|
285
|
+
for (const row of component.blocks) {
|
|
286
|
+
for (const block of row) {
|
|
287
|
+
this.idct(block);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
208
293
|
// Convert YCbCr to RGB
|
|
209
294
|
return this.convertToRGB();
|
|
210
295
|
}
|
|
@@ -241,7 +326,7 @@ class JPEGDecoder {
|
|
|
241
326
|
if (precision !== 0) {
|
|
242
327
|
throw new Error("16-bit quantization tables not supported");
|
|
243
328
|
}
|
|
244
|
-
const table = new
|
|
329
|
+
const table = new Uint8Array(64);
|
|
245
330
|
for (let i = 0; i < 64; i++) {
|
|
246
331
|
table[ZIGZAG[i]] = this.data[this.pos++];
|
|
247
332
|
}
|
|
@@ -255,13 +340,13 @@ class JPEGDecoder {
|
|
|
255
340
|
const info = this.data[this.pos++];
|
|
256
341
|
const tableId = info & 0x0F;
|
|
257
342
|
const tableClass = (info >> 4) & 0x0F;
|
|
258
|
-
const bits = new
|
|
343
|
+
const bits = new Uint8Array(16);
|
|
259
344
|
let numSymbols = 0;
|
|
260
345
|
for (let i = 0; i < 16; i++) {
|
|
261
346
|
bits[i] = this.data[this.pos++];
|
|
262
347
|
numSymbols += bits[i];
|
|
263
348
|
}
|
|
264
|
-
const huffVal = new
|
|
349
|
+
const huffVal = new Uint8Array(numSymbols);
|
|
265
350
|
for (let i = 0; i < numSymbols; i++) {
|
|
266
351
|
huffVal[i] = this.data[this.pos++];
|
|
267
352
|
}
|
|
@@ -276,9 +361,9 @@ class JPEGDecoder {
|
|
|
276
361
|
}
|
|
277
362
|
}
|
|
278
363
|
buildHuffmanTable(bits, huffVal) {
|
|
279
|
-
const maxCode = new
|
|
280
|
-
const minCode = new
|
|
281
|
-
const valPtr = new
|
|
364
|
+
const maxCode = new Int32Array(16).fill(-1);
|
|
365
|
+
const minCode = new Int32Array(16).fill(-1);
|
|
366
|
+
const valPtr = new Int32Array(16).fill(-1);
|
|
282
367
|
const codes = new Map();
|
|
283
368
|
let code = 0;
|
|
284
369
|
let valIndex = 0;
|
|
@@ -332,16 +417,26 @@ class JPEGDecoder {
|
|
|
332
417
|
parseSOS() {
|
|
333
418
|
const _length = this.readUint16();
|
|
334
419
|
const numComponents = this.data[this.pos++];
|
|
420
|
+
// Track which components are included in this scan
|
|
421
|
+
this.scanComponentIds = [];
|
|
335
422
|
for (let i = 0; i < numComponents; i++) {
|
|
336
423
|
const id = this.data[this.pos++];
|
|
337
424
|
const tables = this.data[this.pos++];
|
|
425
|
+
this.scanComponentIds.push(id);
|
|
338
426
|
const component = this.components.find((c) => c.id === id);
|
|
339
427
|
if (component) {
|
|
340
428
|
component.dcTable = (tables >> 4) & 0x0F;
|
|
341
429
|
component.acTable = tables & 0x0F;
|
|
342
430
|
}
|
|
343
431
|
}
|
|
344
|
-
|
|
432
|
+
// Parse spectral selection and successive approximation parameters
|
|
433
|
+
// These are used in progressive JPEGs to define which coefficients
|
|
434
|
+
// are encoded and at what bit precision
|
|
435
|
+
this.spectralStart = this.data[this.pos++]; // Ss: Start of spectral selection (0-63)
|
|
436
|
+
this.spectralEnd = this.data[this.pos++]; // Se: End of spectral selection (0-63)
|
|
437
|
+
const successiveApprox = this.data[this.pos++];
|
|
438
|
+
this.successiveHigh = (successiveApprox >> 4) & 0x0F; // Ah: Successive approximation bit position high
|
|
439
|
+
this.successiveLow = successiveApprox & 0x0F; // Al: Successive approximation bit position low
|
|
345
440
|
}
|
|
346
441
|
parseDRI() {
|
|
347
442
|
const _length = this.readUint16();
|
|
@@ -353,108 +448,360 @@ class JPEGDecoder {
|
|
|
353
448
|
const maxV = Math.max(...this.components.map((c) => c.v));
|
|
354
449
|
const mcuWidth = Math.ceil(this.width / (8 * maxH));
|
|
355
450
|
const mcuHeight = Math.ceil(this.height / (8 * maxV));
|
|
356
|
-
// Initialize bit buffer
|
|
451
|
+
// Initialize bit buffer for this scan
|
|
357
452
|
this.bitBuffer = 0;
|
|
358
453
|
this.bitCount = 0;
|
|
359
|
-
//
|
|
454
|
+
// Reset DC predictors and EOB run at the start of each scan (JPEG spec requirement)
|
|
455
|
+
this.eobRun = 0;
|
|
360
456
|
for (const component of this.components) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
457
|
+
component.pred = 0;
|
|
458
|
+
}
|
|
459
|
+
// Initialize or preserve blocks for each component
|
|
460
|
+
// For progressive JPEGs, blocks must be preserved across multiple scans
|
|
461
|
+
// to accumulate coefficients from different spectral bands and bit refinements
|
|
462
|
+
for (const component of this.components) {
|
|
463
|
+
const blocksAcross = mcuWidth * component.h;
|
|
464
|
+
const blocksDown = mcuHeight * component.v;
|
|
465
|
+
// Only initialize blocks if they don't exist yet (first scan)
|
|
466
|
+
// Check both for undefined/null and empty array
|
|
467
|
+
if (!component.blocks || component.blocks.length === 0) {
|
|
468
|
+
component.blocks = Array(blocksDown).fill(null).map(() => Array(blocksAcross).fill(null).map(() => new Int32Array(64)));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Decode entropy-coded data.
|
|
472
|
+
// JPEG defines different MCU sequencing for interleaved vs non-interleaved scans.
|
|
473
|
+
// For single-component (non-interleaved) scans, the entropy-coded stream is ordered
|
|
474
|
+
// as a simple raster over that component's block grid; decoding in interleaved MCU
|
|
475
|
+
// order can permute blocks and produce artifacts that look like half-width duplication.
|
|
476
|
+
if (this.scanComponentIds.length === 1) {
|
|
477
|
+
const componentId = this.scanComponentIds[0];
|
|
478
|
+
const component = this.components.find((c) => c.id === componentId);
|
|
479
|
+
if (!component)
|
|
480
|
+
return;
|
|
481
|
+
const blocksAcross = mcuWidth * component.h;
|
|
482
|
+
const blocksDown = mcuHeight * component.v;
|
|
483
|
+
for (let blockY = 0; blockY < blocksDown; blockY++) {
|
|
484
|
+
for (let blockX = 0; blockX < blocksAcross; blockX++) {
|
|
485
|
+
if (blockY >= component.blocks.length ||
|
|
486
|
+
blockX >= component.blocks[0].length) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (this.options.tolerantDecoding) {
|
|
490
|
+
try {
|
|
491
|
+
this.decodeBlock(component, blockY, blockX);
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
if (e instanceof EndOfScanError) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
try {
|
|
501
|
+
this.decodeBlock(component, blockY, blockX);
|
|
502
|
+
}
|
|
503
|
+
catch (e) {
|
|
504
|
+
if (e instanceof EndOfScanError) {
|
|
505
|
+
return;
|
|
379
506
|
}
|
|
507
|
+
throw e;
|
|
380
508
|
}
|
|
381
509
|
}
|
|
382
510
|
}
|
|
383
511
|
}
|
|
512
|
+
return;
|
|
384
513
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
514
|
+
// Interleaved scan: MCU order with per-component sampling factors.
|
|
515
|
+
outerLoop: for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
|
|
516
|
+
for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
|
|
517
|
+
for (const component of this.components) {
|
|
518
|
+
if (this.scanComponentIds.length > 0 &&
|
|
519
|
+
!this.scanComponentIds.includes(component.id)) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (let v = 0; v < component.v; v++) {
|
|
523
|
+
for (let h = 0; h < component.h; h++) {
|
|
524
|
+
const blockY = mcuY * component.v + v;
|
|
525
|
+
const blockX = mcuX * component.h + h;
|
|
526
|
+
if (blockY < component.blocks.length &&
|
|
527
|
+
blockX < component.blocks[0].length) {
|
|
528
|
+
if (this.options.tolerantDecoding) {
|
|
529
|
+
try {
|
|
530
|
+
this.decodeBlock(component, blockY, blockX);
|
|
531
|
+
}
|
|
532
|
+
catch (e) {
|
|
533
|
+
if (e instanceof EndOfScanError) {
|
|
534
|
+
break outerLoop;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
try {
|
|
540
|
+
this.decodeBlock(component, blockY, blockX);
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
if (e instanceof EndOfScanError) {
|
|
544
|
+
break outerLoop;
|
|
545
|
+
}
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
388
554
|
}
|
|
389
555
|
}
|
|
390
556
|
decodeBlock(component, blockY, blockX) {
|
|
391
557
|
const block = component.blocks[blockY][blockX];
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
558
|
+
// EOB run handling (progressive JPEG AC scans):
|
|
559
|
+
// - In first AC scans (Ah=0), an EOB run means the following blocks have no new
|
|
560
|
+
// coefficients in the current spectral band and can be skipped entirely.
|
|
561
|
+
// - In AC refinement scans (Ah>0), blocks in an EOB run STILL contain refinement
|
|
562
|
+
// bits for existing non-zero coefficients; skipping would desynchronize the bitstream.
|
|
563
|
+
const isACScan = this.spectralEnd > 0 && this.spectralStart > 0;
|
|
564
|
+
const isACRefinementScan = isACScan && this.successiveHigh > 0;
|
|
565
|
+
if (isACScan && !isACRefinementScan && this.eobRun > 0) {
|
|
566
|
+
this.eobRun--;
|
|
567
|
+
// Block remains as-is (zeros or previous values from earlier scans)
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
// Progressive JPEG support:
|
|
571
|
+
// - Spectral selection (spectralStart, spectralEnd): Defines which DCT coefficients are decoded
|
|
572
|
+
// * DC only: Ss=0, Se=0
|
|
573
|
+
// * Low-frequency AC: Ss=1, Se=5
|
|
574
|
+
// * High-frequency AC: Ss=6, Se=63
|
|
575
|
+
// - Successive approximation (successiveHigh, successiveLow): Defines bit precision
|
|
576
|
+
// * First scan: Ah=0, Al=n (decode high bits, shift left by Al)
|
|
577
|
+
// * Refinement scan: Ah=n, Al=n-1 (refine lower bits by adding bit at position Al)
|
|
578
|
+
//
|
|
579
|
+
// Implementation: Processes scans sequentially, accumulating coefficients across
|
|
580
|
+
// multiple scans. Supports full successive approximation bit refinement for both
|
|
581
|
+
// DC and AC coefficients. IDCT is deferred until all scans complete to preserve
|
|
582
|
+
// frequency-domain data for proper accumulation.
|
|
583
|
+
// Decode DC coefficient (if spectralStart == 0)
|
|
584
|
+
if (this.spectralStart === 0) {
|
|
585
|
+
const dcTable = this.dcTables[component.dcTable];
|
|
586
|
+
if (!dcTable) {
|
|
587
|
+
throw new Error(`Missing DC table ${component.dcTable}`);
|
|
588
|
+
}
|
|
589
|
+
if (this.successiveHigh === 0) {
|
|
590
|
+
// First DC scan: decode the DC coefficient
|
|
591
|
+
const dcLen = this.decodeHuffman(dcTable);
|
|
592
|
+
const dcDiff = dcLen > 0 ? this.receiveBits(dcLen) : 0;
|
|
593
|
+
component.pred += dcDiff;
|
|
594
|
+
// For successive approximation, shift the coefficient left by Al bits
|
|
595
|
+
const coeff = component.pred << this.successiveLow;
|
|
596
|
+
block[0] = coeff * this.qTables[component.qTable][0];
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
// DC refinement scan: add a refinement bit
|
|
600
|
+
const bit = this.readBit();
|
|
601
|
+
if (bit) {
|
|
602
|
+
// Add the refinement bit at position Al
|
|
603
|
+
const refinement = 1 << this.successiveLow;
|
|
604
|
+
block[0] += refinement * this.qTables[component.qTable][0];
|
|
414
605
|
}
|
|
415
|
-
|
|
416
|
-
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Decode AC coefficients (if spectralEnd > 0)
|
|
609
|
+
// Note: For DC-only scans (Ss=0, Se=0), this block is skipped entirely
|
|
610
|
+
// For AC-only scans (Ss>0), this decodes the specified AC coefficient range
|
|
611
|
+
if (this.spectralEnd > 0) {
|
|
612
|
+
const acTable = this.acTables[component.acTable];
|
|
613
|
+
if (!acTable) {
|
|
614
|
+
throw new Error(`Missing AC table ${component.acTable}`);
|
|
615
|
+
}
|
|
616
|
+
if (this.successiveHigh === 0) {
|
|
617
|
+
// First AC scan: decode new coefficients
|
|
618
|
+
// Start from spectralStart, but ensure k >= 1 (AC coefficients start at index 1)
|
|
619
|
+
let k = this.spectralStart === 0 ? 1 : this.spectralStart;
|
|
620
|
+
while (k <= this.spectralEnd && k < 64) {
|
|
621
|
+
const rs = this.decodeHuffman(acTable);
|
|
622
|
+
const r = (rs >> 4) & 0x0F;
|
|
623
|
+
const s = rs & 0x0F;
|
|
624
|
+
if (s === 0) {
|
|
625
|
+
if (r === 15) {
|
|
626
|
+
k += 16;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// EOB or EOBn (end of block, possibly with run length)
|
|
630
|
+
// EOBn is ONLY used in progressive JPEG with spectral selection
|
|
631
|
+
// For baseline JPEG, (r,0) where r!=0,15 should not occur
|
|
632
|
+
if (this.isProgressive && r > 0) {
|
|
633
|
+
// Progressive JPEG: EOBn with additional unsigned bits
|
|
634
|
+
// Formula: eobRun = (1 << r) - 1 + additionalBits
|
|
635
|
+
// This specifies how many ADDITIONAL blocks (after current) to skip
|
|
636
|
+
const additionalBits = this.receiveUnsignedBits(r);
|
|
637
|
+
this.eobRun = (1 << r) - 1 + additionalBits;
|
|
638
|
+
}
|
|
639
|
+
// For both baseline and progressive: end current block
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
k += r;
|
|
645
|
+
// Check bounds: if k exceeds spectralEnd, stop decoding
|
|
646
|
+
// (spectralEnd is guaranteed to be <= 63 per JPEG spec)
|
|
647
|
+
if (k > this.spectralEnd)
|
|
648
|
+
break;
|
|
649
|
+
// For successive approximation, shift the coefficient left by Al bits
|
|
650
|
+
const coeff = this.receiveBits(s) << this.successiveLow;
|
|
651
|
+
block[ZIGZAG[k]] = coeff *
|
|
652
|
+
this.qTables[component.qTable][ZIGZAG[k]];
|
|
653
|
+
k++;
|
|
654
|
+
}
|
|
417
655
|
}
|
|
418
656
|
}
|
|
419
657
|
else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
658
|
+
const qTable = this.qTables[component.qTable];
|
|
659
|
+
const hadEobRunAtStart = this.eobRun > 0;
|
|
660
|
+
let successiveACState = hadEobRunAtStart ? 4 : 0;
|
|
661
|
+
let successiveACNextValue = 0;
|
|
662
|
+
let runLength = 0;
|
|
663
|
+
let kk = this.spectralStart === 0 ? 1 : this.spectralStart;
|
|
664
|
+
while (kk <= this.spectralEnd && kk < 64) {
|
|
665
|
+
const z = ZIGZAG[kk];
|
|
666
|
+
const current = block[z];
|
|
667
|
+
const direction = current < 0 ? -1 : 1;
|
|
668
|
+
switch (successiveACState) {
|
|
669
|
+
case 0: {
|
|
670
|
+
const rs = this.decodeHuffman(acTable);
|
|
671
|
+
const realR = (rs >> 4) & 0x0f;
|
|
672
|
+
const realS = rs & 0x0f;
|
|
673
|
+
if (realS === 0) {
|
|
674
|
+
if (realR < 15) {
|
|
675
|
+
const additionalBits = this.receiveUnsignedBits(realR);
|
|
676
|
+
this.eobRun = (1 << realR) - 1 + additionalBits;
|
|
677
|
+
successiveACState = 4;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
runLength = 16;
|
|
681
|
+
successiveACState = 1;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
if (realS !== 1) {
|
|
686
|
+
throw new Error("Invalid AC refinement size");
|
|
687
|
+
}
|
|
688
|
+
successiveACNextValue = this.receiveBits(realS);
|
|
689
|
+
runLength = realR;
|
|
690
|
+
successiveACState = realR ? 2 : 3;
|
|
691
|
+
}
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
case 1:
|
|
695
|
+
case 2:
|
|
696
|
+
if (current !== 0) {
|
|
697
|
+
const bit = this.readBit();
|
|
698
|
+
if (bit) {
|
|
699
|
+
const refinement = (1 << this.successiveLow) * qTable[z];
|
|
700
|
+
block[z] += direction * refinement;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
runLength--;
|
|
705
|
+
if (runLength === 0) {
|
|
706
|
+
successiveACState = successiveACState === 2 ? 3 : 0;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
case 3:
|
|
711
|
+
if (current !== 0) {
|
|
712
|
+
const bit = this.readBit();
|
|
713
|
+
if (bit) {
|
|
714
|
+
const refinement = (1 << this.successiveLow) * qTable[z];
|
|
715
|
+
block[z] += direction * refinement;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
const newCoeff = successiveACNextValue << this.successiveLow;
|
|
720
|
+
block[z] = newCoeff * qTable[z];
|
|
721
|
+
successiveACState = 0;
|
|
722
|
+
}
|
|
723
|
+
break;
|
|
724
|
+
case 4:
|
|
725
|
+
if (current !== 0) {
|
|
726
|
+
const bit = this.readBit();
|
|
727
|
+
if (bit) {
|
|
728
|
+
const refinement = (1 << this.successiveLow) * qTable[z];
|
|
729
|
+
block[z] += direction * refinement;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
kk++;
|
|
735
|
+
}
|
|
736
|
+
// Only consume one EOB-run block here if we started this block already within
|
|
737
|
+
// an existing EOB run. If we *encountered* an EOBn symbol in this block, the
|
|
738
|
+
// run applies to subsequent blocks (per our eobRun convention).
|
|
739
|
+
if (successiveACState === 4 && hadEobRunAtStart && this.eobRun > 0) {
|
|
740
|
+
this.eobRun--;
|
|
741
|
+
}
|
|
426
742
|
}
|
|
427
743
|
}
|
|
428
|
-
// Perform IDCT
|
|
429
|
-
|
|
744
|
+
// Perform IDCT only for baseline JPEGs
|
|
745
|
+
// For progressive JPEGs, IDCT is deferred until all scans are complete
|
|
746
|
+
// to preserve frequency-domain coefficients for accumulation across scans
|
|
747
|
+
if (!this.isProgressive) {
|
|
748
|
+
this.idct(block);
|
|
749
|
+
}
|
|
430
750
|
}
|
|
431
751
|
decodeHuffman(table) {
|
|
752
|
+
// Use table-based decoding (more reliable)
|
|
432
753
|
let code = 0;
|
|
433
754
|
for (let len = 0; len < 16; len++) {
|
|
434
755
|
code = (code << 1) | this.readBit();
|
|
435
756
|
if (table.minCode[len] !== -1 && code <= table.maxCode[len]) {
|
|
436
757
|
const index = table.valPtr[len] + (code - table.minCode[len]);
|
|
437
|
-
|
|
758
|
+
if (index >= 0 && index < table.huffVal.length) {
|
|
759
|
+
return table.huffVal[index];
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
throw new Error(`Huffman table index out of bounds: ${index} (table size: ${table.huffVal.length})`);
|
|
763
|
+
}
|
|
438
764
|
}
|
|
439
765
|
}
|
|
440
766
|
throw new Error("Invalid Huffman code");
|
|
441
767
|
}
|
|
442
768
|
readBit() {
|
|
443
769
|
if (this.bitCount === 0) {
|
|
444
|
-
|
|
445
|
-
|
|
770
|
+
// Check bounds
|
|
771
|
+
if (this.pos >= this.data.length) {
|
|
772
|
+
throw new Error("Unexpected end of JPEG data");
|
|
773
|
+
}
|
|
774
|
+
const byte = this.data[this.pos++];
|
|
775
|
+
// Handle byte stuffing (0xFF 0x00) and restart markers
|
|
446
776
|
if (byte === 0xFF) {
|
|
777
|
+
if (this.pos >= this.data.length) {
|
|
778
|
+
throw new Error("Unexpected end of JPEG data after 0xFF");
|
|
779
|
+
}
|
|
447
780
|
const nextByte = this.data[this.pos];
|
|
448
781
|
if (nextByte === 0x00) {
|
|
782
|
+
// Byte stuffing - skip the 0x00
|
|
783
|
+
// The 0xFF byte value is used as-is (already assigned above)
|
|
449
784
|
this.pos++;
|
|
450
785
|
}
|
|
451
786
|
else if (nextByte >= 0xD0 && nextByte <= 0xD7) {
|
|
452
|
-
// Restart marker - reset DC predictors
|
|
453
|
-
this.pos++;
|
|
787
|
+
// Restart marker - reset DC predictors and bit stream
|
|
788
|
+
this.pos++; // Skip marker type byte
|
|
454
789
|
for (const component of this.components) {
|
|
455
790
|
component.pred = 0;
|
|
456
791
|
}
|
|
457
|
-
|
|
792
|
+
// Progressive AC refinement uses EOB runs; restart markers reset EOBRUN as well.
|
|
793
|
+
this.eobRun = 0;
|
|
794
|
+
// Reset bit stream (restart markers are byte-aligned)
|
|
795
|
+
this.bitBuffer = 0;
|
|
796
|
+
this.bitCount = 0;
|
|
797
|
+
// Recursively call readBit to get the next bit after restart
|
|
798
|
+
return this.readBit();
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
// Other marker found in scan data - this indicates end of scan
|
|
802
|
+
// Back up to before the marker
|
|
803
|
+
this.pos--;
|
|
804
|
+
throw new EndOfScanError();
|
|
458
805
|
}
|
|
459
806
|
}
|
|
460
807
|
this.bitBuffer = byte;
|
|
@@ -474,10 +821,24 @@ class JPEGDecoder {
|
|
|
474
821
|
}
|
|
475
822
|
return value;
|
|
476
823
|
}
|
|
824
|
+
receiveUnsignedBits(n) {
|
|
825
|
+
// Read n bits as an unsigned integer (no magnitude conversion)
|
|
826
|
+
// Input validation: n should be between 0 and 16
|
|
827
|
+
if (n < 0 || n > 16) {
|
|
828
|
+
throw new Error(`Invalid bit count: ${n} (must be 0-16)`);
|
|
829
|
+
}
|
|
830
|
+
if (n === 0)
|
|
831
|
+
return 0;
|
|
832
|
+
let value = 0;
|
|
833
|
+
for (let i = 0; i < n; i++) {
|
|
834
|
+
value = (value << 1) | this.readBit();
|
|
835
|
+
}
|
|
836
|
+
return value;
|
|
837
|
+
}
|
|
477
838
|
idct(block) {
|
|
478
839
|
// Simplified 2D IDCT
|
|
479
840
|
// This is a basic implementation - not optimized
|
|
480
|
-
const temp = new
|
|
841
|
+
const temp = new Float32Array(64);
|
|
481
842
|
// 1D IDCT on rows
|
|
482
843
|
for (let i = 0; i < 8; i++) {
|
|
483
844
|
const offset = i * 8;
|
|
@@ -534,10 +895,14 @@ class JPEGDecoder {
|
|
|
534
895
|
for (let row = 0; row < this.height; row++) {
|
|
535
896
|
for (let col = 0; col < this.width; col++) {
|
|
536
897
|
// Y component
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
898
|
+
// Scale pixel position by component sampling factors to get correct block position
|
|
899
|
+
// This is necessary when component has h>1 or v>1 (e.g., 4:2:0 chroma subsampling)
|
|
900
|
+
const yRow = Math.floor(row * y.v / maxV);
|
|
901
|
+
const yCol = Math.floor(col * y.h / maxH);
|
|
902
|
+
const yBlockRow = Math.floor(yRow / 8);
|
|
903
|
+
const yBlockCol = Math.floor(yCol / 8);
|
|
904
|
+
const yBlockY = yRow % 8;
|
|
905
|
+
const yBlockX = yCol % 8;
|
|
541
906
|
let yVal = 0;
|
|
542
907
|
if (yBlockRow < y.blocks.length && yBlockCol < y.blocks[0].length) {
|
|
543
908
|
yVal = y.blocks[yBlockRow][yBlockCol][yBlockY * 8 + yBlockX];
|