cross-image 0.2.2 → 0.2.4
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 +333 -168
- package/esm/mod.d.ts +2 -0
- package/esm/mod.js +2 -0
- package/esm/src/formats/apng.d.ts +13 -1
- package/esm/src/formats/apng.js +97 -0
- package/esm/src/formats/ascii.d.ts +11 -1
- package/esm/src/formats/ascii.js +24 -0
- package/esm/src/formats/avif.d.ts +96 -0
- package/esm/src/formats/avif.js +607 -0
- package/esm/src/formats/bmp.d.ts +11 -1
- package/esm/src/formats/bmp.js +73 -0
- package/esm/src/formats/dng.d.ts +13 -1
- package/esm/src/formats/dng.js +26 -4
- package/esm/src/formats/gif.d.ts +15 -2
- package/esm/src/formats/gif.js +146 -4
- package/esm/src/formats/heic.d.ts +96 -0
- package/esm/src/formats/heic.js +608 -0
- package/esm/src/formats/ico.d.ts +11 -1
- package/esm/src/formats/ico.js +28 -0
- package/esm/src/formats/jpeg.d.ts +19 -1
- package/esm/src/formats/jpeg.js +709 -4
- package/esm/src/formats/pam.d.ts +11 -1
- package/esm/src/formats/pam.js +66 -0
- package/esm/src/formats/pcx.d.ts +11 -1
- package/esm/src/formats/pcx.js +45 -0
- package/esm/src/formats/png.d.ts +13 -1
- package/esm/src/formats/png.js +87 -0
- 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 +11 -1
- package/esm/src/formats/ppm.js +34 -0
- package/esm/src/formats/tiff.d.ts +13 -1
- package/esm/src/formats/tiff.js +165 -0
- package/esm/src/formats/webp.d.ts +16 -2
- package/esm/src/formats/webp.js +303 -62
- package/esm/src/image.d.ts +60 -0
- package/esm/src/image.js +253 -5
- package/esm/src/types.d.ts +59 -1
- package/esm/src/utils/image_processing.d.ts +55 -0
- package/esm/src/utils/image_processing.js +210 -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 +18 -1
- package/script/mod.d.ts +2 -0
- package/script/mod.js +5 -1
- package/script/src/formats/apng.d.ts +13 -1
- package/script/src/formats/apng.js +97 -0
- package/script/src/formats/ascii.d.ts +11 -1
- package/script/src/formats/ascii.js +24 -0
- package/script/src/formats/avif.d.ts +96 -0
- package/script/src/formats/avif.js +611 -0
- package/script/src/formats/bmp.d.ts +11 -1
- package/script/src/formats/bmp.js +73 -0
- package/script/src/formats/dng.d.ts +13 -1
- package/script/src/formats/dng.js +26 -4
- package/script/src/formats/gif.d.ts +15 -2
- package/script/src/formats/gif.js +146 -4
- package/script/src/formats/heic.d.ts +96 -0
- package/script/src/formats/heic.js +612 -0
- package/script/src/formats/ico.d.ts +11 -1
- package/script/src/formats/ico.js +28 -0
- package/script/src/formats/jpeg.d.ts +19 -1
- package/script/src/formats/jpeg.js +709 -4
- package/script/src/formats/pam.d.ts +11 -1
- package/script/src/formats/pam.js +66 -0
- package/script/src/formats/pcx.d.ts +11 -1
- package/script/src/formats/pcx.js +45 -0
- package/script/src/formats/png.d.ts +13 -1
- package/script/src/formats/png.js +87 -0
- 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 +11 -1
- package/script/src/formats/ppm.js +34 -0
- package/script/src/formats/tiff.d.ts +13 -1
- package/script/src/formats/tiff.js +165 -0
- package/script/src/formats/webp.d.ts +16 -2
- package/script/src/formats/webp.js +303 -62
- package/script/src/image.d.ts +60 -0
- package/script/src/image.js +251 -3
- package/script/src/types.d.ts +59 -1
- package/script/src/utils/image_processing.d.ts +55 -0
- package/script/src/utils/image_processing.js +216 -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
package/esm/src/formats/pam.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PAM format handler
|
|
4
4
|
* Implements the Netpbm PAM (Portable Arbitrary Map) format.
|
|
@@ -39,5 +39,15 @@ export declare class PAMFormat implements ImageFormat {
|
|
|
39
39
|
* @returns Encoded PAM image bytes
|
|
40
40
|
*/
|
|
41
41
|
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
42
|
+
/**
|
|
43
|
+
* Get the list of metadata fields supported by PAM format
|
|
44
|
+
*/
|
|
45
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
46
|
+
/**
|
|
47
|
+
* Extract metadata from PAM data without fully decoding the pixel data
|
|
48
|
+
* @param data Raw PAM data
|
|
49
|
+
* @returns Extracted metadata or undefined
|
|
50
|
+
*/
|
|
51
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
42
52
|
}
|
|
43
53
|
//# sourceMappingURL=pam.d.ts.map
|
package/esm/src/formats/pam.js
CHANGED
|
@@ -174,4 +174,70 @@ export class PAMFormat {
|
|
|
174
174
|
output.set(data, headerBytes.length);
|
|
175
175
|
return Promise.resolve(output);
|
|
176
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Get the list of metadata fields supported by PAM format
|
|
179
|
+
*/
|
|
180
|
+
getSupportedMetadata() {
|
|
181
|
+
return []; // PAM format doesn't support metadata preservation
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Extract metadata from PAM data without fully decoding the pixel data
|
|
185
|
+
* @param data Raw PAM data
|
|
186
|
+
* @returns Extracted metadata or undefined
|
|
187
|
+
*/
|
|
188
|
+
extractMetadata(data) {
|
|
189
|
+
if (!this.canDecode(data)) {
|
|
190
|
+
return Promise.resolve(undefined);
|
|
191
|
+
}
|
|
192
|
+
const metadata = {
|
|
193
|
+
format: "pam",
|
|
194
|
+
compression: "none",
|
|
195
|
+
frameCount: 1,
|
|
196
|
+
bitDepth: 8, // PAM typically uses 8 bits per channel
|
|
197
|
+
colorType: "rgba",
|
|
198
|
+
};
|
|
199
|
+
// Try to parse the header to get actual color type
|
|
200
|
+
const decoder = new TextDecoder();
|
|
201
|
+
const headerText = decoder.decode(data.slice(0, Math.min(200, data.length)));
|
|
202
|
+
// Look for TUPLTYPE to determine color type
|
|
203
|
+
const tupltypeMatch = headerText.match(/TUPLTYPE\s+(\S+)/);
|
|
204
|
+
if (tupltypeMatch) {
|
|
205
|
+
const tupltype = tupltypeMatch[1];
|
|
206
|
+
if (tupltype === "GRAYSCALE") {
|
|
207
|
+
metadata.colorType = "grayscale";
|
|
208
|
+
}
|
|
209
|
+
else if (tupltype === "RGB") {
|
|
210
|
+
metadata.colorType = "rgb";
|
|
211
|
+
}
|
|
212
|
+
else if (tupltype === "RGB_ALPHA") {
|
|
213
|
+
metadata.colorType = "rgba";
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Look for DEPTH to determine number of channels
|
|
217
|
+
const depthMatch = headerText.match(/DEPTH\s+(\d+)/);
|
|
218
|
+
if (depthMatch) {
|
|
219
|
+
const depth = parseInt(depthMatch[1]);
|
|
220
|
+
if (depth === 1) {
|
|
221
|
+
metadata.colorType = "grayscale";
|
|
222
|
+
}
|
|
223
|
+
else if (depth === 3) {
|
|
224
|
+
metadata.colorType = "rgb";
|
|
225
|
+
}
|
|
226
|
+
else if (depth === 4) {
|
|
227
|
+
metadata.colorType = "rgba";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Look for MAXVAL to determine bit depth
|
|
231
|
+
const maxvalMatch = headerText.match(/MAXVAL\s+(\d+)/);
|
|
232
|
+
if (maxvalMatch) {
|
|
233
|
+
const maxval = parseInt(maxvalMatch[1]);
|
|
234
|
+
if (maxval === 255) {
|
|
235
|
+
metadata.bitDepth = 8;
|
|
236
|
+
}
|
|
237
|
+
else if (maxval === 65535) {
|
|
238
|
+
metadata.bitDepth = 16;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return Promise.resolve(metadata);
|
|
242
|
+
}
|
|
177
243
|
}
|
package/esm/src/formats/pcx.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PCX format handler
|
|
4
4
|
* Implements PCX decoder and encoder
|
|
@@ -9,5 +9,15 @@ export declare class PCXFormat implements ImageFormat {
|
|
|
9
9
|
canDecode(data: Uint8Array): boolean;
|
|
10
10
|
decode(data: Uint8Array): Promise<ImageData>;
|
|
11
11
|
encode(image: ImageData): Promise<Uint8Array>;
|
|
12
|
+
/**
|
|
13
|
+
* Get the list of metadata fields supported by PCX format
|
|
14
|
+
*/
|
|
15
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
16
|
+
/**
|
|
17
|
+
* Extract metadata from PCX data without fully decoding the pixel data
|
|
18
|
+
* @param data Raw PCX data
|
|
19
|
+
* @returns Extracted metadata or undefined
|
|
20
|
+
*/
|
|
21
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
12
22
|
}
|
|
13
23
|
//# sourceMappingURL=pcx.d.ts.map
|
package/esm/src/formats/pcx.js
CHANGED
|
@@ -201,4 +201,49 @@ export class PCXFormat {
|
|
|
201
201
|
result.set(rleData, header.length);
|
|
202
202
|
return Promise.resolve(result);
|
|
203
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the list of metadata fields supported by PCX format
|
|
206
|
+
*/
|
|
207
|
+
getSupportedMetadata() {
|
|
208
|
+
return [
|
|
209
|
+
"dpiX", // DPI in header
|
|
210
|
+
"dpiY", // DPI in header
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Extract metadata from PCX data without fully decoding the pixel data
|
|
215
|
+
* @param data Raw PCX data
|
|
216
|
+
* @returns Extracted metadata or undefined
|
|
217
|
+
*/
|
|
218
|
+
extractMetadata(data) {
|
|
219
|
+
if (!this.canDecode(data)) {
|
|
220
|
+
return Promise.resolve(undefined);
|
|
221
|
+
}
|
|
222
|
+
const metadata = {
|
|
223
|
+
format: "pcx",
|
|
224
|
+
compression: "rle",
|
|
225
|
+
frameCount: 1,
|
|
226
|
+
bitDepth: data[3], // Bits per pixel per plane
|
|
227
|
+
colorType: "rgb",
|
|
228
|
+
};
|
|
229
|
+
// Check number of planes to determine color type
|
|
230
|
+
const numPlanes = data[65];
|
|
231
|
+
if (numPlanes === 1) {
|
|
232
|
+
metadata.colorType = "indexed";
|
|
233
|
+
}
|
|
234
|
+
else if (numPlanes === 3) {
|
|
235
|
+
metadata.colorType = "rgb";
|
|
236
|
+
}
|
|
237
|
+
else if (numPlanes === 4) {
|
|
238
|
+
metadata.colorType = "rgba";
|
|
239
|
+
}
|
|
240
|
+
// DPI information
|
|
241
|
+
const dpiX = data[12] | (data[13] << 8);
|
|
242
|
+
const dpiY = data[14] | (data[15] << 8);
|
|
243
|
+
if (dpiX > 0)
|
|
244
|
+
metadata.dpiX = dpiX;
|
|
245
|
+
if (dpiY > 0)
|
|
246
|
+
metadata.dpiY = dpiY;
|
|
247
|
+
return Promise.resolve(metadata);
|
|
248
|
+
}
|
|
204
249
|
}
|
package/esm/src/formats/png.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
import { PNGBase } from "./png_base.js";
|
|
3
3
|
/**
|
|
4
4
|
* PNG format handler
|
|
@@ -27,5 +27,17 @@ export declare class PNGFormat extends PNGBase implements ImageFormat {
|
|
|
27
27
|
* @returns Encoded PNG image bytes
|
|
28
28
|
*/
|
|
29
29
|
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the list of metadata fields supported by PNG format
|
|
32
|
+
* Delegates to PNGBase implementation
|
|
33
|
+
*/
|
|
34
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
35
|
+
/**
|
|
36
|
+
* Extract metadata from PNG data without fully decoding the pixel data
|
|
37
|
+
* This quickly parses PNG chunks to extract metadata
|
|
38
|
+
* @param data Raw PNG data
|
|
39
|
+
* @returns Extracted metadata or undefined
|
|
40
|
+
*/
|
|
41
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
30
42
|
}
|
|
31
43
|
//# sourceMappingURL=png.d.ts.map
|
package/esm/src/formats/png.js
CHANGED
|
@@ -137,4 +137,91 @@ export class PNGFormat extends PNGBase {
|
|
|
137
137
|
// Concatenate all chunks
|
|
138
138
|
return this.concatenateArrays(chunks);
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Get the list of metadata fields supported by PNG format
|
|
142
|
+
* Delegates to PNGBase implementation
|
|
143
|
+
*/
|
|
144
|
+
getSupportedMetadata() {
|
|
145
|
+
return super.getSupportedMetadata();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract metadata from PNG data without fully decoding the pixel data
|
|
149
|
+
* This quickly parses PNG chunks to extract metadata
|
|
150
|
+
* @param data Raw PNG data
|
|
151
|
+
* @returns Extracted metadata or undefined
|
|
152
|
+
*/
|
|
153
|
+
extractMetadata(data) {
|
|
154
|
+
if (!this.canDecode(data)) {
|
|
155
|
+
return Promise.resolve(undefined);
|
|
156
|
+
}
|
|
157
|
+
let pos = 8; // Skip PNG signature
|
|
158
|
+
let width = 0;
|
|
159
|
+
let height = 0;
|
|
160
|
+
const metadata = {
|
|
161
|
+
format: "png",
|
|
162
|
+
compression: "deflate",
|
|
163
|
+
frameCount: 1,
|
|
164
|
+
};
|
|
165
|
+
// Parse chunks for metadata only
|
|
166
|
+
while (pos < data.length) {
|
|
167
|
+
if (pos + 8 > data.length)
|
|
168
|
+
break;
|
|
169
|
+
const length = this.readUint32(data, pos);
|
|
170
|
+
pos += 4;
|
|
171
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
172
|
+
pos += 4;
|
|
173
|
+
if (pos + length + 4 > data.length)
|
|
174
|
+
break;
|
|
175
|
+
const chunkData = data.slice(pos, pos + length);
|
|
176
|
+
pos += length;
|
|
177
|
+
pos += 4; // Skip CRC
|
|
178
|
+
if (type === "IHDR") {
|
|
179
|
+
width = this.readUint32(chunkData, 0);
|
|
180
|
+
height = this.readUint32(chunkData, 4);
|
|
181
|
+
// Parse bit depth and color type from IHDR
|
|
182
|
+
if (chunkData.length >= 9) {
|
|
183
|
+
metadata.bitDepth = chunkData[8];
|
|
184
|
+
const colorTypeCode = chunkData[9];
|
|
185
|
+
// PNG color types: 0=grayscale, 2=rgb, 3=indexed, 4=grayscale+alpha, 6=rgba
|
|
186
|
+
switch (colorTypeCode) {
|
|
187
|
+
case 0:
|
|
188
|
+
metadata.colorType = "grayscale";
|
|
189
|
+
break;
|
|
190
|
+
case 2:
|
|
191
|
+
metadata.colorType = "rgb";
|
|
192
|
+
break;
|
|
193
|
+
case 3:
|
|
194
|
+
metadata.colorType = "indexed";
|
|
195
|
+
break;
|
|
196
|
+
case 4:
|
|
197
|
+
metadata.colorType = "grayscale-alpha";
|
|
198
|
+
break;
|
|
199
|
+
case 6:
|
|
200
|
+
metadata.colorType = "rgba";
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (type === "pHYs") {
|
|
206
|
+
// Physical pixel dimensions
|
|
207
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
208
|
+
}
|
|
209
|
+
else if (type === "tEXt") {
|
|
210
|
+
// Text chunk
|
|
211
|
+
this.parseTextChunk(chunkData, metadata);
|
|
212
|
+
}
|
|
213
|
+
else if (type === "iTXt") {
|
|
214
|
+
// International text chunk
|
|
215
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
216
|
+
}
|
|
217
|
+
else if (type === "eXIf") {
|
|
218
|
+
// EXIF chunk
|
|
219
|
+
this.parseExifChunk(chunkData, metadata);
|
|
220
|
+
}
|
|
221
|
+
else if (type === "IEND") {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
226
|
+
}
|
|
140
227
|
}
|
|
@@ -76,6 +76,8 @@ export declare abstract class PNGBase {
|
|
|
76
76
|
* Parse eXIf (EXIF) chunk
|
|
77
77
|
*/
|
|
78
78
|
protected parseExifChunk(data: Uint8Array, metadata: ImageMetadata): void;
|
|
79
|
+
protected parseGPSIFD(data: Uint8Array, gpsIfdOffset: number, littleEndian: boolean, metadata: ImageMetadata): void;
|
|
80
|
+
protected readRational(data: Uint8Array, offset: number, littleEndian: boolean): number;
|
|
79
81
|
/**
|
|
80
82
|
* Create pHYs (physical pixel dimensions) chunk
|
|
81
83
|
*/
|
|
@@ -88,6 +90,8 @@ export declare abstract class PNGBase {
|
|
|
88
90
|
* Create eXIf (EXIF) chunk
|
|
89
91
|
*/
|
|
90
92
|
protected createExifChunk(metadata: ImageMetadata): Uint8Array | null;
|
|
93
|
+
protected createGPSIFD(metadata: ImageMetadata, gpsIfdStart: number): number[];
|
|
94
|
+
protected writeRational(output: number[], numerator: number, denominator: number): void;
|
|
91
95
|
/**
|
|
92
96
|
* Concatenate multiple byte arrays into a single Uint8Array
|
|
93
97
|
*/
|
|
@@ -104,5 +108,9 @@ export declare abstract class PNGBase {
|
|
|
104
108
|
* Shared method to avoid duplication between PNG and APNG encoding
|
|
105
109
|
*/
|
|
106
110
|
protected addMetadataChunks(chunks: Uint8Array[], metadata: ImageMetadata | undefined): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get the list of metadata fields supported by PNG format
|
|
113
|
+
*/
|
|
114
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
107
115
|
}
|
|
108
116
|
//# sourceMappingURL=png_base.d.ts.map
|
|
@@ -315,6 +315,7 @@ export class PNGBase {
|
|
|
315
315
|
const numEntries = littleEndian
|
|
316
316
|
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
317
317
|
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
318
|
+
let gpsIfdOffset = 0;
|
|
318
319
|
for (let i = 0; i < numEntries; i++) {
|
|
319
320
|
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
320
321
|
if (entryOffset + 12 > data.length)
|
|
@@ -339,12 +340,96 @@ export class PNGBase {
|
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
}
|
|
343
|
+
// GPS IFD Pointer tag (0x8825)
|
|
344
|
+
if (tag === 0x8825) {
|
|
345
|
+
gpsIfdOffset = littleEndian
|
|
346
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
347
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
348
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
349
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Parse GPS IFD if present
|
|
353
|
+
if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
|
|
354
|
+
this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
catch (_e) {
|
|
345
358
|
// Ignore EXIF parsing errors
|
|
346
359
|
}
|
|
347
360
|
}
|
|
361
|
+
parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
|
|
362
|
+
try {
|
|
363
|
+
const numEntries = littleEndian
|
|
364
|
+
? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
|
|
365
|
+
: (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
|
|
366
|
+
let latRef = "";
|
|
367
|
+
let lonRef = "";
|
|
368
|
+
let latitude;
|
|
369
|
+
let longitude;
|
|
370
|
+
for (let i = 0; i < numEntries; i++) {
|
|
371
|
+
const entryOffset = gpsIfdOffset + 2 + i * 12;
|
|
372
|
+
if (entryOffset + 12 > data.length)
|
|
373
|
+
break;
|
|
374
|
+
const tag = littleEndian
|
|
375
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
376
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
377
|
+
const type = littleEndian
|
|
378
|
+
? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
|
|
379
|
+
: (data[entryOffset + 2] << 8) | data[entryOffset + 3];
|
|
380
|
+
const valueOffset = littleEndian
|
|
381
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
382
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
383
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
384
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
385
|
+
// GPSLatitudeRef (0x0001)
|
|
386
|
+
if (tag === 0x0001 && type === 2) {
|
|
387
|
+
latRef = String.fromCharCode(data[entryOffset + 8]);
|
|
388
|
+
}
|
|
389
|
+
// GPSLatitude (0x0002)
|
|
390
|
+
if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
|
|
391
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
392
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
393
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
394
|
+
latitude = degrees + minutes / 60 + seconds / 3600;
|
|
395
|
+
}
|
|
396
|
+
// GPSLongitudeRef (0x0003)
|
|
397
|
+
if (tag === 0x0003 && type === 2) {
|
|
398
|
+
lonRef = String.fromCharCode(data[entryOffset + 8]);
|
|
399
|
+
}
|
|
400
|
+
// GPSLongitude (0x0004)
|
|
401
|
+
if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
|
|
402
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
403
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
404
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
405
|
+
longitude = degrees + minutes / 60 + seconds / 3600;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Apply hemisphere references
|
|
409
|
+
if (latitude !== undefined && latRef) {
|
|
410
|
+
metadata.latitude = latRef === "S" ? -latitude : latitude;
|
|
411
|
+
}
|
|
412
|
+
if (longitude !== undefined && lonRef) {
|
|
413
|
+
metadata.longitude = lonRef === "W" ? -longitude : longitude;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (_e) {
|
|
417
|
+
// Ignore GPS parsing errors
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
readRational(data, offset, littleEndian) {
|
|
421
|
+
const numerator = littleEndian
|
|
422
|
+
? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
|
|
423
|
+
(data[offset + 3] << 24)
|
|
424
|
+
: (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
425
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
426
|
+
const denominator = littleEndian
|
|
427
|
+
? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
|
|
428
|
+
(data[offset + 7] << 24)
|
|
429
|
+
: (data[offset + 4] << 24) | (data[offset + 5] << 16) |
|
|
430
|
+
(data[offset + 6] << 8) | data[offset + 7];
|
|
431
|
+
return denominator !== 0 ? numerator / denominator : 0;
|
|
432
|
+
}
|
|
348
433
|
/**
|
|
349
434
|
* Create pHYs (physical pixel dimensions) chunk
|
|
350
435
|
*/
|
|
@@ -385,14 +470,19 @@ export class PNGBase {
|
|
|
385
470
|
value: new TextEncoder().encode(dateStr),
|
|
386
471
|
});
|
|
387
472
|
}
|
|
388
|
-
if
|
|
473
|
+
// Check if we have GPS data
|
|
474
|
+
const hasGPS = metadata.latitude !== undefined &&
|
|
475
|
+
metadata.longitude !== undefined;
|
|
476
|
+
if (entries.length === 0 && !hasGPS)
|
|
389
477
|
return null;
|
|
390
478
|
const exif = [];
|
|
391
479
|
exif.push(0x49, 0x49); // "II"
|
|
392
480
|
exif.push(0x2a, 0x00); // 42
|
|
393
481
|
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
394
|
-
|
|
395
|
-
|
|
482
|
+
// Number of entries (add GPS IFD pointer if we have GPS data)
|
|
483
|
+
const ifd0Entries = entries.length + (hasGPS ? 1 : 0);
|
|
484
|
+
exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
|
|
485
|
+
let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
|
|
396
486
|
for (const entry of entries) {
|
|
397
487
|
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
398
488
|
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
@@ -408,6 +498,16 @@ export class PNGBase {
|
|
|
408
498
|
dataOffset += entry.value.length;
|
|
409
499
|
}
|
|
410
500
|
}
|
|
501
|
+
// Add GPS IFD pointer if we have GPS data
|
|
502
|
+
let gpsIfdOffset = 0;
|
|
503
|
+
if (hasGPS) {
|
|
504
|
+
gpsIfdOffset = dataOffset;
|
|
505
|
+
// GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
|
|
506
|
+
exif.push(0x25, 0x88); // Tag
|
|
507
|
+
exif.push(0x04, 0x00); // Type
|
|
508
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
509
|
+
exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
|
|
510
|
+
}
|
|
411
511
|
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
412
512
|
for (const entry of entries) {
|
|
413
513
|
if (entry.value.length > 4) {
|
|
@@ -416,8 +516,65 @@ export class PNGBase {
|
|
|
416
516
|
}
|
|
417
517
|
}
|
|
418
518
|
}
|
|
519
|
+
// Add GPS IFD if we have GPS data
|
|
520
|
+
if (hasGPS) {
|
|
521
|
+
const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
|
|
522
|
+
for (const byte of gpsIfd) {
|
|
523
|
+
exif.push(byte);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
419
526
|
return new Uint8Array(exif);
|
|
420
527
|
}
|
|
528
|
+
createGPSIFD(metadata, gpsIfdStart) {
|
|
529
|
+
const gps = [];
|
|
530
|
+
const numEntries = 4;
|
|
531
|
+
gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
532
|
+
const latitude = metadata.latitude;
|
|
533
|
+
const longitude = metadata.longitude;
|
|
534
|
+
const absLat = Math.abs(latitude);
|
|
535
|
+
const absLon = Math.abs(longitude);
|
|
536
|
+
const latDeg = Math.floor(absLat);
|
|
537
|
+
const latMin = Math.floor((absLat - latDeg) * 60);
|
|
538
|
+
const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
|
|
539
|
+
const lonDeg = Math.floor(absLon);
|
|
540
|
+
const lonMin = Math.floor((absLon - lonDeg) * 60);
|
|
541
|
+
const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
|
|
542
|
+
let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
|
|
543
|
+
// Entry 1: GPSLatitudeRef
|
|
544
|
+
gps.push(0x01, 0x00);
|
|
545
|
+
gps.push(0x02, 0x00);
|
|
546
|
+
gps.push(0x02, 0x00, 0x00, 0x00);
|
|
547
|
+
gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
|
|
548
|
+
// Entry 2: GPSLatitude
|
|
549
|
+
gps.push(0x02, 0x00);
|
|
550
|
+
gps.push(0x05, 0x00);
|
|
551
|
+
gps.push(0x03, 0x00, 0x00, 0x00);
|
|
552
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
553
|
+
dataOffset += 24;
|
|
554
|
+
// Entry 3: GPSLongitudeRef
|
|
555
|
+
gps.push(0x03, 0x00);
|
|
556
|
+
gps.push(0x02, 0x00);
|
|
557
|
+
gps.push(0x02, 0x00, 0x00, 0x00);
|
|
558
|
+
gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
|
|
559
|
+
// Entry 4: GPSLongitude
|
|
560
|
+
gps.push(0x04, 0x00);
|
|
561
|
+
gps.push(0x05, 0x00);
|
|
562
|
+
gps.push(0x03, 0x00, 0x00, 0x00);
|
|
563
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
564
|
+
gps.push(0x00, 0x00, 0x00, 0x00);
|
|
565
|
+
// Write rationals
|
|
566
|
+
this.writeRational(gps, latDeg, 1);
|
|
567
|
+
this.writeRational(gps, latMin, 1);
|
|
568
|
+
this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
|
|
569
|
+
this.writeRational(gps, lonDeg, 1);
|
|
570
|
+
this.writeRational(gps, lonMin, 1);
|
|
571
|
+
this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
|
|
572
|
+
return gps;
|
|
573
|
+
}
|
|
574
|
+
writeRational(output, numerator, denominator) {
|
|
575
|
+
output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
|
|
576
|
+
output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
|
|
577
|
+
}
|
|
421
578
|
/**
|
|
422
579
|
* Concatenate multiple byte arrays into a single Uint8Array
|
|
423
580
|
*/
|
|
@@ -484,4 +641,20 @@ export class PNGBase {
|
|
|
484
641
|
}
|
|
485
642
|
}
|
|
486
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Get the list of metadata fields supported by PNG format
|
|
646
|
+
*/
|
|
647
|
+
getSupportedMetadata() {
|
|
648
|
+
return [
|
|
649
|
+
"creationDate", // eXIf chunk
|
|
650
|
+
"latitude", // eXIf chunk (GPS IFD)
|
|
651
|
+
"longitude", // eXIf chunk (GPS IFD)
|
|
652
|
+
"dpiX", // pHYs chunk
|
|
653
|
+
"dpiY", // pHYs chunk
|
|
654
|
+
"title", // tEXt chunk
|
|
655
|
+
"author", // tEXt chunk
|
|
656
|
+
"description", // tEXt chunk
|
|
657
|
+
"copyright", // tEXt chunk
|
|
658
|
+
];
|
|
659
|
+
}
|
|
487
660
|
}
|
package/esm/src/formats/ppm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* PPM format handler
|
|
4
4
|
* Implements the Netpbm PPM (Portable PixMap) format.
|
|
@@ -46,5 +46,15 @@ export declare class PPMFormat implements ImageFormat {
|
|
|
46
46
|
* Check if a byte is whitespace (space, tab, CR, LF)
|
|
47
47
|
*/
|
|
48
48
|
private isWhitespace;
|
|
49
|
+
/**
|
|
50
|
+
* Get the list of metadata fields supported by PPM format
|
|
51
|
+
*/
|
|
52
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
53
|
+
/**
|
|
54
|
+
* Extract metadata from PPM data without fully decoding the pixel data
|
|
55
|
+
* @param data Raw PPM data
|
|
56
|
+
* @returns Extracted metadata or undefined
|
|
57
|
+
*/
|
|
58
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
49
59
|
}
|
|
50
60
|
//# sourceMappingURL=ppm.d.ts.map
|
package/esm/src/formats/ppm.js
CHANGED
|
@@ -239,4 +239,38 @@ export class PPMFormat {
|
|
|
239
239
|
isWhitespace(byte) {
|
|
240
240
|
return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
|
|
241
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Get the list of metadata fields supported by PPM format
|
|
244
|
+
*/
|
|
245
|
+
getSupportedMetadata() {
|
|
246
|
+
return []; // PPM format doesn't support metadata preservation
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Extract metadata from PPM data without fully decoding the pixel data
|
|
250
|
+
* @param data Raw PPM data
|
|
251
|
+
* @returns Extracted metadata or undefined
|
|
252
|
+
*/
|
|
253
|
+
extractMetadata(data) {
|
|
254
|
+
if (!this.canDecode(data)) {
|
|
255
|
+
return Promise.resolve(undefined);
|
|
256
|
+
}
|
|
257
|
+
const metadata = {
|
|
258
|
+
format: "ppm",
|
|
259
|
+
compression: "none",
|
|
260
|
+
frameCount: 1,
|
|
261
|
+
bitDepth: 8,
|
|
262
|
+
colorType: "rgb",
|
|
263
|
+
};
|
|
264
|
+
// PPM is always RGB, uncompressed, and typically 8-bit
|
|
265
|
+
// P3 is ASCII, P6 is binary
|
|
266
|
+
if (data[1] === 0x33) {
|
|
267
|
+
// '3'
|
|
268
|
+
metadata.compression = "none"; // ASCII encoding
|
|
269
|
+
}
|
|
270
|
+
else if (data[1] === 0x36) {
|
|
271
|
+
// '6'
|
|
272
|
+
metadata.compression = "none"; // Binary encoding
|
|
273
|
+
}
|
|
274
|
+
return Promise.resolve(metadata);
|
|
275
|
+
}
|
|
242
276
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, MultiFrameImageData } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options for TIFF encoding
|
|
4
4
|
*/
|
|
@@ -74,5 +74,17 @@ export declare class TIFFFormat implements ImageFormat {
|
|
|
74
74
|
* Returns null if the TIFF uses unsupported features
|
|
75
75
|
*/
|
|
76
76
|
private decodePureJSFromIFD;
|
|
77
|
+
/**
|
|
78
|
+
* Get list of metadata fields supported by TIFF format
|
|
79
|
+
* TIFF supports extensive EXIF metadata including GPS and InteropIFD
|
|
80
|
+
*/
|
|
81
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
82
|
+
/**
|
|
83
|
+
* Extract metadata from TIFF data without fully decoding the pixel data
|
|
84
|
+
* This quickly parses IFD entries to extract metadata
|
|
85
|
+
* @param data Raw TIFF data
|
|
86
|
+
* @returns Extracted metadata or undefined
|
|
87
|
+
*/
|
|
88
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
77
89
|
}
|
|
78
90
|
//# sourceMappingURL=tiff.d.ts.map
|