cross-image 0.4.0 → 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.
- package/README.md +109 -1
- 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/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 +15 -0
- package/esm/src/image.js +29 -1
- package/esm/src/types.d.ts +66 -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/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/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 +15 -0
- package/script/src/image.js +28 -0
- package/script/src/types.d.ts +66 -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
|
@@ -43,7 +43,48 @@ export declare abstract class PNGBase {
|
|
|
43
43
|
/**
|
|
44
44
|
* Filter PNG data for encoding (using filter type 0 - None)
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Apply PNG filter to image data based on compression level
|
|
48
|
+
* @param data Raw RGBA pixel data
|
|
49
|
+
* @param width Image width
|
|
50
|
+
* @param height Image height
|
|
51
|
+
* @param compressionLevel Compression level (0-9, default 6)
|
|
52
|
+
* @returns Filtered data with filter type byte per scanline
|
|
53
|
+
*/
|
|
54
|
+
protected filterData(data: Uint8Array, width: number, height: number, compressionLevel?: number): Uint8Array;
|
|
55
|
+
/**
|
|
56
|
+
* Apply filter type 0 (None) - no filtering
|
|
57
|
+
*/
|
|
58
|
+
private applyNoFilter;
|
|
59
|
+
/**
|
|
60
|
+
* Apply filter type 1 (Sub) - subtract left pixel
|
|
61
|
+
*/
|
|
62
|
+
private applySubFilter;
|
|
63
|
+
/**
|
|
64
|
+
* Apply filter type 2 (Up) - subtract above pixel
|
|
65
|
+
*/
|
|
66
|
+
private applyUpFilter;
|
|
67
|
+
/**
|
|
68
|
+
* Apply filter type 3 (Average) - subtract average of left and above
|
|
69
|
+
*/
|
|
70
|
+
private applyAverageFilter;
|
|
71
|
+
/**
|
|
72
|
+
* Apply filter type 4 (Paeth) - Paeth predictor
|
|
73
|
+
*/
|
|
74
|
+
private applyPaethFilter;
|
|
75
|
+
/**
|
|
76
|
+
* Calculate sum of absolute differences for a filtered scanline
|
|
77
|
+
* Lower values indicate better compression potential
|
|
78
|
+
*/
|
|
79
|
+
private calculateFilterScore;
|
|
80
|
+
/**
|
|
81
|
+
* Apply adaptive filtering - choose best filter per scanline
|
|
82
|
+
*/
|
|
83
|
+
private applyAdaptiveFilter;
|
|
84
|
+
/**
|
|
85
|
+
* Filter a single scanline with specified filter type
|
|
86
|
+
*/
|
|
87
|
+
private filterScanline;
|
|
47
88
|
/**
|
|
48
89
|
* Get bytes per pixel for a given color type and bit depth
|
|
49
90
|
*/
|
|
@@ -144,18 +144,211 @@ class PNGBase {
|
|
|
144
144
|
/**
|
|
145
145
|
* Filter PNG data for encoding (using filter type 0 - None)
|
|
146
146
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Apply PNG filter to image data based on compression level
|
|
149
|
+
* @param data Raw RGBA pixel data
|
|
150
|
+
* @param width Image width
|
|
151
|
+
* @param height Image height
|
|
152
|
+
* @param compressionLevel Compression level (0-9, default 6)
|
|
153
|
+
* @returns Filtered data with filter type byte per scanline
|
|
154
|
+
*/
|
|
155
|
+
filterData(data, width, height, compressionLevel = 6) {
|
|
156
|
+
// Choose filtering strategy based on compression level
|
|
157
|
+
if (compressionLevel <= 2) {
|
|
158
|
+
// Fast: No filtering
|
|
159
|
+
return this.applyNoFilter(data, width, height);
|
|
160
|
+
}
|
|
161
|
+
else if (compressionLevel <= 6) {
|
|
162
|
+
// Balanced: Sub filter
|
|
163
|
+
return this.applySubFilter(data, width, height);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
// Best: Adaptive filtering (choose best filter per scanline)
|
|
167
|
+
return this.applyAdaptiveFilter(data, width, height);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Apply filter type 0 (None) - no filtering
|
|
172
|
+
*/
|
|
173
|
+
applyNoFilter(data, width, height) {
|
|
174
|
+
const bytesPerScanline = width * 4;
|
|
175
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
150
176
|
let pos = 0;
|
|
151
177
|
for (let y = 0; y < height; y++) {
|
|
152
178
|
filtered[pos++] = 0; // Filter type: None
|
|
153
|
-
|
|
154
|
-
|
|
179
|
+
const scanlineStart = y * bytesPerScanline;
|
|
180
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
181
|
+
filtered[pos++] = data[scanlineStart + x];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return filtered;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Apply filter type 1 (Sub) - subtract left pixel
|
|
188
|
+
*/
|
|
189
|
+
applySubFilter(data, width, height) {
|
|
190
|
+
const bytesPerScanline = width * 4;
|
|
191
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
192
|
+
let pos = 0;
|
|
193
|
+
for (let y = 0; y < height; y++) {
|
|
194
|
+
filtered[pos++] = 1; // Filter type: Sub
|
|
195
|
+
const scanlineStart = y * bytesPerScanline;
|
|
196
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
197
|
+
const current = data[scanlineStart + x];
|
|
198
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
199
|
+
filtered[pos++] = (current - left) & 0xff;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return filtered;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Apply filter type 2 (Up) - subtract above pixel
|
|
206
|
+
*/
|
|
207
|
+
applyUpFilter(data, width, height) {
|
|
208
|
+
const bytesPerScanline = width * 4;
|
|
209
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
210
|
+
let pos = 0;
|
|
211
|
+
for (let y = 0; y < height; y++) {
|
|
212
|
+
filtered[pos++] = 2; // Filter type: Up
|
|
213
|
+
const scanlineStart = y * bytesPerScanline;
|
|
214
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
215
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
216
|
+
const current = data[scanlineStart + x];
|
|
217
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
218
|
+
filtered[pos++] = (current - up) & 0xff;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return filtered;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Apply filter type 3 (Average) - subtract average of left and above
|
|
225
|
+
*/
|
|
226
|
+
applyAverageFilter(data, width, height) {
|
|
227
|
+
const bytesPerScanline = width * 4;
|
|
228
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
229
|
+
let pos = 0;
|
|
230
|
+
for (let y = 0; y < height; y++) {
|
|
231
|
+
filtered[pos++] = 3; // Filter type: Average
|
|
232
|
+
const scanlineStart = y * bytesPerScanline;
|
|
233
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
234
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
235
|
+
const current = data[scanlineStart + x];
|
|
236
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
237
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
238
|
+
const avg = Math.floor((left + up) / 2);
|
|
239
|
+
filtered[pos++] = (current - avg) & 0xff;
|
|
155
240
|
}
|
|
156
241
|
}
|
|
157
242
|
return filtered;
|
|
158
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* Apply filter type 4 (Paeth) - Paeth predictor
|
|
246
|
+
*/
|
|
247
|
+
applyPaethFilter(data, width, height) {
|
|
248
|
+
const bytesPerScanline = width * 4;
|
|
249
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
250
|
+
let pos = 0;
|
|
251
|
+
for (let y = 0; y < height; y++) {
|
|
252
|
+
filtered[pos++] = 4; // Filter type: Paeth
|
|
253
|
+
const scanlineStart = y * bytesPerScanline;
|
|
254
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
255
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
256
|
+
const current = data[scanlineStart + x];
|
|
257
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
258
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
259
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
260
|
+
const paeth = this.paethPredictor(left, up, upLeft);
|
|
261
|
+
filtered[pos++] = (current - paeth) & 0xff;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return filtered;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Calculate sum of absolute differences for a filtered scanline
|
|
268
|
+
* Lower values indicate better compression potential
|
|
269
|
+
*/
|
|
270
|
+
calculateFilterScore(filtered) {
|
|
271
|
+
let sum = 0;
|
|
272
|
+
for (let i = 1; i < filtered.length; i++) {
|
|
273
|
+
const byte = filtered[i];
|
|
274
|
+
// Penalize larger absolute values
|
|
275
|
+
sum += byte < 128 ? byte : (256 - byte);
|
|
276
|
+
}
|
|
277
|
+
return sum;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Apply adaptive filtering - choose best filter per scanline
|
|
281
|
+
*/
|
|
282
|
+
applyAdaptiveFilter(data, width, height) {
|
|
283
|
+
const bytesPerScanline = width * 4;
|
|
284
|
+
const filtered = new Uint8Array(height * (1 + bytesPerScanline));
|
|
285
|
+
let outPos = 0;
|
|
286
|
+
// Try each filter type and choose the best for each scanline
|
|
287
|
+
const filters = [
|
|
288
|
+
(y) => this.filterScanline(data, y, width, 0), // None
|
|
289
|
+
(y) => this.filterScanline(data, y, width, 1), // Sub
|
|
290
|
+
(y) => this.filterScanline(data, y, width, 2), // Up
|
|
291
|
+
(y) => this.filterScanline(data, y, width, 3), // Average
|
|
292
|
+
(y) => this.filterScanline(data, y, width, 4), // Paeth
|
|
293
|
+
];
|
|
294
|
+
for (let y = 0; y < height; y++) {
|
|
295
|
+
let bestFilter = null;
|
|
296
|
+
let bestScore = Infinity;
|
|
297
|
+
// Try each filter type
|
|
298
|
+
for (const filterFn of filters) {
|
|
299
|
+
const result = filterFn(y);
|
|
300
|
+
const score = this.calculateFilterScore(result);
|
|
301
|
+
if (score < bestScore) {
|
|
302
|
+
bestScore = score;
|
|
303
|
+
bestFilter = result;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Copy best filter result
|
|
307
|
+
if (bestFilter) {
|
|
308
|
+
filtered.set(bestFilter, outPos);
|
|
309
|
+
outPos += bestFilter.length;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return filtered;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Filter a single scanline with specified filter type
|
|
316
|
+
*/
|
|
317
|
+
filterScanline(data, y, width, filterType) {
|
|
318
|
+
const bytesPerScanline = width * 4;
|
|
319
|
+
const result = new Uint8Array(1 + bytesPerScanline);
|
|
320
|
+
result[0] = filterType;
|
|
321
|
+
const scanlineStart = y * bytesPerScanline;
|
|
322
|
+
const prevScanlineStart = (y - 1) * bytesPerScanline;
|
|
323
|
+
for (let x = 0; x < bytesPerScanline; x++) {
|
|
324
|
+
const current = data[scanlineStart + x];
|
|
325
|
+
const left = x >= 4 ? data[scanlineStart + x - 4] : 0;
|
|
326
|
+
const up = y > 0 ? data[prevScanlineStart + x] : 0;
|
|
327
|
+
const upLeft = (y > 0 && x >= 4) ? data[prevScanlineStart + x - 4] : 0;
|
|
328
|
+
let filtered;
|
|
329
|
+
switch (filterType) {
|
|
330
|
+
case 0: // None
|
|
331
|
+
filtered = current;
|
|
332
|
+
break;
|
|
333
|
+
case 1: // Sub
|
|
334
|
+
filtered = (current - left) & 0xff;
|
|
335
|
+
break;
|
|
336
|
+
case 2: // Up
|
|
337
|
+
filtered = (current - up) & 0xff;
|
|
338
|
+
break;
|
|
339
|
+
case 3: // Average
|
|
340
|
+
filtered = (current - Math.floor((left + up) / 2)) & 0xff;
|
|
341
|
+
break;
|
|
342
|
+
case 4: // Paeth
|
|
343
|
+
filtered = (current - this.paethPredictor(left, up, upLeft)) & 0xff;
|
|
344
|
+
break;
|
|
345
|
+
default:
|
|
346
|
+
filtered = current;
|
|
347
|
+
}
|
|
348
|
+
result[x + 1] = filtered;
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
159
352
|
/**
|
|
160
353
|
* Get bytes per pixel for a given color type and bit depth
|
|
161
354
|
*/
|
|
@@ -5,6 +5,7 @@ const tiff_lzw_js_1 = require("../utils/tiff_lzw.js");
|
|
|
5
5
|
const tiff_packbits_js_1 = require("../utils/tiff_packbits.js");
|
|
6
6
|
const tiff_deflate_js_1 = require("../utils/tiff_deflate.js");
|
|
7
7
|
const security_js_1 = require("../utils/security.js");
|
|
8
|
+
const image_processing_js_1 = require("../utils/image_processing.js");
|
|
8
9
|
// Constants for unit conversions
|
|
9
10
|
const DEFAULT_DPI = 72;
|
|
10
11
|
/**
|
|
@@ -139,6 +140,7 @@ class TIFFFormat {
|
|
|
139
140
|
const compression = opts?.compression ?? "none";
|
|
140
141
|
const grayscale = opts?.grayscale ?? false;
|
|
141
142
|
const rgb = opts?.rgb ?? false;
|
|
143
|
+
const cmyk = opts?.cmyk ?? false;
|
|
142
144
|
// Convert RGBA to grayscale if requested
|
|
143
145
|
let sourceData;
|
|
144
146
|
let samplesPerPixel;
|
|
@@ -153,6 +155,16 @@ class TIFFFormat {
|
|
|
153
155
|
sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
158
|
+
else if (cmyk) {
|
|
159
|
+
// Convert RGBA to CMYK
|
|
160
|
+
const cmykData = (0, image_processing_js_1.rgbaToCmyk)(data);
|
|
161
|
+
sourceData = new Uint8Array(width * height * 4);
|
|
162
|
+
samplesPerPixel = 4;
|
|
163
|
+
// Convert Float32Array CMYK (0-1) to Uint8Array (0-255)
|
|
164
|
+
for (let i = 0; i < cmykData.length; i++) {
|
|
165
|
+
sourceData[i] = Math.round(cmykData[i] * 255);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
156
168
|
else if (rgb) {
|
|
157
169
|
// Convert RGBA to RGB (strip alpha channel)
|
|
158
170
|
sourceData = new Uint8Array(width * height * 3);
|
|
@@ -244,8 +256,8 @@ class TIFFFormat {
|
|
|
244
256
|
}
|
|
245
257
|
// Compression (0x0103) - 1 = uncompressed, 5 = LZW
|
|
246
258
|
this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
|
|
247
|
-
// PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
|
|
248
|
-
this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : 2);
|
|
259
|
+
// PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB, 5 = CMYK
|
|
260
|
+
this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : (cmyk ? 5 : 2));
|
|
249
261
|
// StripOffsets (0x0111)
|
|
250
262
|
this.writeIFDEntry(result, 0x0111, 4, 1, 8);
|
|
251
263
|
// SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
|
|
@@ -795,14 +807,16 @@ class TIFFFormat {
|
|
|
795
807
|
}
|
|
796
808
|
// Check photometric interpretation
|
|
797
809
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
798
|
-
if (photometric !== 0 && photometric !== 1 && photometric !== 2
|
|
799
|
-
|
|
810
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
|
|
811
|
+
photometric !== 5) {
|
|
812
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
|
|
800
813
|
return null;
|
|
801
814
|
}
|
|
802
815
|
// Get samples per pixel
|
|
803
816
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
804
817
|
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
805
818
|
// For RGB, expect 3 or 4 samples per pixel
|
|
819
|
+
// For CMYK, expect 4 samples per pixel
|
|
806
820
|
if (!samplesPerPixel) {
|
|
807
821
|
return null;
|
|
808
822
|
}
|
|
@@ -814,6 +828,10 @@ class TIFFFormat {
|
|
|
814
828
|
// RGB requires 3 or 4 samples per pixel
|
|
815
829
|
return null;
|
|
816
830
|
}
|
|
831
|
+
if (photometric === 5 && samplesPerPixel !== 4) {
|
|
832
|
+
// CMYK requires 4 samples per pixel
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
817
835
|
// Get strip offset
|
|
818
836
|
const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
|
|
819
837
|
if (!stripOffset || stripOffset >= data.length) {
|
|
@@ -871,6 +889,29 @@ class TIFFFormat {
|
|
|
871
889
|
}
|
|
872
890
|
}
|
|
873
891
|
}
|
|
892
|
+
else if (photometric === 5) {
|
|
893
|
+
// CMYK image - convert to RGB
|
|
894
|
+
for (let y = 0; y < height; y++) {
|
|
895
|
+
for (let x = 0; x < width; x++) {
|
|
896
|
+
const dstIdx = (y * width + x) * 4;
|
|
897
|
+
if (srcPos + 4 > pixelData.length) {
|
|
898
|
+
return null; // Not enough data
|
|
899
|
+
}
|
|
900
|
+
// TIFF stores CMYK in order, values are 0-255
|
|
901
|
+
// Convert to 0-1 range for conversion
|
|
902
|
+
const c = pixelData[srcPos++] / 255;
|
|
903
|
+
const m = pixelData[srcPos++] / 255;
|
|
904
|
+
const yVal = pixelData[srcPos++] / 255;
|
|
905
|
+
const k = pixelData[srcPos++] / 255;
|
|
906
|
+
// Convert CMYK to RGB
|
|
907
|
+
const [r, g, b] = (0, image_processing_js_1.cmykToRgb)(c, m, yVal, k);
|
|
908
|
+
rgba[dstIdx] = r; // R
|
|
909
|
+
rgba[dstIdx + 1] = g; // G
|
|
910
|
+
rgba[dstIdx + 2] = b; // B
|
|
911
|
+
rgba[dstIdx + 3] = 255; // A (opaque)
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
874
915
|
else {
|
|
875
916
|
// RGB/RGBA image
|
|
876
917
|
for (let y = 0; y < height; y++) {
|
|
@@ -903,14 +944,16 @@ class TIFFFormat {
|
|
|
903
944
|
}
|
|
904
945
|
// Check photometric interpretation
|
|
905
946
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
906
|
-
if (photometric !== 0 && photometric !== 1 && photometric !== 2
|
|
907
|
-
|
|
947
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
|
|
948
|
+
photometric !== 5) {
|
|
949
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
|
|
908
950
|
return null;
|
|
909
951
|
}
|
|
910
952
|
// Get samples per pixel
|
|
911
953
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
912
954
|
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
913
955
|
// For RGB, expect 3 or 4 samples per pixel
|
|
956
|
+
// For CMYK, expect 4 samples per pixel
|
|
914
957
|
if (!samplesPerPixel) {
|
|
915
958
|
return null;
|
|
916
959
|
}
|
|
@@ -922,6 +965,10 @@ class TIFFFormat {
|
|
|
922
965
|
// RGB requires 3 or 4 samples per pixel
|
|
923
966
|
return null;
|
|
924
967
|
}
|
|
968
|
+
if (photometric === 5 && samplesPerPixel !== 4) {
|
|
969
|
+
// CMYK requires 4 samples per pixel
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
925
972
|
// Get strip offset
|
|
926
973
|
const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
|
|
927
974
|
if (!stripOffset || stripOffset >= data.length) {
|
|
@@ -979,6 +1026,29 @@ class TIFFFormat {
|
|
|
979
1026
|
}
|
|
980
1027
|
}
|
|
981
1028
|
}
|
|
1029
|
+
else if (photometric === 5) {
|
|
1030
|
+
// CMYK image - convert to RGB
|
|
1031
|
+
for (let y = 0; y < height; y++) {
|
|
1032
|
+
for (let x = 0; x < width; x++) {
|
|
1033
|
+
const dstIdx = (y * width + x) * 4;
|
|
1034
|
+
if (srcPos + 4 > pixelData.length) {
|
|
1035
|
+
return null; // Not enough data
|
|
1036
|
+
}
|
|
1037
|
+
// TIFF stores CMYK in order, values are 0-255
|
|
1038
|
+
// Convert to 0-1 range for conversion
|
|
1039
|
+
const c = pixelData[srcPos++] / 255;
|
|
1040
|
+
const m = pixelData[srcPos++] / 255;
|
|
1041
|
+
const yVal = pixelData[srcPos++] / 255;
|
|
1042
|
+
const k = pixelData[srcPos++] / 255;
|
|
1043
|
+
// Convert CMYK to RGB
|
|
1044
|
+
const [r, g, b] = (0, image_processing_js_1.cmykToRgb)(c, m, yVal, k);
|
|
1045
|
+
rgba[dstIdx] = r; // R
|
|
1046
|
+
rgba[dstIdx + 1] = g; // G
|
|
1047
|
+
rgba[dstIdx + 2] = b; // B
|
|
1048
|
+
rgba[dstIdx + 3] = 255; // A (opaque)
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
982
1052
|
else {
|
|
983
1053
|
// RGB/RGBA image
|
|
984
1054
|
for (let y = 0; y < height; y++) {
|
package/script/src/image.d.ts
CHANGED
|
@@ -350,5 +350,20 @@ export declare class Image {
|
|
|
350
350
|
* @returns This image instance for chaining
|
|
351
351
|
*/
|
|
352
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;
|
|
353
368
|
}
|
|
354
369
|
//# sourceMappingURL=image.d.ts.map
|
package/script/src/image.js
CHANGED
|
@@ -867,6 +867,34 @@ class Image {
|
|
|
867
867
|
this.imageData.data = (0, image_processing_js_1.flipVertical)(this.imageData.data, this.imageData.width, this.imageData.height);
|
|
868
868
|
return this;
|
|
869
869
|
}
|
|
870
|
+
/**
|
|
871
|
+
* Convert the image to CMYK color space
|
|
872
|
+
* Returns a Float32Array with 4 values per pixel (C, M, Y, K) in 0-1 range
|
|
873
|
+
* @returns CMYK image data as Float32Array
|
|
874
|
+
*/
|
|
875
|
+
toCMYK() {
|
|
876
|
+
if (!this.imageData)
|
|
877
|
+
throw new Error("No image loaded");
|
|
878
|
+
return (0, image_processing_js_1.rgbaToCmyk)(this.imageData.data);
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Create an Image from CMYK data
|
|
882
|
+
* @param cmykData CMYK image data (4 values per pixel in 0-1 range)
|
|
883
|
+
* @param width Image width
|
|
884
|
+
* @param height Image height
|
|
885
|
+
* @param alpha Optional alpha value for all pixels (0-255, default: 255)
|
|
886
|
+
* @returns New Image instance
|
|
887
|
+
*/
|
|
888
|
+
static fromCMYK(cmykData, width, height, alpha = 255) {
|
|
889
|
+
const rgbaData = (0, image_processing_js_1.cmykToRgba)(cmykData, alpha);
|
|
890
|
+
const image = new Image();
|
|
891
|
+
image.imageData = {
|
|
892
|
+
width,
|
|
893
|
+
height,
|
|
894
|
+
data: rgbaData,
|
|
895
|
+
};
|
|
896
|
+
return image;
|
|
897
|
+
}
|
|
870
898
|
}
|
|
871
899
|
exports.Image = Image;
|
|
872
900
|
Object.defineProperty(Image, "formats", {
|
package/script/src/types.d.ts
CHANGED
|
@@ -135,6 +135,42 @@ export interface ResizeOptions {
|
|
|
135
135
|
*/
|
|
136
136
|
fit?: "stretch" | "fit" | "fill" | "cover" | "contain";
|
|
137
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Options for PNG encoding.
|
|
140
|
+
*/
|
|
141
|
+
export interface PNGEncoderOptions {
|
|
142
|
+
/**
|
|
143
|
+
* Compression level (0-9)
|
|
144
|
+
* - 0-2: No filtering (fastest)
|
|
145
|
+
* - 3-6: Sub filter (balanced, default is 6)
|
|
146
|
+
* - 7-9: Adaptive filtering per scanline (best compression)
|
|
147
|
+
*
|
|
148
|
+
* Default: 6 (balanced)
|
|
149
|
+
*
|
|
150
|
+
* Note: Affects PNG filter selection. The native deflate compression
|
|
151
|
+
* is used regardless of level.
|
|
152
|
+
*/
|
|
153
|
+
compressionLevel?: number;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Options for APNG (Animated PNG) encoding.
|
|
157
|
+
*
|
|
158
|
+
* APNG uses PNG encoding for each frame.
|
|
159
|
+
*/
|
|
160
|
+
export interface APNGEncoderOptions extends PNGEncoderOptions {
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Options for GIF encoding.
|
|
164
|
+
*/
|
|
165
|
+
export interface GIFEncoderOptions {
|
|
166
|
+
/**
|
|
167
|
+
* Loop count for animated GIFs.
|
|
168
|
+
* - 0 (default): Loop infinitely
|
|
169
|
+
* - 1+: Loop a specific number of times
|
|
170
|
+
* - undefined or not set: Loop infinitely (same as 0)
|
|
171
|
+
*/
|
|
172
|
+
loop?: number;
|
|
173
|
+
}
|
|
138
174
|
/**
|
|
139
175
|
* Options for ASCII art encoding
|
|
140
176
|
*/
|
|
@@ -158,6 +194,8 @@ export interface TIFFEncoderOptions {
|
|
|
158
194
|
grayscale?: boolean;
|
|
159
195
|
/** Encode as RGB without alpha channel (default: false, ignored if grayscale is true) */
|
|
160
196
|
rgb?: boolean;
|
|
197
|
+
/** Encode as CMYK color space (default: false, ignored if grayscale is true) */
|
|
198
|
+
cmyk?: boolean;
|
|
161
199
|
}
|
|
162
200
|
/**
|
|
163
201
|
* Options for WebP encoding
|
|
@@ -189,6 +227,34 @@ export interface JPEGEncoderOptions {
|
|
|
189
227
|
*/
|
|
190
228
|
progressive?: boolean;
|
|
191
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Options for AVIF encoding.
|
|
232
|
+
*
|
|
233
|
+
* Note: AVIF encoding is currently delegated to runtime APIs (OffscreenCanvas).
|
|
234
|
+
* Many runtimes ignore `quality` for AVIF, or may not support AVIF encoding at all.
|
|
235
|
+
*/
|
|
236
|
+
export interface AVIFEncoderOptions {
|
|
237
|
+
/**
|
|
238
|
+
* Best-effort encoding quality.
|
|
239
|
+
*
|
|
240
|
+
* Accepts either 0-1 (canvas-style) or 1-100 (library-style).
|
|
241
|
+
*/
|
|
242
|
+
quality?: number;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Options for HEIC encoding.
|
|
246
|
+
*
|
|
247
|
+
* Note: HEIC encoding is currently delegated to runtime APIs (OffscreenCanvas).
|
|
248
|
+
* Many runtimes do not support HEIC encoding.
|
|
249
|
+
*/
|
|
250
|
+
export interface HEICEncoderOptions {
|
|
251
|
+
/**
|
|
252
|
+
* Best-effort encoding quality.
|
|
253
|
+
*
|
|
254
|
+
* Accepts either 0-1 (canvas-style) or 1-100 (library-style).
|
|
255
|
+
*/
|
|
256
|
+
quality?: number;
|
|
257
|
+
}
|
|
192
258
|
/**
|
|
193
259
|
* Common options for decode APIs.
|
|
194
260
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 utilities.
|
|
3
|
+
*
|
|
4
|
+
* Designed to work across Deno, Node.js, and Bun without external dependencies.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Encode bytes into a standard Base64 string.
|
|
8
|
+
*/
|
|
9
|
+
export declare function encodeBase64(bytes: Uint8Array): string;
|
|
10
|
+
/**
|
|
11
|
+
* Decode a Base64 (or Base64URL) string into bytes.
|
|
12
|
+
*
|
|
13
|
+
* - Whitespace is ignored.
|
|
14
|
+
* - Missing padding is tolerated.
|
|
15
|
+
* - `-`/`_` are accepted as URL-safe variants.
|
|
16
|
+
*/
|
|
17
|
+
export declare function decodeBase64(base64: string): Uint8Array;
|
|
18
|
+
export interface ParsedDataUrl {
|
|
19
|
+
mime: string;
|
|
20
|
+
bytes: Uint8Array;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a base64 data URL.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toDataUrl(mime: string, bytes: Uint8Array): string;
|
|
26
|
+
/**
|
|
27
|
+
* Parse a base64 data URL.
|
|
28
|
+
*
|
|
29
|
+
* Supports `data:<mime>;base64,<payload>`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseDataUrl(url: string): ParsedDataUrl;
|
|
32
|
+
//# sourceMappingURL=base64.d.ts.map
|