cross-image 0.4.1 → 0.4.2

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.
@@ -214,10 +214,18 @@ class JPEGDecoder {
214
214
  writable: true,
215
215
  value: 0
216
216
  }); // Remaining blocks to skip due to EOBn
217
+ // Storage for quantized coefficients (when extractCoefficients is true)
218
+ Object.defineProperty(this, "quantizedCoefficients", {
219
+ enumerable: true,
220
+ configurable: true,
221
+ writable: true,
222
+ value: null
223
+ });
217
224
  this.data = data;
218
225
  this.options = {
219
226
  tolerantDecoding: settings.tolerantDecoding ?? true,
220
227
  onWarning: settings.onWarning,
228
+ extractCoefficients: settings.extractCoefficients ?? false,
221
229
  };
222
230
  }
223
231
  decode() {
@@ -279,7 +287,8 @@ class JPEGDecoder {
279
287
  // For progressive JPEGs, perform IDCT on all blocks after all scans are complete
280
288
  // This ensures that frequency-domain coefficients from multiple scans are properly
281
289
  // accumulated before transformation to spatial domain
282
- if (this.isProgressive) {
290
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
291
+ if (this.isProgressive && !this.options.extractCoefficients) {
283
292
  for (const component of this.components) {
284
293
  if (component.blocks) {
285
294
  for (const row of component.blocks) {
@@ -290,9 +299,63 @@ class JPEGDecoder {
290
299
  }
291
300
  }
292
301
  }
302
+ // If extracting coefficients, store them before converting to RGB
303
+ if (this.options.extractCoefficients) {
304
+ this.storeQuantizedCoefficients();
305
+ }
293
306
  // Convert YCbCr to RGB
294
307
  return this.convertToRGB();
295
308
  }
309
+ /**
310
+ * Get the quantized DCT coefficients after decoding
311
+ * Only available if extractCoefficients option was set to true
312
+ * @returns JPEGQuantizedCoefficients or undefined if not available
313
+ */
314
+ getQuantizedCoefficients() {
315
+ return this.quantizedCoefficients ?? undefined;
316
+ }
317
+ /**
318
+ * Store quantized coefficients in the output structure
319
+ * Called after decoding when extractCoefficients is true
320
+ */
321
+ storeQuantizedCoefficients() {
322
+ // Calculate MCU dimensions
323
+ const maxH = Math.max(...this.components.map((c) => c.h));
324
+ const maxV = Math.max(...this.components.map((c) => c.v));
325
+ const mcuWidth = Math.ceil(this.width / (8 * maxH));
326
+ const mcuHeight = Math.ceil(this.height / (8 * maxV));
327
+ // Build component coefficients
328
+ const componentCoeffs = this.components.map((comp) => ({
329
+ id: comp.id,
330
+ h: comp.h,
331
+ v: comp.v,
332
+ qTable: comp.qTable,
333
+ blocks: comp.blocks.map((row) => row.map((block) => {
334
+ // Convert to Int32Array if not already
335
+ if (block instanceof Int32Array) {
336
+ return block;
337
+ }
338
+ return new Int32Array(block);
339
+ })),
340
+ }));
341
+ // Copy quantization tables
342
+ const qTables = this.qTables.map((table) => {
343
+ if (table instanceof Uint8Array) {
344
+ return new Uint8Array(table);
345
+ }
346
+ return new Uint8Array(table);
347
+ });
348
+ this.quantizedCoefficients = {
349
+ format: "jpeg",
350
+ width: this.width,
351
+ height: this.height,
352
+ isProgressive: this.isProgressive,
353
+ components: componentCoeffs,
354
+ quantizationTables: qTables,
355
+ mcuWidth,
356
+ mcuHeight,
357
+ };
358
+ }
296
359
  readMarker() {
297
360
  while (this.pos < this.data.length && this.data[this.pos] !== 0xFF) {
298
361
  this.pos++;
@@ -593,7 +656,13 @@ class JPEGDecoder {
593
656
  component.pred += dcDiff;
594
657
  // For successive approximation, shift the coefficient left by Al bits
595
658
  const coeff = component.pred << this.successiveLow;
596
- block[0] = coeff * this.qTables[component.qTable][0];
659
+ // When extracting coefficients, store quantized value without dequantization
660
+ if (this.options.extractCoefficients) {
661
+ block[0] = coeff;
662
+ }
663
+ else {
664
+ block[0] = coeff * this.qTables[component.qTable][0];
665
+ }
597
666
  }
598
667
  else {
599
668
  // DC refinement scan: add a refinement bit
@@ -601,7 +670,12 @@ class JPEGDecoder {
601
670
  if (bit) {
602
671
  // Add the refinement bit at position Al
603
672
  const refinement = 1 << this.successiveLow;
604
- block[0] += refinement * this.qTables[component.qTable][0];
673
+ if (this.options.extractCoefficients) {
674
+ block[0] += refinement;
675
+ }
676
+ else {
677
+ block[0] += refinement * this.qTables[component.qTable][0];
678
+ }
605
679
  }
606
680
  }
607
681
  }
@@ -648,8 +722,14 @@ class JPEGDecoder {
648
722
  break;
649
723
  // For successive approximation, shift the coefficient left by Al bits
650
724
  const coeff = this.receiveBits(s) << this.successiveLow;
651
- block[ZIGZAG[k]] = coeff *
652
- this.qTables[component.qTable][ZIGZAG[k]];
725
+ // When extracting coefficients, store quantized value without dequantization
726
+ if (this.options.extractCoefficients) {
727
+ block[ZIGZAG[k]] = coeff;
728
+ }
729
+ else {
730
+ block[ZIGZAG[k]] = coeff *
731
+ this.qTables[component.qTable][ZIGZAG[k]];
732
+ }
653
733
  k++;
654
734
  }
655
735
  }
@@ -696,7 +776,10 @@ class JPEGDecoder {
696
776
  if (current !== 0) {
697
777
  const bit = this.readBit();
698
778
  if (bit) {
699
- const refinement = (1 << this.successiveLow) * qTable[z];
779
+ // When extracting coefficients, don't dequantize
780
+ const refinement = this.options.extractCoefficients
781
+ ? (1 << this.successiveLow)
782
+ : (1 << this.successiveLow) * qTable[z];
700
783
  block[z] += direction * refinement;
701
784
  }
702
785
  }
@@ -711,13 +794,17 @@ class JPEGDecoder {
711
794
  if (current !== 0) {
712
795
  const bit = this.readBit();
713
796
  if (bit) {
714
- const refinement = (1 << this.successiveLow) * qTable[z];
797
+ // When extracting coefficients, don't dequantize
798
+ const refinement = this.options.extractCoefficients
799
+ ? (1 << this.successiveLow)
800
+ : (1 << this.successiveLow) * qTable[z];
715
801
  block[z] += direction * refinement;
716
802
  }
717
803
  }
718
804
  else {
719
805
  const newCoeff = successiveACNextValue << this.successiveLow;
720
- block[z] = newCoeff * qTable[z];
806
+ // When extracting coefficients, don't dequantize
807
+ block[z] = this.options.extractCoefficients ? newCoeff : newCoeff * qTable[z];
721
808
  successiveACState = 0;
722
809
  }
723
810
  break;
@@ -725,7 +812,10 @@ class JPEGDecoder {
725
812
  if (current !== 0) {
726
813
  const bit = this.readBit();
727
814
  if (bit) {
728
- const refinement = (1 << this.successiveLow) * qTable[z];
815
+ // When extracting coefficients, don't dequantize
816
+ const refinement = this.options.extractCoefficients
817
+ ? (1 << this.successiveLow)
818
+ : (1 << this.successiveLow) * qTable[z];
729
819
  block[z] += direction * refinement;
730
820
  }
731
821
  }
@@ -744,7 +834,8 @@ class JPEGDecoder {
744
834
  // Perform IDCT only for baseline JPEGs
745
835
  // For progressive JPEGs, IDCT is deferred until all scans are complete
746
836
  // to preserve frequency-domain coefficients for accumulation across scans
747
- if (!this.isProgressive) {
837
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
838
+ if (!this.isProgressive && !this.options.extractCoefficients) {
748
839
  this.idct(block);
749
840
  }
750
841
  }
@@ -5,6 +5,7 @@
5
5
  * This is a simplified implementation focusing on correctness over performance.
6
6
  * For production use with better quality/size, the OffscreenCanvas API is preferred.
7
7
  */
8
+ import type { JPEGQuantizedCoefficients } from "../types.js";
8
9
  export interface JPEGEncoderOptions {
9
10
  quality?: number;
10
11
  progressive?: boolean;
@@ -43,5 +44,23 @@ export declare class JPEGEncoder {
43
44
  private forwardDCT;
44
45
  private encodeDC;
45
46
  private encodeAC;
47
+ /**
48
+ * Encode JPEG from pre-quantized DCT coefficients
49
+ * Skips DCT and quantization - uses provided coefficients directly
50
+ * Useful for steganography where coefficients are modified and re-encoded
51
+ * @param coeffs JPEG quantized coefficients
52
+ * @param _options Optional encoding options (currently unused)
53
+ * @returns Encoded JPEG bytes
54
+ */
55
+ encodeFromCoefficients(coeffs: JPEGQuantizedCoefficients, _options?: JPEGEncoderOptions): Uint8Array;
56
+ private writeDQTFromCoeffs;
57
+ private writeSOF0FromCoeffs;
58
+ private writeSOF2FromCoeffs;
59
+ private encodeScanFromCoeffs;
60
+ private encodeACFromCoeffs;
61
+ private encodeProgressiveFromCoeffs;
62
+ private encodeProgressiveDCScanFromCoeffs;
63
+ private encodeProgressiveACScanFromCoeffs;
64
+ private encodeOnlyACFromCoeffs;
46
65
  }
47
66
  //# sourceMappingURL=jpeg_encoder.d.ts.map
@@ -1256,5 +1256,272 @@ class JPEGEncoder {
1256
1256
  bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
1257
1257
  }
1258
1258
  }
1259
+ /**
1260
+ * Encode JPEG from pre-quantized DCT coefficients
1261
+ * Skips DCT and quantization - uses provided coefficients directly
1262
+ * Useful for steganography where coefficients are modified and re-encoded
1263
+ * @param coeffs JPEG quantized coefficients
1264
+ * @param _options Optional encoding options (currently unused)
1265
+ * @returns Encoded JPEG bytes
1266
+ */
1267
+ encodeFromCoefficients(coeffs, _options) {
1268
+ const output = [];
1269
+ const { width, height, components, quantizationTables, isProgressive } = coeffs;
1270
+ // SOI (Start of Image)
1271
+ output.push(0xff, 0xd8);
1272
+ // APP0 (JFIF marker) - use default 72 DPI
1273
+ this.writeAPP0(output, 72, 72);
1274
+ // DQT (Define Quantization Tables) - use original tables from coefficients
1275
+ this.writeDQTFromCoeffs(output, quantizationTables);
1276
+ // SOF (Start of Frame)
1277
+ if (isProgressive) {
1278
+ this.writeSOF2FromCoeffs(output, width, height, components);
1279
+ }
1280
+ else {
1281
+ this.writeSOF0FromCoeffs(output, width, height, components);
1282
+ }
1283
+ // DHT (Define Huffman Tables)
1284
+ this.writeDHT(output);
1285
+ if (isProgressive) {
1286
+ // Progressive encoding: encode from coefficients
1287
+ this.encodeProgressiveFromCoeffs(output, coeffs);
1288
+ }
1289
+ else {
1290
+ // Baseline encoding: single scan
1291
+ this.writeSOS(output);
1292
+ // Encode scan data from coefficients
1293
+ const scanData = this.encodeScanFromCoeffs(coeffs);
1294
+ for (let i = 0; i < scanData.length; i++) {
1295
+ output.push(scanData[i]);
1296
+ }
1297
+ }
1298
+ // EOI (End of Image)
1299
+ output.push(0xff, 0xd9);
1300
+ return new Uint8Array(output);
1301
+ }
1302
+ writeDQTFromCoeffs(output, quantizationTables) {
1303
+ // Write each quantization table
1304
+ for (let tableId = 0; tableId < quantizationTables.length; tableId++) {
1305
+ const table = quantizationTables[tableId];
1306
+ if (!table)
1307
+ continue;
1308
+ output.push(0xff, 0xdb); // DQT marker
1309
+ output.push(0x00, 0x43); // Length (67 bytes)
1310
+ output.push(tableId); // Table ID, 8-bit precision
1311
+ // Write table values in zigzag order
1312
+ for (let i = 0; i < 64; i++) {
1313
+ output.push(table[ZIGZAG[i]] ?? 1);
1314
+ }
1315
+ }
1316
+ }
1317
+ writeSOF0FromCoeffs(output, width, height, components) {
1318
+ const numComponents = components.length;
1319
+ const length = 8 + numComponents * 3;
1320
+ output.push(0xff, 0xc0); // SOF0 marker
1321
+ output.push((length >> 8) & 0xff, length & 0xff);
1322
+ output.push(0x08); // Precision (8 bits)
1323
+ output.push((height >> 8) & 0xff, height & 0xff);
1324
+ output.push((width >> 8) & 0xff, width & 0xff);
1325
+ output.push(numComponents);
1326
+ for (const comp of components) {
1327
+ output.push(comp.id);
1328
+ output.push((comp.h << 4) | comp.v);
1329
+ output.push(comp.qTable);
1330
+ }
1331
+ }
1332
+ writeSOF2FromCoeffs(output, width, height, components) {
1333
+ const numComponents = components.length;
1334
+ const length = 8 + numComponents * 3;
1335
+ output.push(0xff, 0xc2); // SOF2 marker (Progressive DCT)
1336
+ output.push((length >> 8) & 0xff, length & 0xff);
1337
+ output.push(0x08); // Precision (8 bits)
1338
+ output.push((height >> 8) & 0xff, height & 0xff);
1339
+ output.push((width >> 8) & 0xff, width & 0xff);
1340
+ output.push(numComponents);
1341
+ for (const comp of components) {
1342
+ output.push(comp.id);
1343
+ output.push((comp.h << 4) | comp.v);
1344
+ output.push(comp.qTable);
1345
+ }
1346
+ }
1347
+ encodeScanFromCoeffs(coeffs) {
1348
+ const bitWriter = new BitWriter();
1349
+ const { components } = coeffs;
1350
+ // DC predictors for each component
1351
+ const dcPreds = new Map();
1352
+ for (const comp of components) {
1353
+ dcPreds.set(comp.id, 0);
1354
+ }
1355
+ // Encode MCUs in raster order
1356
+ // For non-subsampled images (1:1:1), each MCU contains one block per component
1357
+ const mcuHeight = coeffs.mcuHeight;
1358
+ const mcuWidth = coeffs.mcuWidth;
1359
+ for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
1360
+ for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
1361
+ // Encode each component's blocks in this MCU
1362
+ for (const comp of components) {
1363
+ // Handle subsampling: component may have multiple blocks per MCU
1364
+ for (let v = 0; v < comp.v; v++) {
1365
+ for (let h = 0; h < comp.h; h++) {
1366
+ const blockY = mcuY * comp.v + v;
1367
+ const blockX = mcuX * comp.h + h;
1368
+ if (blockY < comp.blocks.length &&
1369
+ blockX < comp.blocks[0].length) {
1370
+ const block = comp.blocks[blockY][blockX];
1371
+ // Get the block in zigzag order for encoding
1372
+ const zigzagBlock = new Int32Array(64);
1373
+ for (let i = 0; i < 64; i++) {
1374
+ zigzagBlock[i] = block[ZIGZAG[i]];
1375
+ }
1376
+ // Encode DC coefficient
1377
+ const prevDC = dcPreds.get(comp.id) ?? 0;
1378
+ const dcHuffman = comp.id === 1
1379
+ ? this.dcLuminanceHuffman
1380
+ : this.dcChrominanceHuffman;
1381
+ const acHuffman = comp.id === 1
1382
+ ? this.acLuminanceHuffman
1383
+ : this.acChrominanceHuffman;
1384
+ // DC coefficient is at index 0 of the block (already in zigzag order from decoder)
1385
+ const dc = block[0];
1386
+ const dcDiff = dc - prevDC;
1387
+ this.encodeDC(dcDiff, dcHuffman, bitWriter);
1388
+ dcPreds.set(comp.id, dc);
1389
+ // Encode AC coefficients (indices 1-63 in zigzag order)
1390
+ this.encodeACFromCoeffs(block, acHuffman, bitWriter);
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ }
1397
+ bitWriter.flush();
1398
+ return Array.from(bitWriter.getBytes());
1399
+ }
1400
+ encodeACFromCoeffs(block, huffTable, bitWriter) {
1401
+ let zeroCount = 0;
1402
+ // Block is stored with DC at index 0, and AC at indices corresponding to zigzag positions
1403
+ // We need to iterate in zigzag order for AC coefficients (indices 1-63)
1404
+ for (let i = 1; i < 64; i++) {
1405
+ const coef = block[ZIGZAG[i]];
1406
+ if (coef === 0) {
1407
+ zeroCount++;
1408
+ }
1409
+ else {
1410
+ // Write any pending zero runs
1411
+ while (zeroCount >= 16) {
1412
+ bitWriter.writeBits(huffTable.codes[0xf0], huffTable.sizes[0xf0]);
1413
+ zeroCount -= 16;
1414
+ }
1415
+ // Clamp coefficient
1416
+ const maxAC = 1023;
1417
+ const clampedCoef = Math.max(-maxAC, Math.min(maxAC, coef));
1418
+ const absCoef = Math.abs(clampedCoef);
1419
+ const size = Math.floor(Math.log2(absCoef)) + 1;
1420
+ const symbol = (zeroCount << 4) | size;
1421
+ bitWriter.writeBits(huffTable.codes[symbol], huffTable.sizes[symbol]);
1422
+ const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
1423
+ bitWriter.writeBits(magnitude, size);
1424
+ zeroCount = 0;
1425
+ }
1426
+ }
1427
+ // Write EOB if there are trailing zeros
1428
+ if (zeroCount > 0) {
1429
+ bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
1430
+ }
1431
+ }
1432
+ encodeProgressiveFromCoeffs(output, coeffs) {
1433
+ const { components } = coeffs;
1434
+ // Pre-extract blocks for each component (already quantized)
1435
+ const componentBlocks = new Map();
1436
+ for (const comp of components) {
1437
+ const blocks = [];
1438
+ for (const row of comp.blocks) {
1439
+ for (const block of row) {
1440
+ blocks.push(block);
1441
+ }
1442
+ }
1443
+ componentBlocks.set(comp.id, blocks);
1444
+ }
1445
+ // Scan 1: DC-only (interleaved across all components)
1446
+ this.writeProgressiveSOS(output, components.map((c) => c.id), 0, 0, 0, 0);
1447
+ const dcScanData = this.encodeProgressiveDCScanFromCoeffs(componentBlocks, components);
1448
+ for (let i = 0; i < dcScanData.length; i++) {
1449
+ output.push(dcScanData[i]);
1450
+ }
1451
+ // Scan 2+: AC coefficients (one scan per component)
1452
+ for (const comp of components) {
1453
+ this.writeProgressiveSOS(output, [comp.id], 1, 63, 0, 0);
1454
+ const blocks = componentBlocks.get(comp.id) ?? [];
1455
+ const acHuffman = comp.id === 1 ? this.acLuminanceHuffman : this.acChrominanceHuffman;
1456
+ const acScanData = this.encodeProgressiveACScanFromCoeffs(blocks, acHuffman);
1457
+ for (let i = 0; i < acScanData.length; i++) {
1458
+ output.push(acScanData[i]);
1459
+ }
1460
+ }
1461
+ }
1462
+ encodeProgressiveDCScanFromCoeffs(componentBlocks, components) {
1463
+ const bitWriter = new BitWriter();
1464
+ const dcPreds = new Map();
1465
+ for (const comp of components) {
1466
+ dcPreds.set(comp.id, 0);
1467
+ }
1468
+ // Get number of blocks (should be same for all components in interleaved scan)
1469
+ const numBlocks = componentBlocks.get(components[0].id)?.length ?? 0;
1470
+ for (let i = 0; i < numBlocks; i++) {
1471
+ for (const comp of components) {
1472
+ const blocks = componentBlocks.get(comp.id);
1473
+ if (!blocks || i >= blocks.length)
1474
+ continue;
1475
+ const block = blocks[i];
1476
+ const dc = block[0]; // DC coefficient at index 0
1477
+ const prevDC = dcPreds.get(comp.id) ?? 0;
1478
+ const dcHuffman = comp.id === 1 ? this.dcLuminanceHuffman : this.dcChrominanceHuffman;
1479
+ this.encodeOnlyDC(dc, prevDC, dcHuffman, bitWriter);
1480
+ dcPreds.set(comp.id, dc);
1481
+ }
1482
+ }
1483
+ bitWriter.flush();
1484
+ return Array.from(bitWriter.getBytes());
1485
+ }
1486
+ encodeProgressiveACScanFromCoeffs(blocks, acHuffman) {
1487
+ const bitWriter = new BitWriter();
1488
+ for (const block of blocks) {
1489
+ this.encodeOnlyACFromCoeffs(block, acHuffman, bitWriter);
1490
+ }
1491
+ bitWriter.flush();
1492
+ return Array.from(bitWriter.getBytes());
1493
+ }
1494
+ encodeOnlyACFromCoeffs(block, acTable, bitWriter) {
1495
+ let zeroCount = 0;
1496
+ // Iterate AC coefficients in zigzag order (indices 1-63)
1497
+ for (let i = 1; i < 64; i++) {
1498
+ const coef = block[ZIGZAG[i]];
1499
+ const clampedCoef = Math.max(-1023, Math.min(1023, coef));
1500
+ if (clampedCoef === 0) {
1501
+ zeroCount++;
1502
+ if (zeroCount === 16) {
1503
+ bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
1504
+ zeroCount = 0;
1505
+ }
1506
+ }
1507
+ else {
1508
+ while (zeroCount >= 16) {
1509
+ bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
1510
+ zeroCount -= 16;
1511
+ }
1512
+ const absCoef = Math.abs(clampedCoef);
1513
+ const size = Math.floor(Math.log2(absCoef)) + 1;
1514
+ const symbol = (zeroCount << 4) | size;
1515
+ bitWriter.writeBits(acTable.codes[symbol], acTable.sizes[symbol]);
1516
+ const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
1517
+ bitWriter.writeBits(magnitude, size);
1518
+ zeroCount = 0;
1519
+ }
1520
+ }
1521
+ // Write EOB if there are trailing zeros
1522
+ if (zeroCount > 0) {
1523
+ bitWriter.writeBits(acTable.codes[0x00], acTable.sizes[0x00]);
1524
+ }
1525
+ }
1259
1526
  }
1260
1527
  exports.JPEGEncoder = JPEGEncoder;