cross-image 0.1.2
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 -0
- package/README.md +606 -0
- package/esm/mod.d.ts +33 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/package.json +3 -0
- package/esm/src/formats/ascii.d.ts +27 -0
- package/esm/src/formats/ascii.d.ts.map +1 -0
- package/esm/src/formats/ascii.js +172 -0
- package/esm/src/formats/bmp.d.ts +19 -0
- package/esm/src/formats/bmp.d.ts.map +1 -0
- package/esm/src/formats/bmp.js +174 -0
- package/esm/src/formats/gif.d.ts +40 -0
- package/esm/src/formats/gif.d.ts.map +1 -0
- package/esm/src/formats/gif.js +385 -0
- package/esm/src/formats/jpeg.d.ts +18 -0
- package/esm/src/formats/jpeg.d.ts.map +1 -0
- package/esm/src/formats/jpeg.js +414 -0
- package/esm/src/formats/png.d.ts +33 -0
- package/esm/src/formats/png.d.ts.map +1 -0
- package/esm/src/formats/png.js +544 -0
- package/esm/src/formats/raw.d.ts +23 -0
- package/esm/src/formats/raw.d.ts.map +1 -0
- package/esm/src/formats/raw.js +98 -0
- package/esm/src/formats/tiff.d.ts +58 -0
- package/esm/src/formats/tiff.d.ts.map +1 -0
- package/esm/src/formats/tiff.js +791 -0
- package/esm/src/formats/webp.d.ts +22 -0
- package/esm/src/formats/webp.d.ts.map +1 -0
- package/esm/src/formats/webp.js +403 -0
- package/esm/src/image.d.ts +124 -0
- package/esm/src/image.d.ts.map +1 -0
- package/esm/src/image.js +320 -0
- package/esm/src/types.d.ts +167 -0
- package/esm/src/types.d.ts.map +1 -0
- package/esm/src/types.js +1 -0
- package/esm/src/utils/gif_decoder.d.ts +42 -0
- package/esm/src/utils/gif_decoder.d.ts.map +1 -0
- package/esm/src/utils/gif_decoder.js +374 -0
- package/esm/src/utils/gif_encoder.d.ts +29 -0
- package/esm/src/utils/gif_encoder.d.ts.map +1 -0
- package/esm/src/utils/gif_encoder.js +226 -0
- package/esm/src/utils/jpeg_decoder.d.ts +39 -0
- package/esm/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_decoder.js +580 -0
- package/esm/src/utils/jpeg_encoder.d.ts +33 -0
- package/esm/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_encoder.js +1017 -0
- package/esm/src/utils/lzw.d.ts +43 -0
- package/esm/src/utils/lzw.d.ts.map +1 -0
- package/esm/src/utils/lzw.js +309 -0
- package/esm/src/utils/resize.d.ts +9 -0
- package/esm/src/utils/resize.d.ts.map +1 -0
- package/esm/src/utils/resize.js +52 -0
- package/esm/src/utils/tiff_lzw.d.ts +44 -0
- package/esm/src/utils/tiff_lzw.d.ts.map +1 -0
- package/esm/src/utils/tiff_lzw.js +306 -0
- package/esm/src/utils/webp_decoder.d.ts +39 -0
- package/esm/src/utils/webp_decoder.d.ts.map +1 -0
- package/esm/src/utils/webp_decoder.js +493 -0
- package/esm/src/utils/webp_encoder.d.ts +72 -0
- package/esm/src/utils/webp_encoder.d.ts.map +1 -0
- package/esm/src/utils/webp_encoder.js +627 -0
- package/package.json +41 -0
- package/script/mod.d.ts +33 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +43 -0
- package/script/package.json +3 -0
- package/script/src/formats/ascii.d.ts +27 -0
- package/script/src/formats/ascii.d.ts.map +1 -0
- package/script/src/formats/ascii.js +176 -0
- package/script/src/formats/bmp.d.ts +19 -0
- package/script/src/formats/bmp.d.ts.map +1 -0
- package/script/src/formats/bmp.js +178 -0
- package/script/src/formats/gif.d.ts +40 -0
- package/script/src/formats/gif.d.ts.map +1 -0
- package/script/src/formats/gif.js +389 -0
- package/script/src/formats/jpeg.d.ts +18 -0
- package/script/src/formats/jpeg.d.ts.map +1 -0
- package/script/src/formats/jpeg.js +451 -0
- package/script/src/formats/png.d.ts +33 -0
- package/script/src/formats/png.d.ts.map +1 -0
- package/script/src/formats/png.js +548 -0
- package/script/src/formats/raw.d.ts +23 -0
- package/script/src/formats/raw.d.ts.map +1 -0
- package/script/src/formats/raw.js +102 -0
- package/script/src/formats/tiff.d.ts +58 -0
- package/script/src/formats/tiff.d.ts.map +1 -0
- package/script/src/formats/tiff.js +795 -0
- package/script/src/formats/webp.d.ts +22 -0
- package/script/src/formats/webp.d.ts.map +1 -0
- package/script/src/formats/webp.js +440 -0
- package/script/src/image.d.ts +124 -0
- package/script/src/image.d.ts.map +1 -0
- package/script/src/image.js +324 -0
- package/script/src/types.d.ts +167 -0
- package/script/src/types.d.ts.map +1 -0
- package/script/src/types.js +2 -0
- package/script/src/utils/gif_decoder.d.ts +42 -0
- package/script/src/utils/gif_decoder.d.ts.map +1 -0
- package/script/src/utils/gif_decoder.js +378 -0
- package/script/src/utils/gif_encoder.d.ts +29 -0
- package/script/src/utils/gif_encoder.d.ts.map +1 -0
- package/script/src/utils/gif_encoder.js +230 -0
- package/script/src/utils/jpeg_decoder.d.ts +39 -0
- package/script/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_decoder.js +584 -0
- package/script/src/utils/jpeg_encoder.d.ts +33 -0
- package/script/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_encoder.js +1021 -0
- package/script/src/utils/lzw.d.ts +43 -0
- package/script/src/utils/lzw.d.ts.map +1 -0
- package/script/src/utils/lzw.js +314 -0
- package/script/src/utils/resize.d.ts +9 -0
- package/script/src/utils/resize.d.ts.map +1 -0
- package/script/src/utils/resize.js +56 -0
- package/script/src/utils/tiff_lzw.d.ts +44 -0
- package/script/src/utils/tiff_lzw.d.ts.map +1 -0
- package/script/src/utils/tiff_lzw.js +311 -0
- package/script/src/utils/webp_decoder.d.ts +39 -0
- package/script/src/utils/webp_decoder.d.ts.map +1 -0
- package/script/src/utils/webp_decoder.js +497 -0
- package/script/src/utils/webp_encoder.d.ts +72 -0
- package/script/src/utils/webp_encoder.d.ts.map +1 -0
- package/script/src/utils/webp_encoder.js +631 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat, WebPEncodeOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* WebP format handler
|
|
4
|
+
* Implements a basic WebP decoder and encoder
|
|
5
|
+
*/
|
|
6
|
+
export declare class WebPFormat implements ImageFormat {
|
|
7
|
+
readonly name = "webp";
|
|
8
|
+
readonly mimeType = "image/webp";
|
|
9
|
+
canDecode(data: Uint8Array): boolean;
|
|
10
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
11
|
+
encode(imageData: ImageData, options?: WebPEncodeOptions): Promise<Uint8Array>;
|
|
12
|
+
private readUint32LE;
|
|
13
|
+
private readUint24LE;
|
|
14
|
+
private decodeUsingRuntime;
|
|
15
|
+
private parseEXIF;
|
|
16
|
+
private parseXMP;
|
|
17
|
+
private injectMetadata;
|
|
18
|
+
private createEXIFChunk;
|
|
19
|
+
private createXMPChunk;
|
|
20
|
+
private escapeXML;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=webp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webp.d.ts","sourceRoot":"","sources":["../../../src/src/formats/webp.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EAEX,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAKrB;;;GAGG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,IAAI,UAAU;IACvB,QAAQ,CAAC,QAAQ,gBAAgB;IAEjC,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAS9B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IAmF5C,MAAM,CACV,SAAS,EAAE,SAAS,EACpB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,UAAU,CAAC;IAsDtB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,YAAY;YAIN,kBAAkB;IAiDhC,OAAO,CAAC,SAAS;IA+DjB,OAAO,CAAC,QAAQ;IA6BhB,OAAO,CAAC,cAAc;IAuFtB,OAAO,CAAC,eAAe;IAyDvB,OAAO,CAAC,cAAc;IAoDtB,OAAO,CAAC,SAAS;CAQlB"}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// Default quality for WebP encoding when not specified
|
|
2
|
+
const DEFAULT_WEBP_QUALITY = 90;
|
|
3
|
+
/**
|
|
4
|
+
* WebP format handler
|
|
5
|
+
* Implements a basic WebP decoder and encoder
|
|
6
|
+
*/
|
|
7
|
+
export class WebPFormat {
|
|
8
|
+
constructor() {
|
|
9
|
+
Object.defineProperty(this, "name", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: "webp"
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "mimeType", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: "image/webp"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
canDecode(data) {
|
|
23
|
+
// WebP signature: "RIFF" + size + "WEBP"
|
|
24
|
+
return data.length >= 12 &&
|
|
25
|
+
data[0] === 0x52 && data[1] === 0x49 && // "RI"
|
|
26
|
+
data[2] === 0x46 && data[3] === 0x46 && // "FF"
|
|
27
|
+
data[8] === 0x57 && data[9] === 0x45 && // "WE"
|
|
28
|
+
data[10] === 0x42 && data[11] === 0x50; // "BP"
|
|
29
|
+
}
|
|
30
|
+
async decode(data) {
|
|
31
|
+
if (!this.canDecode(data)) {
|
|
32
|
+
throw new Error("Invalid WebP signature");
|
|
33
|
+
}
|
|
34
|
+
// Parse WebP structure
|
|
35
|
+
let pos = 12; // Skip RIFF header
|
|
36
|
+
let width = 0;
|
|
37
|
+
let height = 0;
|
|
38
|
+
const metadata = {};
|
|
39
|
+
// Read all chunks to extract metadata
|
|
40
|
+
while (pos + 8 <= data.length) {
|
|
41
|
+
const chunkType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
42
|
+
const chunkSize = this.readUint32LE(data, pos + 4);
|
|
43
|
+
pos += 8;
|
|
44
|
+
// Stop if we've gone past the end
|
|
45
|
+
if (pos + chunkSize > data.length)
|
|
46
|
+
break;
|
|
47
|
+
const chunkData = data.slice(pos, pos + chunkSize);
|
|
48
|
+
if (chunkType === "VP8 ") {
|
|
49
|
+
// Lossy format - extract dimensions
|
|
50
|
+
if (chunkData.length >= 10) {
|
|
51
|
+
const frameTag = chunkData[0] | (chunkData[1] << 8) |
|
|
52
|
+
(chunkData[2] << 16);
|
|
53
|
+
const keyFrame = (frameTag & 1) === 0;
|
|
54
|
+
if (keyFrame && chunkData[3] === 0x9d && chunkData[4] === 0x01 &&
|
|
55
|
+
chunkData[5] === 0x2a) {
|
|
56
|
+
width = chunkData[6] | ((chunkData[7] & 0x3f) << 8);
|
|
57
|
+
height = chunkData[8] | ((chunkData[9] & 0x3f) << 8);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else if (chunkType === "VP8L") {
|
|
62
|
+
// Lossless format - extract dimensions
|
|
63
|
+
if (chunkData.length >= 5 && chunkData[0] === 0x2f) {
|
|
64
|
+
const bits = this.readUint32LE(chunkData, 1);
|
|
65
|
+
width = (bits & 0x3fff) + 1;
|
|
66
|
+
height = ((bits >> 14) & 0x3fff) + 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (chunkType === "VP8X") {
|
|
70
|
+
// Extended format - extract dimensions
|
|
71
|
+
if (chunkData.length >= 10) {
|
|
72
|
+
width = this.readUint24LE(chunkData, 4) + 1;
|
|
73
|
+
height = this.readUint24LE(chunkData, 7) + 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (chunkType === "EXIF") {
|
|
77
|
+
// EXIF metadata chunk
|
|
78
|
+
this.parseEXIF(chunkData, metadata);
|
|
79
|
+
}
|
|
80
|
+
else if (chunkType === "XMP ") {
|
|
81
|
+
// XMP metadata chunk
|
|
82
|
+
this.parseXMP(chunkData, metadata);
|
|
83
|
+
}
|
|
84
|
+
pos += chunkSize;
|
|
85
|
+
// Chunks are padded to even length
|
|
86
|
+
if (chunkSize % 2 === 1)
|
|
87
|
+
pos++;
|
|
88
|
+
}
|
|
89
|
+
if (width === 0 || height === 0) {
|
|
90
|
+
throw new Error("Could not determine WebP dimensions");
|
|
91
|
+
}
|
|
92
|
+
// For a pure JS implementation, we'd need to implement full WebP decoding
|
|
93
|
+
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
94
|
+
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
95
|
+
return {
|
|
96
|
+
width,
|
|
97
|
+
height,
|
|
98
|
+
data: rgba,
|
|
99
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async encode(imageData, options) {
|
|
103
|
+
const { width, height, data, metadata } = imageData;
|
|
104
|
+
const quality = options?.quality ?? DEFAULT_WEBP_QUALITY;
|
|
105
|
+
const forceLossless = options?.lossless ?? false;
|
|
106
|
+
// Determine if we should use lossless encoding
|
|
107
|
+
// Use lossless if: quality is 100, or lossless flag is set
|
|
108
|
+
const useLossless = quality === 100 || forceLossless;
|
|
109
|
+
// Try to use runtime encoding if available (better quality and compression)
|
|
110
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
111
|
+
try {
|
|
112
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
113
|
+
const ctx = canvas.getContext("2d");
|
|
114
|
+
if (ctx) {
|
|
115
|
+
const imgData = ctx.createImageData(width, height);
|
|
116
|
+
const imgDataData = new Uint8ClampedArray(data);
|
|
117
|
+
imgData.data.set(imgDataData);
|
|
118
|
+
ctx.putImageData(imgData, 0, 0);
|
|
119
|
+
const blob = await canvas.convertToBlob({
|
|
120
|
+
type: "image/webp",
|
|
121
|
+
quality: quality / 100, // Convert 1-100 to 0-1
|
|
122
|
+
});
|
|
123
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
124
|
+
const encoded = new Uint8Array(arrayBuffer);
|
|
125
|
+
// Inject metadata if present
|
|
126
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
127
|
+
const injected = this.injectMetadata(encoded, metadata);
|
|
128
|
+
return injected;
|
|
129
|
+
}
|
|
130
|
+
return encoded;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (_error) {
|
|
134
|
+
// Fall through to pure JS encoder
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Fallback to pure JavaScript encoder
|
|
138
|
+
// VP8L (lossless) encoder with optional quality-based quantization
|
|
139
|
+
const { WebPEncoder } = await import("../utils/webp_encoder.js");
|
|
140
|
+
const encoder = new WebPEncoder(width, height, data);
|
|
141
|
+
const encoded = encoder.encode(useLossless ? 100 : quality);
|
|
142
|
+
// Inject metadata if present
|
|
143
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
144
|
+
return this.injectMetadata(encoded, metadata);
|
|
145
|
+
}
|
|
146
|
+
return encoded;
|
|
147
|
+
}
|
|
148
|
+
readUint32LE(data, offset) {
|
|
149
|
+
return data[offset] | (data[offset + 1] << 8) |
|
|
150
|
+
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
151
|
+
}
|
|
152
|
+
readUint24LE(data, offset) {
|
|
153
|
+
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
|
|
154
|
+
}
|
|
155
|
+
async decodeUsingRuntime(data, _width, _height) {
|
|
156
|
+
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
157
|
+
if (typeof ImageDecoder !== "undefined") {
|
|
158
|
+
try {
|
|
159
|
+
const decoder = new ImageDecoder({ data, type: "image/webp" });
|
|
160
|
+
const result = await decoder.decode();
|
|
161
|
+
const bitmap = result.image;
|
|
162
|
+
// Create a canvas to extract pixel data
|
|
163
|
+
const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
|
|
164
|
+
const ctx = canvas.getContext("2d");
|
|
165
|
+
if (!ctx)
|
|
166
|
+
throw new Error("Could not get canvas context");
|
|
167
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
168
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
169
|
+
bitmap.close();
|
|
170
|
+
return new Uint8Array(imageData.data.buffer);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// ImageDecoder API failed, fall through to pure JS decoder
|
|
174
|
+
console.warn("WebP decoding with ImageDecoder failed, using pure JS decoder:", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Fallback to pure JavaScript decoder (VP8L lossless only)
|
|
178
|
+
try {
|
|
179
|
+
const { WebPDecoder } = await import("../utils/webp_decoder.js");
|
|
180
|
+
const decoder = new WebPDecoder(data);
|
|
181
|
+
const result = decoder.decode();
|
|
182
|
+
return result.data;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
throw new Error(`WebP decoding failed: ${error}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Metadata parsing and injection methods
|
|
189
|
+
parseEXIF(data, metadata) {
|
|
190
|
+
// EXIF data parsing (similar to JPEG/PNG EXIF parsing)
|
|
191
|
+
if (data.length < 8)
|
|
192
|
+
return;
|
|
193
|
+
try {
|
|
194
|
+
const byteOrder = String.fromCharCode(data[0], data[1]);
|
|
195
|
+
const littleEndian = byteOrder === "II";
|
|
196
|
+
const ifd0Offset = littleEndian
|
|
197
|
+
? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
|
|
198
|
+
: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
|
|
199
|
+
if (ifd0Offset + 2 > data.length)
|
|
200
|
+
return;
|
|
201
|
+
const numEntries = littleEndian
|
|
202
|
+
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
203
|
+
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
204
|
+
// Parse basic EXIF tags (simplified version)
|
|
205
|
+
for (let i = 0; i < numEntries && i < 50; i++) {
|
|
206
|
+
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
207
|
+
if (entryOffset + 12 > data.length)
|
|
208
|
+
break;
|
|
209
|
+
const tag = littleEndian
|
|
210
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
211
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
212
|
+
// DateTime tag
|
|
213
|
+
if (tag === 0x0132) {
|
|
214
|
+
const valueOffset = littleEndian
|
|
215
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
216
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
217
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
218
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
219
|
+
if (valueOffset < data.length) {
|
|
220
|
+
const endIndex = data.indexOf(0, valueOffset);
|
|
221
|
+
if (endIndex > valueOffset) {
|
|
222
|
+
const dateStr = new TextDecoder().decode(data.slice(valueOffset, endIndex));
|
|
223
|
+
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
224
|
+
if (match) {
|
|
225
|
+
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
catch (_e) {
|
|
233
|
+
// Ignore EXIF parsing errors
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
parseXMP(data, metadata) {
|
|
237
|
+
// XMP is XML-based metadata - simple parsing for common fields
|
|
238
|
+
try {
|
|
239
|
+
const xmpStr = new TextDecoder().decode(data);
|
|
240
|
+
// Extract title
|
|
241
|
+
const titleMatch = xmpStr.match(/<dc:title[^>]*>([^<]+)<\/dc:title>/);
|
|
242
|
+
if (titleMatch)
|
|
243
|
+
metadata.title = titleMatch[1].trim();
|
|
244
|
+
// Extract description
|
|
245
|
+
const descMatch = xmpStr.match(/<dc:description[^>]*>([^<]+)<\/dc:description>/);
|
|
246
|
+
if (descMatch)
|
|
247
|
+
metadata.description = descMatch[1].trim();
|
|
248
|
+
// Extract creator/author
|
|
249
|
+
const creatorMatch = xmpStr.match(/<dc:creator[^>]*>([^<]+)<\/dc:creator>/);
|
|
250
|
+
if (creatorMatch)
|
|
251
|
+
metadata.author = creatorMatch[1].trim();
|
|
252
|
+
// Extract rights/copyright
|
|
253
|
+
const rightsMatch = xmpStr.match(/<dc:rights[^>]*>([^<]+)<\/dc:rights>/);
|
|
254
|
+
if (rightsMatch)
|
|
255
|
+
metadata.copyright = rightsMatch[1].trim();
|
|
256
|
+
}
|
|
257
|
+
catch (_e) {
|
|
258
|
+
// Ignore XMP parsing errors
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
injectMetadata(webpData, metadata) {
|
|
262
|
+
// WebP files are RIFF containers: RIFF + size + WEBP + chunks
|
|
263
|
+
// We need to inject EXIF and/or XMP chunks before the image data
|
|
264
|
+
const chunks = [];
|
|
265
|
+
// Copy RIFF header (12 bytes)
|
|
266
|
+
chunks.push(webpData.slice(0, 12));
|
|
267
|
+
// Create metadata chunks
|
|
268
|
+
const metadataChunks = [];
|
|
269
|
+
// Create EXIF chunk if we have date or other EXIF data
|
|
270
|
+
if (metadata.creationDate) {
|
|
271
|
+
const exifData = this.createEXIFChunk(metadata);
|
|
272
|
+
if (exifData) {
|
|
273
|
+
metadataChunks.push(exifData);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Create XMP chunk if we have text metadata
|
|
277
|
+
if (metadata.title || metadata.description || metadata.author ||
|
|
278
|
+
metadata.copyright) {
|
|
279
|
+
const xmpData = this.createXMPChunk(metadata);
|
|
280
|
+
if (xmpData) {
|
|
281
|
+
metadataChunks.push(xmpData);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// Copy original chunks (skip header)
|
|
285
|
+
let pos = 12;
|
|
286
|
+
while (pos + 8 <= webpData.length) {
|
|
287
|
+
const chunkType = String.fromCharCode(webpData[pos], webpData[pos + 1], webpData[pos + 2], webpData[pos + 3]);
|
|
288
|
+
const chunkSize = this.readUint32LE(webpData, pos + 4);
|
|
289
|
+
// Don't copy existing EXIF/XMP chunks (we'll add new ones)
|
|
290
|
+
if (chunkType !== "EXIF" && chunkType !== "XMP ") {
|
|
291
|
+
const chunkEnd = pos + 8 + chunkSize + (chunkSize % 2);
|
|
292
|
+
chunks.push(webpData.slice(pos, chunkEnd));
|
|
293
|
+
}
|
|
294
|
+
pos += 8 + chunkSize;
|
|
295
|
+
if (chunkSize % 2 === 1)
|
|
296
|
+
pos++; // Padding
|
|
297
|
+
}
|
|
298
|
+
// Insert metadata chunks after VP8/VP8L/VP8X chunk
|
|
299
|
+
const result = [chunks[0]]; // RIFF header
|
|
300
|
+
if (chunks.length > 1) {
|
|
301
|
+
result.push(chunks[1]); // First chunk (VP8/VP8L/VP8X)
|
|
302
|
+
}
|
|
303
|
+
result.push(...metadataChunks);
|
|
304
|
+
for (let i = 2; i < chunks.length; i++) {
|
|
305
|
+
result.push(chunks[i]);
|
|
306
|
+
}
|
|
307
|
+
// Recalculate total size
|
|
308
|
+
const totalSize = result.reduce((sum, chunk) => sum + chunk.length, 0) - 8;
|
|
309
|
+
const finalData = new Uint8Array(totalSize + 8);
|
|
310
|
+
// Write RIFF header with updated size
|
|
311
|
+
finalData.set(new TextEncoder().encode("RIFF"), 0);
|
|
312
|
+
finalData[4] = totalSize & 0xff;
|
|
313
|
+
finalData[5] = (totalSize >> 8) & 0xff;
|
|
314
|
+
finalData[6] = (totalSize >> 16) & 0xff;
|
|
315
|
+
finalData[7] = (totalSize >> 24) & 0xff;
|
|
316
|
+
finalData.set(new TextEncoder().encode("WEBP"), 8);
|
|
317
|
+
// Copy all chunks
|
|
318
|
+
let offset = 12;
|
|
319
|
+
for (let i = 1; i < result.length; i++) {
|
|
320
|
+
finalData.set(result[i], offset);
|
|
321
|
+
offset += result[i].length;
|
|
322
|
+
}
|
|
323
|
+
return finalData;
|
|
324
|
+
}
|
|
325
|
+
createEXIFChunk(metadata) {
|
|
326
|
+
if (!metadata.creationDate)
|
|
327
|
+
return null;
|
|
328
|
+
const exifData = [];
|
|
329
|
+
// Byte order marker (little endian)
|
|
330
|
+
exifData.push(0x49, 0x49); // "II"
|
|
331
|
+
exifData.push(0x2a, 0x00); // 42
|
|
332
|
+
// IFD0 offset
|
|
333
|
+
exifData.push(0x08, 0x00, 0x00, 0x00);
|
|
334
|
+
// Number of entries
|
|
335
|
+
exifData.push(0x01, 0x00);
|
|
336
|
+
// DateTime entry
|
|
337
|
+
const date = metadata.creationDate;
|
|
338
|
+
const dateStr = `${date.getFullYear()}:${String(date.getMonth() + 1).padStart(2, "0")}:${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}\0`;
|
|
339
|
+
const dateBytes = new TextEncoder().encode(dateStr);
|
|
340
|
+
// Tag 0x0132, Type 2 (ASCII), Count, Offset
|
|
341
|
+
exifData.push(0x32, 0x01, 0x02, 0x00);
|
|
342
|
+
exifData.push(dateBytes.length & 0xff, (dateBytes.length >> 8) & 0xff, (dateBytes.length >> 16) & 0xff, (dateBytes.length >> 24) & 0xff);
|
|
343
|
+
exifData.push(0x12, 0x00, 0x00, 0x00); // Offset to data
|
|
344
|
+
// Next IFD
|
|
345
|
+
exifData.push(0x00, 0x00, 0x00, 0x00);
|
|
346
|
+
// Date string data
|
|
347
|
+
for (const byte of dateBytes) {
|
|
348
|
+
exifData.push(byte);
|
|
349
|
+
}
|
|
350
|
+
// Create chunk header
|
|
351
|
+
const chunkData = new Uint8Array(exifData);
|
|
352
|
+
const chunk = new Uint8Array(8 + chunkData.length);
|
|
353
|
+
chunk.set(new TextEncoder().encode("EXIF"), 0);
|
|
354
|
+
chunk[4] = chunkData.length & 0xff;
|
|
355
|
+
chunk[5] = (chunkData.length >> 8) & 0xff;
|
|
356
|
+
chunk[6] = (chunkData.length >> 16) & 0xff;
|
|
357
|
+
chunk[7] = (chunkData.length >> 24) & 0xff;
|
|
358
|
+
chunk.set(chunkData, 8);
|
|
359
|
+
return chunk;
|
|
360
|
+
}
|
|
361
|
+
createXMPChunk(metadata) {
|
|
362
|
+
const xmpParts = [];
|
|
363
|
+
xmpParts.push('<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>');
|
|
364
|
+
xmpParts.push('<x:xmpmeta xmlns:x="adobe:ns:meta/">');
|
|
365
|
+
xmpParts.push('<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">');
|
|
366
|
+
xmpParts.push('<rdf:Description xmlns:dc="http://purl.org/dc/elements/1.1/">');
|
|
367
|
+
if (metadata.title) {
|
|
368
|
+
xmpParts.push(`<dc:title>${this.escapeXML(metadata.title)}</dc:title>`);
|
|
369
|
+
}
|
|
370
|
+
if (metadata.description) {
|
|
371
|
+
xmpParts.push(`<dc:description>${this.escapeXML(metadata.description)}</dc:description>`);
|
|
372
|
+
}
|
|
373
|
+
if (metadata.author) {
|
|
374
|
+
xmpParts.push(`<dc:creator>${this.escapeXML(metadata.author)}</dc:creator>`);
|
|
375
|
+
}
|
|
376
|
+
if (metadata.copyright) {
|
|
377
|
+
xmpParts.push(`<dc:rights>${this.escapeXML(metadata.copyright)}</dc:rights>`);
|
|
378
|
+
}
|
|
379
|
+
xmpParts.push("</rdf:Description>");
|
|
380
|
+
xmpParts.push("</rdf:RDF>");
|
|
381
|
+
xmpParts.push("</x:xmpmeta>");
|
|
382
|
+
xmpParts.push('<?xpacket end="w"?>');
|
|
383
|
+
const xmpStr = xmpParts.join("\n");
|
|
384
|
+
const xmpData = new TextEncoder().encode(xmpStr);
|
|
385
|
+
// Create chunk
|
|
386
|
+
const chunk = new Uint8Array(8 + xmpData.length);
|
|
387
|
+
chunk.set(new TextEncoder().encode("XMP "), 0);
|
|
388
|
+
chunk[4] = xmpData.length & 0xff;
|
|
389
|
+
chunk[5] = (xmpData.length >> 8) & 0xff;
|
|
390
|
+
chunk[6] = (xmpData.length >> 16) & 0xff;
|
|
391
|
+
chunk[7] = (xmpData.length >> 24) & 0xff;
|
|
392
|
+
chunk.set(xmpData, 8);
|
|
393
|
+
return chunk;
|
|
394
|
+
}
|
|
395
|
+
escapeXML(str) {
|
|
396
|
+
return str
|
|
397
|
+
.replace(/&/g, "&")
|
|
398
|
+
.replace(/</g, "<")
|
|
399
|
+
.replace(/>/g, ">")
|
|
400
|
+
.replace(/"/g, """)
|
|
401
|
+
.replace(/'/g, "'");
|
|
402
|
+
}
|
|
403
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ImageFormat, ImageMetadata, MultiFrameImageData, ResizeOptions } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Main Image class for reading, manipulating, and saving images
|
|
4
|
+
*/
|
|
5
|
+
export declare class Image {
|
|
6
|
+
private imageData;
|
|
7
|
+
private static formats;
|
|
8
|
+
/**
|
|
9
|
+
* Get the current image width
|
|
10
|
+
*/
|
|
11
|
+
get width(): number;
|
|
12
|
+
/**
|
|
13
|
+
* Get the current image height
|
|
14
|
+
*/
|
|
15
|
+
get height(): number;
|
|
16
|
+
/**
|
|
17
|
+
* Get the current image data
|
|
18
|
+
*/
|
|
19
|
+
get data(): Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Get the current image metadata
|
|
22
|
+
*/
|
|
23
|
+
get metadata(): ImageMetadata | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Set or update image metadata
|
|
26
|
+
* @param metadata Metadata to set or merge
|
|
27
|
+
* @param merge If true, merges with existing metadata. If false, replaces it. Default: true
|
|
28
|
+
*/
|
|
29
|
+
setMetadata(metadata: ImageMetadata, merge?: boolean): this;
|
|
30
|
+
/**
|
|
31
|
+
* Get a specific metadata field
|
|
32
|
+
* @param key The metadata field to retrieve
|
|
33
|
+
* @returns The metadata value or undefined
|
|
34
|
+
*/
|
|
35
|
+
getMetadataField<K extends keyof ImageMetadata>(key: K): ImageMetadata[K] | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Get position (latitude, longitude) from metadata
|
|
38
|
+
* @returns Object with latitude and longitude, or undefined if not available
|
|
39
|
+
*/
|
|
40
|
+
getPosition(): {
|
|
41
|
+
latitude: number;
|
|
42
|
+
longitude: number;
|
|
43
|
+
} | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Set position (latitude, longitude) in metadata
|
|
46
|
+
* @param latitude GPS latitude
|
|
47
|
+
* @param longitude GPS longitude
|
|
48
|
+
*/
|
|
49
|
+
setPosition(latitude: number, longitude: number): this;
|
|
50
|
+
/**
|
|
51
|
+
* Get physical dimensions from metadata
|
|
52
|
+
* @returns Object with DPI and physical dimensions, or undefined if not available
|
|
53
|
+
*/
|
|
54
|
+
getDimensions(): {
|
|
55
|
+
dpiX?: number;
|
|
56
|
+
dpiY?: number;
|
|
57
|
+
physicalWidth?: number;
|
|
58
|
+
physicalHeight?: number;
|
|
59
|
+
} | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Set physical dimensions in metadata
|
|
62
|
+
* @param dpiX Dots per inch (horizontal)
|
|
63
|
+
* @param dpiY Dots per inch (vertical), defaults to dpiX if not provided
|
|
64
|
+
*/
|
|
65
|
+
setDPI(dpiX: number, dpiY?: number): this;
|
|
66
|
+
/**
|
|
67
|
+
* Register a custom image format
|
|
68
|
+
* @param format Custom format implementation
|
|
69
|
+
*/
|
|
70
|
+
static registerFormat(format: ImageFormat): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get all registered formats
|
|
73
|
+
*/
|
|
74
|
+
static getFormats(): readonly ImageFormat[];
|
|
75
|
+
/**
|
|
76
|
+
* Read an image from bytes
|
|
77
|
+
* @param data Raw image data
|
|
78
|
+
* @param format Optional format hint (e.g., "png", "jpeg", "webp")
|
|
79
|
+
* @returns Image instance
|
|
80
|
+
*/
|
|
81
|
+
static read(data: Uint8Array, format?: string): Promise<Image>;
|
|
82
|
+
/**
|
|
83
|
+
* Read all frames from a multi-frame image (GIF animation, multi-page TIFF)
|
|
84
|
+
* @param data Raw image data
|
|
85
|
+
* @param format Optional format hint (e.g., "gif", "tiff")
|
|
86
|
+
* @returns MultiFrameImageData with all frames
|
|
87
|
+
*/
|
|
88
|
+
static readFrames(data: Uint8Array, format?: string): Promise<MultiFrameImageData>;
|
|
89
|
+
/**
|
|
90
|
+
* Save multi-frame image data to bytes in the specified format
|
|
91
|
+
* @param format Format name (e.g., "gif", "tiff")
|
|
92
|
+
* @param imageData Multi-frame image data to save
|
|
93
|
+
* @param options Optional format-specific encoding options
|
|
94
|
+
* @returns Encoded image bytes
|
|
95
|
+
*/
|
|
96
|
+
static saveFrames(format: string, imageData: MultiFrameImageData, options?: unknown): Promise<Uint8Array>;
|
|
97
|
+
/**
|
|
98
|
+
* Create an image from raw RGBA data
|
|
99
|
+
* @param width Image width
|
|
100
|
+
* @param height Image height
|
|
101
|
+
* @param data Raw RGBA pixel data (4 bytes per pixel)
|
|
102
|
+
* @returns Image instance
|
|
103
|
+
*/
|
|
104
|
+
static fromRGBA(width: number, height: number, data: Uint8Array): Image;
|
|
105
|
+
/**
|
|
106
|
+
* Resize the image
|
|
107
|
+
* @param options Resize options
|
|
108
|
+
* @returns This image instance for chaining
|
|
109
|
+
*/
|
|
110
|
+
resize(options: ResizeOptions): this;
|
|
111
|
+
/**
|
|
112
|
+
* Save the image to bytes in the specified format
|
|
113
|
+
* @param format Format name (e.g., "png", "jpeg", "webp", "ascii")
|
|
114
|
+
* @param options Optional format-specific encoding options
|
|
115
|
+
* @returns Encoded image bytes
|
|
116
|
+
*/
|
|
117
|
+
save(format: string, options?: unknown): Promise<Uint8Array>;
|
|
118
|
+
/**
|
|
119
|
+
* Clone this image
|
|
120
|
+
* @returns New image instance with copied data and metadata
|
|
121
|
+
*/
|
|
122
|
+
clone(): Image;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=image.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../src/src/image.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,WAAW,EACX,aAAa,EACb,mBAAmB,EACnB,aAAa,EACd,MAAM,YAAY,CAAC;AAWpB;;GAEG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,MAAM,CAAC,OAAO,CASpB;IAEF;;OAEG;IACH,IAAI,KAAK,IAAI,MAAM,CAGlB;IAED;;OAEG;IACH,IAAI,MAAM,IAAI,MAAM,CAGnB;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,UAAU,CAGrB;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,aAAa,GAAG,SAAS,CAGxC;IAED;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,UAAO,GAAG,IAAI;IAmBxD;;;;OAIG;IACH,gBAAgB,CAAC,CAAC,SAAS,MAAM,aAAa,EAC5C,GAAG,EAAE,CAAC,GACL,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS;IAK/B;;;OAGG;IACH,WAAW,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS;IAWlE;;;;OAIG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI;IAItD;;;OAGG;IACH,aAAa,IAAI;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,GAAG,SAAS;IAgBb;;;;OAIG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAezC;;;OAGG;IACH,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI;IAIhD;;OAEG;IACH,MAAM,CAAC,UAAU,IAAI,SAAS,WAAW,EAAE;IAI3C;;;;;OAKG;WACU,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAuBpE;;;;;OAKG;WACU,UAAU,CACrB,IAAI,EAAE,UAAU,EAChB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,mBAAmB,CAAC;IAqB/B;;;;;;OAMG;WACU,UAAU,CACrB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,mBAAmB,EAC9B,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC,UAAU,CAAC;IAgBtB;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,KAAK;IAcvE;;;;OAIG;IACH,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAqCpC;;;;;OAKG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IAWlE;;;OAGG;IACH,KAAK,IAAI,KAAK;CAmBf"}
|