cross-image 0.2.4 → 0.4.1

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 (117) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +615 -333
  3. package/esm/mod.d.ts +6 -4
  4. package/esm/mod.js +4 -2
  5. package/esm/src/formats/apng.d.ts +7 -5
  6. package/esm/src/formats/apng.js +15 -10
  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 +17 -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 +5 -5
  16. package/esm/src/formats/gif.js +17 -13
  17. package/esm/src/formats/heic.d.ts +3 -3
  18. package/esm/src/formats/heic.js +17 -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 +4 -3
  28. package/esm/src/formats/png.js +9 -3
  29. package/esm/src/formats/png_base.d.ts +42 -1
  30. package/esm/src/formats/png_base.js +200 -10
  31. package/esm/src/formats/ppm.d.ts +3 -3
  32. package/esm/src/formats/ppm.js +2 -2
  33. package/esm/src/formats/tiff.d.ts +7 -18
  34. package/esm/src/formats/tiff.js +162 -27
  35. package/esm/src/formats/webp.d.ts +3 -3
  36. package/esm/src/formats/webp.js +11 -8
  37. package/esm/src/image.d.ts +26 -3
  38. package/esm/src/image.js +66 -22
  39. package/esm/src/types.d.ts +122 -4
  40. package/esm/src/utils/base64.d.ts +32 -0
  41. package/esm/src/utils/base64.js +173 -0
  42. package/esm/src/utils/gif_decoder.d.ts +4 -1
  43. package/esm/src/utils/gif_decoder.js +91 -65
  44. package/esm/src/utils/gif_encoder.d.ts +3 -1
  45. package/esm/src/utils/gif_encoder.js +4 -2
  46. package/esm/src/utils/image_processing.d.ts +31 -0
  47. package/esm/src/utils/image_processing.js +232 -70
  48. package/esm/src/utils/jpeg_decoder.d.ts +17 -4
  49. package/esm/src/utils/jpeg_decoder.js +448 -83
  50. package/esm/src/utils/jpeg_encoder.d.ts +15 -1
  51. package/esm/src/utils/jpeg_encoder.js +263 -24
  52. package/esm/src/utils/resize.js +51 -20
  53. package/esm/src/utils/tiff_deflate.d.ts +18 -0
  54. package/esm/src/utils/tiff_deflate.js +27 -0
  55. package/esm/src/utils/tiff_packbits.d.ts +24 -0
  56. package/esm/src/utils/tiff_packbits.js +90 -0
  57. package/esm/src/utils/webp_decoder.d.ts +3 -1
  58. package/esm/src/utils/webp_decoder.js +144 -63
  59. package/esm/src/utils/webp_encoder.js +5 -11
  60. package/package.json +1 -1
  61. package/script/mod.d.ts +6 -4
  62. package/script/mod.js +13 -3
  63. package/script/src/formats/apng.d.ts +7 -5
  64. package/script/src/formats/apng.js +15 -10
  65. package/script/src/formats/ascii.d.ts +3 -3
  66. package/script/src/formats/ascii.js +1 -1
  67. package/script/src/formats/avif.d.ts +3 -3
  68. package/script/src/formats/avif.js +17 -7
  69. package/script/src/formats/bmp.d.ts +3 -3
  70. package/script/src/formats/bmp.js +2 -2
  71. package/script/src/formats/dng.d.ts +1 -1
  72. package/script/src/formats/dng.js +1 -1
  73. package/script/src/formats/gif.d.ts +5 -5
  74. package/script/src/formats/gif.js +17 -13
  75. package/script/src/formats/heic.d.ts +3 -3
  76. package/script/src/formats/heic.js +17 -7
  77. package/script/src/formats/ico.d.ts +3 -3
  78. package/script/src/formats/ico.js +4 -4
  79. package/script/src/formats/jpeg.d.ts +3 -3
  80. package/script/src/formats/jpeg.js +23 -11
  81. package/script/src/formats/pam.d.ts +3 -3
  82. package/script/src/formats/pam.js +2 -2
  83. package/script/src/formats/pcx.d.ts +3 -3
  84. package/script/src/formats/pcx.js +2 -2
  85. package/script/src/formats/png.d.ts +4 -3
  86. package/script/src/formats/png.js +9 -3
  87. package/script/src/formats/png_base.d.ts +42 -1
  88. package/script/src/formats/png_base.js +200 -10
  89. package/script/src/formats/ppm.d.ts +3 -3
  90. package/script/src/formats/ppm.js +2 -2
  91. package/script/src/formats/tiff.d.ts +7 -18
  92. package/script/src/formats/tiff.js +162 -27
  93. package/script/src/formats/webp.d.ts +3 -3
  94. package/script/src/formats/webp.js +11 -8
  95. package/script/src/image.d.ts +26 -3
  96. package/script/src/image.js +64 -20
  97. package/script/src/types.d.ts +122 -4
  98. package/script/src/utils/base64.d.ts +32 -0
  99. package/script/src/utils/base64.js +179 -0
  100. package/script/src/utils/gif_decoder.d.ts +4 -1
  101. package/script/src/utils/gif_decoder.js +91 -65
  102. package/script/src/utils/gif_encoder.d.ts +3 -1
  103. package/script/src/utils/gif_encoder.js +4 -2
  104. package/script/src/utils/image_processing.d.ts +31 -0
  105. package/script/src/utils/image_processing.js +236 -70
  106. package/script/src/utils/jpeg_decoder.d.ts +17 -4
  107. package/script/src/utils/jpeg_decoder.js +448 -83
  108. package/script/src/utils/jpeg_encoder.d.ts +15 -1
  109. package/script/src/utils/jpeg_encoder.js +263 -24
  110. package/script/src/utils/resize.js +51 -20
  111. package/script/src/utils/tiff_deflate.d.ts +18 -0
  112. package/script/src/utils/tiff_deflate.js +31 -0
  113. package/script/src/utils/tiff_packbits.d.ts +24 -0
  114. package/script/src/utils/tiff_packbits.js +94 -0
  115. package/script/src/utils/webp_decoder.d.ts +3 -1
  116. package/script/src/utils/webp_decoder.js +144 -63
  117. package/script/src/utils/webp_encoder.js +5 -11
@@ -1,12 +1,15 @@
1
1
  import { TIFFLZWDecoder, TIFFLZWEncoder } from "../utils/tiff_lzw.js";
2
+ import { packBitsCompress, packBitsDecompress } from "../utils/tiff_packbits.js";
3
+ import { deflateCompress, deflateDecompress } from "../utils/tiff_deflate.js";
2
4
  import { validateImageDimensions } from "../utils/security.js";
5
+ import { cmykToRgb, rgbaToCmyk } from "../utils/image_processing.js";
3
6
  // Constants for unit conversions
4
7
  const DEFAULT_DPI = 72;
5
8
  /**
6
9
  * TIFF format handler
7
- * Implements pure-JS TIFF decoder for uncompressed and LZW-compressed RGB/RGBA images
8
- * and encoder for uncompressed and LZW-compressed RGBA TIFFs. Falls back to ImageDecoder
9
- * for other compressed TIFFs (JPEG, PackBits, etc.)
10
+ * Implements pure-JS TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
11
+ * and encoder for uncompressed, LZW, PackBits, and Deflate-compressed RGBA TIFFs. Falls back to ImageDecoder
12
+ * for JPEG-compressed TIFFs.
10
13
  * Supports multi-page TIFF files.
11
14
  */
12
15
  export class TIFFFormat {
@@ -52,7 +55,7 @@ export class TIFFFormat {
52
55
  * @param data Raw TIFF image data
53
56
  * @returns Decoded image data with RGBA pixels of first page
54
57
  */
55
- async decode(data) {
58
+ async decode(data, _options) {
56
59
  if (!this.canDecode(data)) {
57
60
  throw new Error("Invalid TIFF signature");
58
61
  }
@@ -128,12 +131,13 @@ export class TIFFFormat {
128
131
  metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
129
132
  };
130
133
  }
131
- encode(imageData, options) {
134
+ async encode(imageData, options) {
132
135
  const { width, height, data, metadata } = imageData;
133
136
  const opts = options;
134
137
  const compression = opts?.compression ?? "none";
135
138
  const grayscale = opts?.grayscale ?? false;
136
139
  const rgb = opts?.rgb ?? false;
140
+ const cmyk = opts?.cmyk ?? false;
137
141
  // Convert RGBA to grayscale if requested
138
142
  let sourceData;
139
143
  let samplesPerPixel;
@@ -148,6 +152,16 @@ export class TIFFFormat {
148
152
  sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
149
153
  }
150
154
  }
155
+ else if (cmyk) {
156
+ // Convert RGBA to CMYK
157
+ const cmykData = rgbaToCmyk(data);
158
+ sourceData = new Uint8Array(width * height * 4);
159
+ samplesPerPixel = 4;
160
+ // Convert Float32Array CMYK (0-1) to Uint8Array (0-255)
161
+ for (let i = 0; i < cmykData.length; i++) {
162
+ sourceData[i] = Math.round(cmykData[i] * 255);
163
+ }
164
+ }
151
165
  else if (rgb) {
152
166
  // Convert RGBA to RGB (strip alpha channel)
153
167
  sourceData = new Uint8Array(width * height * 3);
@@ -172,6 +186,16 @@ export class TIFFFormat {
172
186
  pixelData = encoder.compress(sourceData);
173
187
  compressionCode = 5;
174
188
  }
189
+ else if (compression === "packbits") {
190
+ // PackBits compress the pixel data
191
+ pixelData = packBitsCompress(sourceData);
192
+ compressionCode = 32773;
193
+ }
194
+ else if (compression === "deflate") {
195
+ // Deflate compress the pixel data
196
+ pixelData = await deflateCompress(sourceData);
197
+ compressionCode = 8;
198
+ }
175
199
  else {
176
200
  // Uncompressed
177
201
  pixelData = sourceData;
@@ -229,8 +253,8 @@ export class TIFFFormat {
229
253
  }
230
254
  // Compression (0x0103) - 1 = uncompressed, 5 = LZW
231
255
  this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
232
- // PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
233
- this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : 2);
256
+ // PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB, 5 = CMYK
257
+ this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : (cmyk ? 5 : 2));
234
258
  // StripOffsets (0x0111)
235
259
  this.writeIFDEntry(result, 0x0111, 4, 1, 8);
236
260
  // SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
@@ -329,7 +353,7 @@ export class TIFFFormat {
329
353
  /**
330
354
  * Decode all pages from a multi-page TIFF
331
355
  */
332
- async decodeFrames(data) {
356
+ async decodeFrames(data, _options) {
333
357
  if (!this.canDecode(data)) {
334
358
  throw new Error("Invalid TIFF signature");
335
359
  }
@@ -392,7 +416,7 @@ export class TIFFFormat {
392
416
  /**
393
417
  * Encode multi-page TIFF
394
418
  */
395
- encodeFrames(imageData, options) {
419
+ async encodeFrames(imageData, options) {
396
420
  const opts = options;
397
421
  const compression = opts?.compression ?? "none";
398
422
  if (imageData.frames.length === 0) {
@@ -417,6 +441,12 @@ export class TIFFFormat {
417
441
  const encoder = new TIFFLZWEncoder();
418
442
  pixelData = encoder.compress(frame.data);
419
443
  }
444
+ else if (compression === "packbits") {
445
+ pixelData = packBitsCompress(frame.data);
446
+ }
447
+ else if (compression === "deflate") {
448
+ pixelData = await deflateCompress(frame.data);
449
+ }
420
450
  else {
421
451
  pixelData = frame.data;
422
452
  }
@@ -453,7 +483,19 @@ export class TIFFFormat {
453
483
  this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
454
484
  dataOffset += 8;
455
485
  // Compression
456
- const compressionCode = compression === "lzw" ? 5 : 1;
486
+ let compressionCode;
487
+ if (compression === "lzw") {
488
+ compressionCode = 5;
489
+ }
490
+ else if (compression === "packbits") {
491
+ compressionCode = 32773;
492
+ }
493
+ else if (compression === "deflate") {
494
+ compressionCode = 8;
495
+ }
496
+ else {
497
+ compressionCode = 1;
498
+ }
457
499
  this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
458
500
  // PhotometricInterpretation
459
501
  this.writeIFDEntry(result, 0x0106, 3, 1, 2);
@@ -464,9 +506,19 @@ export class TIFFFormat {
464
506
  // RowsPerStrip
465
507
  this.writeIFDEntry(result, 0x0116, 4, 1, frame.height);
466
508
  // StripByteCounts
467
- const pixelDataSize = compression === "lzw"
468
- ? new TIFFLZWEncoder().compress(frame.data).length
469
- : frame.data.length;
509
+ let pixelDataSize;
510
+ if (compression === "lzw") {
511
+ pixelDataSize = new TIFFLZWEncoder().compress(frame.data).length;
512
+ }
513
+ else if (compression === "packbits") {
514
+ pixelDataSize = packBitsCompress(frame.data).length;
515
+ }
516
+ else if (compression === "deflate") {
517
+ pixelDataSize = (await deflateCompress(frame.data)).length;
518
+ }
519
+ else {
520
+ pixelDataSize = frame.data.length;
521
+ }
470
522
  this.writeIFDEntry(result, 0x0117, 4, 1, pixelDataSize);
471
523
  // XResolution
472
524
  const xResOffset = dataOffset;
@@ -569,7 +621,7 @@ export class TIFFFormat {
569
621
  const isLittleEndian = data[0] === 0x49;
570
622
  // Try pure JavaScript decoder first
571
623
  try {
572
- const pureJSResult = this.decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian);
624
+ const pureJSResult = await this.decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian);
573
625
  if (pureJSResult) {
574
626
  return pureJSResult;
575
627
  }
@@ -693,7 +745,7 @@ export class TIFFFormat {
693
745
  async decodeUsingRuntime(data, width, height) {
694
746
  // Try pure JavaScript decoder first for uncompressed TIFFs
695
747
  try {
696
- const pureJSResult = this.decodePureJS(data, width, height);
748
+ const pureJSResult = await this.decodePureJS(data, width, height);
697
749
  if (pureJSResult) {
698
750
  return pureJSResult;
699
751
  }
@@ -731,10 +783,10 @@ export class TIFFFormat {
731
783
  return new TextDecoder().decode(data.slice(offset, endIndex));
732
784
  }
733
785
  /**
734
- * Pure JavaScript TIFF decoder for uncompressed and LZW-compressed RGB/RGBA images
786
+ * Pure JavaScript TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
735
787
  * Returns null if the TIFF uses unsupported features
736
788
  */
737
- decodePureJS(data, width, height) {
789
+ async decodePureJS(data, width, height) {
738
790
  // Validate minimum TIFF header size
739
791
  if (data.length < 8) {
740
792
  return null;
@@ -745,20 +797,23 @@ export class TIFFFormat {
745
797
  const ifdOffset = this.readUint32(data, 4, isLittleEndian);
746
798
  // Check compression
747
799
  const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
748
- if (compression !== 1 && compression !== 5) {
749
- // Only support uncompressed (1) and LZW (5)
800
+ if (compression !== 1 && compression !== 5 && compression !== 8 &&
801
+ compression !== 32773) {
802
+ // Support: 1 = uncompressed, 5 = LZW, 8 = Deflate, 32773 = PackBits
750
803
  return null;
751
804
  }
752
805
  // Check photometric interpretation
753
806
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
754
- if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
755
- // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
807
+ if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
808
+ photometric !== 5) {
809
+ // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
756
810
  return null;
757
811
  }
758
812
  // Get samples per pixel
759
813
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
760
814
  // For grayscale (photometric 0 or 1), expect 1 sample per pixel
761
815
  // For RGB, expect 3 or 4 samples per pixel
816
+ // For CMYK, expect 4 samples per pixel
762
817
  if (!samplesPerPixel) {
763
818
  return null;
764
819
  }
@@ -770,6 +825,10 @@ export class TIFFFormat {
770
825
  // RGB requires 3 or 4 samples per pixel
771
826
  return null;
772
827
  }
828
+ if (photometric === 5 && samplesPerPixel !== 4) {
829
+ // CMYK requires 4 samples per pixel
830
+ return null;
831
+ }
773
832
  // Get strip offset
774
833
  const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
775
834
  if (!stripOffset || stripOffset >= data.length) {
@@ -788,6 +847,16 @@ export class TIFFFormat {
788
847
  const decoder = new TIFFLZWDecoder(compressedData);
789
848
  pixelData = decoder.decompress();
790
849
  }
850
+ else if (compression === 32773) {
851
+ // PackBits compressed
852
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
853
+ pixelData = packBitsDecompress(compressedData);
854
+ }
855
+ else if (compression === 8) {
856
+ // Deflate compressed
857
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
858
+ pixelData = await deflateDecompress(compressedData);
859
+ }
791
860
  else {
792
861
  // Uncompressed
793
862
  pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
@@ -817,6 +886,29 @@ export class TIFFFormat {
817
886
  }
818
887
  }
819
888
  }
889
+ else if (photometric === 5) {
890
+ // CMYK image - convert to RGB
891
+ for (let y = 0; y < height; y++) {
892
+ for (let x = 0; x < width; x++) {
893
+ const dstIdx = (y * width + x) * 4;
894
+ if (srcPos + 4 > pixelData.length) {
895
+ return null; // Not enough data
896
+ }
897
+ // TIFF stores CMYK in order, values are 0-255
898
+ // Convert to 0-1 range for conversion
899
+ const c = pixelData[srcPos++] / 255;
900
+ const m = pixelData[srcPos++] / 255;
901
+ const yVal = pixelData[srcPos++] / 255;
902
+ const k = pixelData[srcPos++] / 255;
903
+ // Convert CMYK to RGB
904
+ const [r, g, b] = cmykToRgb(c, m, yVal, k);
905
+ rgba[dstIdx] = r; // R
906
+ rgba[dstIdx + 1] = g; // G
907
+ rgba[dstIdx + 2] = b; // B
908
+ rgba[dstIdx + 3] = 255; // A (opaque)
909
+ }
910
+ }
911
+ }
820
912
  else {
821
913
  // RGB/RGBA image
822
914
  for (let y = 0; y < height; y++) {
@@ -839,23 +931,26 @@ export class TIFFFormat {
839
931
  * Pure JavaScript TIFF decoder for a specific IFD
840
932
  * Returns null if the TIFF uses unsupported features
841
933
  */
842
- decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian) {
934
+ async decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian) {
843
935
  // Check compression
844
936
  const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
845
- if (compression !== 1 && compression !== 5) {
846
- // Only support uncompressed (1) and LZW (5)
937
+ if (compression !== 1 && compression !== 5 && compression !== 8 &&
938
+ compression !== 32773) {
939
+ // Support: 1 = uncompressed, 5 = LZW, 8 = Deflate, 32773 = PackBits
847
940
  return null;
848
941
  }
849
942
  // Check photometric interpretation
850
943
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
851
- if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
852
- // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
944
+ if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
945
+ photometric !== 5) {
946
+ // Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
853
947
  return null;
854
948
  }
855
949
  // Get samples per pixel
856
950
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
857
951
  // For grayscale (photometric 0 or 1), expect 1 sample per pixel
858
952
  // For RGB, expect 3 or 4 samples per pixel
953
+ // For CMYK, expect 4 samples per pixel
859
954
  if (!samplesPerPixel) {
860
955
  return null;
861
956
  }
@@ -867,6 +962,10 @@ export class TIFFFormat {
867
962
  // RGB requires 3 or 4 samples per pixel
868
963
  return null;
869
964
  }
965
+ if (photometric === 5 && samplesPerPixel !== 4) {
966
+ // CMYK requires 4 samples per pixel
967
+ return null;
968
+ }
870
969
  // Get strip offset
871
970
  const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
872
971
  if (!stripOffset || stripOffset >= data.length) {
@@ -885,6 +984,16 @@ export class TIFFFormat {
885
984
  const decoder = new TIFFLZWDecoder(compressedData);
886
985
  pixelData = decoder.decompress();
887
986
  }
987
+ else if (compression === 32773) {
988
+ // PackBits compressed
989
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
990
+ pixelData = packBitsDecompress(compressedData);
991
+ }
992
+ else if (compression === 8) {
993
+ // Deflate compressed
994
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
995
+ pixelData = await deflateDecompress(compressedData);
996
+ }
888
997
  else {
889
998
  // Uncompressed
890
999
  pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
@@ -914,6 +1023,29 @@ export class TIFFFormat {
914
1023
  }
915
1024
  }
916
1025
  }
1026
+ else if (photometric === 5) {
1027
+ // CMYK image - convert to RGB
1028
+ for (let y = 0; y < height; y++) {
1029
+ for (let x = 0; x < width; x++) {
1030
+ const dstIdx = (y * width + x) * 4;
1031
+ if (srcPos + 4 > pixelData.length) {
1032
+ return null; // Not enough data
1033
+ }
1034
+ // TIFF stores CMYK in order, values are 0-255
1035
+ // Convert to 0-1 range for conversion
1036
+ const c = pixelData[srcPos++] / 255;
1037
+ const m = pixelData[srcPos++] / 255;
1038
+ const yVal = pixelData[srcPos++] / 255;
1039
+ const k = pixelData[srcPos++] / 255;
1040
+ // Convert CMYK to RGB
1041
+ const [r, g, b] = cmykToRgb(c, m, yVal, k);
1042
+ rgba[dstIdx] = r; // R
1043
+ rgba[dstIdx + 1] = g; // G
1044
+ rgba[dstIdx + 2] = b; // B
1045
+ rgba[dstIdx + 3] = 255; // A (opaque)
1046
+ }
1047
+ }
1048
+ }
917
1049
  else {
918
1050
  // RGB/RGBA image
919
1051
  for (let y = 0; y < height; y++) {
@@ -1003,9 +1135,12 @@ export class TIFFFormat {
1003
1135
  else if (compression === 5) {
1004
1136
  metadata.compression = "lzw";
1005
1137
  }
1006
- else if (compression === 7) {
1138
+ else if (compression === 7 || compression === 6) {
1007
1139
  metadata.compression = "jpeg";
1008
1140
  }
1141
+ else if (compression === 8) {
1142
+ metadata.compression = "deflate";
1143
+ }
1009
1144
  else if (compression === 32773) {
1010
1145
  metadata.compression = "packbits";
1011
1146
  }
@@ -1,4 +1,4 @@
1
- import type { ImageData, ImageFormat, ImageMetadata, WebPEncodeOptions } from "../types.js";
1
+ import type { ImageData, ImageDecoderOptions, ImageFormat, ImageMetadata, WebPEncoderOptions } from "../types.js";
2
2
  /**
3
3
  * WebP format handler
4
4
  * Implements a basic WebP decoder and encoder
@@ -19,14 +19,14 @@ export declare class WebPFormat implements ImageFormat {
19
19
  * @param data Raw WebP image data
20
20
  * @returns Decoded image data with RGBA pixels
21
21
  */
22
- decode(data: Uint8Array): Promise<ImageData>;
22
+ decode(data: Uint8Array, settings?: ImageDecoderOptions): Promise<ImageData>;
23
23
  /**
24
24
  * Encode RGBA image data to WebP format
25
25
  * @param imageData Image data to encode
26
26
  * @param options Optional WebP encoding options
27
27
  * @returns Encoded WebP image bytes
28
28
  */
29
- encode(imageData: ImageData, options?: WebPEncodeOptions): Promise<Uint8Array>;
29
+ encode(imageData: ImageData, options?: WebPEncoderOptions): Promise<Uint8Array>;
30
30
  private readUint24LE;
31
31
  private decodeUsingRuntime;
32
32
  private parseEXIF;
@@ -42,7 +42,7 @@ export class WebPFormat {
42
42
  * @param data Raw WebP image data
43
43
  * @returns Decoded image data with RGBA pixels
44
44
  */
45
- async decode(data) {
45
+ async decode(data, settings) {
46
46
  if (!this.canDecode(data)) {
47
47
  throw new Error("Invalid WebP signature");
48
48
  }
@@ -108,7 +108,7 @@ export class WebPFormat {
108
108
  validateImageDimensions(width, height);
109
109
  // For a pure JS implementation, we'd need to implement full WebP decoding
110
110
  // which is very complex. Instead, we'll use the browser/runtime's decoder.
111
- const rgba = await this.decodeUsingRuntime(data, width, height);
111
+ const rgba = await this.decodeUsingRuntime(data, width, height, settings);
112
112
  return {
113
113
  width,
114
114
  height,
@@ -171,9 +171,10 @@ export class WebPFormat {
171
171
  readUint24LE(data, offset) {
172
172
  return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
173
173
  }
174
- async decodeUsingRuntime(data, _width, _height) {
174
+ async decodeUsingRuntime(data, _width, _height, settings) {
175
175
  // Try to use ImageDecoder API if available (Deno, modern browsers)
176
- if (typeof ImageDecoder !== "undefined") {
176
+ if (settings?.runtimeDecoding !== "never" &&
177
+ typeof ImageDecoder !== "undefined") {
177
178
  try {
178
179
  const decoder = new ImageDecoder({ data, type: "image/webp" });
179
180
  const result = await decoder.decode();
@@ -188,15 +189,17 @@ export class WebPFormat {
188
189
  bitmap.close();
189
190
  return new Uint8Array(imageData.data.buffer);
190
191
  }
191
- catch (error) {
192
+ catch (_error) {
192
193
  // ImageDecoder API failed, fall through to pure JS decoder
193
- console.warn("WebP decoding with ImageDecoder failed, using pure JS decoder:", error);
194
194
  }
195
195
  }
196
- // Fallback to pure JavaScript decoder (VP8L lossless only)
196
+ // Fallback to pure JavaScript decoder (VP8L lossless only) with tolerant mode
197
197
  try {
198
198
  const { WebPDecoder } = await import("../utils/webp_decoder.js");
199
- const decoder = new WebPDecoder(data);
199
+ const decoder = new WebPDecoder(data, {
200
+ tolerantDecoding: settings?.tolerantDecoding ?? true,
201
+ onWarning: settings?.onWarning,
202
+ });
200
203
  const result = decoder.decode();
201
204
  return result.data;
202
205
  }
@@ -1,4 +1,4 @@
1
- import type { ImageFormat, ImageMetadata, MultiFrameImageData, ResizeOptions } from "./types.js";
1
+ import type { ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData, ResizeOptions } from "./types.js";
2
2
  /**
3
3
  * Main Image class for reading, manipulating, and saving images
4
4
  */
@@ -78,7 +78,11 @@ export declare class Image {
78
78
  * @param format Optional format hint (e.g., "png", "jpeg", "webp")
79
79
  * @returns Image instance
80
80
  */
81
- static decode(data: Uint8Array, format?: string): Promise<Image>;
81
+ static decode(data: Uint8Array): Promise<Image>;
82
+ static decode(data: Uint8Array, format: string): Promise<Image>;
83
+ static decode(data: Uint8Array, format: string, options?: ImageDecoderOptions): Promise<Image>;
84
+ static decode(data: Uint8Array, options?: ImageDecoderOptions): Promise<Image>;
85
+ private static decodeWithSettings;
82
86
  /**
83
87
  * Get supported metadata fields for a specific format
84
88
  * @param format Format name (e.g., "jpeg", "png", "webp")
@@ -108,7 +112,11 @@ export declare class Image {
108
112
  * @param format Optional format hint (e.g., "gif", "tiff")
109
113
  * @returns MultiFrameImageData with all frames
110
114
  */
111
- static decodeFrames(data: Uint8Array, format?: string): Promise<MultiFrameImageData>;
115
+ static decodeFrames(data: Uint8Array): Promise<MultiFrameImageData>;
116
+ static decodeFrames(data: Uint8Array, format: string): Promise<MultiFrameImageData>;
117
+ static decodeFrames(data: Uint8Array, format: string, options?: ImageDecoderOptions): Promise<MultiFrameImageData>;
118
+ static decodeFrames(data: Uint8Array, options?: ImageDecoderOptions): Promise<MultiFrameImageData>;
119
+ private static decodeFramesWithSettings;
112
120
  /**
113
121
  * Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
114
122
  * @deprecated Use `decodeFrames()` instead. This method will be removed in a future version.
@@ -342,5 +350,20 @@ export declare class Image {
342
350
  * @returns This image instance for chaining
343
351
  */
344
352
  flipVertical(): this;
353
+ /**
354
+ * Convert the image to CMYK color space
355
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
356
+ * @returns CMYK image data as Float32Array
357
+ */
358
+ toCMYK(): Float32Array;
359
+ /**
360
+ * Create an Image from CMYK data
361
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
362
+ * @param width Image width
363
+ * @param height Image height
364
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
365
+ * @returns New Image instance
366
+ */
367
+ static fromCMYK(cmykData: Float32Array, width: number, height: number, alpha?: number): Image;
345
368
  }
346
369
  //# sourceMappingURL=image.d.ts.map
package/esm/src/image.js CHANGED
@@ -1,5 +1,5 @@
1
- import { resizeBicubic, resizeBilinear, resizeNearest, } from "./utils/resize.js";
2
- import { adjustBrightness, adjustContrast, adjustExposure, adjustHue, adjustSaturation, boxBlur, composite, crop, fillRect, flipHorizontal, flipVertical, gaussianBlur, grayscale, invert, medianFilter, rotate180, rotate270, rotate90, sepia, sharpen, } from "./utils/image_processing.js";
1
+ import { resizeBicubic, resizeBilinear, resizeNearest } from "./utils/resize.js";
2
+ import { adjustBrightness, adjustContrast, adjustExposure, adjustHue, adjustSaturation, boxBlur, cmykToRgba, composite, crop, fillRect, flipHorizontal, flipVertical, gaussianBlur, grayscale, invert, medianFilter, rgbaToCmyk, rotate180, rotate270, rotate90, sepia, sharpen, } from "./utils/image_processing.js";
3
3
  import { PNGFormat } from "./formats/png.js";
4
4
  import { APNGFormat } from "./formats/apng.js";
5
5
  import { JPEGFormat } from "./formats/jpeg.js";
@@ -158,26 +158,37 @@ export class Image {
158
158
  static getFormats() {
159
159
  return Image.formats;
160
160
  }
161
- /**
162
- * Decode an image from bytes
163
- * @param data Raw image data
164
- * @param format Optional format hint (e.g., "png", "jpeg", "webp")
165
- * @returns Image instance
166
- */
167
- static async decode(data, format) {
161
+ static async decode(data, formatOrOptions, options) {
162
+ // Backward-compatible overloads:
163
+ // - decode(data)
164
+ // - decode(data, "jpeg")
165
+ // New:
166
+ // - decode(data, { tolerantDecoding, onWarning, runtimeDecoding })
167
+ // - decode(data, "jpeg", { ...options })
168
+ if (typeof formatOrOptions === "string") {
169
+ return await Image.decodeWithSettings(data, formatOrOptions, options);
170
+ }
171
+ return await Image.decodeWithSettings(data, undefined, formatOrOptions);
172
+ }
173
+ static async decodeWithSettings(data, format, settings) {
168
174
  const image = new Image();
175
+ const normalizedSettings = {
176
+ tolerantDecoding: settings?.tolerantDecoding ?? true,
177
+ onWarning: settings?.onWarning,
178
+ runtimeDecoding: settings?.runtimeDecoding ?? "prefer",
179
+ };
169
180
  // Try specified format first
170
181
  if (format) {
171
182
  const handler = Image.formats.find((f) => f.name === format);
172
183
  if (handler && handler.canDecode(data)) {
173
- image.imageData = await handler.decode(data);
184
+ image.imageData = await handler.decode(data, normalizedSettings);
174
185
  return image;
175
186
  }
176
187
  }
177
188
  // Auto-detect format
178
189
  for (const handler of Image.formats) {
179
190
  if (handler.canDecode(data)) {
180
- image.imageData = await handler.decode(data);
191
+ image.imageData = await handler.decode(data, normalizedSettings);
181
192
  return image;
182
193
  }
183
194
  }
@@ -227,26 +238,31 @@ export class Image {
227
238
  * @returns Image instance
228
239
  */
229
240
  static read(data, format) {
230
- return Image.decode(data, format);
241
+ return format ? Image.decode(data, format) : Image.decode(data);
231
242
  }
232
- /**
233
- * Decode all frames from a multi-frame image (GIF animation, multi-page TIFF)
234
- * @param data Raw image data
235
- * @param format Optional format hint (e.g., "gif", "tiff")
236
- * @returns MultiFrameImageData with all frames
237
- */
238
- static async decodeFrames(data, format) {
243
+ static async decodeFrames(data, formatOrOptions, options) {
244
+ if (typeof formatOrOptions === "string") {
245
+ return await Image.decodeFramesWithSettings(data, formatOrOptions, options);
246
+ }
247
+ return await Image.decodeFramesWithSettings(data, undefined, formatOrOptions);
248
+ }
249
+ static async decodeFramesWithSettings(data, format, settings) {
250
+ const normalizedSettings = {
251
+ tolerantDecoding: settings?.tolerantDecoding ?? true,
252
+ onWarning: settings?.onWarning,
253
+ runtimeDecoding: settings?.runtimeDecoding ?? "prefer",
254
+ };
239
255
  // Try specified format first
240
256
  if (format) {
241
257
  const handler = Image.formats.find((f) => f.name === format);
242
258
  if (handler && handler.canDecode(data) && handler.decodeFrames) {
243
- return await handler.decodeFrames(data);
259
+ return await handler.decodeFrames(data, normalizedSettings);
244
260
  }
245
261
  }
246
262
  // Auto-detect format
247
263
  for (const handler of Image.formats) {
248
264
  if (handler.canDecode(data) && handler.decodeFrames) {
249
- return await handler.decodeFrames(data);
265
+ return await handler.decodeFrames(data, normalizedSettings);
250
266
  }
251
267
  }
252
268
  throw new Error("Unsupported or unrecognized multi-frame image format");
@@ -259,7 +275,7 @@ export class Image {
259
275
  * @returns MultiFrameImageData with all frames
260
276
  */
261
277
  static readFrames(data, format) {
262
- return Image.decodeFrames(data, format);
278
+ return format ? Image.decodeFrames(data, format) : Image.decodeFrames(data);
263
279
  }
264
280
  /**
265
281
  * Encode multi-frame image data to bytes in the specified format
@@ -848,6 +864,34 @@ export class Image {
848
864
  this.imageData.data = flipVertical(this.imageData.data, this.imageData.width, this.imageData.height);
849
865
  return this;
850
866
  }
867
+ /**
868
+ * Convert the image to CMYK color space
869
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
870
+ * @returns CMYK image data as Float32Array
871
+ */
872
+ toCMYK() {
873
+ if (!this.imageData)
874
+ throw new Error("No image loaded");
875
+ return rgbaToCmyk(this.imageData.data);
876
+ }
877
+ /**
878
+ * Create an Image from CMYK data
879
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
880
+ * @param width Image width
881
+ * @param height Image height
882
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
883
+ * @returns New Image instance
884
+ */
885
+ static fromCMYK(cmykData, width, height, alpha = 255) {
886
+ const rgbaData = cmykToRgba(cmykData, alpha);
887
+ const image = new Image();
888
+ image.imageData = {
889
+ width,
890
+ height,
891
+ data: rgbaData,
892
+ };
893
+ return image;
894
+ }
851
895
  }
852
896
  Object.defineProperty(Image, "formats", {
853
897
  enumerable: true,