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,13 +1,26 @@
1
1
  "use strict";
2
2
  /**
3
- * Basic baseline JPEG decoder implementation
4
- * Supports baseline DCT JPEG images (the most common format)
3
+ * JPEG decoder implementation supporting both baseline and progressive DCT
5
4
  *
6
- * This is a simplified implementation that handles common JPEG files.
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
- break; // Stop after first scan for baseline JPEG
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 Array(64);
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 Array(16);
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 Array(numSymbols);
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 Array(16).fill(-1);
280
- const minCode = new Array(16).fill(-1);
281
- const valPtr = new Array(16).fill(-1);
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
- this.pos += 3; // Skip spectral selection and successive approximation
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
- // Initialize blocks for each component
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
- const blocksAcross = Math.ceil(this.width * component.h / (8 * maxH));
362
- const blocksDown = Math.ceil(this.height * component.v / (8 * maxV));
363
- component.blocks = Array(blocksDown).fill(null).map(() => Array(blocksAcross).fill(null).map(() => new Array(64).fill(0)));
364
- }
365
- // Decode MCUs
366
- try {
367
- for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
368
- for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
369
- // Decode all components in this MCU
370
- for (const component of this.components) {
371
- for (let v = 0; v < component.v; v++) {
372
- for (let h = 0; h < component.h; h++) {
373
- const blockY = mcuY * component.v + v;
374
- const blockX = mcuX * component.h + h;
375
- if (blockY < component.blocks.length &&
376
- blockX < component.blocks[0].length) {
377
- this.decodeBlock(component, blockY, blockX);
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
- catch (e) {
386
- // If we run into issues during decoding, we may still have partial data
387
- console.warn("JPEG decode warning:", e);
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
- // Decode DC coefficient
393
- const dcTable = this.dcTables[component.dcTable];
394
- if (!dcTable) {
395
- throw new Error(`Missing DC table ${component.dcTable}`);
396
- }
397
- const dcLen = this.decodeHuffman(dcTable);
398
- const dcDiff = dcLen > 0 ? this.receiveBits(dcLen) : 0;
399
- component.pred += dcDiff;
400
- block[0] = component.pred * this.qTables[component.qTable][0];
401
- // Decode AC coefficients
402
- const acTable = this.acTables[component.acTable];
403
- if (!acTable) {
404
- throw new Error(`Missing AC table ${component.acTable}`);
405
- }
406
- let k = 1;
407
- while (k < 64) {
408
- const rs = this.decodeHuffman(acTable);
409
- const r = (rs >> 4) & 0x0F;
410
- const s = rs & 0x0F;
411
- if (s === 0) {
412
- if (r === 15) {
413
- k += 16;
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
- else {
416
- break; // EOB
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
- k += r;
421
- if (k >= 64)
422
- break;
423
- block[ZIGZAG[k]] = this.receiveBits(s) *
424
- this.qTables[component.qTable][ZIGZAG[k]];
425
- k++;
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
- this.idct(block);
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
- return table.huffVal[index];
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
- let byte = this.data[this.pos++];
445
- // Handle byte stuffing (0xFF 0x00)
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
- byte = this.data[this.pos++];
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 Array(64);
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
- const yBlockRow = Math.floor(row / 8);
538
- const yBlockCol = Math.floor(col / 8);
539
- const yBlockY = row % 8;
540
- const yBlockX = col % 8;
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];