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
@@ -141,18 +141,211 @@ export class PNGBase {
141
141
  /**
142
142
  * Filter PNG data for encoding (using filter type 0 - None)
143
143
  */
144
- filterData(data, width, height) {
145
- // Use filter type 0 (None) for simplicity
146
- const filtered = new Uint8Array(height * (1 + width * 4));
144
+ /**
145
+ * Apply PNG filter to image data based on compression level
146
+ * @param data Raw RGBA pixel data
147
+ * @param width Image width
148
+ * @param height Image height
149
+ * @param compressionLevel Compression level (0-9, default 6)
150
+ * @returns Filtered data with filter type byte per scanline
151
+ */
152
+ filterData(data, width, height, compressionLevel = 6) {
153
+ // Choose filtering strategy based on compression level
154
+ if (compressionLevel <= 2) {
155
+ // Fast: No filtering
156
+ return this.applyNoFilter(data, width, height);
157
+ }
158
+ else if (compressionLevel <= 6) {
159
+ // Balanced: Sub filter
160
+ return this.applySubFilter(data, width, height);
161
+ }
162
+ else {
163
+ // Best: Adaptive filtering (choose best filter per scanline)
164
+ return this.applyAdaptiveFilter(data, width, height);
165
+ }
166
+ }
167
+ /**
168
+ * Apply filter type 0 (None) - no filtering
169
+ */
170
+ applyNoFilter(data, width, height) {
171
+ const bytesPerScanline = width * 4;
172
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
147
173
  let pos = 0;
148
174
  for (let y = 0; y < height; y++) {
149
175
  filtered[pos++] = 0; // Filter type: None
150
- for (let x = 0; x < width * 4; x++) {
151
- filtered[pos++] = data[y * width * 4 + x];
176
+ const scanlineStart = y * bytesPerScanline;
177
+ for (let x = 0; x < bytesPerScanline; x++) {
178
+ filtered[pos++] = data[scanlineStart + x];
179
+ }
180
+ }
181
+ return filtered;
182
+ }
183
+ /**
184
+ * Apply filter type 1 (Sub) - subtract left pixel
185
+ */
186
+ applySubFilter(data, width, height) {
187
+ const bytesPerScanline = width * 4;
188
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
189
+ let pos = 0;
190
+ for (let y = 0; y < height; y++) {
191
+ filtered[pos++] = 1; // Filter type: Sub
192
+ const scanlineStart = y * bytesPerScanline;
193
+ for (let x = 0; x < bytesPerScanline; x++) {
194
+ const current = data[scanlineStart + x];
195
+ const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
196
+ filtered[pos++] = (current - left) & 0xff;
197
+ }
198
+ }
199
+ return filtered;
200
+ }
201
+ /**
202
+ * Apply filter type 2 (Up) - subtract above pixel
203
+ */
204
+ applyUpFilter(data, width, height) {
205
+ const bytesPerScanline = width * 4;
206
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
207
+ let pos = 0;
208
+ for (let y = 0; y < height; y++) {
209
+ filtered[pos++] = 2; // Filter type: Up
210
+ const scanlineStart = y * bytesPerScanline;
211
+ const prevScanlineStart = (y - 1) * bytesPerScanline;
212
+ for (let x = 0; x < bytesPerScanline; x++) {
213
+ const current = data[scanlineStart + x];
214
+ const up = y > 0 ? data[prevScanlineStart + x] : 0;
215
+ filtered[pos++] = (current - up) & 0xff;
216
+ }
217
+ }
218
+ return filtered;
219
+ }
220
+ /**
221
+ * Apply filter type 3 (Average) - subtract average of left and above
222
+ */
223
+ applyAverageFilter(data, width, height) {
224
+ const bytesPerScanline = width * 4;
225
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
226
+ let pos = 0;
227
+ for (let y = 0; y < height; y++) {
228
+ filtered[pos++] = 3; // Filter type: Average
229
+ const scanlineStart = y * bytesPerScanline;
230
+ const prevScanlineStart = (y - 1) * bytesPerScanline;
231
+ for (let x = 0; x < bytesPerScanline; x++) {
232
+ const current = data[scanlineStart + x];
233
+ const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
234
+ const up = y > 0 ? data[prevScanlineStart + x] : 0;
235
+ const avg = Math.floor((left + up) / 2);
236
+ filtered[pos++] = (current - avg) & 0xff;
152
237
  }
153
238
  }
154
239
  return filtered;
155
240
  }
241
+ /**
242
+ * Apply filter type 4 (Paeth) - Paeth predictor
243
+ */
244
+ applyPaethFilter(data, width, height) {
245
+ const bytesPerScanline = width * 4;
246
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
247
+ let pos = 0;
248
+ for (let y = 0; y < height; y++) {
249
+ filtered[pos++] = 4; // Filter type: Paeth
250
+ const scanlineStart = y * bytesPerScanline;
251
+ const prevScanlineStart = (y - 1) * bytesPerScanline;
252
+ for (let x = 0; x < bytesPerScanline; x++) {
253
+ const current = data[scanlineStart + x];
254
+ const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
255
+ const up = y > 0 ? data[prevScanlineStart + x] : 0;
256
+ const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
257
+ const paeth = this.paethPredictor(left, up, upLeft);
258
+ filtered[pos++] = (current - paeth) & 0xff;
259
+ }
260
+ }
261
+ return filtered;
262
+ }
263
+ /**
264
+ * Calculate sum of absolute differences for a filtered scanline
265
+ * Lower values indicate better compression potential
266
+ */
267
+ calculateFilterScore(filtered) {
268
+ let sum = 0;
269
+ for (let i = 1; i < filtered.length; i++) {
270
+ const byte = filtered[i];
271
+ // Penalize larger absolute values
272
+ sum += byte < 128 ? byte : (256 - byte);
273
+ }
274
+ return sum;
275
+ }
276
+ /**
277
+ * Apply adaptive filtering - choose best filter per scanline
278
+ */
279
+ applyAdaptiveFilter(data, width, height) {
280
+ const bytesPerScanline = width * 4;
281
+ const filtered = new Uint8Array(height * (1 + bytesPerScanline));
282
+ let outPos = 0;
283
+ // Try each filter type and choose the best for each scanline
284
+ const filters = [
285
+ (y) => this.filterScanline(data, y, width, 0), // None
286
+ (y) => this.filterScanline(data, y, width, 1), // Sub
287
+ (y) => this.filterScanline(data, y, width, 2), // Up
288
+ (y) => this.filterScanline(data, y, width, 3), // Average
289
+ (y) => this.filterScanline(data, y, width, 4), // Paeth
290
+ ];
291
+ for (let y = 0; y < height; y++) {
292
+ let bestFilter = null;
293
+ let bestScore = Infinity;
294
+ // Try each filter type
295
+ for (const filterFn of filters) {
296
+ const result = filterFn(y);
297
+ const score = this.calculateFilterScore(result);
298
+ if (score < bestScore) {
299
+ bestScore = score;
300
+ bestFilter = result;
301
+ }
302
+ }
303
+ // Copy best filter result
304
+ if (bestFilter) {
305
+ filtered.set(bestFilter, outPos);
306
+ outPos += bestFilter.length;
307
+ }
308
+ }
309
+ return filtered;
310
+ }
311
+ /**
312
+ * Filter a single scanline with specified filter type
313
+ */
314
+ filterScanline(data, y, width, filterType) {
315
+ const bytesPerScanline = width * 4;
316
+ const result = new Uint8Array(1 + bytesPerScanline);
317
+ result[0] = filterType;
318
+ const scanlineStart = y * bytesPerScanline;
319
+ const prevScanlineStart = (y - 1) * bytesPerScanline;
320
+ for (let x = 0; x < bytesPerScanline; x++) {
321
+ const current = data[scanlineStart + x];
322
+ const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
323
+ const up = y > 0 ? data[prevScanlineStart + x] : 0;
324
+ const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
325
+ let filtered;
326
+ switch (filterType) {
327
+ case 0: // None
328
+ filtered = current;
329
+ break;
330
+ case 1: // Sub
331
+ filtered = (current - left) & 0xff;
332
+ break;
333
+ case 2: // Up
334
+ filtered = (current - up) & 0xff;
335
+ break;
336
+ case 3: // Average
337
+ filtered = (current - Math.floor((left + up) / 2)) & 0xff;
338
+ break;
339
+ case 4: // Paeth
340
+ filtered = (current - this.paethPredictor(left, up, upLeft)) & 0xff;
341
+ break;
342
+ default:
343
+ filtered = current;
344
+ }
345
+ result[x + 1] = filtered;
346
+ }
347
+ return result;
348
+ }
156
349
  /**
157
350
  * Get bytes per pixel for a given color type and bit depth
158
351
  */
@@ -2,6 +2,7 @@ import { TIFFLZWDecoder, TIFFLZWEncoder } from "../utils/tiff_lzw.js";
2
2
  import { packBitsCompress, packBitsDecompress } from "../utils/tiff_packbits.js";
3
3
  import { deflateCompress, deflateDecompress } from "../utils/tiff_deflate.js";
4
4
  import { validateImageDimensions } from "../utils/security.js";
5
+ import { cmykToRgb, rgbaToCmyk } from "../utils/image_processing.js";
5
6
  // Constants for unit conversions
6
7
  const DEFAULT_DPI = 72;
7
8
  /**
@@ -136,6 +137,7 @@ export class TIFFFormat {
136
137
  const compression = opts?.compression ?? "none";
137
138
  const grayscale = opts?.grayscale ?? false;
138
139
  const rgb = opts?.rgb ?? false;
140
+ const cmyk = opts?.cmyk ?? false;
139
141
  // Convert RGBA to grayscale if requested
140
142
  let sourceData;
141
143
  let samplesPerPixel;
@@ -150,6 +152,16 @@ export class TIFFFormat {
150
152
  sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
151
153
  }
152
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
+ }
153
165
  else if (rgb) {
154
166
  // Convert RGBA to RGB (strip alpha channel)
155
167
  sourceData = new Uint8Array(width * height * 3);
@@ -241,8 +253,8 @@ export class TIFFFormat {
241
253
  }
242
254
  // Compression (0x0103) - 1 = uncompressed, 5 = LZW
243
255
  this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
244
- // PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
245
- 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));
246
258
  // StripOffsets (0x0111)
247
259
  this.writeIFDEntry(result, 0x0111, 4, 1, 8);
248
260
  // SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
@@ -792,14 +804,16 @@ export class TIFFFormat {
792
804
  }
793
805
  // Check photometric interpretation
794
806
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
795
- if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
796
- // 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
797
810
  return null;
798
811
  }
799
812
  // Get samples per pixel
800
813
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
801
814
  // For grayscale (photometric 0 or 1), expect 1 sample per pixel
802
815
  // For RGB, expect 3 or 4 samples per pixel
816
+ // For CMYK, expect 4 samples per pixel
803
817
  if (!samplesPerPixel) {
804
818
  return null;
805
819
  }
@@ -811,6 +825,10 @@ export class TIFFFormat {
811
825
  // RGB requires 3 or 4 samples per pixel
812
826
  return null;
813
827
  }
828
+ if (photometric === 5 && samplesPerPixel !== 4) {
829
+ // CMYK requires 4 samples per pixel
830
+ return null;
831
+ }
814
832
  // Get strip offset
815
833
  const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
816
834
  if (!stripOffset || stripOffset >= data.length) {
@@ -868,6 +886,29 @@ export class TIFFFormat {
868
886
  }
869
887
  }
870
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
+ }
871
912
  else {
872
913
  // RGB/RGBA image
873
914
  for (let y = 0; y < height; y++) {
@@ -900,14 +941,16 @@ export class TIFFFormat {
900
941
  }
901
942
  // Check photometric interpretation
902
943
  const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
903
- if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
904
- // 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
905
947
  return null;
906
948
  }
907
949
  // Get samples per pixel
908
950
  const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
909
951
  // For grayscale (photometric 0 or 1), expect 1 sample per pixel
910
952
  // For RGB, expect 3 or 4 samples per pixel
953
+ // For CMYK, expect 4 samples per pixel
911
954
  if (!samplesPerPixel) {
912
955
  return null;
913
956
  }
@@ -919,6 +962,10 @@ export class TIFFFormat {
919
962
  // RGB requires 3 or 4 samples per pixel
920
963
  return null;
921
964
  }
965
+ if (photometric === 5 && samplesPerPixel !== 4) {
966
+ // CMYK requires 4 samples per pixel
967
+ return null;
968
+ }
922
969
  // Get strip offset
923
970
  const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
924
971
  if (!stripOffset || stripOffset >= data.length) {
@@ -976,6 +1023,29 @@ export class TIFFFormat {
976
1023
  }
977
1024
  }
978
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
+ }
979
1049
  else {
980
1050
  // RGB/RGBA image
981
1051
  for (let y = 0; y < height; y++) {
@@ -1,4 +1,4 @@
1
- import type { ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData, ResizeOptions } from "./types.js";
1
+ import type { CoefficientData, ImageDecoderOptions, ImageFormat, ImageMetadata, MultiFrameImageData, ResizeOptions } from "./types.js";
2
2
  /**
3
3
  * Main Image class for reading, manipulating, and saving images
4
4
  */
@@ -98,6 +98,44 @@ export declare class Image {
98
98
  * @returns Metadata extracted from the image, or undefined if extraction fails or format is unsupported
99
99
  */
100
100
  static extractMetadata(data: Uint8Array, format?: string): Promise<ImageMetadata | undefined>;
101
+ /**
102
+ * Extract coefficients from encoded image data
103
+ * For JPEG, this returns quantized DCT coefficients that can be modified for steganography
104
+ * and re-encoded using encodeFromCoefficients()
105
+ * @param data Raw image data
106
+ * @param format Optional format hint (e.g., "jpeg")
107
+ * @param options Optional decoder options
108
+ * @returns Format-specific coefficient structure or undefined if not supported
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * // Extract JPEG coefficients for steganography
113
+ * const coeffs = await Image.extractCoefficients(jpegData, "jpeg");
114
+ * if (coeffs) {
115
+ * // Modify coefficients for steganography...
116
+ * const modified = await Image.encodeFromCoefficients(coeffs, "jpeg");
117
+ * }
118
+ * ```
119
+ */
120
+ static extractCoefficients(data: Uint8Array, format?: string, options?: ImageDecoderOptions): Promise<CoefficientData | undefined>;
121
+ /**
122
+ * Encode image from coefficients
123
+ * For JPEG, accepts quantized DCT coefficients and produces a valid JPEG file
124
+ * Useful for steganography where coefficients are extracted, modified, and re-encoded
125
+ * @param coeffs Format-specific coefficient structure
126
+ * @param format Optional format hint (auto-detected from coeffs.format if available)
127
+ * @param options Optional format-specific encoding options
128
+ * @returns Encoded image bytes
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * // Re-encode modified JPEG coefficients
133
+ * const coeffs = await Image.extractCoefficients(jpegData, "jpeg");
134
+ * // Modify coefficients...
135
+ * const encoded = await Image.encodeFromCoefficients(coeffs, "jpeg");
136
+ * ```
137
+ */
138
+ static encodeFromCoefficients(coeffs: CoefficientData, format?: string, options?: unknown): Promise<Uint8Array>;
101
139
  /**
102
140
  * Read an image from bytes
103
141
  * @deprecated Use `decode()` instead. This method will be removed in a future version.
@@ -350,5 +388,20 @@ export declare class Image {
350
388
  * @returns This image instance for chaining
351
389
  */
352
390
  flipVertical(): this;
391
+ /**
392
+ * Convert the image to CMYK color space
393
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
394
+ * @returns CMYK image data as Float32Array
395
+ */
396
+ toCMYK(): Float32Array;
397
+ /**
398
+ * Create an Image from CMYK data
399
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
400
+ * @param width Image width
401
+ * @param height Image height
402
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
403
+ * @returns New Image instance
404
+ */
405
+ static fromCMYK(cmykData: Float32Array, width: number, height: number, alpha?: number): Image;
353
406
  }
354
407
  //# sourceMappingURL=image.d.ts.map
package/esm/src/image.js CHANGED
@@ -1,5 +1,5 @@
1
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";
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";
@@ -230,6 +230,74 @@ export class Image {
230
230
  }
231
231
  return undefined;
232
232
  }
233
+ /**
234
+ * Extract coefficients from encoded image data
235
+ * For JPEG, this returns quantized DCT coefficients that can be modified for steganography
236
+ * and re-encoded using encodeFromCoefficients()
237
+ * @param data Raw image data
238
+ * @param format Optional format hint (e.g., "jpeg")
239
+ * @param options Optional decoder options
240
+ * @returns Format-specific coefficient structure or undefined if not supported
241
+ *
242
+ * @example
243
+ * ```ts
244
+ * // Extract JPEG coefficients for steganography
245
+ * const coeffs = await Image.extractCoefficients(jpegData, "jpeg");
246
+ * if (coeffs) {
247
+ * // Modify coefficients for steganography...
248
+ * const modified = await Image.encodeFromCoefficients(coeffs, "jpeg");
249
+ * }
250
+ * ```
251
+ */
252
+ static async extractCoefficients(data, format, options) {
253
+ // Try specified format first
254
+ if (format) {
255
+ const handler = Image.formats.find((f) => f.name === format);
256
+ if (handler && handler.canDecode(data) && handler.extractCoefficients) {
257
+ return await handler.extractCoefficients(data, options);
258
+ }
259
+ }
260
+ // Auto-detect format
261
+ for (const handler of Image.formats) {
262
+ if (handler.canDecode(data) && handler.extractCoefficients) {
263
+ return await handler.extractCoefficients(data, options);
264
+ }
265
+ }
266
+ return undefined;
267
+ }
268
+ /**
269
+ * Encode image from coefficients
270
+ * For JPEG, accepts quantized DCT coefficients and produces a valid JPEG file
271
+ * Useful for steganography where coefficients are extracted, modified, and re-encoded
272
+ * @param coeffs Format-specific coefficient structure
273
+ * @param format Optional format hint (auto-detected from coeffs.format if available)
274
+ * @param options Optional format-specific encoding options
275
+ * @returns Encoded image bytes
276
+ *
277
+ * @example
278
+ * ```ts
279
+ * // Re-encode modified JPEG coefficients
280
+ * const coeffs = await Image.extractCoefficients(jpegData, "jpeg");
281
+ * // Modify coefficients...
282
+ * const encoded = await Image.encodeFromCoefficients(coeffs, "jpeg");
283
+ * ```
284
+ */
285
+ static async encodeFromCoefficients(coeffs, format, options) {
286
+ // Detect format from coefficient structure or use provided format
287
+ const detectedFormat = format ??
288
+ coeffs.format;
289
+ if (!detectedFormat) {
290
+ throw new Error("Format must be specified or present in coefficient data");
291
+ }
292
+ const handler = Image.formats.find((f) => f.name === detectedFormat);
293
+ if (!handler) {
294
+ throw new Error(`Unknown format: ${detectedFormat}`);
295
+ }
296
+ if (!handler.encodeFromCoefficients) {
297
+ throw new Error(`Format ${detectedFormat} does not support encoding from coefficients`);
298
+ }
299
+ return await handler.encodeFromCoefficients(coeffs, options);
300
+ }
233
301
  /**
234
302
  * Read an image from bytes
235
303
  * @deprecated Use `decode()` instead. This method will be removed in a future version.
@@ -864,6 +932,34 @@ export class Image {
864
932
  this.imageData.data = flipVertical(this.imageData.data, this.imageData.width, this.imageData.height);
865
933
  return this;
866
934
  }
935
+ /**
936
+ * Convert the image to CMYK color space
937
+ * Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
938
+ * @returns CMYK image data as Float32Array
939
+ */
940
+ toCMYK() {
941
+ if (!this.imageData)
942
+ throw new Error("No image loaded");
943
+ return rgbaToCmyk(this.imageData.data);
944
+ }
945
+ /**
946
+ * Create an Image from CMYK data
947
+ * @param cmykData CMYK image data (4 values per pixel in 0-1 range)
948
+ * @param width Image width
949
+ * @param height Image height
950
+ * @param alpha Optional alpha value for all pixels (0-255, default: 255)
951
+ * @returns New Image instance
952
+ */
953
+ static fromCMYK(cmykData, width, height, alpha = 255) {
954
+ const rgbaData = cmykToRgba(cmykData, alpha);
955
+ const image = new Image();
956
+ image.imageData = {
957
+ width,
958
+ height,
959
+ data: rgbaData,
960
+ };
961
+ return image;
962
+ }
867
963
  }
868
964
  Object.defineProperty(Image, "formats", {
869
965
  enumerable: true,