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,544 @@
|
|
|
1
|
+
// Constants for unit conversions
|
|
2
|
+
const INCHES_PER_METER = 39.3701;
|
|
3
|
+
/**
|
|
4
|
+
* PNG format handler
|
|
5
|
+
* Implements a pure JavaScript PNG decoder and encoder
|
|
6
|
+
*/
|
|
7
|
+
export class PNGFormat {
|
|
8
|
+
constructor() {
|
|
9
|
+
Object.defineProperty(this, "name", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: "png"
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "mimeType", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: "image/png"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
canDecode(data) {
|
|
23
|
+
// PNG signature: 137 80 78 71 13 10 26 10
|
|
24
|
+
return data.length >= 8 &&
|
|
25
|
+
data[0] === 137 && data[1] === 80 &&
|
|
26
|
+
data[2] === 78 && data[3] === 71 &&
|
|
27
|
+
data[4] === 13 && data[5] === 10 &&
|
|
28
|
+
data[6] === 26 && data[7] === 10;
|
|
29
|
+
}
|
|
30
|
+
async decode(data) {
|
|
31
|
+
if (!this.canDecode(data)) {
|
|
32
|
+
throw new Error("Invalid PNG signature");
|
|
33
|
+
}
|
|
34
|
+
let pos = 8; // Skip PNG signature
|
|
35
|
+
let width = 0;
|
|
36
|
+
let height = 0;
|
|
37
|
+
let bitDepth = 0;
|
|
38
|
+
let colorType = 0;
|
|
39
|
+
const chunks = [];
|
|
40
|
+
const metadata = {};
|
|
41
|
+
// Parse chunks
|
|
42
|
+
while (pos < data.length) {
|
|
43
|
+
const length = this.readUint32(data, pos);
|
|
44
|
+
pos += 4;
|
|
45
|
+
const type = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
46
|
+
pos += 4;
|
|
47
|
+
const chunkData = data.slice(pos, pos + length);
|
|
48
|
+
pos += length;
|
|
49
|
+
pos += 4; // Skip CRC
|
|
50
|
+
if (type === "IHDR") {
|
|
51
|
+
width = this.readUint32(chunkData, 0);
|
|
52
|
+
height = this.readUint32(chunkData, 4);
|
|
53
|
+
bitDepth = chunkData[8];
|
|
54
|
+
colorType = chunkData[9];
|
|
55
|
+
}
|
|
56
|
+
else if (type === "IDAT") {
|
|
57
|
+
chunks.push({ type, data: chunkData });
|
|
58
|
+
}
|
|
59
|
+
else if (type === "pHYs") {
|
|
60
|
+
// Physical pixel dimensions
|
|
61
|
+
this.parsePhysChunk(chunkData, metadata, width, height);
|
|
62
|
+
}
|
|
63
|
+
else if (type === "tEXt") {
|
|
64
|
+
// Text chunk
|
|
65
|
+
this.parseTextChunk(chunkData, metadata);
|
|
66
|
+
}
|
|
67
|
+
else if (type === "iTXt") {
|
|
68
|
+
// International text chunk
|
|
69
|
+
this.parseITxtChunk(chunkData, metadata);
|
|
70
|
+
}
|
|
71
|
+
else if (type === "eXIf") {
|
|
72
|
+
// EXIF chunk
|
|
73
|
+
this.parseExifChunk(chunkData, metadata);
|
|
74
|
+
}
|
|
75
|
+
else if (type === "IEND") {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (width === 0 || height === 0) {
|
|
80
|
+
throw new Error("Invalid PNG: missing IHDR chunk");
|
|
81
|
+
}
|
|
82
|
+
// Concatenate IDAT chunks
|
|
83
|
+
const idatData = this.concatenateChunks(chunks);
|
|
84
|
+
// Decompress data
|
|
85
|
+
const decompressed = await this.inflate(idatData);
|
|
86
|
+
// Unfilter and convert to RGBA
|
|
87
|
+
const rgba = this.unfilterAndConvert(decompressed, width, height, bitDepth, colorType);
|
|
88
|
+
return {
|
|
89
|
+
width,
|
|
90
|
+
height,
|
|
91
|
+
data: rgba,
|
|
92
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async encode(imageData) {
|
|
96
|
+
const { width, height, data, metadata } = imageData;
|
|
97
|
+
// Prepare IHDR chunk
|
|
98
|
+
const ihdr = new Uint8Array(13);
|
|
99
|
+
this.writeUint32(ihdr, 0, width);
|
|
100
|
+
this.writeUint32(ihdr, 4, height);
|
|
101
|
+
ihdr[8] = 8; // bit depth
|
|
102
|
+
ihdr[9] = 6; // color type: RGBA
|
|
103
|
+
ihdr[10] = 0; // compression method
|
|
104
|
+
ihdr[11] = 0; // filter method
|
|
105
|
+
ihdr[12] = 0; // interlace method
|
|
106
|
+
// Filter and compress image data
|
|
107
|
+
const filtered = this.filterData(data, width, height);
|
|
108
|
+
const compressed = await this.deflate(filtered);
|
|
109
|
+
// Build PNG
|
|
110
|
+
const chunks = [];
|
|
111
|
+
chunks.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])); // PNG signature
|
|
112
|
+
chunks.push(this.createChunk("IHDR", ihdr));
|
|
113
|
+
// Add metadata chunks if available
|
|
114
|
+
if (metadata) {
|
|
115
|
+
// Add pHYs chunk for DPI information
|
|
116
|
+
if (metadata.dpiX !== undefined || metadata.dpiY !== undefined) {
|
|
117
|
+
const physChunk = this.createPhysChunk(metadata);
|
|
118
|
+
chunks.push(this.createChunk("pHYs", physChunk));
|
|
119
|
+
}
|
|
120
|
+
// Add tEXt chunks for standard metadata
|
|
121
|
+
if (metadata.title !== undefined) {
|
|
122
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Title", metadata.title)));
|
|
123
|
+
}
|
|
124
|
+
if (metadata.author !== undefined) {
|
|
125
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Author", metadata.author)));
|
|
126
|
+
}
|
|
127
|
+
if (metadata.description !== undefined) {
|
|
128
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Description", metadata.description)));
|
|
129
|
+
}
|
|
130
|
+
if (metadata.copyright !== undefined) {
|
|
131
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Copyright", metadata.copyright)));
|
|
132
|
+
}
|
|
133
|
+
// Add custom metadata fields
|
|
134
|
+
if (metadata.custom) {
|
|
135
|
+
for (const [key, value] of Object.entries(metadata.custom)) {
|
|
136
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk(key, String(value))));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Add EXIF chunk for GPS data and creation date
|
|
140
|
+
if (metadata.latitude !== undefined || metadata.longitude !== undefined ||
|
|
141
|
+
metadata.creationDate !== undefined) {
|
|
142
|
+
const exifChunk = this.createExifChunk(metadata);
|
|
143
|
+
if (exifChunk) {
|
|
144
|
+
chunks.push(this.createChunk("eXIf", exifChunk));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
chunks.push(this.createChunk("IDAT", compressed));
|
|
149
|
+
chunks.push(this.createChunk("IEND", new Uint8Array(0)));
|
|
150
|
+
// Concatenate all chunks
|
|
151
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
152
|
+
const result = new Uint8Array(totalLength);
|
|
153
|
+
let offset = 0;
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
result.set(chunk, offset);
|
|
156
|
+
offset += chunk.length;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
readUint32(data, offset) {
|
|
161
|
+
return (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
162
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
163
|
+
}
|
|
164
|
+
writeUint32(data, offset, value) {
|
|
165
|
+
data[offset] = (value >>> 24) & 0xff;
|
|
166
|
+
data[offset + 1] = (value >>> 16) & 0xff;
|
|
167
|
+
data[offset + 2] = (value >>> 8) & 0xff;
|
|
168
|
+
data[offset + 3] = value & 0xff;
|
|
169
|
+
}
|
|
170
|
+
concatenateChunks(chunks) {
|
|
171
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.data.length, 0);
|
|
172
|
+
const result = new Uint8Array(totalLength);
|
|
173
|
+
let offset = 0;
|
|
174
|
+
for (const chunk of chunks) {
|
|
175
|
+
result.set(chunk.data, offset);
|
|
176
|
+
offset += chunk.data.length;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
async inflate(data) {
|
|
181
|
+
// Use DecompressionStream API (available in Deno, Node 17+, and browsers)
|
|
182
|
+
const stream = new Response(data).body
|
|
183
|
+
.pipeThrough(new DecompressionStream("deflate"));
|
|
184
|
+
const decompressed = await new Response(stream).arrayBuffer();
|
|
185
|
+
return new Uint8Array(decompressed);
|
|
186
|
+
}
|
|
187
|
+
async deflate(data) {
|
|
188
|
+
// Use CompressionStream API (available in Deno, Node 17+, and browsers)
|
|
189
|
+
const stream = new Response(data).body
|
|
190
|
+
.pipeThrough(new CompressionStream("deflate"));
|
|
191
|
+
const compressed = await new Response(stream).arrayBuffer();
|
|
192
|
+
return new Uint8Array(compressed);
|
|
193
|
+
}
|
|
194
|
+
unfilterAndConvert(data, width, height, bitDepth, colorType) {
|
|
195
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
196
|
+
const bytesPerPixel = this.getBytesPerPixel(colorType, bitDepth);
|
|
197
|
+
const scanlineLength = width * bytesPerPixel;
|
|
198
|
+
let dataPos = 0;
|
|
199
|
+
const scanlines = [];
|
|
200
|
+
for (let y = 0; y < height; y++) {
|
|
201
|
+
const filterType = data[dataPos++];
|
|
202
|
+
const scanline = new Uint8Array(scanlineLength);
|
|
203
|
+
for (let x = 0; x < scanlineLength; x++) {
|
|
204
|
+
scanline[x] = data[dataPos++];
|
|
205
|
+
}
|
|
206
|
+
this.unfilterScanline(scanline, y > 0 ? scanlines[y - 1] : null, filterType, bytesPerPixel);
|
|
207
|
+
scanlines.push(scanline);
|
|
208
|
+
// Convert to RGBA
|
|
209
|
+
for (let x = 0; x < width; x++) {
|
|
210
|
+
const outIdx = (y * width + x) * 4;
|
|
211
|
+
if (colorType === 6) { // RGBA
|
|
212
|
+
rgba[outIdx] = scanline[x * 4];
|
|
213
|
+
rgba[outIdx + 1] = scanline[x * 4 + 1];
|
|
214
|
+
rgba[outIdx + 2] = scanline[x * 4 + 2];
|
|
215
|
+
rgba[outIdx + 3] = scanline[x * 4 + 3];
|
|
216
|
+
}
|
|
217
|
+
else if (colorType === 2) { // RGB
|
|
218
|
+
rgba[outIdx] = scanline[x * 3];
|
|
219
|
+
rgba[outIdx + 1] = scanline[x * 3 + 1];
|
|
220
|
+
rgba[outIdx + 2] = scanline[x * 3 + 2];
|
|
221
|
+
rgba[outIdx + 3] = 255;
|
|
222
|
+
}
|
|
223
|
+
else if (colorType === 0) { // Grayscale
|
|
224
|
+
const gray = scanline[x];
|
|
225
|
+
rgba[outIdx] = gray;
|
|
226
|
+
rgba[outIdx + 1] = gray;
|
|
227
|
+
rgba[outIdx + 2] = gray;
|
|
228
|
+
rgba[outIdx + 3] = 255;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
throw new Error(`Unsupported PNG color type: ${colorType}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return rgba;
|
|
236
|
+
}
|
|
237
|
+
unfilterScanline(scanline, prevLine, filterType, bytesPerPixel) {
|
|
238
|
+
for (let x = 0; x < scanline.length; x++) {
|
|
239
|
+
const left = x >= bytesPerPixel ? scanline[x - bytesPerPixel] : 0;
|
|
240
|
+
const above = prevLine ? prevLine[x] : 0;
|
|
241
|
+
const upperLeft = (x >= bytesPerPixel && prevLine)
|
|
242
|
+
? prevLine[x - bytesPerPixel]
|
|
243
|
+
: 0;
|
|
244
|
+
switch (filterType) {
|
|
245
|
+
case 0: // None
|
|
246
|
+
break;
|
|
247
|
+
case 1: // Sub
|
|
248
|
+
scanline[x] = (scanline[x] + left) & 0xff;
|
|
249
|
+
break;
|
|
250
|
+
case 2: // Up
|
|
251
|
+
scanline[x] = (scanline[x] + above) & 0xff;
|
|
252
|
+
break;
|
|
253
|
+
case 3: // Average
|
|
254
|
+
scanline[x] = (scanline[x] + Math.floor((left + above) / 2)) & 0xff;
|
|
255
|
+
break;
|
|
256
|
+
case 4: // Paeth
|
|
257
|
+
scanline[x] =
|
|
258
|
+
(scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
paethPredictor(a, b, c) {
|
|
264
|
+
const p = a + b - c;
|
|
265
|
+
const pa = Math.abs(p - a);
|
|
266
|
+
const pb = Math.abs(p - b);
|
|
267
|
+
const pc = Math.abs(p - c);
|
|
268
|
+
if (pa <= pb && pa <= pc)
|
|
269
|
+
return a;
|
|
270
|
+
if (pb <= pc)
|
|
271
|
+
return b;
|
|
272
|
+
return c;
|
|
273
|
+
}
|
|
274
|
+
filterData(data, width, height) {
|
|
275
|
+
// Use filter type 0 (None) for simplicity
|
|
276
|
+
const filtered = new Uint8Array(height * (1 + width * 4));
|
|
277
|
+
let pos = 0;
|
|
278
|
+
for (let y = 0; y < height; y++) {
|
|
279
|
+
filtered[pos++] = 0; // Filter type: None
|
|
280
|
+
for (let x = 0; x < width * 4; x++) {
|
|
281
|
+
filtered[pos++] = data[y * width * 4 + x];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return filtered;
|
|
285
|
+
}
|
|
286
|
+
getBytesPerPixel(colorType, bitDepth) {
|
|
287
|
+
const bitsPerPixel = this.getBitsPerPixel(colorType, bitDepth);
|
|
288
|
+
return Math.ceil(bitsPerPixel / 8);
|
|
289
|
+
}
|
|
290
|
+
getBitsPerPixel(colorType, bitDepth) {
|
|
291
|
+
switch (colorType) {
|
|
292
|
+
case 0: // Grayscale
|
|
293
|
+
return bitDepth;
|
|
294
|
+
case 2: // RGB
|
|
295
|
+
return bitDepth * 3;
|
|
296
|
+
case 3: // Palette
|
|
297
|
+
return bitDepth;
|
|
298
|
+
case 4: // Grayscale + Alpha
|
|
299
|
+
return bitDepth * 2;
|
|
300
|
+
case 6: // RGBA
|
|
301
|
+
return bitDepth * 4;
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Unknown color type: ${colorType}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
createChunk(type, data) {
|
|
307
|
+
const chunk = new Uint8Array(12 + data.length);
|
|
308
|
+
this.writeUint32(chunk, 0, data.length);
|
|
309
|
+
chunk[4] = type.charCodeAt(0);
|
|
310
|
+
chunk[5] = type.charCodeAt(1);
|
|
311
|
+
chunk[6] = type.charCodeAt(2);
|
|
312
|
+
chunk[7] = type.charCodeAt(3);
|
|
313
|
+
chunk.set(data, 8);
|
|
314
|
+
const crc = this.crc32(chunk.slice(4, 8 + data.length));
|
|
315
|
+
this.writeUint32(chunk, 8 + data.length, crc);
|
|
316
|
+
return chunk;
|
|
317
|
+
}
|
|
318
|
+
crc32(data) {
|
|
319
|
+
let crc = 0xffffffff;
|
|
320
|
+
for (let i = 0; i < data.length; i++) {
|
|
321
|
+
crc ^= data[i];
|
|
322
|
+
for (let j = 0; j < 8; j++) {
|
|
323
|
+
crc = (crc & 1) ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
327
|
+
}
|
|
328
|
+
// Metadata parsing methods
|
|
329
|
+
parsePhysChunk(data, metadata, width, height) {
|
|
330
|
+
if (data.length < 9)
|
|
331
|
+
return;
|
|
332
|
+
const pixelsPerUnitX = this.readUint32(data, 0);
|
|
333
|
+
const pixelsPerUnitY = this.readUint32(data, 4);
|
|
334
|
+
const unit = data[8]; // 0 = unknown, 1 = meter
|
|
335
|
+
if (unit === 1 && pixelsPerUnitX > 0 && pixelsPerUnitY > 0) {
|
|
336
|
+
// Convert pixels per meter to DPI
|
|
337
|
+
metadata.dpiX = Math.round(pixelsPerUnitX / INCHES_PER_METER);
|
|
338
|
+
metadata.dpiY = Math.round(pixelsPerUnitY / INCHES_PER_METER);
|
|
339
|
+
metadata.physicalWidth = width / metadata.dpiX;
|
|
340
|
+
metadata.physicalHeight = height / metadata.dpiY;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
parseTextChunk(data, metadata) {
|
|
344
|
+
// tEXt format: keyword\0text
|
|
345
|
+
const nullIndex = data.indexOf(0);
|
|
346
|
+
if (nullIndex === -1)
|
|
347
|
+
return;
|
|
348
|
+
const keyword = new TextDecoder().decode(data.slice(0, nullIndex));
|
|
349
|
+
const text = new TextDecoder().decode(data.slice(nullIndex + 1));
|
|
350
|
+
// Map standard keywords to metadata fields
|
|
351
|
+
switch (keyword.toLowerCase()) {
|
|
352
|
+
case "title":
|
|
353
|
+
metadata.title = text;
|
|
354
|
+
break;
|
|
355
|
+
case "author":
|
|
356
|
+
metadata.author = text;
|
|
357
|
+
break;
|
|
358
|
+
case "description":
|
|
359
|
+
metadata.description = text;
|
|
360
|
+
break;
|
|
361
|
+
case "copyright":
|
|
362
|
+
metadata.copyright = text;
|
|
363
|
+
break;
|
|
364
|
+
default:
|
|
365
|
+
// Store as custom metadata
|
|
366
|
+
if (!metadata.custom)
|
|
367
|
+
metadata.custom = {};
|
|
368
|
+
metadata.custom[keyword] = text;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
parseITxtChunk(data, metadata) {
|
|
372
|
+
// iTXt format: keyword\0compressed_flag\0compression_method\0language\0translated_keyword\0text
|
|
373
|
+
let pos = 0;
|
|
374
|
+
const nullIndex = data.indexOf(0, pos);
|
|
375
|
+
if (nullIndex === -1 || pos >= data.length)
|
|
376
|
+
return;
|
|
377
|
+
const keyword = new TextDecoder().decode(data.slice(pos, nullIndex));
|
|
378
|
+
pos = nullIndex + 1;
|
|
379
|
+
if (pos + 2 > data.length)
|
|
380
|
+
return; // Need at least 2 bytes for flags
|
|
381
|
+
const _compressionFlag = data[pos++];
|
|
382
|
+
const _compressionMethod = data[pos++];
|
|
383
|
+
const languageNullIndex = data.indexOf(0, pos);
|
|
384
|
+
if (languageNullIndex === -1 || pos >= data.length)
|
|
385
|
+
return;
|
|
386
|
+
pos = languageNullIndex + 1;
|
|
387
|
+
const translatedKeywordNullIndex = data.indexOf(0, pos);
|
|
388
|
+
if (translatedKeywordNullIndex === -1 || pos >= data.length)
|
|
389
|
+
return;
|
|
390
|
+
pos = translatedKeywordNullIndex + 1;
|
|
391
|
+
if (pos >= data.length)
|
|
392
|
+
return; // No text data
|
|
393
|
+
const text = new TextDecoder("utf-8").decode(data.slice(pos));
|
|
394
|
+
// Map to metadata fields (same as tEXt)
|
|
395
|
+
switch (keyword.toLowerCase()) {
|
|
396
|
+
case "title":
|
|
397
|
+
metadata.title = text;
|
|
398
|
+
break;
|
|
399
|
+
case "author":
|
|
400
|
+
metadata.author = text;
|
|
401
|
+
break;
|
|
402
|
+
case "description":
|
|
403
|
+
metadata.description = text;
|
|
404
|
+
break;
|
|
405
|
+
case "copyright":
|
|
406
|
+
metadata.copyright = text;
|
|
407
|
+
break;
|
|
408
|
+
default:
|
|
409
|
+
if (!metadata.custom)
|
|
410
|
+
metadata.custom = {};
|
|
411
|
+
metadata.custom[keyword] = text;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
parseExifChunk(data, metadata) {
|
|
415
|
+
// Basic EXIF parsing for GPS and date
|
|
416
|
+
// EXIF data starts with byte order marker
|
|
417
|
+
if (data.length < 8)
|
|
418
|
+
return;
|
|
419
|
+
try {
|
|
420
|
+
const byteOrder = String.fromCharCode(data[0], data[1]);
|
|
421
|
+
const littleEndian = byteOrder === "II";
|
|
422
|
+
// Skip to IFD0 offset
|
|
423
|
+
const ifd0Offset = littleEndian
|
|
424
|
+
? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
|
|
425
|
+
: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
|
|
426
|
+
if (ifd0Offset + 2 > data.length)
|
|
427
|
+
return;
|
|
428
|
+
// Read number of IFD entries with bounds check
|
|
429
|
+
const numEntries = littleEndian
|
|
430
|
+
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
431
|
+
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
432
|
+
// Parse IFD entries looking for GPS and DateTime tags
|
|
433
|
+
for (let i = 0; i < numEntries; i++) {
|
|
434
|
+
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
435
|
+
if (entryOffset + 12 > data.length)
|
|
436
|
+
break;
|
|
437
|
+
const tag = littleEndian
|
|
438
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
439
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
440
|
+
// DateTime tag (0x0132)
|
|
441
|
+
if (tag === 0x0132) {
|
|
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 nullIndex = data.indexOf(0, valueOffset);
|
|
449
|
+
if (nullIndex > valueOffset) {
|
|
450
|
+
const dateStr = new TextDecoder().decode(data.slice(valueOffset, nullIndex));
|
|
451
|
+
// Parse EXIF datetime format: "YYYY:MM:DD HH:MM:SS"
|
|
452
|
+
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
453
|
+
if (match) {
|
|
454
|
+
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
catch (_e) {
|
|
462
|
+
// Ignore EXIF parsing errors
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
createPhysChunk(metadata) {
|
|
466
|
+
const chunk = new Uint8Array(9);
|
|
467
|
+
// Default to 72 DPI if not specified
|
|
468
|
+
const dpiX = metadata.dpiX ?? 72;
|
|
469
|
+
const dpiY = metadata.dpiY ?? 72;
|
|
470
|
+
// Convert DPI to pixels per meter
|
|
471
|
+
const pixelsPerMeterX = Math.round(dpiX * INCHES_PER_METER);
|
|
472
|
+
const pixelsPerMeterY = Math.round(dpiY * INCHES_PER_METER);
|
|
473
|
+
this.writeUint32(chunk, 0, pixelsPerMeterX);
|
|
474
|
+
this.writeUint32(chunk, 4, pixelsPerMeterY);
|
|
475
|
+
chunk[8] = 1; // Unit is meters
|
|
476
|
+
return chunk;
|
|
477
|
+
}
|
|
478
|
+
createTextChunk(keyword, text) {
|
|
479
|
+
const keywordBytes = new TextEncoder().encode(keyword);
|
|
480
|
+
const textBytes = new TextEncoder().encode(text);
|
|
481
|
+
const chunk = new Uint8Array(keywordBytes.length + 1 + textBytes.length);
|
|
482
|
+
chunk.set(keywordBytes, 0);
|
|
483
|
+
chunk[keywordBytes.length] = 0; // Null separator
|
|
484
|
+
chunk.set(textBytes, keywordBytes.length + 1);
|
|
485
|
+
return chunk;
|
|
486
|
+
}
|
|
487
|
+
createExifChunk(metadata) {
|
|
488
|
+
// Create a minimal EXIF structure
|
|
489
|
+
const entries = [];
|
|
490
|
+
// Add DateTime if available
|
|
491
|
+
if (metadata.creationDate) {
|
|
492
|
+
const date = metadata.creationDate;
|
|
493
|
+
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`;
|
|
494
|
+
entries.push({
|
|
495
|
+
tag: 0x0132,
|
|
496
|
+
type: 2, // ASCII
|
|
497
|
+
value: new TextEncoder().encode(dateStr),
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (entries.length === 0)
|
|
501
|
+
return null;
|
|
502
|
+
// Build minimal EXIF structure
|
|
503
|
+
const exif = [];
|
|
504
|
+
// Byte order marker (little endian)
|
|
505
|
+
exif.push(0x49, 0x49); // "II"
|
|
506
|
+
exif.push(0x2a, 0x00); // 42
|
|
507
|
+
// Offset to IFD0 (8 bytes from start)
|
|
508
|
+
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
509
|
+
// Number of IFD entries
|
|
510
|
+
exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
|
|
511
|
+
// Calculate data offset (after all entries)
|
|
512
|
+
let dataOffset = 8 + 2 + entries.length * 12 + 4;
|
|
513
|
+
for (const entry of entries) {
|
|
514
|
+
// Tag
|
|
515
|
+
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
516
|
+
// Type
|
|
517
|
+
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
518
|
+
// Count
|
|
519
|
+
const count = entry.value.length;
|
|
520
|
+
exif.push(count & 0xff, (count >> 8) & 0xff, (count >> 16) & 0xff, (count >> 24) & 0xff);
|
|
521
|
+
// Value/Offset
|
|
522
|
+
if (entry.value.length <= 4) {
|
|
523
|
+
for (let i = 0; i < 4; i++) {
|
|
524
|
+
exif.push(i < entry.value.length ? entry.value[i] : 0);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
exif.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
529
|
+
dataOffset += entry.value.length;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Next IFD offset (0 = no more IFDs)
|
|
533
|
+
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
534
|
+
// Append data for entries that didn't fit in value field
|
|
535
|
+
for (const entry of entries) {
|
|
536
|
+
if (entry.value.length > 4) {
|
|
537
|
+
for (const byte of entry.value) {
|
|
538
|
+
exif.push(byte);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return new Uint8Array(exif);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* RAW format handler
|
|
4
|
+
* Implements a simple uncompressed RGBA format with a minimal header
|
|
5
|
+
*
|
|
6
|
+
* Format structure:
|
|
7
|
+
* - Magic bytes (4 bytes): "RGBA" (0x52 0x47 0x42 0x41)
|
|
8
|
+
* - Width (4 bytes, big-endian)
|
|
9
|
+
* - Height (4 bytes, big-endian)
|
|
10
|
+
* - RGBA pixel data (width * height * 4 bytes)
|
|
11
|
+
*/
|
|
12
|
+
export declare class RAWFormat implements ImageFormat {
|
|
13
|
+
readonly name = "raw";
|
|
14
|
+
readonly mimeType = "image/raw";
|
|
15
|
+
private readonly MAGIC_BYTES;
|
|
16
|
+
private readonly HEADER_SIZE;
|
|
17
|
+
canDecode(data: Uint8Array): boolean;
|
|
18
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
19
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
20
|
+
private readUint32;
|
|
21
|
+
private writeUint32;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=raw.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raw.d.ts","sourceRoot":"","sources":["../../../src/src/formats/raw.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1D;;;;;;;;;GASG;AACH,qBAAa,SAAU,YAAW,WAAW;IAC3C,QAAQ,CAAC,IAAI,SAAS;IACtB,QAAQ,CAAC,QAAQ,eAAe;IAEhC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4C;IACxE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAM;IAElC,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAYpC,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IA8B5C,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IA4BjD,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,WAAW;CAMpB"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAW format handler
|
|
3
|
+
* Implements a simple uncompressed RGBA format with a minimal header
|
|
4
|
+
*
|
|
5
|
+
* Format structure:
|
|
6
|
+
* - Magic bytes (4 bytes): "RGBA" (0x52 0x47 0x42 0x41)
|
|
7
|
+
* - Width (4 bytes, big-endian)
|
|
8
|
+
* - Height (4 bytes, big-endian)
|
|
9
|
+
* - RGBA pixel data (width * height * 4 bytes)
|
|
10
|
+
*/
|
|
11
|
+
export class RAWFormat {
|
|
12
|
+
constructor() {
|
|
13
|
+
Object.defineProperty(this, "name", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: "raw"
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "mimeType", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: "image/raw"
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "MAGIC_BYTES", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: new Uint8Array([0x52, 0x47, 0x42, 0x41])
|
|
30
|
+
}); // "RGBA"
|
|
31
|
+
Object.defineProperty(this, "HEADER_SIZE", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: 12
|
|
36
|
+
}); // 4 bytes magic + 4 bytes width + 4 bytes height
|
|
37
|
+
}
|
|
38
|
+
canDecode(data) {
|
|
39
|
+
// Check if data has at least header size and matches magic bytes
|
|
40
|
+
if (data.length < this.HEADER_SIZE) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return data[0] === this.MAGIC_BYTES[0] &&
|
|
44
|
+
data[1] === this.MAGIC_BYTES[1] &&
|
|
45
|
+
data[2] === this.MAGIC_BYTES[2] &&
|
|
46
|
+
data[3] === this.MAGIC_BYTES[3];
|
|
47
|
+
}
|
|
48
|
+
decode(data) {
|
|
49
|
+
if (!this.canDecode(data)) {
|
|
50
|
+
throw new Error("Invalid RAW signature");
|
|
51
|
+
}
|
|
52
|
+
// Read width and height from header (big-endian)
|
|
53
|
+
const width = this.readUint32(data, 4);
|
|
54
|
+
const height = this.readUint32(data, 8);
|
|
55
|
+
// Validate dimensions
|
|
56
|
+
if (width <= 0 || height <= 0) {
|
|
57
|
+
throw new Error(`Invalid RAW dimensions: ${width}x${height}`);
|
|
58
|
+
}
|
|
59
|
+
const expectedDataLength = width * height * 4;
|
|
60
|
+
const actualDataLength = data.length - this.HEADER_SIZE;
|
|
61
|
+
if (actualDataLength !== expectedDataLength) {
|
|
62
|
+
throw new Error(`Invalid RAW data length: expected ${expectedDataLength}, got ${actualDataLength}`);
|
|
63
|
+
}
|
|
64
|
+
// Extract pixel data
|
|
65
|
+
const pixelData = new Uint8Array(expectedDataLength);
|
|
66
|
+
pixelData.set(data.subarray(this.HEADER_SIZE));
|
|
67
|
+
return Promise.resolve({ width, height, data: pixelData });
|
|
68
|
+
}
|
|
69
|
+
encode(imageData) {
|
|
70
|
+
const { width, height, data } = imageData;
|
|
71
|
+
// Validate input
|
|
72
|
+
if (data.length !== width * height * 4) {
|
|
73
|
+
throw new Error(`Data length mismatch: expected ${width * height * 4}, got ${data.length}`);
|
|
74
|
+
}
|
|
75
|
+
// Create output buffer with header + pixel data
|
|
76
|
+
const output = new Uint8Array(this.HEADER_SIZE + data.length);
|
|
77
|
+
// Write magic bytes
|
|
78
|
+
output.set(this.MAGIC_BYTES, 0);
|
|
79
|
+
// Write width and height (big-endian)
|
|
80
|
+
this.writeUint32(output, 4, width);
|
|
81
|
+
this.writeUint32(output, 8, height);
|
|
82
|
+
// Write pixel data
|
|
83
|
+
output.set(data, this.HEADER_SIZE);
|
|
84
|
+
return Promise.resolve(output);
|
|
85
|
+
}
|
|
86
|
+
readUint32(data, offset) {
|
|
87
|
+
return ((data[offset] << 24) |
|
|
88
|
+
(data[offset + 1] << 16) |
|
|
89
|
+
(data[offset + 2] << 8) |
|
|
90
|
+
data[offset + 3]);
|
|
91
|
+
}
|
|
92
|
+
writeUint32(data, offset, value) {
|
|
93
|
+
data[offset] = (value >>> 24) & 0xff;
|
|
94
|
+
data[offset + 1] = (value >>> 16) & 0xff;
|
|
95
|
+
data[offset + 2] = (value >>> 8) & 0xff;
|
|
96
|
+
data[offset + 3] = value & 0xff;
|
|
97
|
+
}
|
|
98
|
+
}
|