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.
Files changed (105) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +507 -333
  3. package/esm/mod.d.ts +4 -4
  4. package/esm/mod.js +2 -2
  5. package/esm/src/formats/apng.d.ts +5 -5
  6. package/esm/src/formats/apng.js +7 -9
  7. package/esm/src/formats/ascii.d.ts +3 -3
  8. package/esm/src/formats/ascii.js +1 -1
  9. package/esm/src/formats/avif.d.ts +3 -3
  10. package/esm/src/formats/avif.js +7 -7
  11. package/esm/src/formats/bmp.d.ts +3 -3
  12. package/esm/src/formats/bmp.js +2 -2
  13. package/esm/src/formats/dng.d.ts +1 -1
  14. package/esm/src/formats/dng.js +1 -1
  15. package/esm/src/formats/gif.d.ts +4 -4
  16. package/esm/src/formats/gif.js +14 -10
  17. package/esm/src/formats/heic.d.ts +3 -3
  18. package/esm/src/formats/heic.js +7 -7
  19. package/esm/src/formats/ico.d.ts +3 -3
  20. package/esm/src/formats/ico.js +4 -4
  21. package/esm/src/formats/jpeg.d.ts +3 -3
  22. package/esm/src/formats/jpeg.js +23 -11
  23. package/esm/src/formats/pam.d.ts +3 -3
  24. package/esm/src/formats/pam.js +2 -2
  25. package/esm/src/formats/pcx.d.ts +3 -3
  26. package/esm/src/formats/pcx.js +2 -2
  27. package/esm/src/formats/png.d.ts +3 -3
  28. package/esm/src/formats/png.js +2 -2
  29. package/esm/src/formats/png_base.js +2 -5
  30. package/esm/src/formats/ppm.d.ts +3 -3
  31. package/esm/src/formats/ppm.js +2 -2
  32. package/esm/src/formats/tiff.d.ts +7 -18
  33. package/esm/src/formats/tiff.js +86 -21
  34. package/esm/src/formats/webp.d.ts +3 -3
  35. package/esm/src/formats/webp.js +11 -8
  36. package/esm/src/image.d.ts +11 -3
  37. package/esm/src/image.js +37 -21
  38. package/esm/src/types.d.ts +56 -4
  39. package/esm/src/utils/gif_decoder.d.ts +4 -1
  40. package/esm/src/utils/gif_decoder.js +91 -65
  41. package/esm/src/utils/image_processing.js +144 -70
  42. package/esm/src/utils/jpeg_decoder.d.ts +17 -4
  43. package/esm/src/utils/jpeg_decoder.js +448 -83
  44. package/esm/src/utils/jpeg_encoder.d.ts +15 -1
  45. package/esm/src/utils/jpeg_encoder.js +263 -24
  46. package/esm/src/utils/resize.js +51 -20
  47. package/esm/src/utils/tiff_deflate.d.ts +18 -0
  48. package/esm/src/utils/tiff_deflate.js +27 -0
  49. package/esm/src/utils/tiff_packbits.d.ts +24 -0
  50. package/esm/src/utils/tiff_packbits.js +90 -0
  51. package/esm/src/utils/webp_decoder.d.ts +3 -1
  52. package/esm/src/utils/webp_decoder.js +144 -63
  53. package/esm/src/utils/webp_encoder.js +5 -11
  54. package/package.json +1 -1
  55. package/script/mod.d.ts +4 -4
  56. package/script/mod.js +2 -2
  57. package/script/src/formats/apng.d.ts +5 -5
  58. package/script/src/formats/apng.js +7 -9
  59. package/script/src/formats/ascii.d.ts +3 -3
  60. package/script/src/formats/ascii.js +1 -1
  61. package/script/src/formats/avif.d.ts +3 -3
  62. package/script/src/formats/avif.js +7 -7
  63. package/script/src/formats/bmp.d.ts +3 -3
  64. package/script/src/formats/bmp.js +2 -2
  65. package/script/src/formats/dng.d.ts +1 -1
  66. package/script/src/formats/dng.js +1 -1
  67. package/script/src/formats/gif.d.ts +4 -4
  68. package/script/src/formats/gif.js +14 -10
  69. package/script/src/formats/heic.d.ts +3 -3
  70. package/script/src/formats/heic.js +7 -7
  71. package/script/src/formats/ico.d.ts +3 -3
  72. package/script/src/formats/ico.js +4 -4
  73. package/script/src/formats/jpeg.d.ts +3 -3
  74. package/script/src/formats/jpeg.js +23 -11
  75. package/script/src/formats/pam.d.ts +3 -3
  76. package/script/src/formats/pam.js +2 -2
  77. package/script/src/formats/pcx.d.ts +3 -3
  78. package/script/src/formats/pcx.js +2 -2
  79. package/script/src/formats/png.d.ts +3 -3
  80. package/script/src/formats/png.js +2 -2
  81. package/script/src/formats/png_base.js +2 -5
  82. package/script/src/formats/ppm.d.ts +3 -3
  83. package/script/src/formats/ppm.js +2 -2
  84. package/script/src/formats/tiff.d.ts +7 -18
  85. package/script/src/formats/tiff.js +86 -21
  86. package/script/src/formats/webp.d.ts +3 -3
  87. package/script/src/formats/webp.js +11 -8
  88. package/script/src/image.d.ts +11 -3
  89. package/script/src/image.js +36 -20
  90. package/script/src/types.d.ts +56 -4
  91. package/script/src/utils/gif_decoder.d.ts +4 -1
  92. package/script/src/utils/gif_decoder.js +91 -65
  93. package/script/src/utils/image_processing.js +144 -70
  94. package/script/src/utils/jpeg_decoder.d.ts +17 -4
  95. package/script/src/utils/jpeg_decoder.js +448 -83
  96. package/script/src/utils/jpeg_encoder.d.ts +15 -1
  97. package/script/src/utils/jpeg_encoder.js +263 -24
  98. package/script/src/utils/resize.js +51 -20
  99. package/script/src/utils/tiff_deflate.d.ts +18 -0
  100. package/script/src/utils/tiff_deflate.js +31 -0
  101. package/script/src/utils/tiff_packbits.d.ts +24 -0
  102. package/script/src/utils/tiff_packbits.js +94 -0
  103. package/script/src/utils/webp_decoder.d.ts +3 -1
  104. package/script/src/utils/webp_decoder.js +144 -63
  105. package/script/src/utils/webp_encoder.js +5 -11
@@ -1,10 +1,23 @@
1
1
  /**
2
- * Basic baseline JPEG decoder implementation
3
- * Supports baseline DCT JPEG images (the most common format)
2
+ * JPEG decoder implementation supporting both baseline and progressive DCT
4
3
  *
5
- * This is a simplified implementation that handles common JPEG files.
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
- break; // Stop after first scan for baseline JPEG
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 Array(64);
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 Array(16);
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 Array(numSymbols);
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 Array(16).fill(-1);
277
- const minCode = new Array(16).fill(-1);
278
- const valPtr = new Array(16).fill(-1);
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
- this.pos += 3; // Skip spectral selection and successive approximation
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
- // Initialize blocks for each component
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
- const blocksAcross = Math.ceil(this.width * component.h / (8 * maxH));
359
- const blocksDown = Math.ceil(this.height * component.v / (8 * maxV));
360
- component.blocks = Array(blocksDown).fill(null).map(() => Array(blocksAcross).fill(null).map(() => new Array(64).fill(0)));
361
- }
362
- // Decode MCUs
363
- try {
364
- for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
365
- for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
366
- // Decode all components in this MCU
367
- for (const component of this.components) {
368
- for (let v = 0; v < component.v; v++) {
369
- for (let h = 0; h < component.h; h++) {
370
- const blockY = mcuY * component.v + v;
371
- const blockX = mcuX * component.h + h;
372
- if (blockY < component.blocks.length &&
373
- blockX < component.blocks[0].length) {
374
- this.decodeBlock(component, blockY, blockX);
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
- catch (e) {
383
- // If we run into issues during decoding, we may still have partial data
384
- console.warn("JPEG decode warning:", e);
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
- // Decode DC coefficient
390
- const dcTable = this.dcTables[component.dcTable];
391
- if (!dcTable) {
392
- throw new Error(`Missing DC table ${component.dcTable}`);
393
- }
394
- const dcLen = this.decodeHuffman(dcTable);
395
- const dcDiff = dcLen > 0 ? this.receiveBits(dcLen) : 0;
396
- component.pred += dcDiff;
397
- block[0] = component.pred * this.qTables[component.qTable][0];
398
- // Decode AC coefficients
399
- const acTable = this.acTables[component.acTable];
400
- if (!acTable) {
401
- throw new Error(`Missing AC table ${component.acTable}`);
402
- }
403
- let k = 1;
404
- while (k < 64) {
405
- const rs = this.decodeHuffman(acTable);
406
- const r = (rs >> 4) & 0x0F;
407
- const s = rs & 0x0F;
408
- if (s === 0) {
409
- if (r === 15) {
410
- k += 16;
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
- else {
413
- break; // EOB
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
- k += r;
418
- if (k >= 64)
419
- break;
420
- block[ZIGZAG[k]] = this.receiveBits(s) *
421
- this.qTables[component.qTable][ZIGZAG[k]];
422
- k++;
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
- this.idct(block);
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
- return table.huffVal[index];
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
- let byte = this.data[this.pos++];
442
- // Handle byte stuffing (0xFF 0x00)
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
- byte = this.data[this.pos++];
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 Array(64);
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
- const yBlockRow = Math.floor(row / 8);
535
- const yBlockCol = Math.floor(col / 8);
536
- const yBlockY = row % 8;
537
- const yBlockX = col % 8;
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];