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,414 @@
|
|
|
1
|
+
// Constants for unit conversions
|
|
2
|
+
const CM_PER_INCH = 2.54;
|
|
3
|
+
/**
|
|
4
|
+
* JPEG format handler
|
|
5
|
+
* Implements a basic JPEG decoder and encoder
|
|
6
|
+
*/
|
|
7
|
+
export class JPEGFormat {
|
|
8
|
+
constructor() {
|
|
9
|
+
Object.defineProperty(this, "name", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: "jpeg"
|
|
14
|
+
});
|
|
15
|
+
Object.defineProperty(this, "mimeType", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: "image/jpeg"
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
canDecode(data) {
|
|
23
|
+
// JPEG signature: FF D8 FF
|
|
24
|
+
return data.length >= 3 &&
|
|
25
|
+
data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff;
|
|
26
|
+
}
|
|
27
|
+
async decode(data) {
|
|
28
|
+
if (!this.canDecode(data)) {
|
|
29
|
+
throw new Error("Invalid JPEG signature");
|
|
30
|
+
}
|
|
31
|
+
// Parse JPEG structure to get dimensions and metadata
|
|
32
|
+
let pos = 2; // Skip initial FF D8
|
|
33
|
+
let width = 0;
|
|
34
|
+
let height = 0;
|
|
35
|
+
const metadata = {};
|
|
36
|
+
while (pos < data.length - 1) {
|
|
37
|
+
if (data[pos] !== 0xff) {
|
|
38
|
+
pos++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const marker = data[pos + 1];
|
|
42
|
+
pos += 2;
|
|
43
|
+
// SOF markers (Start of Frame)
|
|
44
|
+
if (marker >= 0xc0 && marker <= 0xcf &&
|
|
45
|
+
marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
46
|
+
const _length = (data[pos] << 8) | data[pos + 1];
|
|
47
|
+
// precision at pos+2
|
|
48
|
+
height = (data[pos + 3] << 8) | data[pos + 4];
|
|
49
|
+
width = (data[pos + 5] << 8) | data[pos + 6];
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
// APP0 marker (JFIF)
|
|
53
|
+
if (marker === 0xe0) {
|
|
54
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
55
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
56
|
+
this.parseJFIF(appData, metadata, width, height);
|
|
57
|
+
pos += length;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
// APP1 marker (EXIF)
|
|
61
|
+
if (marker === 0xe1) {
|
|
62
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
63
|
+
const appData = data.slice(pos + 2, pos + length);
|
|
64
|
+
this.parseEXIF(appData, metadata);
|
|
65
|
+
pos += length;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Skip other markers
|
|
69
|
+
if (marker === 0xd9 || marker === 0xda)
|
|
70
|
+
break; // EOI or SOS
|
|
71
|
+
if (marker >= 0xd0 && marker <= 0xd8)
|
|
72
|
+
continue; // RST markers have no length
|
|
73
|
+
if (marker === 0x01)
|
|
74
|
+
continue; // TEM has no length
|
|
75
|
+
const length = (data[pos] << 8) | data[pos + 1];
|
|
76
|
+
pos += length;
|
|
77
|
+
}
|
|
78
|
+
if (width === 0 || height === 0) {
|
|
79
|
+
throw new Error("Could not determine JPEG dimensions");
|
|
80
|
+
}
|
|
81
|
+
// For a pure JS implementation, we'd need to implement full JPEG decoding
|
|
82
|
+
// which is very complex. Instead, we'll use the browser/runtime's decoder.
|
|
83
|
+
const rgba = await this.decodeUsingRuntime(data, width, height);
|
|
84
|
+
return {
|
|
85
|
+
width,
|
|
86
|
+
height,
|
|
87
|
+
data: rgba,
|
|
88
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async encode(imageData) {
|
|
92
|
+
const { width, height, data, metadata } = imageData;
|
|
93
|
+
// Try to use runtime encoding if available (better quality)
|
|
94
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
95
|
+
try {
|
|
96
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
97
|
+
const ctx = canvas.getContext("2d");
|
|
98
|
+
if (ctx) {
|
|
99
|
+
const imgData = ctx.createImageData(width, height);
|
|
100
|
+
const imgDataData = new Uint8ClampedArray(data);
|
|
101
|
+
imgData.data.set(imgDataData);
|
|
102
|
+
ctx.putImageData(imgData, 0, 0);
|
|
103
|
+
const blob = await canvas.convertToBlob({
|
|
104
|
+
type: "image/jpeg",
|
|
105
|
+
quality: 0.9,
|
|
106
|
+
});
|
|
107
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
108
|
+
const encoded = new Uint8Array(arrayBuffer);
|
|
109
|
+
// If we have metadata, we need to inject it into the JPEG
|
|
110
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
111
|
+
return this.injectMetadata(encoded, metadata);
|
|
112
|
+
}
|
|
113
|
+
return encoded;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (_error) {
|
|
117
|
+
// Fall through to pure JS encoder
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Fallback to pure JavaScript encoder
|
|
121
|
+
const { JPEGEncoder } = await import("../utils/jpeg_encoder.js");
|
|
122
|
+
const dpiX = metadata?.dpiX ?? 72;
|
|
123
|
+
const dpiY = metadata?.dpiY ?? 72;
|
|
124
|
+
const encoder = new JPEGEncoder(85); // Quality 85
|
|
125
|
+
const encoded = encoder.encode(width, height, data, dpiX, dpiY);
|
|
126
|
+
// Add EXIF metadata if present
|
|
127
|
+
if (metadata && Object.keys(metadata).length > 0) {
|
|
128
|
+
return this.injectMetadata(encoded, metadata);
|
|
129
|
+
}
|
|
130
|
+
return encoded;
|
|
131
|
+
}
|
|
132
|
+
injectMetadata(encoded, metadata) {
|
|
133
|
+
// Find the position after SOI and APP0 to inject APP1 (EXIF)
|
|
134
|
+
let pos = 2; // After SOI (0xFF 0xD8)
|
|
135
|
+
// Skip APP0 if present
|
|
136
|
+
if (pos + 2 < encoded.length && encoded[pos] === 0xff &&
|
|
137
|
+
encoded[pos + 1] === 0xe0) {
|
|
138
|
+
const length = (encoded[pos + 2] << 8) | encoded[pos + 3];
|
|
139
|
+
pos += length + 2;
|
|
140
|
+
}
|
|
141
|
+
// Create EXIF data
|
|
142
|
+
const exifData = this.createEXIFData(metadata);
|
|
143
|
+
if (exifData.length === 0) {
|
|
144
|
+
return encoded;
|
|
145
|
+
}
|
|
146
|
+
// Create APP1 marker with EXIF data
|
|
147
|
+
// APP1 structure: FF E1 [length 2 bytes] "Exif\0\0" [exif data]
|
|
148
|
+
const app1Length = 2 + 6 + exifData.length; // length field + "Exif\0\0" + data
|
|
149
|
+
const app1 = new Uint8Array(2 + 2 + 6 + exifData.length); // marker + length + "Exif\0\0" + data
|
|
150
|
+
app1[0] = 0xff;
|
|
151
|
+
app1[1] = 0xe1; // APP1 marker
|
|
152
|
+
app1[2] = (app1Length >> 8) & 0xff;
|
|
153
|
+
app1[3] = app1Length & 0xff;
|
|
154
|
+
app1[4] = 0x45; // 'E'
|
|
155
|
+
app1[5] = 0x78; // 'x'
|
|
156
|
+
app1[6] = 0x69; // 'i'
|
|
157
|
+
app1[7] = 0x66; // 'f'
|
|
158
|
+
app1[8] = 0x00;
|
|
159
|
+
app1[9] = 0x00;
|
|
160
|
+
app1.set(exifData, 10);
|
|
161
|
+
// Inject APP1 into the JPEG
|
|
162
|
+
const result = new Uint8Array(encoded.length + app1.length);
|
|
163
|
+
result.set(encoded.slice(0, pos), 0);
|
|
164
|
+
result.set(app1, pos);
|
|
165
|
+
result.set(encoded.slice(pos), pos + app1.length);
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
async decodeUsingRuntime(data, _width, _height) {
|
|
169
|
+
// Try to use ImageDecoder API if available (Deno, modern browsers)
|
|
170
|
+
if (typeof ImageDecoder !== "undefined") {
|
|
171
|
+
try {
|
|
172
|
+
const decoder = new ImageDecoder({ data, type: "image/jpeg" });
|
|
173
|
+
const result = await decoder.decode();
|
|
174
|
+
const bitmap = result.image;
|
|
175
|
+
// Create a canvas to extract pixel data
|
|
176
|
+
const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
|
|
177
|
+
const ctx = canvas.getContext("2d");
|
|
178
|
+
if (!ctx)
|
|
179
|
+
throw new Error("Could not get canvas context");
|
|
180
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
181
|
+
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
182
|
+
bitmap.close();
|
|
183
|
+
return new Uint8Array(imageData.data.buffer);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
// ImageDecoder API failed, fall through to pure JS decoder
|
|
187
|
+
console.warn("JPEG decoding with ImageDecoder failed, using pure JS decoder:", error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Fallback to pure JavaScript decoder
|
|
191
|
+
try {
|
|
192
|
+
const { JPEGDecoder } = await import("../utils/jpeg_decoder.js");
|
|
193
|
+
const decoder = new JPEGDecoder(data);
|
|
194
|
+
return decoder.decode();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
throw new Error(`JPEG decoding failed: ${error}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Metadata parsing and creation methods
|
|
201
|
+
parseJFIF(data, metadata, width, height) {
|
|
202
|
+
// JFIF format: "JFIF\0" version units xDensity yDensity
|
|
203
|
+
if (data.length < 14)
|
|
204
|
+
return;
|
|
205
|
+
// Check for JFIF identifier
|
|
206
|
+
if (data[0] !== 0x4a || data[1] !== 0x46 || data[2] !== 0x49 ||
|
|
207
|
+
data[3] !== 0x46 || data[4] !== 0x00)
|
|
208
|
+
return;
|
|
209
|
+
const units = data[7]; // 0=no units, 1=dpi, 2=dpcm
|
|
210
|
+
const xDensity = (data[8] << 8) | data[9];
|
|
211
|
+
const yDensity = (data[10] << 8) | data[11];
|
|
212
|
+
if (units === 1 && xDensity > 0 && yDensity > 0) {
|
|
213
|
+
// Units are DPI
|
|
214
|
+
metadata.dpiX = xDensity;
|
|
215
|
+
metadata.dpiY = yDensity;
|
|
216
|
+
metadata.physicalWidth = width / xDensity;
|
|
217
|
+
metadata.physicalHeight = height / yDensity;
|
|
218
|
+
}
|
|
219
|
+
else if (units === 2 && xDensity > 0 && yDensity > 0) {
|
|
220
|
+
// Units are dots per cm, convert to DPI
|
|
221
|
+
metadata.dpiX = Math.round(xDensity * CM_PER_INCH);
|
|
222
|
+
metadata.dpiY = Math.round(yDensity * CM_PER_INCH);
|
|
223
|
+
metadata.physicalWidth = width / metadata.dpiX;
|
|
224
|
+
metadata.physicalHeight = height / metadata.dpiY;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
parseEXIF(data, metadata) {
|
|
228
|
+
// Check for EXIF identifier
|
|
229
|
+
if (data.length < 6 || data[0] !== 0x45 || data[1] !== 0x78 ||
|
|
230
|
+
data[2] !== 0x69 || data[3] !== 0x66 || data[4] !== 0x00 ||
|
|
231
|
+
data[5] !== 0x00)
|
|
232
|
+
return;
|
|
233
|
+
// Skip "Exif\0\0" header
|
|
234
|
+
const exifData = data.slice(6);
|
|
235
|
+
if (exifData.length < 8)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
const byteOrder = String.fromCharCode(exifData[0], exifData[1]);
|
|
239
|
+
const littleEndian = byteOrder === "II";
|
|
240
|
+
// Read IFD0 offset
|
|
241
|
+
const ifd0Offset = littleEndian
|
|
242
|
+
? exifData[4] | (exifData[5] << 8) | (exifData[6] << 16) |
|
|
243
|
+
(exifData[7] << 24)
|
|
244
|
+
: (exifData[4] << 24) | (exifData[5] << 16) | (exifData[6] << 8) |
|
|
245
|
+
exifData[7];
|
|
246
|
+
if (ifd0Offset + 2 > exifData.length)
|
|
247
|
+
return;
|
|
248
|
+
// Read number of entries with bounds check
|
|
249
|
+
const numEntries = littleEndian
|
|
250
|
+
? exifData[ifd0Offset] | (exifData[ifd0Offset + 1] << 8)
|
|
251
|
+
: (exifData[ifd0Offset] << 8) | exifData[ifd0Offset + 1];
|
|
252
|
+
// Parse entries
|
|
253
|
+
for (let i = 0; i < numEntries; i++) {
|
|
254
|
+
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
255
|
+
if (entryOffset + 12 > exifData.length)
|
|
256
|
+
break;
|
|
257
|
+
const tag = littleEndian
|
|
258
|
+
? exifData[entryOffset] | (exifData[entryOffset + 1] << 8)
|
|
259
|
+
: (exifData[entryOffset] << 8) | exifData[entryOffset + 1];
|
|
260
|
+
// DateTime tag (0x0132)
|
|
261
|
+
if (tag === 0x0132) {
|
|
262
|
+
const valueOffset = littleEndian
|
|
263
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
264
|
+
(exifData[entryOffset + 10] << 16) |
|
|
265
|
+
(exifData[entryOffset + 11] << 24)
|
|
266
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
267
|
+
(exifData[entryOffset + 9] << 16) |
|
|
268
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
269
|
+
if (valueOffset < exifData.length) {
|
|
270
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
271
|
+
if (endIndex > valueOffset) {
|
|
272
|
+
const dateStr = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
273
|
+
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
274
|
+
if (match) {
|
|
275
|
+
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ImageDescription tag (0x010E)
|
|
281
|
+
if (tag === 0x010e) {
|
|
282
|
+
const valueOffset = littleEndian
|
|
283
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
284
|
+
(exifData[entryOffset + 10] << 16) |
|
|
285
|
+
(exifData[entryOffset + 11] << 24)
|
|
286
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
287
|
+
(exifData[entryOffset + 9] << 16) |
|
|
288
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
289
|
+
if (valueOffset < exifData.length) {
|
|
290
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
291
|
+
if (endIndex > valueOffset) {
|
|
292
|
+
metadata.description = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Artist tag (0x013B)
|
|
297
|
+
if (tag === 0x013b) {
|
|
298
|
+
const valueOffset = littleEndian
|
|
299
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
300
|
+
(exifData[entryOffset + 10] << 16) |
|
|
301
|
+
(exifData[entryOffset + 11] << 24)
|
|
302
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
303
|
+
(exifData[entryOffset + 9] << 16) |
|
|
304
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
305
|
+
if (valueOffset < exifData.length) {
|
|
306
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
307
|
+
if (endIndex > valueOffset) {
|
|
308
|
+
metadata.author = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Copyright tag (0x8298)
|
|
313
|
+
if (tag === 0x8298) {
|
|
314
|
+
const valueOffset = littleEndian
|
|
315
|
+
? exifData[entryOffset + 8] | (exifData[entryOffset + 9] << 8) |
|
|
316
|
+
(exifData[entryOffset + 10] << 16) |
|
|
317
|
+
(exifData[entryOffset + 11] << 24)
|
|
318
|
+
: (exifData[entryOffset + 8] << 24) |
|
|
319
|
+
(exifData[entryOffset + 9] << 16) |
|
|
320
|
+
(exifData[entryOffset + 10] << 8) | exifData[entryOffset + 11];
|
|
321
|
+
if (valueOffset < exifData.length) {
|
|
322
|
+
const endIndex = exifData.indexOf(0, valueOffset);
|
|
323
|
+
if (endIndex > valueOffset) {
|
|
324
|
+
metadata.copyright = new TextDecoder().decode(exifData.slice(valueOffset, endIndex));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
catch (_e) {
|
|
331
|
+
// Ignore EXIF parsing errors
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
createEXIFData(metadata) {
|
|
335
|
+
const entries = [];
|
|
336
|
+
// Add DateTime if available
|
|
337
|
+
if (metadata.creationDate) {
|
|
338
|
+
const date = metadata.creationDate;
|
|
339
|
+
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`;
|
|
340
|
+
entries.push({
|
|
341
|
+
tag: 0x0132,
|
|
342
|
+
type: 2, // ASCII
|
|
343
|
+
value: new TextEncoder().encode(dateStr),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// Add ImageDescription
|
|
347
|
+
if (metadata.description) {
|
|
348
|
+
entries.push({
|
|
349
|
+
tag: 0x010e,
|
|
350
|
+
type: 2,
|
|
351
|
+
value: new TextEncoder().encode(metadata.description + "\0"),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Add Artist
|
|
355
|
+
if (metadata.author) {
|
|
356
|
+
entries.push({
|
|
357
|
+
tag: 0x013b,
|
|
358
|
+
type: 2,
|
|
359
|
+
value: new TextEncoder().encode(metadata.author + "\0"),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// Add Copyright
|
|
363
|
+
if (metadata.copyright) {
|
|
364
|
+
entries.push({
|
|
365
|
+
tag: 0x8298,
|
|
366
|
+
type: 2,
|
|
367
|
+
value: new TextEncoder().encode(metadata.copyright + "\0"),
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
if (entries.length === 0)
|
|
371
|
+
return [];
|
|
372
|
+
// Build EXIF structure
|
|
373
|
+
const exif = [];
|
|
374
|
+
// Byte order marker (little endian)
|
|
375
|
+
exif.push(0x49, 0x49); // "II"
|
|
376
|
+
exif.push(0x2a, 0x00); // 42
|
|
377
|
+
// Offset to IFD0 (8 bytes from start)
|
|
378
|
+
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
379
|
+
// Number of entries
|
|
380
|
+
exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
|
|
381
|
+
// Calculate data offset
|
|
382
|
+
let dataOffset = 8 + 2 + entries.length * 12 + 4;
|
|
383
|
+
for (const entry of entries) {
|
|
384
|
+
// Tag
|
|
385
|
+
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
386
|
+
// Type
|
|
387
|
+
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
388
|
+
// Count
|
|
389
|
+
const count = entry.value.length;
|
|
390
|
+
exif.push(count & 0xff, (count >> 8) & 0xff, (count >> 16) & 0xff, (count >> 24) & 0xff);
|
|
391
|
+
// Value/Offset
|
|
392
|
+
if (entry.value.length <= 4) {
|
|
393
|
+
for (let i = 0; i < 4; i++) {
|
|
394
|
+
exif.push(i < entry.value.length ? entry.value[i] : 0);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
exif.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
399
|
+
dataOffset += entry.value.length;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
// Next IFD offset (0 = no more IFDs)
|
|
403
|
+
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
404
|
+
// Append data for entries that didn't fit in value field
|
|
405
|
+
for (const entry of entries) {
|
|
406
|
+
if (entry.value.length > 4) {
|
|
407
|
+
for (const byte of entry.value) {
|
|
408
|
+
exif.push(byte);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return exif;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* PNG format handler
|
|
4
|
+
* Implements a pure JavaScript PNG decoder and encoder
|
|
5
|
+
*/
|
|
6
|
+
export declare class PNGFormat implements ImageFormat {
|
|
7
|
+
readonly name = "png";
|
|
8
|
+
readonly mimeType = "image/png";
|
|
9
|
+
canDecode(data: Uint8Array): boolean;
|
|
10
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
11
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
12
|
+
private readUint32;
|
|
13
|
+
private writeUint32;
|
|
14
|
+
private concatenateChunks;
|
|
15
|
+
private inflate;
|
|
16
|
+
private deflate;
|
|
17
|
+
private unfilterAndConvert;
|
|
18
|
+
private unfilterScanline;
|
|
19
|
+
private paethPredictor;
|
|
20
|
+
private filterData;
|
|
21
|
+
private getBytesPerPixel;
|
|
22
|
+
private getBitsPerPixel;
|
|
23
|
+
private createChunk;
|
|
24
|
+
private crc32;
|
|
25
|
+
private parsePhysChunk;
|
|
26
|
+
private parseTextChunk;
|
|
27
|
+
private parseITxtChunk;
|
|
28
|
+
private parseExifChunk;
|
|
29
|
+
private createPhysChunk;
|
|
30
|
+
private createTextChunk;
|
|
31
|
+
private createExifChunk;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=png.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../../src/src/formats/png.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAKzE;;;GAGG;AACH,qBAAa,SAAU,YAAW,WAAW;IAC3C,QAAQ,CAAC,IAAI,SAAS;IACtB,QAAQ,CAAC,QAAQ,eAAe;IAEhC,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO;IAS9B,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC;IA+E5C,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC;IAuGvD,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,iBAAiB;YAgBX,OAAO;YAQP,OAAO;IAQrB,OAAO,CAAC,kBAAkB;IA0D1B,OAAO,CAAC,gBAAgB;IAiCxB,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,UAAU;IAmBlB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,KAAK;IAab,OAAO,CAAC,cAAc;IAqBtB,OAAO,CAAC,cAAc;IA6BtB,OAAO,CAAC,cAAc;IA4CtB,OAAO,CAAC,cAAc;IAmEtB,OAAO,CAAC,eAAe;IAkBvB,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,eAAe;CAkFxB"}
|