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