cross-image 0.4.0 → 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.
Files changed (62) hide show
  1. package/README.md +186 -5
  2. package/esm/mod.d.ts +3 -1
  3. package/esm/mod.js +2 -0
  4. package/esm/src/formats/apng.d.ts +5 -3
  5. package/esm/src/formats/apng.js +11 -4
  6. package/esm/src/formats/avif.d.ts +2 -2
  7. package/esm/src/formats/avif.js +11 -1
  8. package/esm/src/formats/gif.d.ts +3 -3
  9. package/esm/src/formats/gif.js +4 -4
  10. package/esm/src/formats/heic.d.ts +2 -2
  11. package/esm/src/formats/heic.js +11 -1
  12. package/esm/src/formats/jpeg.d.ts +21 -1
  13. package/esm/src/formats/jpeg.js +59 -0
  14. package/esm/src/formats/png.d.ts +3 -2
  15. package/esm/src/formats/png.js +8 -2
  16. package/esm/src/formats/png_base.d.ts +42 -1
  17. package/esm/src/formats/png_base.js +198 -5
  18. package/esm/src/formats/tiff.js +76 -6
  19. package/esm/src/image.d.ts +54 -1
  20. package/esm/src/image.js +97 -1
  21. package/esm/src/types.d.ts +129 -0
  22. package/esm/src/utils/base64.d.ts +32 -0
  23. package/esm/src/utils/base64.js +173 -0
  24. package/esm/src/utils/gif_encoder.d.ts +3 -1
  25. package/esm/src/utils/gif_encoder.js +4 -2
  26. package/esm/src/utils/image_processing.d.ts +31 -0
  27. package/esm/src/utils/image_processing.js +88 -0
  28. package/esm/src/utils/jpeg_decoder.d.ts +25 -2
  29. package/esm/src/utils/jpeg_decoder.js +101 -10
  30. package/esm/src/utils/jpeg_encoder.d.ts +19 -0
  31. package/esm/src/utils/jpeg_encoder.js +267 -0
  32. package/package.json +1 -1
  33. package/script/mod.d.ts +3 -1
  34. package/script/mod.js +11 -1
  35. package/script/src/formats/apng.d.ts +5 -3
  36. package/script/src/formats/apng.js +11 -4
  37. package/script/src/formats/avif.d.ts +2 -2
  38. package/script/src/formats/avif.js +11 -1
  39. package/script/src/formats/gif.d.ts +3 -3
  40. package/script/src/formats/gif.js +4 -4
  41. package/script/src/formats/heic.d.ts +2 -2
  42. package/script/src/formats/heic.js +11 -1
  43. package/script/src/formats/jpeg.d.ts +21 -1
  44. package/script/src/formats/jpeg.js +59 -0
  45. package/script/src/formats/png.d.ts +3 -2
  46. package/script/src/formats/png.js +8 -2
  47. package/script/src/formats/png_base.d.ts +42 -1
  48. package/script/src/formats/png_base.js +198 -5
  49. package/script/src/formats/tiff.js +76 -6
  50. package/script/src/image.d.ts +54 -1
  51. package/script/src/image.js +96 -0
  52. package/script/src/types.d.ts +129 -0
  53. package/script/src/utils/base64.d.ts +32 -0
  54. package/script/src/utils/base64.js +179 -0
  55. package/script/src/utils/gif_encoder.d.ts +3 -1
  56. package/script/src/utils/gif_encoder.js +4 -2
  57. package/script/src/utils/image_processing.d.ts +31 -0
  58. package/script/src/utils/image_processing.js +92 -0
  59. package/script/src/utils/jpeg_decoder.d.ts +25 -2
  60. package/script/src/utils/jpeg_decoder.js +101 -10
  61. package/script/src/utils/jpeg_encoder.d.ts +19 -0
  62. package/script/src/utils/jpeg_encoder.js +267 -0
@@ -211,10 +211,18 @@ export class JPEGDecoder {
211
211
  writable: true,
212
212
  value: 0
213
213
  }); // Remaining blocks to skip due to EOBn
214
+ // Storage for quantized coefficients (when extractCoefficients is true)
215
+ Object.defineProperty(this, "quantizedCoefficients", {
216
+ enumerable: true,
217
+ configurable: true,
218
+ writable: true,
219
+ value: null
220
+ });
214
221
  this.data = data;
215
222
  this.options = {
216
223
  tolerantDecoding: settings.tolerantDecoding ?? true,
217
224
  onWarning: settings.onWarning,
225
+ extractCoefficients: settings.extractCoefficients ?? false,
218
226
  };
219
227
  }
220
228
  decode() {
@@ -276,7 +284,8 @@ export class JPEGDecoder {
276
284
  // For progressive JPEGs, perform IDCT on all blocks after all scans are complete
277
285
  // This ensures that frequency-domain coefficients from multiple scans are properly
278
286
  // accumulated before transformation to spatial domain
279
- if (this.isProgressive) {
287
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
288
+ if (this.isProgressive && !this.options.extractCoefficients) {
280
289
  for (const component of this.components) {
281
290
  if (component.blocks) {
282
291
  for (const row of component.blocks) {
@@ -287,9 +296,63 @@ export class JPEGDecoder {
287
296
  }
288
297
  }
289
298
  }
299
+ // If extracting coefficients, store them before converting to RGB
300
+ if (this.options.extractCoefficients) {
301
+ this.storeQuantizedCoefficients();
302
+ }
290
303
  // Convert YCbCr to RGB
291
304
  return this.convertToRGB();
292
305
  }
306
+ /**
307
+ * Get the quantized DCT coefficients after decoding
308
+ * Only available if extractCoefficients option was set to true
309
+ * @returns JPEGQuantizedCoefficients or undefined if not available
310
+ */
311
+ getQuantizedCoefficients() {
312
+ return this.quantizedCoefficients ?? undefined;
313
+ }
314
+ /**
315
+ * Store quantized coefficients in the output structure
316
+ * Called after decoding when extractCoefficients is true
317
+ */
318
+ storeQuantizedCoefficients() {
319
+ // Calculate MCU dimensions
320
+ const maxH = Math.max(...this.components.map((c) => c.h));
321
+ const maxV = Math.max(...this.components.map((c) => c.v));
322
+ const mcuWidth = Math.ceil(this.width / (8 * maxH));
323
+ const mcuHeight = Math.ceil(this.height / (8 * maxV));
324
+ // Build component coefficients
325
+ const componentCoeffs = this.components.map((comp) => ({
326
+ id: comp.id,
327
+ h: comp.h,
328
+ v: comp.v,
329
+ qTable: comp.qTable,
330
+ blocks: comp.blocks.map((row) => row.map((block) => {
331
+ // Convert to Int32Array if not already
332
+ if (block instanceof Int32Array) {
333
+ return block;
334
+ }
335
+ return new Int32Array(block);
336
+ })),
337
+ }));
338
+ // Copy quantization tables
339
+ const qTables = this.qTables.map((table) => {
340
+ if (table instanceof Uint8Array) {
341
+ return new Uint8Array(table);
342
+ }
343
+ return new Uint8Array(table);
344
+ });
345
+ this.quantizedCoefficients = {
346
+ format: "jpeg",
347
+ width: this.width,
348
+ height: this.height,
349
+ isProgressive: this.isProgressive,
350
+ components: componentCoeffs,
351
+ quantizationTables: qTables,
352
+ mcuWidth,
353
+ mcuHeight,
354
+ };
355
+ }
293
356
  readMarker() {
294
357
  while (this.pos < this.data.length && this.data[this.pos] !== 0xFF) {
295
358
  this.pos++;
@@ -590,7 +653,13 @@ export class JPEGDecoder {
590
653
  component.pred += dcDiff;
591
654
  // For successive approximation, shift the coefficient left by Al bits
592
655
  const coeff = component.pred << this.successiveLow;
593
- block[0] = coeff * this.qTables[component.qTable][0];
656
+ // When extracting coefficients, store quantized value without dequantization
657
+ if (this.options.extractCoefficients) {
658
+ block[0] = coeff;
659
+ }
660
+ else {
661
+ block[0] = coeff * this.qTables[component.qTable][0];
662
+ }
594
663
  }
595
664
  else {
596
665
  // DC refinement scan: add a refinement bit
@@ -598,7 +667,12 @@ export class JPEGDecoder {
598
667
  if (bit) {
599
668
  // Add the refinement bit at position Al
600
669
  const refinement = 1 << this.successiveLow;
601
- block[0] += refinement * this.qTables[component.qTable][0];
670
+ if (this.options.extractCoefficients) {
671
+ block[0] += refinement;
672
+ }
673
+ else {
674
+ block[0] += refinement * this.qTables[component.qTable][0];
675
+ }
602
676
  }
603
677
  }
604
678
  }
@@ -645,8 +719,14 @@ export class JPEGDecoder {
645
719
  break;
646
720
  // For successive approximation, shift the coefficient left by Al bits
647
721
  const coeff = this.receiveBits(s) << this.successiveLow;
648
- block[ZIGZAG[k]] = coeff *
649
- this.qTables[component.qTable][ZIGZAG[k]];
722
+ // When extracting coefficients, store quantized value without dequantization
723
+ if (this.options.extractCoefficients) {
724
+ block[ZIGZAG[k]] = coeff;
725
+ }
726
+ else {
727
+ block[ZIGZAG[k]] = coeff *
728
+ this.qTables[component.qTable][ZIGZAG[k]];
729
+ }
650
730
  k++;
651
731
  }
652
732
  }
@@ -693,7 +773,10 @@ export class JPEGDecoder {
693
773
  if (current !== 0) {
694
774
  const bit = this.readBit();
695
775
  if (bit) {
696
- const refinement = (1 << this.successiveLow) * qTable[z];
776
+ // When extracting coefficients, don't dequantize
777
+ const refinement = this.options.extractCoefficients
778
+ ? (1 << this.successiveLow)
779
+ : (1 << this.successiveLow) * qTable[z];
697
780
  block[z] += direction * refinement;
698
781
  }
699
782
  }
@@ -708,13 +791,17 @@ export class JPEGDecoder {
708
791
  if (current !== 0) {
709
792
  const bit = this.readBit();
710
793
  if (bit) {
711
- const refinement = (1 << this.successiveLow) * qTable[z];
794
+ // When extracting coefficients, don't dequantize
795
+ const refinement = this.options.extractCoefficients
796
+ ? (1 << this.successiveLow)
797
+ : (1 << this.successiveLow) * qTable[z];
712
798
  block[z] += direction * refinement;
713
799
  }
714
800
  }
715
801
  else {
716
802
  const newCoeff = successiveACNextValue << this.successiveLow;
717
- block[z] = newCoeff * qTable[z];
803
+ // When extracting coefficients, don't dequantize
804
+ block[z] = this.options.extractCoefficients ? newCoeff : newCoeff * qTable[z];
718
805
  successiveACState = 0;
719
806
  }
720
807
  break;
@@ -722,7 +809,10 @@ export class JPEGDecoder {
722
809
  if (current !== 0) {
723
810
  const bit = this.readBit();
724
811
  if (bit) {
725
- const refinement = (1 << this.successiveLow) * qTable[z];
812
+ // When extracting coefficients, don't dequantize
813
+ const refinement = this.options.extractCoefficients
814
+ ? (1 << this.successiveLow)
815
+ : (1 << this.successiveLow) * qTable[z];
726
816
  block[z] += direction * refinement;
727
817
  }
728
818
  }
@@ -741,7 +831,8 @@ export class JPEGDecoder {
741
831
  // Perform IDCT only for baseline JPEGs
742
832
  // For progressive JPEGs, IDCT is deferred until all scans are complete
743
833
  // to preserve frequency-domain coefficients for accumulation across scans
744
- if (!this.isProgressive) {
834
+ // Skip IDCT when extracting coefficients - we want the quantized DCT values
835
+ if (!this.isProgressive && !this.options.extractCoefficients) {
745
836
  this.idct(block);
746
837
  }
747
838
  }
@@ -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
@@ -1253,4 +1253,271 @@ export class JPEGEncoder {
1253
1253
  bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
1254
1254
  }
1255
1255
  }
1256
+ /**
1257
+ * Encode JPEG from pre-quantized DCT coefficients
1258
+ * Skips DCT and quantization - uses provided coefficients directly
1259
+ * Useful for steganography where coefficients are modified and re-encoded
1260
+ * @param coeffs JPEG quantized coefficients
1261
+ * @param _options Optional encoding options (currently unused)
1262
+ * @returns Encoded JPEG bytes
1263
+ */
1264
+ encodeFromCoefficients(coeffs, _options) {
1265
+ const output = [];
1266
+ const { width, height, components, quantizationTables, isProgressive } = coeffs;
1267
+ // SOI (Start of Image)
1268
+ output.push(0xff, 0xd8);
1269
+ // APP0 (JFIF marker) - use default 72 DPI
1270
+ this.writeAPP0(output, 72, 72);
1271
+ // DQT (Define Quantization Tables) - use original tables from coefficients
1272
+ this.writeDQTFromCoeffs(output, quantizationTables);
1273
+ // SOF (Start of Frame)
1274
+ if (isProgressive) {
1275
+ this.writeSOF2FromCoeffs(output, width, height, components);
1276
+ }
1277
+ else {
1278
+ this.writeSOF0FromCoeffs(output, width, height, components);
1279
+ }
1280
+ // DHT (Define Huffman Tables)
1281
+ this.writeDHT(output);
1282
+ if (isProgressive) {
1283
+ // Progressive encoding: encode from coefficients
1284
+ this.encodeProgressiveFromCoeffs(output, coeffs);
1285
+ }
1286
+ else {
1287
+ // Baseline encoding: single scan
1288
+ this.writeSOS(output);
1289
+ // Encode scan data from coefficients
1290
+ const scanData = this.encodeScanFromCoeffs(coeffs);
1291
+ for (let i = 0; i < scanData.length; i++) {
1292
+ output.push(scanData[i]);
1293
+ }
1294
+ }
1295
+ // EOI (End of Image)
1296
+ output.push(0xff, 0xd9);
1297
+ return new Uint8Array(output);
1298
+ }
1299
+ writeDQTFromCoeffs(output, quantizationTables) {
1300
+ // Write each quantization table
1301
+ for (let tableId = 0; tableId < quantizationTables.length; tableId++) {
1302
+ const table = quantizationTables[tableId];
1303
+ if (!table)
1304
+ continue;
1305
+ output.push(0xff, 0xdb); // DQT marker
1306
+ output.push(0x00, 0x43); // Length (67 bytes)
1307
+ output.push(tableId); // Table ID, 8-bit precision
1308
+ // Write table values in zigzag order
1309
+ for (let i = 0; i < 64; i++) {
1310
+ output.push(table[ZIGZAG[i]] ?? 1);
1311
+ }
1312
+ }
1313
+ }
1314
+ writeSOF0FromCoeffs(output, width, height, components) {
1315
+ const numComponents = components.length;
1316
+ const length = 8 + numComponents * 3;
1317
+ output.push(0xff, 0xc0); // SOF0 marker
1318
+ output.push((length >> 8) & 0xff, length & 0xff);
1319
+ output.push(0x08); // Precision (8 bits)
1320
+ output.push((height >> 8) & 0xff, height & 0xff);
1321
+ output.push((width >> 8) & 0xff, width & 0xff);
1322
+ output.push(numComponents);
1323
+ for (const comp of components) {
1324
+ output.push(comp.id);
1325
+ output.push((comp.h << 4) | comp.v);
1326
+ output.push(comp.qTable);
1327
+ }
1328
+ }
1329
+ writeSOF2FromCoeffs(output, width, height, components) {
1330
+ const numComponents = components.length;
1331
+ const length = 8 + numComponents * 3;
1332
+ output.push(0xff, 0xc2); // SOF2 marker (Progressive DCT)
1333
+ output.push((length >> 8) & 0xff, length & 0xff);
1334
+ output.push(0x08); // Precision (8 bits)
1335
+ output.push((height >> 8) & 0xff, height & 0xff);
1336
+ output.push((width >> 8) & 0xff, width & 0xff);
1337
+ output.push(numComponents);
1338
+ for (const comp of components) {
1339
+ output.push(comp.id);
1340
+ output.push((comp.h << 4) | comp.v);
1341
+ output.push(comp.qTable);
1342
+ }
1343
+ }
1344
+ encodeScanFromCoeffs(coeffs) {
1345
+ const bitWriter = new BitWriter();
1346
+ const { components } = coeffs;
1347
+ // DC predictors for each component
1348
+ const dcPreds = new Map();
1349
+ for (const comp of components) {
1350
+ dcPreds.set(comp.id, 0);
1351
+ }
1352
+ // Encode MCUs in raster order
1353
+ // For non-subsampled images (1:1:1), each MCU contains one block per component
1354
+ const mcuHeight = coeffs.mcuHeight;
1355
+ const mcuWidth = coeffs.mcuWidth;
1356
+ for (let mcuY = 0; mcuY < mcuHeight; mcuY++) {
1357
+ for (let mcuX = 0; mcuX < mcuWidth; mcuX++) {
1358
+ // Encode each component's blocks in this MCU
1359
+ for (const comp of components) {
1360
+ // Handle subsampling: component may have multiple blocks per MCU
1361
+ for (let v = 0; v < comp.v; v++) {
1362
+ for (let h = 0; h < comp.h; h++) {
1363
+ const blockY = mcuY * comp.v + v;
1364
+ const blockX = mcuX * comp.h + h;
1365
+ if (blockY < comp.blocks.length &&
1366
+ blockX < comp.blocks[0].length) {
1367
+ const block = comp.blocks[blockY][blockX];
1368
+ // Get the block in zigzag order for encoding
1369
+ const zigzagBlock = new Int32Array(64);
1370
+ for (let i = 0; i < 64; i++) {
1371
+ zigzagBlock[i] = block[ZIGZAG[i]];
1372
+ }
1373
+ // Encode DC coefficient
1374
+ const prevDC = dcPreds.get(comp.id) ?? 0;
1375
+ const dcHuffman = comp.id === 1
1376
+ ? this.dcLuminanceHuffman
1377
+ : this.dcChrominanceHuffman;
1378
+ const acHuffman = comp.id === 1
1379
+ ? this.acLuminanceHuffman
1380
+ : this.acChrominanceHuffman;
1381
+ // DC coefficient is at index 0 of the block (already in zigzag order from decoder)
1382
+ const dc = block[0];
1383
+ const dcDiff = dc - prevDC;
1384
+ this.encodeDC(dcDiff, dcHuffman, bitWriter);
1385
+ dcPreds.set(comp.id, dc);
1386
+ // Encode AC coefficients (indices 1-63 in zigzag order)
1387
+ this.encodeACFromCoeffs(block, acHuffman, bitWriter);
1388
+ }
1389
+ }
1390
+ }
1391
+ }
1392
+ }
1393
+ }
1394
+ bitWriter.flush();
1395
+ return Array.from(bitWriter.getBytes());
1396
+ }
1397
+ encodeACFromCoeffs(block, huffTable, bitWriter) {
1398
+ let zeroCount = 0;
1399
+ // Block is stored with DC at index 0, and AC at indices corresponding to zigzag positions
1400
+ // We need to iterate in zigzag order for AC coefficients (indices 1-63)
1401
+ for (let i = 1; i < 64; i++) {
1402
+ const coef = block[ZIGZAG[i]];
1403
+ if (coef === 0) {
1404
+ zeroCount++;
1405
+ }
1406
+ else {
1407
+ // Write any pending zero runs
1408
+ while (zeroCount >= 16) {
1409
+ bitWriter.writeBits(huffTable.codes[0xf0], huffTable.sizes[0xf0]);
1410
+ zeroCount -= 16;
1411
+ }
1412
+ // Clamp coefficient
1413
+ const maxAC = 1023;
1414
+ const clampedCoef = Math.max(-maxAC, Math.min(maxAC, coef));
1415
+ const absCoef = Math.abs(clampedCoef);
1416
+ const size = Math.floor(Math.log2(absCoef)) + 1;
1417
+ const symbol = (zeroCount << 4) | size;
1418
+ bitWriter.writeBits(huffTable.codes[symbol], huffTable.sizes[symbol]);
1419
+ const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
1420
+ bitWriter.writeBits(magnitude, size);
1421
+ zeroCount = 0;
1422
+ }
1423
+ }
1424
+ // Write EOB if there are trailing zeros
1425
+ if (zeroCount > 0) {
1426
+ bitWriter.writeBits(huffTable.codes[0x00], huffTable.sizes[0x00]);
1427
+ }
1428
+ }
1429
+ encodeProgressiveFromCoeffs(output, coeffs) {
1430
+ const { components } = coeffs;
1431
+ // Pre-extract blocks for each component (already quantized)
1432
+ const componentBlocks = new Map();
1433
+ for (const comp of components) {
1434
+ const blocks = [];
1435
+ for (const row of comp.blocks) {
1436
+ for (const block of row) {
1437
+ blocks.push(block);
1438
+ }
1439
+ }
1440
+ componentBlocks.set(comp.id, blocks);
1441
+ }
1442
+ // Scan 1: DC-only (interleaved across all components)
1443
+ this.writeProgressiveSOS(output, components.map((c) => c.id), 0, 0, 0, 0);
1444
+ const dcScanData = this.encodeProgressiveDCScanFromCoeffs(componentBlocks, components);
1445
+ for (let i = 0; i < dcScanData.length; i++) {
1446
+ output.push(dcScanData[i]);
1447
+ }
1448
+ // Scan 2+: AC coefficients (one scan per component)
1449
+ for (const comp of components) {
1450
+ this.writeProgressiveSOS(output, [comp.id], 1, 63, 0, 0);
1451
+ const blocks = componentBlocks.get(comp.id) ?? [];
1452
+ const acHuffman = comp.id === 1 ? this.acLuminanceHuffman : this.acChrominanceHuffman;
1453
+ const acScanData = this.encodeProgressiveACScanFromCoeffs(blocks, acHuffman);
1454
+ for (let i = 0; i < acScanData.length; i++) {
1455
+ output.push(acScanData[i]);
1456
+ }
1457
+ }
1458
+ }
1459
+ encodeProgressiveDCScanFromCoeffs(componentBlocks, components) {
1460
+ const bitWriter = new BitWriter();
1461
+ const dcPreds = new Map();
1462
+ for (const comp of components) {
1463
+ dcPreds.set(comp.id, 0);
1464
+ }
1465
+ // Get number of blocks (should be same for all components in interleaved scan)
1466
+ const numBlocks = componentBlocks.get(components[0].id)?.length ?? 0;
1467
+ for (let i = 0; i < numBlocks; i++) {
1468
+ for (const comp of components) {
1469
+ const blocks = componentBlocks.get(comp.id);
1470
+ if (!blocks || i >= blocks.length)
1471
+ continue;
1472
+ const block = blocks[i];
1473
+ const dc = block[0]; // DC coefficient at index 0
1474
+ const prevDC = dcPreds.get(comp.id) ?? 0;
1475
+ const dcHuffman = comp.id === 1 ? this.dcLuminanceHuffman : this.dcChrominanceHuffman;
1476
+ this.encodeOnlyDC(dc, prevDC, dcHuffman, bitWriter);
1477
+ dcPreds.set(comp.id, dc);
1478
+ }
1479
+ }
1480
+ bitWriter.flush();
1481
+ return Array.from(bitWriter.getBytes());
1482
+ }
1483
+ encodeProgressiveACScanFromCoeffs(blocks, acHuffman) {
1484
+ const bitWriter = new BitWriter();
1485
+ for (const block of blocks) {
1486
+ this.encodeOnlyACFromCoeffs(block, acHuffman, bitWriter);
1487
+ }
1488
+ bitWriter.flush();
1489
+ return Array.from(bitWriter.getBytes());
1490
+ }
1491
+ encodeOnlyACFromCoeffs(block, acTable, bitWriter) {
1492
+ let zeroCount = 0;
1493
+ // Iterate AC coefficients in zigzag order (indices 1-63)
1494
+ for (let i = 1; i < 64; i++) {
1495
+ const coef = block[ZIGZAG[i]];
1496
+ const clampedCoef = Math.max(-1023, Math.min(1023, coef));
1497
+ if (clampedCoef === 0) {
1498
+ zeroCount++;
1499
+ if (zeroCount === 16) {
1500
+ bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
1501
+ zeroCount = 0;
1502
+ }
1503
+ }
1504
+ else {
1505
+ while (zeroCount >= 16) {
1506
+ bitWriter.writeBits(acTable.codes[0xf0], acTable.sizes[0xf0]);
1507
+ zeroCount -= 16;
1508
+ }
1509
+ const absCoef = Math.abs(clampedCoef);
1510
+ const size = Math.floor(Math.log2(absCoef)) + 1;
1511
+ const symbol = (zeroCount << 4) | size;
1512
+ bitWriter.writeBits(acTable.codes[symbol], acTable.sizes[symbol]);
1513
+ const magnitude = clampedCoef < 0 ? clampedCoef + (1 << size) - 1 : clampedCoef;
1514
+ bitWriter.writeBits(magnitude, size);
1515
+ zeroCount = 0;
1516
+ }
1517
+ }
1518
+ // Write EOB if there are trailing zeros
1519
+ if (zeroCount > 0) {
1520
+ bitWriter.writeBits(acTable.codes[0x00], acTable.sizes[0x00]);
1521
+ }
1522
+ }
1256
1523
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cross-image",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "A pure JavaScript, dependency-free, cross-runtime image processing library for Deno, Node.js, and Bun.",
5
5
  "keywords": [
6
6
  "image",
package/script/mod.d.ts CHANGED
@@ -43,7 +43,7 @@
43
43
  * ```
44
44
  */
45
45
  export { Image } from "./src/image.js";
46
- export type { ASCIIEncoderOptions, FrameMetadata, ImageData, ImageDecoderOptions, ImageFormat, ImageFrame, ImageMetadata, JPEGEncoderOptions, MultiFrameImageData, ResizeOptions, TIFFEncoderOptions, WebPEncoderOptions, } from "./src/types.js";
46
+ export type { APNGEncoderOptions, ASCIIEncoderOptions, AVIFEncoderOptions, CoefficientData, FrameMetadata, GIFEncoderOptions, HEICEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageFrame, ImageMetadata, JPEGComponentCoefficients, JPEGEncoderOptions, JPEGQuantizedCoefficients, MultiFrameImageData, PNGEncoderOptions, ResizeOptions, TIFFEncoderOptions, WebPEncoderOptions, } from "./src/types.js";
47
47
  export { PNGFormat } from "./src/formats/png.js";
48
48
  export { APNGFormat } from "./src/formats/apng.js";
49
49
  export { JPEGFormat } from "./src/formats/jpeg.js";
@@ -59,4 +59,6 @@ export { PPMFormat } from "./src/formats/ppm.js";
59
59
  export { ASCIIFormat } from "./src/formats/ascii.js";
60
60
  export { HEICFormat } from "./src/formats/heic.js";
61
61
  export { AVIFFormat } from "./src/formats/avif.js";
62
+ export { decodeBase64, encodeBase64, parseDataUrl, toDataUrl } from "./src/utils/base64.js";
63
+ export { cmykToRgb, cmykToRgba, rgbaToCmyk, rgbToCmyk } from "./src/utils/image_processing.js";
62
64
  //# sourceMappingURL=mod.d.ts.map
package/script/mod.js CHANGED
@@ -44,7 +44,7 @@
44
44
  * ```
45
45
  */
46
46
  Object.defineProperty(exports, "__esModule", { value: true });
47
- exports.AVIFFormat = exports.HEICFormat = exports.ASCIIFormat = exports.PPMFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
47
+ exports.rgbToCmyk = exports.rgbaToCmyk = exports.cmykToRgba = exports.cmykToRgb = exports.toDataUrl = exports.parseDataUrl = exports.encodeBase64 = exports.decodeBase64 = exports.AVIFFormat = exports.HEICFormat = exports.ASCIIFormat = exports.PPMFormat = exports.PCXFormat = exports.PAMFormat = exports.DNGFormat = exports.ICOFormat = exports.BMPFormat = exports.TIFFFormat = exports.GIFFormat = exports.WebPFormat = exports.JPEGFormat = exports.APNGFormat = exports.PNGFormat = exports.Image = void 0;
48
48
  var image_js_1 = require("./src/image.js");
49
49
  Object.defineProperty(exports, "Image", { enumerable: true, get: function () { return image_js_1.Image; } });
50
50
  var png_js_1 = require("./src/formats/png.js");
@@ -77,3 +77,13 @@ var heic_js_1 = require("./src/formats/heic.js");
77
77
  Object.defineProperty(exports, "HEICFormat", { enumerable: true, get: function () { return heic_js_1.HEICFormat; } });
78
78
  var avif_js_1 = require("./src/formats/avif.js");
79
79
  Object.defineProperty(exports, "AVIFFormat", { enumerable: true, get: function () { return avif_js_1.AVIFFormat; } });
80
+ var base64_js_1 = require("./src/utils/base64.js");
81
+ Object.defineProperty(exports, "decodeBase64", { enumerable: true, get: function () { return base64_js_1.decodeBase64; } });
82
+ Object.defineProperty(exports, "encodeBase64", { enumerable: true, get: function () { return base64_js_1.encodeBase64; } });
83
+ Object.defineProperty(exports, "parseDataUrl", { enumerable: true, get: function () { return base64_js_1.parseDataUrl; } });
84
+ Object.defineProperty(exports, "toDataUrl", { enumerable: true, get: function () { return base64_js_1.toDataUrl; } });
85
+ var image_processing_js_1 = require("./src/utils/image_processing.js");
86
+ Object.defineProperty(exports, "cmykToRgb", { enumerable: true, get: function () { return image_processing_js_1.cmykToRgb; } });
87
+ Object.defineProperty(exports, "cmykToRgba", { enumerable: true, get: function () { return image_processing_js_1.cmykToRgba; } });
88
+ Object.defineProperty(exports, "rgbaToCmyk", { enumerable: true, get: function () { return image_processing_js_1.rgbaToCmyk; } });
89
+ Object.defineProperty(exports, "rgbToCmyk", { enumerable: true, get: function () { return image_processing_js_1.rgbToCmyk; } });
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
1
+ import type { APNGEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
2
2
  import { PNGBase } from "./png_base.js";
3
3
  /**
4
4
  * APNG (Animated PNG) format handler
@@ -36,15 +36,17 @@ export declare class APNGFormat extends PNGBase implements ImageFormat {
36
36
  /**
37
37
  * Encode RGBA image data to APNG format (single frame)
38
38
  * @param imageData Image data to encode
39
+ * @param options Encoding options (compressionLevel 0-9, default 6)
39
40
  * @returns Encoded APNG image bytes
40
41
  */
41
- encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
42
+ encode(imageData: ImageData, options?: APNGEncoderOptions): Promise<Uint8Array>;
42
43
  /**
43
44
  * Encode multi-frame image data to APNG format
44
45
  * @param imageData Multi-frame image data to encode
46
+ * @param options Encoding options (compressionLevel 0-9, default 6)
45
47
  * @returns Encoded APNG image bytes
46
48
  */
47
- encodeFrames(imageData: MultiFrameImageData, _options?: unknown): Promise<Uint8Array>;
49
+ encodeFrames(imageData: MultiFrameImageData, options?: APNGEncoderOptions): Promise<Uint8Array>;
48
50
  private decodeFrameData;
49
51
  /**
50
52
  * Get the list of metadata fields supported by APNG format
@@ -253,9 +253,10 @@ class APNGFormat extends png_base_js_1.PNGBase {
253
253
  /**
254
254
  * Encode RGBA image data to APNG format (single frame)
255
255
  * @param imageData Image data to encode
256
+ * @param options Encoding options (compressionLevel 0-9, default 6)
256
257
  * @returns Encoded APNG image bytes
257
258
  */
258
- encode(imageData, _options) {
259
+ encode(imageData, options) {
259
260
  // For single frame, create a multi-frame with one frame
260
261
  const multiFrame = {
261
262
  width: imageData.width,
@@ -268,15 +269,21 @@ class APNGFormat extends png_base_js_1.PNGBase {
268
269
  }],
269
270
  metadata: imageData.metadata,
270
271
  };
271
- return this.encodeFrames(multiFrame, _options);
272
+ return this.encodeFrames(multiFrame, options);
272
273
  }
273
274
  /**
274
275
  * Encode multi-frame image data to APNG format
275
276
  * @param imageData Multi-frame image data to encode
277
+ * @param options Encoding options (compressionLevel 0-9, default 6)
276
278
  * @returns Encoded APNG image bytes
277
279
  */
278
- async encodeFrames(imageData, _options) {
280
+ async encodeFrames(imageData, options) {
279
281
  const { width, height, frames, metadata } = imageData;
282
+ const compressionLevel = options?.compressionLevel ?? 6;
283
+ // Validate compression level
284
+ if (compressionLevel < 0 || compressionLevel > 9) {
285
+ throw new Error("Compression level must be between 0 and 9");
286
+ }
280
287
  if (frames.length === 0) {
281
288
  throw new Error("No frames to encode");
282
289
  }
@@ -334,7 +341,7 @@ class APNGFormat extends png_base_js_1.PNGBase {
334
341
  fctl[25] = 0; // blend_op: APNG_BLEND_OP_SOURCE
335
342
  chunks.push(this.createChunk("fcTL", fctl));
336
343
  // Filter and compress frame data
337
- const filtered = this.filterData(frame.data, frame.width, frame.height);
344
+ const filtered = this.filterData(frame.data, frame.width, frame.height, compressionLevel);
338
345
  const compressed = await this.deflate(filtered);
339
346
  if (i === 0) {
340
347
  // First frame uses IDAT
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
1
+ import type { AVIFEncoderOptions, ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata } from "../types.js";
2
2
  /**
3
3
  * AVIF format handler
4
4
  * Supports AVIF images using runtime APIs (ImageDecoder/OffscreenCanvas)
@@ -32,7 +32,7 @@ export declare class AVIFFormat implements ImageFormat {
32
32
  * @param imageData Image data to encode
33
33
  * @returns Encoded AVIF image bytes
34
34
  */
35
- encode(imageData: ImageData, _options?: unknown): Promise<Uint8Array>;
35
+ encode(imageData: ImageData, options?: AVIFEncoderOptions): Promise<Uint8Array>;
36
36
  /**
37
37
  * Decode using runtime APIs
38
38
  * @param data Raw AVIF data
@@ -82,8 +82,9 @@ class AVIFFormat {
82
82
  * @param imageData Image data to encode
83
83
  * @returns Encoded AVIF image bytes
84
84
  */
85
- async encode(imageData, _options) {
85
+ async encode(imageData, options) {
86
86
  const { width, height, data, metadata: _metadata } = imageData;
87
+ const requestedQuality = options?.quality;
87
88
  // Try to use runtime encoding if available
88
89
  if (typeof OffscreenCanvas !== "undefined") {
89
90
  try {
@@ -94,10 +95,19 @@ class AVIFFormat {
94
95
  const imgDataData = new Uint8ClampedArray(data);
95
96
  imgData.data.set(imgDataData);
96
97
  ctx.putImageData(imgData, 0, 0);
98
+ const quality = requestedQuality === undefined
99
+ ? undefined
100
+ : (requestedQuality <= 1
101
+ ? Math.max(0, Math.min(1, requestedQuality))
102
+ : Math.max(1, Math.min(100, requestedQuality)) / 100);
97
103
  // Try to encode as AVIF
98
104
  const blob = await canvas.convertToBlob({
99
105
  type: "image/avif",
106
+ ...(quality === undefined ? {} : { quality }),
100
107
  });
108
+ if (blob.type !== "image/avif") {
109
+ throw new Error(`Runtime did not encode AVIF (got '${blob.type || "(empty)"}')`);
110
+ }
101
111
  const arrayBuffer = await blob.arrayBuffer();
102
112
  const encoded = new Uint8Array(arrayBuffer);
103
113
  // Note: Metadata injection for AVIF is complex and would require