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
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
import { validateImageDimensions } from "../utils/security.js";
|
|
2
|
+
/**
|
|
3
|
+
* HEIC format handler
|
|
4
|
+
* Supports HEIC/HEIF images using runtime APIs (ImageDecoder/OffscreenCanvas)
|
|
5
|
+
* Note: Pure JavaScript encode/decode is not supported due to complexity
|
|
6
|
+
*/
|
|
7
|
+
export class HEICFormat {
|
|
8
|
+
constructor() {
|
|
9
|
+
/** Format name identifier */
|
|
10
|
+
Object.defineProperty(this, "name", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: "heic"
|
|
15
|
+
});
|
|
16
|
+
/** MIME type for HEIC images */
|
|
17
|
+
Object.defineProperty(this, "mimeType", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: "image/heic"
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Check if the given data is a HEIC/HEIF image
|
|
26
|
+
* @param data Raw image data to check
|
|
27
|
+
* @returns true if data has HEIC/HEIF signature
|
|
28
|
+
*/
|
|
29
|
+
canDecode(data) {
|
|
30
|
+
// HEIC/HEIF files are ISO Base Media File Format (ISOBMFF) containers
|
|
31
|
+
// They start with ftyp box which contains brand identifiers
|
|
32
|
+
if (data.length < 12)
|
|
33
|
+
return false;
|
|
34
|
+
// Check for ftyp box at the start
|
|
35
|
+
// Bytes 4-7 should be "ftyp"
|
|
36
|
+
if (data[4] === 0x66 && data[5] === 0x74 && // "ft"
|
|
37
|
+
data[6] === 0x79 && data[7] === 0x70 // "yp"
|
|
38
|
+
) {
|
|
39
|
+
// Check for HEIC/HEIF brand identifiers
|
|
40
|
+
// Common brands: heic, heix, hevc, hevx, mif1, msf1
|
|
41
|
+
const brand = String.fromCharCode(data[8], data[9], data[10], data[11]);
|
|
42
|
+
return brand === "heic" || brand === "heix" || brand === "hevc" ||
|
|
43
|
+
brand === "hevx" || brand === "mif1" || brand === "msf1";
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decode HEIC image data to RGBA
|
|
49
|
+
* Uses runtime APIs (ImageDecoder) for decoding
|
|
50
|
+
* @param data Raw HEIC image data
|
|
51
|
+
* @returns Decoded image data with RGBA pixels
|
|
52
|
+
*/
|
|
53
|
+
async decode(data) {
|
|
54
|
+
if (!this.canDecode(data)) {
|
|
55
|
+
throw new Error("Invalid HEIC signature");
|
|
56
|
+
}
|
|
57
|
+
// Extract metadata before decoding pixels
|
|
58
|
+
const metadata = await this.extractMetadata(data);
|
|
59
|
+
// Use runtime decoder
|
|
60
|
+
const { width, height, rgba } = await this.decodeUsingRuntime(data);
|
|
61
|
+
// Validate dimensions for security
|
|
62
|
+
validateImageDimensions(width, height);
|
|
63
|
+
return {
|
|
64
|
+
width,
|
|
65
|
+
height,
|
|
66
|
+
data: rgba,
|
|
67
|
+
metadata,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Encode RGBA image data to HEIC format
|
|
72
|
+
* Uses runtime APIs (OffscreenCanvas) for encoding
|
|
73
|
+
*
|
|
74
|
+
* Note: Metadata injection is not currently implemented. Metadata may be lost during encoding
|
|
75
|
+
* as it would require parsing and modifying the ISOBMFF container structure.
|
|
76
|
+
*
|
|
77
|
+
* @param imageData Image data to encode
|
|
78
|
+
* @returns Encoded HEIC image bytes
|
|
79
|
+
*/
|
|
80
|
+
async encode(imageData) {
|
|
81
|
+
const { width, height, data, metadata } = imageData;
|
|
82
|
+
// Try to use runtime encoding if available
|
|
83
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
84
|
+
try {
|
|
85
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
86
|
+
const ctx = canvas.getContext("2d");
|
|
87
|
+
if (ctx) {
|
|
88
|
+
const imgData = ctx.createImageData(width, height);
|
|
89
|
+
const imgDataData = new Uint8ClampedArray(data);
|
|
90
|
+
imgData.data.set(imgDataData);
|
|
91
|
+
ctx.putImageData(imgData, 0, 0);
|
|
92
|
+
// Try to encode as HEIC
|
|
93
|
+
const blob = await canvas.convertToBlob({
|
|
94
|
+
type: "image/heic",
|
|
95
|
+
});
|
|
96
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
97
|
+
const encoded = new Uint8Array(arrayBuffer);
|
|
98
|
+
// Note: Metadata injection for HEIC is complex and would require
|
|
99
|
+
// parsing and modifying the ISOBMFF container structure
|
|
100
|
+
// For now, we rely on the runtime encoder to preserve metadata
|
|
101
|
+
// if it was passed through the canvas
|
|
102
|
+
if (metadata) {
|
|
103
|
+
// Future enhancement: inject metadata into HEIC container
|
|
104
|
+
console.warn("HEIC metadata injection not yet implemented, metadata may be lost");
|
|
105
|
+
}
|
|
106
|
+
return encoded;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
throw new Error(`HEIC encoding failed: ${error}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
throw new Error("HEIC encoding requires OffscreenCanvas API (not available in this runtime)");
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decode using runtime APIs
|
|
117
|
+
* @param data Raw HEIC data
|
|
118
|
+
* @returns Decoded image dimensions and pixel data
|
|
119
|
+
*/
|
|
120
|
+
async decodeUsingRuntime(data) {
|
|
121
|
+
// Try to use ImageDecoder API if available
|
|
122
|
+
if (typeof ImageDecoder !== "undefined") {
|
|
123
|
+
try {
|
|
124
|
+
const decoder = new ImageDecoder({ data, type: "image/heic" });
|
|
125
|
+
const result = await decoder.decode();
|
|
126
|
+
const bitmap = result.image;
|
|
127
|
+
// Create a canvas to extract pixel data
|
|
128
|
+
const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
|
|
129
|
+
const ctx = canvas.getContext("2d");
|
|
130
|
+
if (!ctx)
|
|
131
|
+
throw new Error("Could not get canvas context");
|
|
132
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
133
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
134
|
+
bitmap.close();
|
|
135
|
+
return {
|
|
136
|
+
width: canvas.width,
|
|
137
|
+
height: canvas.height,
|
|
138
|
+
rgba: new Uint8Array(imageData.data.buffer),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw new Error(`HEIC decoding with ImageDecoder failed: ${error}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new Error("HEIC decoding requires ImageDecoder API (not available in this runtime)");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse EXIF metadata from HEIC data
|
|
149
|
+
*
|
|
150
|
+
* Note: This is a simplified implementation that searches for EXIF headers linearly.
|
|
151
|
+
* A full implementation would require navigating the ISOBMFF box structure to find
|
|
152
|
+
* the 'meta' box and then the 'Exif' item. This simplified approach may not work
|
|
153
|
+
* in all cases but is suitable for basic metadata extraction when runtime APIs are
|
|
154
|
+
* not available or as a fallback.
|
|
155
|
+
*
|
|
156
|
+
* @param data Raw HEIC data
|
|
157
|
+
* @param metadata Metadata object to populate
|
|
158
|
+
*/
|
|
159
|
+
parseEXIF(data, metadata) {
|
|
160
|
+
// For now, we'll attempt a simple search for EXIF header
|
|
161
|
+
// This is a simplified approach and may not work in all cases
|
|
162
|
+
try {
|
|
163
|
+
// Look for Exif header
|
|
164
|
+
for (let i = 0; i < data.length - 6; i++) {
|
|
165
|
+
if (data[i] === 0x45 && data[i + 1] === 0x78 && // "Ex"
|
|
166
|
+
data[i + 2] === 0x69 && data[i + 3] === 0x66 && // "if"
|
|
167
|
+
data[i + 4] === 0x00 && data[i + 5] === 0x00 // padding
|
|
168
|
+
) {
|
|
169
|
+
// Found EXIF header, parse TIFF structure
|
|
170
|
+
const exifData = data.slice(i + 6);
|
|
171
|
+
this.parseTIFFExif(exifData, metadata);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (_e) {
|
|
177
|
+
// Ignore EXIF parsing errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse TIFF-formatted EXIF data
|
|
182
|
+
* @param data EXIF data in TIFF format
|
|
183
|
+
* @param metadata Metadata object to populate
|
|
184
|
+
*/
|
|
185
|
+
parseTIFFExif(data, metadata) {
|
|
186
|
+
if (data.length < 8)
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
const byteOrder = String.fromCharCode(data[0], data[1]);
|
|
190
|
+
const littleEndian = byteOrder === "II";
|
|
191
|
+
const ifd0Offset = littleEndian
|
|
192
|
+
? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
|
|
193
|
+
: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
|
|
194
|
+
if (ifd0Offset + 2 > data.length)
|
|
195
|
+
return;
|
|
196
|
+
const numEntries = littleEndian
|
|
197
|
+
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
198
|
+
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
199
|
+
let gpsIfdOffset = 0;
|
|
200
|
+
// Parse IFD0 entries
|
|
201
|
+
for (let i = 0; i < numEntries && i < 100; i++) {
|
|
202
|
+
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
203
|
+
if (entryOffset + 12 > data.length)
|
|
204
|
+
break;
|
|
205
|
+
const tag = littleEndian
|
|
206
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
207
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
208
|
+
const type = littleEndian
|
|
209
|
+
? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
|
|
210
|
+
: (data[entryOffset + 2] << 8) | data[entryOffset + 3];
|
|
211
|
+
// DateTime (0x0132)
|
|
212
|
+
if (tag === 0x0132) {
|
|
213
|
+
const valueOffset = littleEndian
|
|
214
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
215
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
216
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
217
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
218
|
+
if (valueOffset < data.length) {
|
|
219
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
220
|
+
if (endIndex > valueOffset) {
|
|
221
|
+
const dateStr = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
222
|
+
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
223
|
+
if (match) {
|
|
224
|
+
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// ImageDescription (0x010E)
|
|
230
|
+
if (tag === 0x010e) {
|
|
231
|
+
const valueOffset = littleEndian
|
|
232
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
233
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
234
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
235
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
236
|
+
if (valueOffset < data.length) {
|
|
237
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
238
|
+
if (endIndex > valueOffset) {
|
|
239
|
+
metadata.description = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Artist (0x013B)
|
|
244
|
+
if (tag === 0x013b) {
|
|
245
|
+
const valueOffset = littleEndian
|
|
246
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
247
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
248
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
249
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
250
|
+
if (valueOffset < data.length) {
|
|
251
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
252
|
+
if (endIndex > valueOffset) {
|
|
253
|
+
metadata.author = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// Copyright (0x8298)
|
|
258
|
+
if (tag === 0x8298) {
|
|
259
|
+
const valueOffset = littleEndian
|
|
260
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
261
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
262
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
263
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
264
|
+
if (valueOffset < data.length) {
|
|
265
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
266
|
+
if (endIndex > valueOffset) {
|
|
267
|
+
metadata.copyright = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Make (0x010F)
|
|
272
|
+
if (tag === 0x010f) {
|
|
273
|
+
const valueOffset = littleEndian
|
|
274
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
275
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
276
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
277
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
278
|
+
if (valueOffset < data.length) {
|
|
279
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
280
|
+
if (endIndex > valueOffset) {
|
|
281
|
+
metadata.cameraMake = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Model (0x0110)
|
|
286
|
+
if (tag === 0x0110) {
|
|
287
|
+
const valueOffset = littleEndian
|
|
288
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
289
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
290
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
291
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
292
|
+
if (valueOffset < data.length) {
|
|
293
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
294
|
+
if (endIndex > valueOffset) {
|
|
295
|
+
metadata.cameraModel = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Orientation (0x0112)
|
|
300
|
+
if (tag === 0x0112) {
|
|
301
|
+
const value = littleEndian
|
|
302
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
|
|
303
|
+
: (data[entryOffset + 8] << 8) | data[entryOffset + 9];
|
|
304
|
+
metadata.orientation = value;
|
|
305
|
+
}
|
|
306
|
+
// Software (0x0131)
|
|
307
|
+
if (tag === 0x0131) {
|
|
308
|
+
const valueOffset = littleEndian
|
|
309
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
310
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
311
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
312
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
313
|
+
if (valueOffset < data.length) {
|
|
314
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
315
|
+
if (endIndex > valueOffset) {
|
|
316
|
+
metadata.software = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// GPS IFD Pointer (0x8825)
|
|
321
|
+
if (tag === 0x8825) {
|
|
322
|
+
gpsIfdOffset = littleEndian
|
|
323
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
324
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
325
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
326
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
327
|
+
}
|
|
328
|
+
// ExifIFD Pointer (0x8769)
|
|
329
|
+
if (tag === 0x8769 && type === 4) {
|
|
330
|
+
const exifIfdOffset = littleEndian
|
|
331
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
332
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
333
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
334
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
335
|
+
if (exifIfdOffset > 0 && exifIfdOffset + 2 <= data.length) {
|
|
336
|
+
this.parseExifSubIFD(data, exifIfdOffset, littleEndian, metadata);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Parse GPS IFD if present
|
|
341
|
+
if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
|
|
342
|
+
this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
catch (_e) {
|
|
346
|
+
// Ignore parsing errors
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Parse Exif Sub-IFD for camera settings
|
|
351
|
+
* @param data EXIF data
|
|
352
|
+
* @param exifIfdOffset Offset to Exif Sub-IFD
|
|
353
|
+
* @param littleEndian Byte order
|
|
354
|
+
* @param metadata Metadata object to populate
|
|
355
|
+
*/
|
|
356
|
+
parseExifSubIFD(data, exifIfdOffset, littleEndian, metadata) {
|
|
357
|
+
try {
|
|
358
|
+
const numEntries = littleEndian
|
|
359
|
+
? data[exifIfdOffset] | (data[exifIfdOffset + 1] << 8)
|
|
360
|
+
: (data[exifIfdOffset] << 8) | data[exifIfdOffset + 1];
|
|
361
|
+
for (let i = 0; i < numEntries && i < 100; i++) {
|
|
362
|
+
const entryOffset = exifIfdOffset + 2 + i * 12;
|
|
363
|
+
if (entryOffset + 12 > data.length)
|
|
364
|
+
break;
|
|
365
|
+
const tag = littleEndian
|
|
366
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
367
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
368
|
+
const type = littleEndian
|
|
369
|
+
? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
|
|
370
|
+
: (data[entryOffset + 2] << 8) | data[entryOffset + 3];
|
|
371
|
+
// ExposureTime (0x829A)
|
|
372
|
+
if (tag === 0x829a && type === 5) {
|
|
373
|
+
const valueOffset = littleEndian
|
|
374
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
375
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
376
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
377
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
378
|
+
if (valueOffset + 8 <= data.length) {
|
|
379
|
+
metadata.exposureTime = this.readRational(data, valueOffset, littleEndian);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// FNumber (0x829D)
|
|
383
|
+
if (tag === 0x829d && type === 5) {
|
|
384
|
+
const valueOffset = littleEndian
|
|
385
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
386
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
387
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
388
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
389
|
+
if (valueOffset + 8 <= data.length) {
|
|
390
|
+
metadata.fNumber = this.readRational(data, valueOffset, littleEndian);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// ISOSpeedRatings (0x8827)
|
|
394
|
+
if (tag === 0x8827 && type === 3) {
|
|
395
|
+
metadata.iso = littleEndian
|
|
396
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
|
|
397
|
+
: (data[entryOffset + 8] << 8) | data[entryOffset + 9];
|
|
398
|
+
}
|
|
399
|
+
// FocalLength (0x920A)
|
|
400
|
+
if (tag === 0x920a && type === 5) {
|
|
401
|
+
const valueOffset = littleEndian
|
|
402
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
403
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
404
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
405
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
406
|
+
if (valueOffset + 8 <= data.length) {
|
|
407
|
+
metadata.focalLength = this.readRational(data, valueOffset, littleEndian);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// UserComment (0x9286)
|
|
411
|
+
if (tag === 0x9286) {
|
|
412
|
+
const count = littleEndian
|
|
413
|
+
? data[entryOffset + 4] | (data[entryOffset + 5] << 8) |
|
|
414
|
+
(data[entryOffset + 6] << 16) | (data[entryOffset + 7] << 24)
|
|
415
|
+
: (data[entryOffset + 4] << 24) | (data[entryOffset + 5] << 16) |
|
|
416
|
+
(data[entryOffset + 6] << 8) | data[entryOffset + 7];
|
|
417
|
+
const valueOffset = littleEndian
|
|
418
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
419
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
420
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
421
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
422
|
+
if (valueOffset + count <= data.length && count > 8) {
|
|
423
|
+
const commentData = data.slice(valueOffset + 8, valueOffset + count);
|
|
424
|
+
metadata.userComment = new TextDecoder().decode(commentData)
|
|
425
|
+
.replace(/\0+$/, "");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Flash (0x9209)
|
|
429
|
+
if (tag === 0x9209 && type === 3) {
|
|
430
|
+
metadata.flash = littleEndian
|
|
431
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
|
|
432
|
+
: (data[entryOffset + 8] << 8) | data[entryOffset + 9];
|
|
433
|
+
}
|
|
434
|
+
// WhiteBalance (0xA403)
|
|
435
|
+
if (tag === 0xa403 && type === 3) {
|
|
436
|
+
metadata.whiteBalance = littleEndian
|
|
437
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8)
|
|
438
|
+
: (data[entryOffset + 8] << 8) | data[entryOffset + 9];
|
|
439
|
+
}
|
|
440
|
+
// LensMake (0xA433)
|
|
441
|
+
if (tag === 0xa433 && type === 2) {
|
|
442
|
+
const valueOffset = littleEndian
|
|
443
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
444
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
445
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
446
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
447
|
+
if (valueOffset < data.length) {
|
|
448
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
449
|
+
if (endIndex > valueOffset) {
|
|
450
|
+
metadata.lensMake = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
// LensModel (0xA434)
|
|
455
|
+
if (tag === 0xa434 && type === 2) {
|
|
456
|
+
const valueOffset = littleEndian
|
|
457
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
458
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
459
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
460
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
461
|
+
if (valueOffset < data.length) {
|
|
462
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
463
|
+
if (endIndex > valueOffset) {
|
|
464
|
+
metadata.lensModel = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
catch (_e) {
|
|
471
|
+
// Ignore parsing errors
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Parse GPS IFD for location data
|
|
476
|
+
* @param data EXIF data
|
|
477
|
+
* @param gpsIfdOffset Offset to GPS IFD
|
|
478
|
+
* @param littleEndian Byte order
|
|
479
|
+
* @param metadata Metadata object to populate
|
|
480
|
+
*/
|
|
481
|
+
parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
|
|
482
|
+
try {
|
|
483
|
+
const numEntries = littleEndian
|
|
484
|
+
? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
|
|
485
|
+
: (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
|
|
486
|
+
let latRef = "";
|
|
487
|
+
let lonRef = "";
|
|
488
|
+
let latitude;
|
|
489
|
+
let longitude;
|
|
490
|
+
for (let i = 0; i < numEntries && i < 100; i++) {
|
|
491
|
+
const entryOffset = gpsIfdOffset + 2 + i * 12;
|
|
492
|
+
if (entryOffset + 12 > data.length)
|
|
493
|
+
break;
|
|
494
|
+
const tag = littleEndian
|
|
495
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
496
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
497
|
+
const type = littleEndian
|
|
498
|
+
? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
|
|
499
|
+
: (data[entryOffset + 2] << 8) | data[entryOffset + 3];
|
|
500
|
+
// GPSLatitudeRef (0x0001)
|
|
501
|
+
if (tag === 0x0001 && type === 2) {
|
|
502
|
+
latRef = String.fromCharCode(data[entryOffset + 8]);
|
|
503
|
+
}
|
|
504
|
+
// GPSLatitude (0x0002)
|
|
505
|
+
if (tag === 0x0002 && type === 5) {
|
|
506
|
+
const valueOffset = littleEndian
|
|
507
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
508
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
509
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
510
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
511
|
+
if (valueOffset + 24 <= data.length) {
|
|
512
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
513
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
514
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
515
|
+
latitude = degrees + minutes / 60 + seconds / 3600;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// GPSLongitudeRef (0x0003)
|
|
519
|
+
if (tag === 0x0003 && type === 2) {
|
|
520
|
+
lonRef = String.fromCharCode(data[entryOffset + 8]);
|
|
521
|
+
}
|
|
522
|
+
// GPSLongitude (0x0004)
|
|
523
|
+
if (tag === 0x0004 && type === 5) {
|
|
524
|
+
const valueOffset = littleEndian
|
|
525
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
526
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
527
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
528
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
529
|
+
if (valueOffset + 24 <= data.length) {
|
|
530
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
531
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
532
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
533
|
+
longitude = degrees + minutes / 60 + seconds / 3600;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Apply reference direction
|
|
538
|
+
if (latitude !== undefined) {
|
|
539
|
+
metadata.latitude = latRef === "S" ? -latitude : latitude;
|
|
540
|
+
}
|
|
541
|
+
if (longitude !== undefined) {
|
|
542
|
+
metadata.longitude = lonRef === "W" ? -longitude : longitude;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
catch (_e) {
|
|
546
|
+
// Ignore parsing errors
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Read a rational value (numerator/denominator)
|
|
551
|
+
* @param data Data buffer
|
|
552
|
+
* @param offset Offset to rational
|
|
553
|
+
* @param littleEndian Byte order
|
|
554
|
+
* @returns Decimal value
|
|
555
|
+
*/
|
|
556
|
+
readRational(data, offset, littleEndian) {
|
|
557
|
+
const numerator = littleEndian
|
|
558
|
+
? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
|
|
559
|
+
(data[offset + 3] << 24)
|
|
560
|
+
: (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
561
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
562
|
+
const denominator = littleEndian
|
|
563
|
+
? data[offset + 4] | (data[offset + 5] << 8) |
|
|
564
|
+
(data[offset + 6] << 16) | (data[offset + 7] << 24)
|
|
565
|
+
: (data[offset + 4] << 24) | (data[offset + 5] << 16) |
|
|
566
|
+
(data[offset + 6] << 8) | data[offset + 7];
|
|
567
|
+
return denominator !== 0 ? numerator / denominator : 0;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Get the list of metadata fields supported by HEIC format
|
|
571
|
+
*/
|
|
572
|
+
getSupportedMetadata() {
|
|
573
|
+
return [
|
|
574
|
+
"creationDate",
|
|
575
|
+
"description",
|
|
576
|
+
"author",
|
|
577
|
+
"copyright",
|
|
578
|
+
"latitude",
|
|
579
|
+
"longitude",
|
|
580
|
+
"cameraMake",
|
|
581
|
+
"cameraModel",
|
|
582
|
+
"iso",
|
|
583
|
+
"exposureTime",
|
|
584
|
+
"fNumber",
|
|
585
|
+
"focalLength",
|
|
586
|
+
"flash",
|
|
587
|
+
"whiteBalance",
|
|
588
|
+
"lensMake",
|
|
589
|
+
"lensModel",
|
|
590
|
+
"orientation",
|
|
591
|
+
"software",
|
|
592
|
+
"userComment",
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Extract metadata from HEIC data without fully decoding the pixel data
|
|
597
|
+
* @param data Raw HEIC data
|
|
598
|
+
* @returns Extracted metadata or undefined
|
|
599
|
+
*/
|
|
600
|
+
extractMetadata(data) {
|
|
601
|
+
if (!this.canDecode(data)) {
|
|
602
|
+
return Promise.resolve(undefined);
|
|
603
|
+
}
|
|
604
|
+
const metadata = {};
|
|
605
|
+
this.parseEXIF(data, metadata);
|
|
606
|
+
return Promise.resolve(Object.keys(metadata).length > 0 ? metadata : undefined);
|
|
607
|
+
}
|
|
608
|
+
}
|
package/esm/src/formats/ico.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
|
* ICO format handler
|
|
4
4
|
* Implements a pure JavaScript ICO (Windows Icon) decoder and encoder
|
|
@@ -37,5 +37,15 @@ export declare class ICOFormat implements ImageFormat {
|
|
|
37
37
|
* @returns Encoded ICO image bytes
|
|
38
38
|
*/
|
|
39
39
|
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
40
|
+
/**
|
|
41
|
+
* Get the list of metadata fields supported by ICO format
|
|
42
|
+
*/
|
|
43
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
44
|
+
/**
|
|
45
|
+
* Extract metadata from ICO data without fully decoding the pixel data
|
|
46
|
+
* @param data Raw ICO data
|
|
47
|
+
* @returns Extracted metadata or undefined
|
|
48
|
+
*/
|
|
49
|
+
extractMetadata(data: Uint8Array): Promise<ImageMetadata | undefined>;
|
|
40
50
|
}
|
|
41
51
|
//# sourceMappingURL=ico.d.ts.map
|
package/esm/src/formats/ico.js
CHANGED
|
@@ -211,4 +211,32 @@ export class ICOFormat {
|
|
|
211
211
|
result.set(pngData, 22);
|
|
212
212
|
return result;
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the list of metadata fields supported by ICO format
|
|
216
|
+
*/
|
|
217
|
+
getSupportedMetadata() {
|
|
218
|
+
return [
|
|
219
|
+
"frameCount", // Number of images in ICO file
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Extract metadata from ICO data without fully decoding the pixel data
|
|
224
|
+
* @param data Raw ICO data
|
|
225
|
+
* @returns Extracted metadata or undefined
|
|
226
|
+
*/
|
|
227
|
+
extractMetadata(data) {
|
|
228
|
+
if (!this.canDecode(data)) {
|
|
229
|
+
return Promise.resolve(undefined);
|
|
230
|
+
}
|
|
231
|
+
// ICO files can contain multiple images, count them
|
|
232
|
+
const count = readUint16LE(data, 4);
|
|
233
|
+
const metadata = {
|
|
234
|
+
format: "ico",
|
|
235
|
+
compression: "none", // ICO typically contains uncompressed PNG or BMP
|
|
236
|
+
frameCount: count,
|
|
237
|
+
bitDepth: 32, // Most modern ICOs use 32-bit RGBA
|
|
238
|
+
colorType: "rgba",
|
|
239
|
+
};
|
|
240
|
+
return Promise.resolve(metadata);
|
|
241
|
+
}
|
|
214
242
|
}
|