cross-image 0.2.1 → 0.2.3
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 +160 -32
- package/esm/mod.d.ts +2 -1
- package/esm/mod.js +2 -1
- package/esm/src/formats/jpeg.d.ts +12 -1
- package/esm/src/formats/jpeg.js +633 -4
- package/esm/src/formats/png_base.d.ts +8 -0
- package/esm/src/formats/png_base.js +176 -3
- package/esm/src/formats/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +10 -1
- package/esm/src/formats/tiff.js +194 -44
- package/esm/src/formats/webp.d.ts +9 -2
- package/esm/src/formats/webp.js +211 -62
- package/esm/src/image.d.ts +81 -0
- package/esm/src/image.js +282 -5
- package/esm/src/types.d.ts +41 -1
- package/esm/src/utils/image_processing.d.ts +98 -0
- package/esm/src/utils/image_processing.js +440 -0
- package/esm/src/utils/metadata/xmp.d.ts +52 -0
- package/esm/src/utils/metadata/xmp.js +325 -0
- package/esm/src/utils/resize.d.ts +4 -0
- package/esm/src/utils/resize.js +74 -0
- package/package.json +1 -1
- package/script/mod.d.ts +2 -1
- package/script/mod.js +4 -2
- package/script/src/formats/jpeg.d.ts +12 -1
- package/script/src/formats/jpeg.js +633 -4
- package/script/src/formats/png_base.d.ts +8 -0
- package/script/src/formats/png_base.js +176 -3
- package/script/src/formats/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +10 -1
- package/script/src/formats/tiff.js +194 -44
- package/script/src/formats/webp.d.ts +9 -2
- package/script/src/formats/webp.js +211 -62
- package/script/src/image.d.ts +81 -0
- package/script/src/image.js +280 -3
- package/script/src/types.d.ts +41 -1
- package/script/src/utils/image_processing.d.ts +98 -0
- package/script/src/utils/image_processing.js +451 -0
- package/script/src/utils/metadata/xmp.d.ts +52 -0
- package/script/src/utils/metadata/xmp.js +333 -0
- package/script/src/utils/resize.d.ts +4 -0
- package/script/src/utils/resize.js +75 -0
|
@@ -135,18 +135,49 @@ class TIFFFormat {
|
|
|
135
135
|
const { width, height, data, metadata } = imageData;
|
|
136
136
|
const opts = options;
|
|
137
137
|
const compression = opts?.compression ?? "none";
|
|
138
|
+
const grayscale = opts?.grayscale ?? false;
|
|
139
|
+
const rgb = opts?.rgb ?? false;
|
|
140
|
+
// Convert RGBA to grayscale if requested
|
|
141
|
+
let sourceData;
|
|
142
|
+
let samplesPerPixel;
|
|
143
|
+
if (grayscale) {
|
|
144
|
+
sourceData = new Uint8Array(width * height);
|
|
145
|
+
samplesPerPixel = 1;
|
|
146
|
+
for (let i = 0; i < width * height; i++) {
|
|
147
|
+
const r = data[i * 4];
|
|
148
|
+
const g = data[i * 4 + 1];
|
|
149
|
+
const b = data[i * 4 + 2];
|
|
150
|
+
// Use standard luminance formula
|
|
151
|
+
sourceData[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (rgb) {
|
|
155
|
+
// Convert RGBA to RGB (strip alpha channel)
|
|
156
|
+
sourceData = new Uint8Array(width * height * 3);
|
|
157
|
+
samplesPerPixel = 3;
|
|
158
|
+
for (let i = 0; i < width * height; i++) {
|
|
159
|
+
sourceData[i * 3] = data[i * 4]; // R
|
|
160
|
+
sourceData[i * 3 + 1] = data[i * 4 + 1]; // G
|
|
161
|
+
sourceData[i * 3 + 2] = data[i * 4 + 2]; // B
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// Keep as RGBA
|
|
166
|
+
sourceData = data;
|
|
167
|
+
samplesPerPixel = 4;
|
|
168
|
+
}
|
|
138
169
|
// Prepare pixel data (compress if needed)
|
|
139
170
|
let pixelData;
|
|
140
171
|
let compressionCode;
|
|
141
172
|
if (compression === "lzw") {
|
|
142
173
|
// LZW compress the pixel data
|
|
143
174
|
const encoder = new tiff_lzw_js_1.TIFFLZWEncoder();
|
|
144
|
-
pixelData = encoder.compress(
|
|
175
|
+
pixelData = encoder.compress(sourceData);
|
|
145
176
|
compressionCode = 5;
|
|
146
177
|
}
|
|
147
178
|
else {
|
|
148
179
|
// Uncompressed
|
|
149
|
-
pixelData =
|
|
180
|
+
pixelData = sourceData;
|
|
150
181
|
compressionCode = 1;
|
|
151
182
|
}
|
|
152
183
|
const result = [];
|
|
@@ -164,7 +195,10 @@ class TIFFFormat {
|
|
|
164
195
|
// IFD (Image File Directory)
|
|
165
196
|
const ifdStart = result.length;
|
|
166
197
|
// Count number of entries (including metadata)
|
|
167
|
-
|
|
198
|
+
// Grayscale: 10 entries (no ExtraSamples)
|
|
199
|
+
// RGB: 11 entries (no ExtraSamples)
|
|
200
|
+
// RGBA: 12 entries (includes ExtraSamples)
|
|
201
|
+
let numEntries = grayscale ? 10 : (rgb ? 11 : 12);
|
|
168
202
|
if (metadata?.description)
|
|
169
203
|
numEntries++;
|
|
170
204
|
if (metadata?.author)
|
|
@@ -182,16 +216,28 @@ class TIFFFormat {
|
|
|
182
216
|
// ImageHeight (0x0101)
|
|
183
217
|
this.writeIFDEntry(result, 0x0101, 4, 1, height);
|
|
184
218
|
// BitsPerSample (0x0102) - 8 bits per channel
|
|
185
|
-
|
|
186
|
-
|
|
219
|
+
if (grayscale) {
|
|
220
|
+
// Single value for grayscale
|
|
221
|
+
this.writeIFDEntry(result, 0x0102, 3, 1, 8);
|
|
222
|
+
}
|
|
223
|
+
else if (rgb) {
|
|
224
|
+
// 3 values for RGB
|
|
225
|
+
this.writeIFDEntry(result, 0x0102, 3, 3, dataOffset);
|
|
226
|
+
dataOffset += 6; // 3 x 2-byte values
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// 4 values for RGBA
|
|
230
|
+
this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
|
|
231
|
+
dataOffset += 8; // 4 x 2-byte values
|
|
232
|
+
}
|
|
187
233
|
// Compression (0x0103) - 1 = uncompressed, 5 = LZW
|
|
188
234
|
this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
|
|
189
|
-
// PhotometricInterpretation (0x0106) - 2 = RGB
|
|
190
|
-
this.writeIFDEntry(result, 0x0106, 3, 1, 2);
|
|
235
|
+
// PhotometricInterpretation (0x0106) - 1 = BlackIsZero (grayscale), 2 = RGB
|
|
236
|
+
this.writeIFDEntry(result, 0x0106, 3, 1, grayscale ? 1 : 2);
|
|
191
237
|
// StripOffsets (0x0111)
|
|
192
238
|
this.writeIFDEntry(result, 0x0111, 4, 1, 8);
|
|
193
|
-
// SamplesPerPixel (0x0115) - 4
|
|
194
|
-
this.writeIFDEntry(result, 0x0115, 3, 1,
|
|
239
|
+
// SamplesPerPixel (0x0115) - 1 for grayscale, 3 for RGB, 4 for RGBA
|
|
240
|
+
this.writeIFDEntry(result, 0x0115, 3, 1, samplesPerPixel);
|
|
195
241
|
// RowsPerStrip (0x0116)
|
|
196
242
|
this.writeIFDEntry(result, 0x0116, 4, 1, height);
|
|
197
243
|
// StripByteCounts (0x0117)
|
|
@@ -204,8 +250,10 @@ class TIFFFormat {
|
|
|
204
250
|
const yResOffset = dataOffset;
|
|
205
251
|
this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
|
|
206
252
|
dataOffset += 8;
|
|
207
|
-
// ExtraSamples (0x0152) - 2 = unassociated alpha
|
|
208
|
-
|
|
253
|
+
// ExtraSamples (0x0152) - 2 = unassociated alpha (only for RGBA)
|
|
254
|
+
if (!grayscale && !rgb) {
|
|
255
|
+
this.writeIFDEntry(result, 0x0152, 3, 1, 2);
|
|
256
|
+
}
|
|
209
257
|
// Optional metadata entries
|
|
210
258
|
if (metadata?.description) {
|
|
211
259
|
const descBytes = new TextEncoder().encode(metadata.description + "\0");
|
|
@@ -232,11 +280,18 @@ class TIFFFormat {
|
|
|
232
280
|
// Next IFD offset (0 = no more IFDs)
|
|
233
281
|
this.writeUint32LE(result, 0);
|
|
234
282
|
// Write variable-length data
|
|
235
|
-
// BitsPerSample values (
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
// BitsPerSample values (only for RGB and RGBA, not for grayscale)
|
|
284
|
+
if (rgb) {
|
|
285
|
+
this.writeUint16LE(result, 8);
|
|
286
|
+
this.writeUint16LE(result, 8);
|
|
287
|
+
this.writeUint16LE(result, 8);
|
|
288
|
+
}
|
|
289
|
+
else if (!grayscale) {
|
|
290
|
+
this.writeUint16LE(result, 8);
|
|
291
|
+
this.writeUint16LE(result, 8);
|
|
292
|
+
this.writeUint16LE(result, 8);
|
|
293
|
+
this.writeUint16LE(result, 8);
|
|
294
|
+
}
|
|
240
295
|
// XResolution value (rational)
|
|
241
296
|
const dpiX = metadata?.dpiX ?? DEFAULT_DPI;
|
|
242
297
|
this.writeUint32LE(result, dpiX);
|
|
@@ -699,14 +754,23 @@ class TIFFFormat {
|
|
|
699
754
|
}
|
|
700
755
|
// Check photometric interpretation
|
|
701
756
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
702
|
-
if (photometric !== 2) {
|
|
703
|
-
//
|
|
757
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
|
|
758
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
|
|
704
759
|
return null;
|
|
705
760
|
}
|
|
706
761
|
// Get samples per pixel
|
|
707
762
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
708
|
-
|
|
709
|
-
|
|
763
|
+
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
764
|
+
// For RGB, expect 3 or 4 samples per pixel
|
|
765
|
+
if (!samplesPerPixel) {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
if ((photometric === 0 || photometric === 1) && samplesPerPixel !== 1) {
|
|
769
|
+
// Grayscale requires 1 sample per pixel
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
if (photometric === 2 && samplesPerPixel !== 3 && samplesPerPixel !== 4) {
|
|
773
|
+
// RGB requires 3 or 4 samples per pixel
|
|
710
774
|
return null;
|
|
711
775
|
}
|
|
712
776
|
// Get strip offset
|
|
@@ -736,17 +800,40 @@ class TIFFFormat {
|
|
|
736
800
|
// Convert to RGBA
|
|
737
801
|
const rgba = new Uint8Array(width * height * 4);
|
|
738
802
|
let srcPos = 0;
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
803
|
+
if (photometric === 0 || photometric === 1) {
|
|
804
|
+
// Grayscale image
|
|
805
|
+
for (let y = 0; y < height; y++) {
|
|
806
|
+
for (let x = 0; x < width; x++) {
|
|
807
|
+
const dstIdx = (y * width + x) * 4;
|
|
808
|
+
if (srcPos >= pixelData.length) {
|
|
809
|
+
return null; // Not enough data
|
|
810
|
+
}
|
|
811
|
+
let gray = pixelData[srcPos++];
|
|
812
|
+
// WhiteIsZero (0) means 0=white, 255=black, so invert
|
|
813
|
+
if (photometric === 0) {
|
|
814
|
+
gray = 255 - gray;
|
|
815
|
+
}
|
|
816
|
+
rgba[dstIdx] = gray; // R
|
|
817
|
+
rgba[dstIdx + 1] = gray; // G
|
|
818
|
+
rgba[dstIdx + 2] = gray; // B
|
|
819
|
+
rgba[dstIdx + 3] = 255; // A (opaque)
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else {
|
|
824
|
+
// RGB/RGBA image
|
|
825
|
+
for (let y = 0; y < height; y++) {
|
|
826
|
+
for (let x = 0; x < width; x++) {
|
|
827
|
+
const dstIdx = (y * width + x) * 4;
|
|
828
|
+
if (srcPos + samplesPerPixel > pixelData.length) {
|
|
829
|
+
return null; // Not enough data
|
|
830
|
+
}
|
|
831
|
+
// TIFF stores RGB(A) in order
|
|
832
|
+
rgba[dstIdx] = pixelData[srcPos++]; // R
|
|
833
|
+
rgba[dstIdx + 1] = pixelData[srcPos++]; // G
|
|
834
|
+
rgba[dstIdx + 2] = pixelData[srcPos++]; // B
|
|
835
|
+
rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
|
|
744
836
|
}
|
|
745
|
-
// TIFF stores RGB(A) in order
|
|
746
|
-
rgba[dstIdx] = pixelData[srcPos++]; // R
|
|
747
|
-
rgba[dstIdx + 1] = pixelData[srcPos++]; // G
|
|
748
|
-
rgba[dstIdx + 2] = pixelData[srcPos++]; // B
|
|
749
|
-
rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
|
|
750
837
|
}
|
|
751
838
|
}
|
|
752
839
|
return rgba;
|
|
@@ -764,14 +851,23 @@ class TIFFFormat {
|
|
|
764
851
|
}
|
|
765
852
|
// Check photometric interpretation
|
|
766
853
|
const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
|
|
767
|
-
if (photometric !== 2) {
|
|
768
|
-
//
|
|
854
|
+
if (photometric !== 0 && photometric !== 1 && photometric !== 2) {
|
|
855
|
+
// Support: 0 = WhiteIsZero, 1 = BlackIsZero, 2 = RGB
|
|
769
856
|
return null;
|
|
770
857
|
}
|
|
771
858
|
// Get samples per pixel
|
|
772
859
|
const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
|
|
773
|
-
|
|
774
|
-
|
|
860
|
+
// For grayscale (photometric 0 or 1), expect 1 sample per pixel
|
|
861
|
+
// For RGB, expect 3 or 4 samples per pixel
|
|
862
|
+
if (!samplesPerPixel) {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
if ((photometric === 0 || photometric === 1) && samplesPerPixel !== 1) {
|
|
866
|
+
// Grayscale requires 1 sample per pixel
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
if (photometric === 2 && samplesPerPixel !== 3 && samplesPerPixel !== 4) {
|
|
870
|
+
// RGB requires 3 or 4 samples per pixel
|
|
775
871
|
return null;
|
|
776
872
|
}
|
|
777
873
|
// Get strip offset
|
|
@@ -801,20 +897,74 @@ class TIFFFormat {
|
|
|
801
897
|
// Convert to RGBA
|
|
802
898
|
const rgba = new Uint8Array(width * height * 4);
|
|
803
899
|
let srcPos = 0;
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
900
|
+
if (photometric === 0 || photometric === 1) {
|
|
901
|
+
// Grayscale image
|
|
902
|
+
for (let y = 0; y < height; y++) {
|
|
903
|
+
for (let x = 0; x < width; x++) {
|
|
904
|
+
const dstIdx = (y * width + x) * 4;
|
|
905
|
+
if (srcPos >= pixelData.length) {
|
|
906
|
+
return null; // Not enough data
|
|
907
|
+
}
|
|
908
|
+
let gray = pixelData[srcPos++];
|
|
909
|
+
// WhiteIsZero (0) means 0=white, 255=black, so invert
|
|
910
|
+
if (photometric === 0) {
|
|
911
|
+
gray = 255 - gray;
|
|
912
|
+
}
|
|
913
|
+
rgba[dstIdx] = gray; // R
|
|
914
|
+
rgba[dstIdx + 1] = gray; // G
|
|
915
|
+
rgba[dstIdx + 2] = gray; // B
|
|
916
|
+
rgba[dstIdx + 3] = 255; // A (opaque)
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
// RGB/RGBA image
|
|
922
|
+
for (let y = 0; y < height; y++) {
|
|
923
|
+
for (let x = 0; x < width; x++) {
|
|
924
|
+
const dstIdx = (y * width + x) * 4;
|
|
925
|
+
if (srcPos + samplesPerPixel > pixelData.length) {
|
|
926
|
+
return null; // Not enough data
|
|
927
|
+
}
|
|
928
|
+
// TIFF stores RGB(A) in order
|
|
929
|
+
rgba[dstIdx] = pixelData[srcPos++]; // R
|
|
930
|
+
rgba[dstIdx + 1] = pixelData[srcPos++]; // G
|
|
931
|
+
rgba[dstIdx + 2] = pixelData[srcPos++]; // B
|
|
932
|
+
rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
|
|
809
933
|
}
|
|
810
|
-
// TIFF stores RGB(A) in order
|
|
811
|
-
rgba[dstIdx] = pixelData[srcPos++]; // R
|
|
812
|
-
rgba[dstIdx + 1] = pixelData[srcPos++]; // G
|
|
813
|
-
rgba[dstIdx + 2] = pixelData[srcPos++]; // B
|
|
814
|
-
rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
|
|
815
934
|
}
|
|
816
935
|
}
|
|
817
936
|
return rgba;
|
|
818
937
|
}
|
|
938
|
+
/**
|
|
939
|
+
* Get list of metadata fields supported by TIFF format
|
|
940
|
+
* TIFF supports extensive EXIF metadata including GPS and InteropIFD
|
|
941
|
+
*/
|
|
942
|
+
getSupportedMetadata() {
|
|
943
|
+
return [
|
|
944
|
+
"creationDate",
|
|
945
|
+
"description",
|
|
946
|
+
"author",
|
|
947
|
+
"copyright",
|
|
948
|
+
"cameraMake",
|
|
949
|
+
"cameraModel",
|
|
950
|
+
"orientation",
|
|
951
|
+
"software",
|
|
952
|
+
"iso",
|
|
953
|
+
"exposureTime",
|
|
954
|
+
"fNumber",
|
|
955
|
+
"focalLength",
|
|
956
|
+
"flash",
|
|
957
|
+
"whiteBalance",
|
|
958
|
+
"lensMake",
|
|
959
|
+
"lensModel",
|
|
960
|
+
"userComment",
|
|
961
|
+
"latitude",
|
|
962
|
+
"longitude",
|
|
963
|
+
"dpiX",
|
|
964
|
+
"dpiY",
|
|
965
|
+
"physicalWidth",
|
|
966
|
+
"physicalHeight",
|
|
967
|
+
];
|
|
968
|
+
}
|
|
819
969
|
}
|
|
820
970
|
exports.TIFFFormat = TIFFFormat;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, WebPEncodeOptions } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata, WebPEncodeOptions } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* WebP format handler
|
|
4
4
|
* Implements a basic WebP decoder and encoder
|
|
@@ -30,10 +30,17 @@ export declare class WebPFormat implements ImageFormat {
|
|
|
30
30
|
private readUint24LE;
|
|
31
31
|
private decodeUsingRuntime;
|
|
32
32
|
private parseEXIF;
|
|
33
|
+
private parseGPSIFD;
|
|
34
|
+
private readRational;
|
|
33
35
|
private parseXMP;
|
|
34
36
|
private injectMetadata;
|
|
35
37
|
private createEXIFChunk;
|
|
38
|
+
private createGPSIFD;
|
|
39
|
+
private writeRational;
|
|
36
40
|
private createXMPChunk;
|
|
37
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Get the list of metadata fields supported by WebP format
|
|
43
|
+
*/
|
|
44
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
38
45
|
}
|
|
39
46
|
//# sourceMappingURL=webp.d.ts.map
|