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.
- package/README.md +186 -5
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +2 -0
- package/esm/src/formats/apng.d.ts +5 -3
- package/esm/src/formats/apng.js +11 -4
- package/esm/src/formats/avif.d.ts +2 -2
- package/esm/src/formats/avif.js +11 -1
- package/esm/src/formats/gif.d.ts +3 -3
- package/esm/src/formats/gif.js +4 -4
- package/esm/src/formats/heic.d.ts +2 -2
- package/esm/src/formats/heic.js +11 -1
- package/esm/src/formats/jpeg.d.ts +21 -1
- package/esm/src/formats/jpeg.js +59 -0
- package/esm/src/formats/png.d.ts +3 -2
- package/esm/src/formats/png.js +8 -2
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +198 -5
- package/esm/src/formats/tiff.js +76 -6
- package/esm/src/image.d.ts +54 -1
- package/esm/src/image.js +97 -1
- package/esm/src/types.d.ts +129 -0
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -1
- package/esm/src/utils/gif_encoder.js +4 -2
- package/esm/src/utils/image_processing.d.ts +31 -0
- package/esm/src/utils/image_processing.js +88 -0
- package/esm/src/utils/jpeg_decoder.d.ts +25 -2
- package/esm/src/utils/jpeg_decoder.js +101 -10
- package/esm/src/utils/jpeg_encoder.d.ts +19 -0
- package/esm/src/utils/jpeg_encoder.js +267 -0
- package/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +11 -1
- package/script/src/formats/apng.d.ts +5 -3
- package/script/src/formats/apng.js +11 -4
- package/script/src/formats/avif.d.ts +2 -2
- package/script/src/formats/avif.js +11 -1
- package/script/src/formats/gif.d.ts +3 -3
- package/script/src/formats/gif.js +4 -4
- package/script/src/formats/heic.d.ts +2 -2
- package/script/src/formats/heic.js +11 -1
- package/script/src/formats/jpeg.d.ts +21 -1
- package/script/src/formats/jpeg.js +59 -0
- package/script/src/formats/png.d.ts +3 -2
- package/script/src/formats/png.js +8 -2
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +198 -5
- package/script/src/formats/tiff.js +76 -6
- package/script/src/image.d.ts +54 -1
- package/script/src/image.js +96 -0
- package/script/src/types.d.ts +129 -0
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_encoder.d.ts +3 -1
- package/script/src/utils/gif_encoder.js +4 -2
- package/script/src/utils/image_processing.d.ts +31 -0
- package/script/src/utils/image_processing.js +92 -0
- package/script/src/utils/jpeg_decoder.d.ts +25 -2
- package/script/src/utils/jpeg_decoder.js +101 -10
- package/script/src/utils/jpeg_encoder.d.ts +19 -0
- 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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
*/
|
package/esm/src/formats/tiff.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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++) {
|
package/esm/src/image.d.ts
CHANGED
|
@@ -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,
|