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