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.
- package/LICENSE +21 -21
- package/README.md +615 -333
- package/esm/mod.d.ts +6 -4
- package/esm/mod.js +4 -2
- package/esm/src/formats/apng.d.ts +7 -5
- package/esm/src/formats/apng.js +15 -10
- package/esm/src/formats/ascii.d.ts +3 -3
- package/esm/src/formats/ascii.js +1 -1
- package/esm/src/formats/avif.d.ts +3 -3
- package/esm/src/formats/avif.js +17 -7
- package/esm/src/formats/bmp.d.ts +3 -3
- package/esm/src/formats/bmp.js +2 -2
- package/esm/src/formats/dng.d.ts +1 -1
- package/esm/src/formats/dng.js +1 -1
- package/esm/src/formats/gif.d.ts +5 -5
- package/esm/src/formats/gif.js +17 -13
- package/esm/src/formats/heic.d.ts +3 -3
- package/esm/src/formats/heic.js +17 -7
- package/esm/src/formats/ico.d.ts +3 -3
- package/esm/src/formats/ico.js +4 -4
- package/esm/src/formats/jpeg.d.ts +3 -3
- package/esm/src/formats/jpeg.js +23 -11
- package/esm/src/formats/pam.d.ts +3 -3
- package/esm/src/formats/pam.js +2 -2
- package/esm/src/formats/pcx.d.ts +3 -3
- package/esm/src/formats/pcx.js +2 -2
- package/esm/src/formats/png.d.ts +4 -3
- package/esm/src/formats/png.js +9 -3
- package/esm/src/formats/png_base.d.ts +42 -1
- package/esm/src/formats/png_base.js +200 -10
- package/esm/src/formats/ppm.d.ts +3 -3
- package/esm/src/formats/ppm.js +2 -2
- package/esm/src/formats/tiff.d.ts +7 -18
- package/esm/src/formats/tiff.js +162 -27
- package/esm/src/formats/webp.d.ts +3 -3
- package/esm/src/formats/webp.js +11 -8
- package/esm/src/image.d.ts +26 -3
- package/esm/src/image.js +66 -22
- package/esm/src/types.d.ts +122 -4
- package/esm/src/utils/base64.d.ts +32 -0
- package/esm/src/utils/base64.js +173 -0
- package/esm/src/utils/gif_decoder.d.ts +4 -1
- package/esm/src/utils/gif_decoder.js +91 -65
- 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 +232 -70
- package/esm/src/utils/jpeg_decoder.d.ts +17 -4
- package/esm/src/utils/jpeg_decoder.js +448 -83
- package/esm/src/utils/jpeg_encoder.d.ts +15 -1
- package/esm/src/utils/jpeg_encoder.js +263 -24
- package/esm/src/utils/resize.js +51 -20
- package/esm/src/utils/tiff_deflate.d.ts +18 -0
- package/esm/src/utils/tiff_deflate.js +27 -0
- package/esm/src/utils/tiff_packbits.d.ts +24 -0
- package/esm/src/utils/tiff_packbits.js +90 -0
- package/esm/src/utils/webp_decoder.d.ts +3 -1
- package/esm/src/utils/webp_decoder.js +144 -63
- package/esm/src/utils/webp_encoder.js +5 -11
- package/package.json +1 -1
- package/script/mod.d.ts +6 -4
- package/script/mod.js +13 -3
- package/script/src/formats/apng.d.ts +7 -5
- package/script/src/formats/apng.js +15 -10
- package/script/src/formats/ascii.d.ts +3 -3
- package/script/src/formats/ascii.js +1 -1
- package/script/src/formats/avif.d.ts +3 -3
- package/script/src/formats/avif.js +17 -7
- package/script/src/formats/bmp.d.ts +3 -3
- package/script/src/formats/bmp.js +2 -2
- package/script/src/formats/dng.d.ts +1 -1
- package/script/src/formats/dng.js +1 -1
- package/script/src/formats/gif.d.ts +5 -5
- package/script/src/formats/gif.js +17 -13
- package/script/src/formats/heic.d.ts +3 -3
- package/script/src/formats/heic.js +17 -7
- package/script/src/formats/ico.d.ts +3 -3
- package/script/src/formats/ico.js +4 -4
- package/script/src/formats/jpeg.d.ts +3 -3
- package/script/src/formats/jpeg.js +23 -11
- package/script/src/formats/pam.d.ts +3 -3
- package/script/src/formats/pam.js +2 -2
- package/script/src/formats/pcx.d.ts +3 -3
- package/script/src/formats/pcx.js +2 -2
- package/script/src/formats/png.d.ts +4 -3
- package/script/src/formats/png.js +9 -3
- package/script/src/formats/png_base.d.ts +42 -1
- package/script/src/formats/png_base.js +200 -10
- package/script/src/formats/ppm.d.ts +3 -3
- package/script/src/formats/ppm.js +2 -2
- package/script/src/formats/tiff.d.ts +7 -18
- package/script/src/formats/tiff.js +162 -27
- package/script/src/formats/webp.d.ts +3 -3
- package/script/src/formats/webp.js +11 -8
- package/script/src/image.d.ts +26 -3
- package/script/src/image.js +64 -20
- package/script/src/types.d.ts +122 -4
- package/script/src/utils/base64.d.ts +32 -0
- package/script/src/utils/base64.js +179 -0
- package/script/src/utils/gif_decoder.d.ts +4 -1
- package/script/src/utils/gif_decoder.js +91 -65
- 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 +236 -70
- package/script/src/utils/jpeg_decoder.d.ts +17 -4
- package/script/src/utils/jpeg_decoder.js +448 -83
- package/script/src/utils/jpeg_encoder.d.ts +15 -1
- package/script/src/utils/jpeg_encoder.js +263 -24
- package/script/src/utils/resize.js +51 -20
- package/script/src/utils/tiff_deflate.d.ts +18 -0
- package/script/src/utils/tiff_deflate.js +31 -0
- package/script/src/utils/tiff_packbits.d.ts +24 -0
- package/script/src/utils/tiff_packbits.js +94 -0
- package/script/src/utils/webp_decoder.d.ts +3 -1
- package/script/src/utils/webp_decoder.js +144 -63
- package/script/src/utils/webp_encoder.js +5 -11
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TIFFFormat = void 0;
|
|
4
4
|
const tiff_lzw_js_1 = require("../utils/tiff_lzw.js");
|
|
5
|
+
const tiff_packbits_js_1 = require("../utils/tiff_packbits.js");
|
|
6
|
+
const tiff_deflate_js_1 = require("../utils/tiff_deflate.js");
|
|
5
7
|
const security_js_1 = require("../utils/security.js");
|
|
8
|
+
const image_processing_js_1 = require("../utils/image_processing.js");
|
|
6
9
|
// Constants for unit conversions
|
|
7
10
|
const DEFAULT_DPI = 72;
|
|
8
11
|
/**
|
|
9
12
|
* TIFF format handler
|
|
10
|
-
* Implements pure-JS TIFF decoder for uncompressed and
|
|
11
|
-
* and encoder for uncompressed and
|
|
12
|
-
* for
|
|
13
|
+
* Implements pure-JS TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
|
|
14
|
+
* and encoder for uncompressed, LZW, PackBits, and Deflate-compressed RGBA TIFFs. Falls back to ImageDecoder
|
|
15
|
+
* for JPEG-compressed TIFFs.
|
|
13
16
|
* Supports multi-page TIFF files.
|
|
14
17
|
*/
|
|
15
18
|
class TIFFFormat {
|
|
@@ -55,7 +58,7 @@ class TIFFFormat {
|
|
|
55
58
|
* @param data Raw TIFF image data
|
|
56
59
|
* @returns Decoded image data with RGBA pixels of first page
|
|
57
60
|
*/
|
|
58
|
-
async decode(data) {
|
|
61
|
+
async decode(data, _options) {
|
|
59
62
|
if (!this.canDecode(data)) {
|
|
60
63
|
throw new Error("Invalid TIFF signature");
|
|
61
64
|
}
|
|
@@ -131,12 +134,13 @@ class TIFFFormat {
|
|
|
131
134
|
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
132
135
|
};
|
|
133
136
|
}
|
|
134
|
-
encode(imageData, options) {
|
|
137
|
+
async encode(imageData, options) {
|
|
135
138
|
const { width, height, data, metadata } = imageData;
|
|
136
139
|
const opts = options;
|
|
137
140
|
const compression = opts?.compression ?? "none";
|
|
138
141
|
const grayscale = opts?.grayscale ?? false;
|
|
139
142
|
const rgb = opts?.rgb ?? false;
|
|
143
|
+
const cmyk = opts?.cmyk ?? false;
|
|
140
144
|
// Convert RGBA to grayscale if requested
|
|
141
145
|
let sourceData;
|
|
142
146
|
let samplesPerPixel;
|
|
@@ -151,6 +155,16 @@ class TIFFFormat {
|
|
|
151
155
|
sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
152
156
|
}
|
|
153
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
|
+
}
|
|
154
168
|
else if (rgb) {
|
|
155
169
|
// Convert RGBA to RGB (strip alpha channel)
|
|
156
170
|
sourceData = new Uint8Array(width * height * 3);
|
|
@@ -175,6 +189,16 @@ class TIFFFormat {
|
|
|
175
189
|
pixelData = encoder.compress(sourceData);
|
|
176
190
|
compressionCode = 5;
|
|
177
191
|
}
|
|
192
|
+
else if (compression === "packbits") {
|
|
193
|
+
// PackBits compress the pixel data
|
|
194
|
+
pixelData = (0, tiff_packbits_js_1.packBitsCompress)(sourceData);
|
|
195
|
+
compressionCode = 32773;
|
|
196
|
+
}
|
|
197
|
+
else if (compression === "deflate") {
|
|
198
|
+
// Deflate compress the pixel data
|
|
199
|
+
pixelData = await (0, tiff_deflate_js_1.deflateCompress)(sourceData);
|
|
200
|
+
compressionCode = 8;
|
|
201
|
+
}
|
|
178
202
|
else {
|
|
179
203
|
// Uncompressed
|
|
180
204
|
pixelData = sourceData;
|
|
@@ -232,8 +256,8 @@ class TIFFFormat {
|
|
|
232
256
|
}
|
|
233
257
|
// Compression (0x0103) - 1 = uncompressed, 5 = LZW
|
|
234
258
|
this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
|
|
235
|
-
// PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
|
|
236
|
-
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));
|
|
237
261
|
// StripOffsets (0x0111)
|
|
238
262
|
this.writeIFDEntry(result, 0x0111, 4, 1, 8);
|
|
239
263
|
// SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
|
|
@@ -332,7 +356,7 @@ class TIFFFormat {
|
|
|
332
356
|
/**
|
|
333
357
|
* Decode all pages from a multi-page TIFF
|
|
334
358
|
*/
|
|
335
|
-
async decodeFrames(data) {
|
|
359
|
+
async decodeFrames(data, _options) {
|
|
336
360
|
if (!this.canDecode(data)) {
|
|
337
361
|
throw new Error("Invalid TIFF signature");
|
|
338
362
|
}
|
|
@@ -395,7 +419,7 @@ class TIFFFormat {
|
|
|
395
419
|
/**
|
|
396
420
|
* Encode multi-page TIFF
|
|
397
421
|
*/
|
|
398
|
-
encodeFrames(imageData, options) {
|
|
422
|
+
async encodeFrames(imageData, options) {
|
|
399
423
|
const opts = options;
|
|
400
424
|
const compression = opts?.compression ?? "none";
|
|
401
425
|
if (imageData.frames.length === 0) {
|
|
@@ -420,6 +444,12 @@ class TIFFFormat {
|
|
|
420
444
|
const encoder = new tiff_lzw_js_1.TIFFLZWEncoder();
|
|
421
445
|
pixelData = encoder.compress(frame.data);
|
|
422
446
|
}
|
|
447
|
+
else if (compression === "packbits") {
|
|
448
|
+
pixelData = (0, tiff_packbits_js_1.packBitsCompress)(frame.data);
|
|
449
|
+
}
|
|
450
|
+
else if (compression === "deflate") {
|
|
451
|
+
pixelData = await (0, tiff_deflate_js_1.deflateCompress)(frame.data);
|
|
452
|
+
}
|
|
423
453
|
else {
|
|
424
454
|
pixelData = frame.data;
|
|
425
455
|
}
|
|
@@ -456,7 +486,19 @@ class TIFFFormat {
|
|
|
456
486
|
this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
|
|
457
487
|
dataOffset += 8;
|
|
458
488
|
// Compression
|
|
459
|
-
|
|
489
|
+
let compressionCode;
|
|
490
|
+
if (compression === "lzw") {
|
|
491
|
+
compressionCode = 5;
|
|
492
|
+
}
|
|
493
|
+
else if (compression === "packbits") {
|
|
494
|
+
compressionCode = 32773;
|
|
495
|
+
}
|
|
496
|
+
else if (compression === "deflate") {
|
|
497
|
+
compressionCode = 8;
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
compressionCode = 1;
|
|
501
|
+
}
|
|
460
502
|
this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
|
|
461
503
|
// PhotometricInterpretation
|
|
462
504
|
this.writeIFDEntry(result, 0x0106, 3, 1, 2);
|
|
@@ -467,9 +509,19 @@ class TIFFFormat {
|
|
|
467
509
|
// RowsPerStrip
|
|
468
510
|
this.writeIFDEntry(result, 0x0116, 4, 1, frame.height);
|
|
469
511
|
// StripByteCounts
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
512
|
+
let pixelDataSize;
|
|
513
|
+
if (compression === "lzw") {
|
|
514
|
+
pixelDataSize = new tiff_lzw_js_1.TIFFLZWEncoder().compress(frame.data).length;
|
|
515
|
+
}
|
|
516
|
+
else if (compression === "packbits") {
|
|
517
|
+
pixelDataSize = (0, tiff_packbits_js_1.packBitsCompress)(frame.data).length;
|
|
518
|
+
}
|
|
519
|
+
else if (compression === "deflate") {
|
|
520
|
+
pixelDataSize = (await (0, tiff_deflate_js_1.deflateCompress)(frame.data)).length;
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
pixelDataSize = frame.data.length;
|
|
524
|
+
}
|
|
473
525
|
this.writeIFDEntry(result, 0x0117, 4, 1, pixelDataSize);
|
|
474
526
|
// XResolution
|
|
475
527
|
const xResOffset = dataOffset;
|
|
@@ -572,7 +624,7 @@ class TIFFFormat {
|
|
|
572
624
|
const isLittleEndian = data[0] === 0x49;
|
|
573
625
|
// Try pure JavaScript decoder first
|
|
574
626
|
try {
|
|
575
|
-
const pureJSResult = this.decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian);
|
|
627
|
+
const pureJSResult = await this.decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian);
|
|
576
628
|
if (pureJSResult) {
|
|
577
629
|
return pureJSResult;
|
|
578
630
|
}
|
|
@@ -696,7 +748,7 @@ class TIFFFormat {
|
|
|
696
748
|
async decodeUsingRuntime(data, width, height) {
|
|
697
749
|
// Try pure JavaScript decoder first for uncompressed TIFFs
|
|
698
750
|
try {
|
|
699
|
-
const pureJSResult = this.decodePureJS(data, width, height);
|
|
751
|
+
const pureJSResult = await this.decodePureJS(data, width, height);
|
|
700
752
|
if (pureJSResult) {
|
|
701
753
|
return pureJSResult;
|
|
702
754
|
}
|
|
@@ -734,10 +786,10 @@ class TIFFFormat {
|
|
|
734
786
|
return new TextDecoder().decode(data.slice(offset, endIndex));
|
|
735
787
|
}
|
|
736
788
|
/**
|
|
737
|
-
* Pure JavaScript TIFF decoder for uncompressed and
|
|
789
|
+
* Pure JavaScript TIFF decoder for uncompressed, LZW, PackBits, and Deflate-compressed RGB/RGBA images
|
|
738
790
|
* Returns null if the TIFF uses unsupported features
|
|
739
791
|
*/
|
|
740
|
-
decodePureJS(data, width, height) {
|
|
792
|
+
async decodePureJS(data, width, height) {
|
|
741
793
|
// Validate minimum TIFF header size
|
|
742
794
|
if (data.length < 8) {
|
|
743
795
|
return null;
|
|
@@ -748,20 +800,23 @@ class TIFFFormat {
|
|
|
748
800
|
const ifdOffset = this.readUint32(data, 4, isLittleEndian);
|
|
749
801
|
// Check compression
|
|
750
802
|
const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
|
|
751
|
-
if (compression !== 1 && compression !== 5
|
|
752
|
-
|
|
803
|
+
if (compression !== 1 && compression !== 5 && compression !== 8 &&
|
|
804
|
+
compression !== 32773) {
|
|
805
|
+
// Support: 1 = uncompressed, 5 = LZW, 8 = Deflate, 32773 = PackBits
|
|
753
806
|
return null;
|
|
754
807
|
}
|
|
755
808
|
// Check photometric interpretation
|
|
756
809
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
757
|
-
if (photometric !== 0 && photometric !== 1 && photometric !== 2
|
|
758
|
-
|
|
810
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
|
|
811
|
+
photometric !== 5) {
|
|
812
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
|
|
759
813
|
return null;
|
|
760
814
|
}
|
|
761
815
|
// Get samples per pixel
|
|
762
816
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
763
817
|
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
764
818
|
// For RGB, expect 3 or 4 samples per pixel
|
|
819
|
+
// For CMYK, expect 4 samples per pixel
|
|
765
820
|
if (!samplesPerPixel) {
|
|
766
821
|
return null;
|
|
767
822
|
}
|
|
@@ -773,6 +828,10 @@ class TIFFFormat {
|
|
|
773
828
|
// RGB requires 3 or 4 samples per pixel
|
|
774
829
|
return null;
|
|
775
830
|
}
|
|
831
|
+
if (photometric === 5 && samplesPerPixel !== 4) {
|
|
832
|
+
// CMYK requires 4 samples per pixel
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
776
835
|
// Get strip offset
|
|
777
836
|
const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
|
|
778
837
|
if (!stripOffset || stripOffset >= data.length) {
|
|
@@ -791,6 +850,16 @@ class TIFFFormat {
|
|
|
791
850
|
const decoder = new tiff_lzw_js_1.TIFFLZWDecoder(compressedData);
|
|
792
851
|
pixelData = decoder.decompress();
|
|
793
852
|
}
|
|
853
|
+
else if (compression === 32773) {
|
|
854
|
+
// PackBits compressed
|
|
855
|
+
const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
856
|
+
pixelData = (0, tiff_packbits_js_1.packBitsDecompress)(compressedData);
|
|
857
|
+
}
|
|
858
|
+
else if (compression === 8) {
|
|
859
|
+
// Deflate compressed
|
|
860
|
+
const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
861
|
+
pixelData = await (0, tiff_deflate_js_1.deflateDecompress)(compressedData);
|
|
862
|
+
}
|
|
794
863
|
else {
|
|
795
864
|
// Uncompressed
|
|
796
865
|
pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
@@ -820,6 +889,29 @@ class TIFFFormat {
|
|
|
820
889
|
}
|
|
821
890
|
}
|
|
822
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
|
+
}
|
|
823
915
|
else {
|
|
824
916
|
// RGB/RGBA image
|
|
825
917
|
for (let y = 0; y < height; y++) {
|
|
@@ -842,23 +934,26 @@ class TIFFFormat {
|
|
|
842
934
|
* Pure JavaScript TIFF decoder for a specific IFD
|
|
843
935
|
* Returns null if the TIFF uses unsupported features
|
|
844
936
|
*/
|
|
845
|
-
decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian) {
|
|
937
|
+
async decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian) {
|
|
846
938
|
// Check compression
|
|
847
939
|
const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
|
|
848
|
-
if (compression !== 1 && compression !== 5
|
|
849
|
-
|
|
940
|
+
if (compression !== 1 && compression !== 5 && compression !== 8 &&
|
|
941
|
+
compression !== 32773) {
|
|
942
|
+
// Support: 1 = uncompressed, 5 = LZW, 8 = Deflate, 32773 = PackBits
|
|
850
943
|
return null;
|
|
851
944
|
}
|
|
852
945
|
// Check photometric interpretation
|
|
853
946
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
854
|
-
if (photometric !== 0 && photometric !== 1 && photometric !== 2
|
|
855
|
-
|
|
947
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2 &&
|
|
948
|
+
photometric !== 5) {
|
|
949
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB, 5 = CMYK
|
|
856
950
|
return null;
|
|
857
951
|
}
|
|
858
952
|
// Get samples per pixel
|
|
859
953
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
860
954
|
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
861
955
|
// For RGB, expect 3 or 4 samples per pixel
|
|
956
|
+
// For CMYK, expect 4 samples per pixel
|
|
862
957
|
if (!samplesPerPixel) {
|
|
863
958
|
return null;
|
|
864
959
|
}
|
|
@@ -870,6 +965,10 @@ class TIFFFormat {
|
|
|
870
965
|
// RGB requires 3 or 4 samples per pixel
|
|
871
966
|
return null;
|
|
872
967
|
}
|
|
968
|
+
if (photometric === 5 && samplesPerPixel !== 4) {
|
|
969
|
+
// CMYK requires 4 samples per pixel
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
873
972
|
// Get strip offset
|
|
874
973
|
const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
|
|
875
974
|
if (!stripOffset || stripOffset >= data.length) {
|
|
@@ -888,6 +987,16 @@ class TIFFFormat {
|
|
|
888
987
|
const decoder = new tiff_lzw_js_1.TIFFLZWDecoder(compressedData);
|
|
889
988
|
pixelData = decoder.decompress();
|
|
890
989
|
}
|
|
990
|
+
else if (compression === 32773) {
|
|
991
|
+
// PackBits compressed
|
|
992
|
+
const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
993
|
+
pixelData = (0, tiff_packbits_js_1.packBitsDecompress)(compressedData);
|
|
994
|
+
}
|
|
995
|
+
else if (compression === 8) {
|
|
996
|
+
// Deflate compressed
|
|
997
|
+
const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
998
|
+
pixelData = await (0, tiff_deflate_js_1.deflateDecompress)(compressedData);
|
|
999
|
+
}
|
|
891
1000
|
else {
|
|
892
1001
|
// Uncompressed
|
|
893
1002
|
pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
|
|
@@ -917,6 +1026,29 @@ class TIFFFormat {
|
|
|
917
1026
|
}
|
|
918
1027
|
}
|
|
919
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
|
+
}
|
|
920
1052
|
else {
|
|
921
1053
|
// RGB/RGBA image
|
|
922
1054
|
for (let y = 0; y < height; y++) {
|
|
@@ -1006,9 +1138,12 @@ class TIFFFormat {
|
|
|
1006
1138
|
else if (compression === 5) {
|
|
1007
1139
|
metadata.compression = "lzw";
|
|
1008
1140
|
}
|
|
1009
|
-
else if (compression === 7) {
|
|
1141
|
+
else if (compression === 7 || compression === 6) {
|
|
1010
1142
|
metadata.compression = "jpeg";
|
|
1011
1143
|
}
|
|
1144
|
+
else if (compression === 8) {
|
|
1145
|
+
metadata.compression = "deflate";
|
|
1146
|
+
}
|
|
1012
1147
|
else if (compression === 32773) {
|
|
1013
1148
|
metadata.compression = "packbits";
|
|
1014
1149
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, ImageMetadata,
|
|
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?:
|
|
29
|
+
encode(imageData: ImageData, options?: WebPEncoderOptions): Promise<Uint8Array>;
|
|
30
30
|
private readUint24LE;
|
|
31
31
|
private decodeUsingRuntime;
|
|
32
32
|
private parseEXIF;
|
|
@@ -78,7 +78,7 @@ class WebPFormat {
|
|
|
78
78
|
* @param data Raw WebP image data
|
|
79
79
|
* @returns Decoded image data with RGBA pixels
|
|
80
80
|
*/
|
|
81
|
-
async decode(data) {
|
|
81
|
+
async decode(data, settings) {
|
|
82
82
|
if (!this.canDecode(data)) {
|
|
83
83
|
throw new Error("Invalid WebP signature");
|
|
84
84
|
}
|
|
@@ -144,7 +144,7 @@ class WebPFormat {
|
|
|
144
144
|
(0, security_js_1.validateImageDimensions)(width, height);
|
|
145
145
|
// For a pure JS implementation, we'd need to implement full WebP decoding
|
|
146
146
|
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
147
|
-
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
147
|
+
const rgba = await this.decodeUsingRuntime(data, width, height, settings);
|
|
148
148
|
return {
|
|
149
149
|
width,
|
|
150
150
|
height,
|
|
@@ -207,9 +207,10 @@ class WebPFormat {
|
|
|
207
207
|
readUint24LE(data, offset) {
|
|
208
208
|
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
|
|
209
209
|
}
|
|
210
|
-
async decodeUsingRuntime(data, _width, _height) {
|
|
210
|
+
async decodeUsingRuntime(data, _width, _height, settings) {
|
|
211
211
|
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
212
|
-
if (
|
|
212
|
+
if (settings?.runtimeDecoding !== "never" &&
|
|
213
|
+
typeof ImageDecoder !== "undefined") {
|
|
213
214
|
try {
|
|
214
215
|
const decoder = new ImageDecoder({ data, type: "image/webp" });
|
|
215
216
|
const result = await decoder.decode();
|
|
@@ -224,15 +225,17 @@ class WebPFormat {
|
|
|
224
225
|
bitmap.close();
|
|
225
226
|
return new Uint8Array(imageData.data.buffer);
|
|
226
227
|
}
|
|
227
|
-
catch (
|
|
228
|
+
catch (_error) {
|
|
228
229
|
// ImageDecoder API failed, fall through to pure JS decoder
|
|
229
|
-
console.warn("WebP decoding with ImageDecoder failed, using pure JS decoder:", error);
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
|
-
// Fallback to pure JavaScript decoder (VP8L lossless only)
|
|
232
|
+
// Fallback to pure JavaScript decoder (VP8L lossless only) with tolerant mode
|
|
233
233
|
try {
|
|
234
234
|
const { WebPDecoder } = await Promise.resolve().then(() => __importStar(require("../utils/webp_decoder.js")));
|
|
235
|
-
const decoder = new WebPDecoder(data
|
|
235
|
+
const decoder = new WebPDecoder(data, {
|
|
236
|
+
tolerantDecoding: settings?.tolerantDecoding ?? true,
|
|
237
|
+
onWarning: settings?.onWarning,
|
|
238
|
+
});
|
|
236
239
|
const result = decoder.decode();
|
|
237
240
|
return result.data;
|
|
238
241
|
}
|
package/script/src/image.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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/script/src/image.js
CHANGED
|
@@ -161,26 +161,37 @@ class Image {
|
|
|
161
161
|
static getFormats() {
|
|
162
162
|
return Image.formats;
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
164
|
+
static async decode(data, formatOrOptions, options) {
|
|
165
|
+
// Backward-compatible overloads:
|
|
166
|
+
// - decode(data)
|
|
167
|
+
// - decode(data, "jpeg")
|
|
168
|
+
// New:
|
|
169
|
+
// - decode(data, { tolerantDecoding, onWarning, runtimeDecoding })
|
|
170
|
+
// - decode(data, "jpeg", { ...options })
|
|
171
|
+
if (typeof formatOrOptions === "string") {
|
|
172
|
+
return await Image.decodeWithSettings(data, formatOrOptions, options);
|
|
173
|
+
}
|
|
174
|
+
return await Image.decodeWithSettings(data, undefined, formatOrOptions);
|
|
175
|
+
}
|
|
176
|
+
static async decodeWithSettings(data, format, settings) {
|
|
171
177
|
const image = new Image();
|
|
178
|
+
const normalizedSettings = {
|
|
179
|
+
tolerantDecoding: settings?.tolerantDecoding ?? true,
|
|
180
|
+
onWarning: settings?.onWarning,
|
|
181
|
+
runtimeDecoding: settings?.runtimeDecoding ?? "prefer",
|
|
182
|
+
};
|
|
172
183
|
// Try specified format first
|
|
173
184
|
if (format) {
|
|
174
185
|
const handler = Image.formats.find((f) => f.name === format);
|
|
175
186
|
if (handler && handler.canDecode(data)) {
|
|
176
|
-
image.imageData = await handler.decode(data);
|
|
187
|
+
image.imageData = await handler.decode(data, normalizedSettings);
|
|
177
188
|
return image;
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
191
|
// Auto-detect format
|
|
181
192
|
for (const handler of Image.formats) {
|
|
182
193
|
if (handler.canDecode(data)) {
|
|
183
|
-
image.imageData = await handler.decode(data);
|
|
194
|
+
image.imageData = await handler.decode(data, normalizedSettings);
|
|
184
195
|
return image;
|
|
185
196
|
}
|
|
186
197
|
}
|
|
@@ -230,26 +241,31 @@ class Image {
|
|
|
230
241
|
* @returns Image instance
|
|
231
242
|
*/
|
|
232
243
|
static read(data, format) {
|
|
233
|
-
return Image.decode(data, format);
|
|
244
|
+
return format ? Image.decode(data, format) : Image.decode(data);
|
|
234
245
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
static async
|
|
246
|
+
static async decodeFrames(data, formatOrOptions, options) {
|
|
247
|
+
if (typeof formatOrOptions === "string") {
|
|
248
|
+
return await Image.decodeFramesWithSettings(data, formatOrOptions, options);
|
|
249
|
+
}
|
|
250
|
+
return await Image.decodeFramesWithSettings(data, undefined, formatOrOptions);
|
|
251
|
+
}
|
|
252
|
+
static async decodeFramesWithSettings(data, format, settings) {
|
|
253
|
+
const normalizedSettings = {
|
|
254
|
+
tolerantDecoding: settings?.tolerantDecoding ?? true,
|
|
255
|
+
onWarning: settings?.onWarning,
|
|
256
|
+
runtimeDecoding: settings?.runtimeDecoding ?? "prefer",
|
|
257
|
+
};
|
|
242
258
|
// Try specified format first
|
|
243
259
|
if (format) {
|
|
244
260
|
const handler = Image.formats.find((f) => f.name === format);
|
|
245
261
|
if (handler && handler.canDecode(data) && handler.decodeFrames) {
|
|
246
|
-
return await handler.decodeFrames(data);
|
|
262
|
+
return await handler.decodeFrames(data, normalizedSettings);
|
|
247
263
|
}
|
|
248
264
|
}
|
|
249
265
|
// Auto-detect format
|
|
250
266
|
for (const handler of Image.formats) {
|
|
251
267
|
if (handler.canDecode(data) && handler.decodeFrames) {
|
|
252
|
-
return await handler.decodeFrames(data);
|
|
268
|
+
return await handler.decodeFrames(data, normalizedSettings);
|
|
253
269
|
}
|
|
254
270
|
}
|
|
255
271
|
throw new Error("Unsupported or unrecognized multi-frame image format");
|
|
@@ -262,7 +278,7 @@ class Image {
|
|
|
262
278
|
* @returns MultiFrameImageData with all frames
|
|
263
279
|
*/
|
|
264
280
|
static readFrames(data, format) {
|
|
265
|
-
return Image.decodeFrames(data, format);
|
|
281
|
+
return format ? Image.decodeFrames(data, format) : Image.decodeFrames(data);
|
|
266
282
|
}
|
|
267
283
|
/**
|
|
268
284
|
* Encode multi-frame image data to bytes in the specified format
|
|
@@ -851,6 +867,34 @@ class Image {
|
|
|
851
867
|
this.imageData.data = (0, image_processing_js_1.flipVertical)(this.imageData.data, this.imageData.width, this.imageData.height);
|
|
852
868
|
return this;
|
|
853
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
|
+
}
|
|
854
898
|
}
|
|
855
899
|
exports.Image = Image;
|
|
856
900
|
Object.defineProperty(Image, "formats", {
|