cross-image 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -16
- package/esm/mod.d.ts +3 -1
- package/esm/mod.js +3 -1
- package/esm/src/formats/apng.d.ts +50 -0
- package/esm/src/formats/apng.js +364 -0
- package/esm/src/formats/bmp.d.ts +0 -6
- package/esm/src/formats/bmp.js +24 -47
- package/esm/src/formats/dng.js +4 -4
- package/esm/src/formats/gif.d.ts +0 -2
- package/esm/src/formats/gif.js +10 -16
- package/esm/src/formats/ico.d.ts +41 -0
- package/esm/src/formats/ico.js +214 -0
- package/esm/src/formats/pcx.js +1 -1
- package/esm/src/formats/png.d.ts +2 -21
- package/esm/src/formats/png.js +5 -429
- package/esm/src/formats/png_base.d.ts +108 -0
- package/esm/src/formats/png_base.js +487 -0
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.js +4 -0
- package/esm/src/utils/byte_utils.d.ts +30 -0
- package/esm/src/utils/byte_utils.js +50 -0
- package/esm/src/utils/gif_encoder.d.ts +3 -2
- package/esm/src/utils/gif_encoder.js +115 -48
- package/package.json +1 -1
- package/script/mod.d.ts +3 -1
- package/script/mod.js +6 -2
- package/script/src/formats/apng.d.ts +50 -0
- package/script/src/formats/apng.js +368 -0
- package/script/src/formats/bmp.d.ts +0 -6
- package/script/src/formats/bmp.js +24 -47
- package/script/src/formats/dng.js +4 -4
- package/script/src/formats/gif.d.ts +0 -2
- package/script/src/formats/gif.js +10 -16
- package/script/src/formats/ico.d.ts +41 -0
- package/script/src/formats/ico.js +218 -0
- package/script/src/formats/pcx.js +1 -1
- package/script/src/formats/png.d.ts +2 -21
- package/script/src/formats/png.js +5 -429
- package/script/src/formats/png_base.d.ts +108 -0
- package/script/src/formats/png_base.js +491 -0
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.js +4 -0
- package/script/src/utils/byte_utils.d.ts +30 -0
- package/script/src/utils/byte_utils.js +58 -0
- package/script/src/utils/gif_encoder.d.ts +3 -2
- package/script/src/utils/gif_encoder.js +115 -48
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// Constants for unit conversions
|
|
2
|
+
const INCHES_PER_METER = 39.3701;
|
|
3
|
+
/**
|
|
4
|
+
* Base class for PNG and APNG format handlers
|
|
5
|
+
* Contains shared utility methods for PNG chunk manipulation and metadata parsing
|
|
6
|
+
*/
|
|
7
|
+
export class PNGBase {
|
|
8
|
+
/**
|
|
9
|
+
* Read a 32-bit unsigned integer (big-endian)
|
|
10
|
+
*/
|
|
11
|
+
readUint32(data, offset) {
|
|
12
|
+
return (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
13
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read a 16-bit unsigned integer (big-endian)
|
|
17
|
+
*/
|
|
18
|
+
readUint16(data, offset) {
|
|
19
|
+
return (data[offset] << 8) | data[offset + 1];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Write a 32-bit unsigned integer (big-endian)
|
|
23
|
+
*/
|
|
24
|
+
writeUint32(data, offset, value) {
|
|
25
|
+
data[offset] = (value >>> 24) & 0xff;
|
|
26
|
+
data[offset + 1] = (value >>> 16) & 0xff;
|
|
27
|
+
data[offset + 2] = (value >>> 8) & 0xff;
|
|
28
|
+
data[offset + 3] = value & 0xff;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Write a 16-bit unsigned integer (big-endian)
|
|
32
|
+
*/
|
|
33
|
+
writeUint16(data, offset, value) {
|
|
34
|
+
data[offset] = (value >>> 8) & 0xff;
|
|
35
|
+
data[offset + 1] = value & 0xff;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Decompress PNG data using deflate
|
|
39
|
+
*/
|
|
40
|
+
async inflate(data) {
|
|
41
|
+
const stream = new Response(data).body
|
|
42
|
+
.pipeThrough(new DecompressionStream("deflate"));
|
|
43
|
+
const decompressed = await new Response(stream).arrayBuffer();
|
|
44
|
+
return new Uint8Array(decompressed);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Compress PNG data using deflate
|
|
48
|
+
*/
|
|
49
|
+
async deflate(data) {
|
|
50
|
+
const stream = new Response(data).body
|
|
51
|
+
.pipeThrough(new CompressionStream("deflate"));
|
|
52
|
+
const compressed = await new Response(stream).arrayBuffer();
|
|
53
|
+
return new Uint8Array(compressed);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Unfilter PNG scanlines and convert to RGBA
|
|
57
|
+
*/
|
|
58
|
+
unfilterAndConvert(data, width, height, bitDepth, colorType) {
|
|
59
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
60
|
+
const bytesPerPixel = this.getBytesPerPixel(colorType, bitDepth);
|
|
61
|
+
const scanlineLength = width * bytesPerPixel;
|
|
62
|
+
let dataPos = 0;
|
|
63
|
+
const scanlines = [];
|
|
64
|
+
for (let y = 0; y < height; y++) {
|
|
65
|
+
const filterType = data[dataPos++];
|
|
66
|
+
const scanline = new Uint8Array(scanlineLength);
|
|
67
|
+
for (let x = 0; x < scanlineLength; x++) {
|
|
68
|
+
scanline[x] = data[dataPos++];
|
|
69
|
+
}
|
|
70
|
+
this.unfilterScanline(scanline, y > 0 ? scanlines[y - 1] : null, filterType, bytesPerPixel);
|
|
71
|
+
scanlines.push(scanline);
|
|
72
|
+
// Convert to RGBA
|
|
73
|
+
for (let x = 0; x < width; x++) {
|
|
74
|
+
const outIdx = (y * width + x) * 4;
|
|
75
|
+
if (colorType === 6) { // RGBA
|
|
76
|
+
rgba[outIdx] = scanline[x * 4];
|
|
77
|
+
rgba[outIdx + 1] = scanline[x * 4 + 1];
|
|
78
|
+
rgba[outIdx + 2] = scanline[x * 4 + 2];
|
|
79
|
+
rgba[outIdx + 3] = scanline[x * 4 + 3];
|
|
80
|
+
}
|
|
81
|
+
else if (colorType === 2) { // RGB
|
|
82
|
+
rgba[outIdx] = scanline[x * 3];
|
|
83
|
+
rgba[outIdx + 1] = scanline[x * 3 + 1];
|
|
84
|
+
rgba[outIdx + 2] = scanline[x * 3 + 2];
|
|
85
|
+
rgba[outIdx + 3] = 255;
|
|
86
|
+
}
|
|
87
|
+
else if (colorType === 0) { // Grayscale
|
|
88
|
+
const gray = scanline[x];
|
|
89
|
+
rgba[outIdx] = gray;
|
|
90
|
+
rgba[outIdx + 1] = gray;
|
|
91
|
+
rgba[outIdx + 2] = gray;
|
|
92
|
+
rgba[outIdx + 3] = 255;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
throw new Error(`Unsupported PNG color type: ${colorType}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return rgba;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Unfilter a single PNG scanline
|
|
103
|
+
*/
|
|
104
|
+
unfilterScanline(scanline, prevLine, filterType, bytesPerPixel) {
|
|
105
|
+
for (let x = 0; x < scanline.length; x++) {
|
|
106
|
+
const left = x >= bytesPerPixel ? scanline[x - bytesPerPixel] : 0;
|
|
107
|
+
const above = prevLine ? prevLine[x] : 0;
|
|
108
|
+
const upperLeft = (x >= bytesPerPixel && prevLine)
|
|
109
|
+
? prevLine[x - bytesPerPixel]
|
|
110
|
+
: 0;
|
|
111
|
+
switch (filterType) {
|
|
112
|
+
case 0: // None
|
|
113
|
+
break;
|
|
114
|
+
case 1: // Sub
|
|
115
|
+
scanline[x] = (scanline[x] + left) & 0xff;
|
|
116
|
+
break;
|
|
117
|
+
case 2: // Up
|
|
118
|
+
scanline[x] = (scanline[x] + above) & 0xff;
|
|
119
|
+
break;
|
|
120
|
+
case 3: // Average
|
|
121
|
+
scanline[x] = (scanline[x] + Math.floor((left + above) / 2)) & 0xff;
|
|
122
|
+
break;
|
|
123
|
+
case 4: // Paeth
|
|
124
|
+
scanline[x] =
|
|
125
|
+
(scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Paeth predictor for PNG filtering
|
|
132
|
+
*/
|
|
133
|
+
paethPredictor(a, b, c) {
|
|
134
|
+
const p = a + b - c;
|
|
135
|
+
const pa = Math.abs(p - a);
|
|
136
|
+
const pb = Math.abs(p - b);
|
|
137
|
+
const pc = Math.abs(p - c);
|
|
138
|
+
if (pa <= pb && pa <= pc)
|
|
139
|
+
return a;
|
|
140
|
+
if (pb <= pc)
|
|
141
|
+
return b;
|
|
142
|
+
return c;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Filter PNG data for encoding (using filter type 0 - None)
|
|
146
|
+
*/
|
|
147
|
+
filterData(data, width, height) {
|
|
148
|
+
// Use filter type 0 (None) for simplicity
|
|
149
|
+
const filtered = new Uint8Array(height * (1 + width * 4));
|
|
150
|
+
let pos = 0;
|
|
151
|
+
for (let y = 0; y < height; y++) {
|
|
152
|
+
filtered[pos++] = 0; // Filter type: None
|
|
153
|
+
for (let x = 0; x < width * 4; x++) {
|
|
154
|
+
filtered[pos++] = data[y * width * 4 + x];
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return filtered;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get bytes per pixel for a given color type and bit depth
|
|
161
|
+
*/
|
|
162
|
+
getBytesPerPixel(colorType, bitDepth) {
|
|
163
|
+
const bitsPerPixel = this.getBitsPerPixel(colorType, bitDepth);
|
|
164
|
+
return Math.ceil(bitsPerPixel / 8);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get bits per pixel for a given color type and bit depth
|
|
168
|
+
*/
|
|
169
|
+
getBitsPerPixel(colorType, bitDepth) {
|
|
170
|
+
switch (colorType) {
|
|
171
|
+
case 0: // Grayscale
|
|
172
|
+
return bitDepth;
|
|
173
|
+
case 2: // RGB
|
|
174
|
+
return bitDepth * 3;
|
|
175
|
+
case 3: // Palette
|
|
176
|
+
return bitDepth;
|
|
177
|
+
case 4: // Grayscale + Alpha
|
|
178
|
+
return bitDepth * 2;
|
|
179
|
+
case 6: // RGBA
|
|
180
|
+
return bitDepth * 4;
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Unknown color type: ${colorType}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Create a PNG chunk with length, type, data, and CRC
|
|
187
|
+
*/
|
|
188
|
+
createChunk(type, data) {
|
|
189
|
+
const chunk = new Uint8Array(12 + data.length);
|
|
190
|
+
this.writeUint32(chunk, 0, data.length);
|
|
191
|
+
chunk[4] = type.charCodeAt(0);
|
|
192
|
+
chunk[5] = type.charCodeAt(1);
|
|
193
|
+
chunk[6] = type.charCodeAt(2);
|
|
194
|
+
chunk[7] = type.charCodeAt(3);
|
|
195
|
+
chunk.set(data, 8);
|
|
196
|
+
const crc = this.crc32(chunk.slice(4, 8 + data.length));
|
|
197
|
+
this.writeUint32(chunk, 8 + data.length, crc);
|
|
198
|
+
return chunk;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Calculate CRC32 checksum
|
|
202
|
+
*/
|
|
203
|
+
crc32(data) {
|
|
204
|
+
let crc = 0xffffffff;
|
|
205
|
+
for (let i = 0; i < data.length; i++) {
|
|
206
|
+
crc ^= data[i];
|
|
207
|
+
for (let j = 0; j < 8; j++) {
|
|
208
|
+
crc = (crc & 1) ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
212
|
+
}
|
|
213
|
+
// Metadata parsing methods
|
|
214
|
+
/**
|
|
215
|
+
* Parse pHYs (physical pixel dimensions) chunk
|
|
216
|
+
*/
|
|
217
|
+
parsePhysChunk(data, metadata, width, height) {
|
|
218
|
+
if (data.length < 9)
|
|
219
|
+
return;
|
|
220
|
+
const pixelsPerUnitX = this.readUint32(data, 0);
|
|
221
|
+
const pixelsPerUnitY = this.readUint32(data, 4);
|
|
222
|
+
const unit = data[8];
|
|
223
|
+
if (unit === 1 && pixelsPerUnitX > 0 && pixelsPerUnitY > 0) {
|
|
224
|
+
metadata.dpiX = Math.round(pixelsPerUnitX / INCHES_PER_METER);
|
|
225
|
+
metadata.dpiY = Math.round(pixelsPerUnitY / INCHES_PER_METER);
|
|
226
|
+
metadata.physicalWidth = width / metadata.dpiX;
|
|
227
|
+
metadata.physicalHeight = height / metadata.dpiY;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Parse tEXt (text) chunk
|
|
232
|
+
*/
|
|
233
|
+
parseTextChunk(data, metadata) {
|
|
234
|
+
const nullIndex = data.indexOf(0);
|
|
235
|
+
if (nullIndex === -1)
|
|
236
|
+
return;
|
|
237
|
+
const keyword = new TextDecoder().decode(data.slice(0, nullIndex));
|
|
238
|
+
const text = new TextDecoder().decode(data.slice(nullIndex + 1));
|
|
239
|
+
switch (keyword.toLowerCase()) {
|
|
240
|
+
case "title":
|
|
241
|
+
metadata.title = text;
|
|
242
|
+
break;
|
|
243
|
+
case "author":
|
|
244
|
+
metadata.author = text;
|
|
245
|
+
break;
|
|
246
|
+
case "description":
|
|
247
|
+
metadata.description = text;
|
|
248
|
+
break;
|
|
249
|
+
case "copyright":
|
|
250
|
+
metadata.copyright = text;
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
if (!metadata.custom)
|
|
254
|
+
metadata.custom = {};
|
|
255
|
+
metadata.custom[keyword] = text;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Parse iTXt (international text) chunk
|
|
260
|
+
*/
|
|
261
|
+
parseITxtChunk(data, metadata) {
|
|
262
|
+
let pos = 0;
|
|
263
|
+
const nullIndex = data.indexOf(0, pos);
|
|
264
|
+
if (nullIndex === -1 || pos >= data.length)
|
|
265
|
+
return;
|
|
266
|
+
const keyword = new TextDecoder().decode(data.slice(pos, nullIndex));
|
|
267
|
+
pos = nullIndex + 1;
|
|
268
|
+
if (pos + 2 > data.length)
|
|
269
|
+
return;
|
|
270
|
+
pos += 2; // Skip compression flag and method
|
|
271
|
+
const languageNullIndex = data.indexOf(0, pos);
|
|
272
|
+
if (languageNullIndex === -1 || pos >= data.length)
|
|
273
|
+
return;
|
|
274
|
+
pos = languageNullIndex + 1;
|
|
275
|
+
const translatedKeywordNullIndex = data.indexOf(0, pos);
|
|
276
|
+
if (translatedKeywordNullIndex === -1 || pos >= data.length)
|
|
277
|
+
return;
|
|
278
|
+
pos = translatedKeywordNullIndex + 1;
|
|
279
|
+
if (pos >= data.length)
|
|
280
|
+
return;
|
|
281
|
+
const text = new TextDecoder("utf-8").decode(data.slice(pos));
|
|
282
|
+
switch (keyword.toLowerCase()) {
|
|
283
|
+
case "title":
|
|
284
|
+
metadata.title = text;
|
|
285
|
+
break;
|
|
286
|
+
case "author":
|
|
287
|
+
metadata.author = text;
|
|
288
|
+
break;
|
|
289
|
+
case "description":
|
|
290
|
+
metadata.description = text;
|
|
291
|
+
break;
|
|
292
|
+
case "copyright":
|
|
293
|
+
metadata.copyright = text;
|
|
294
|
+
break;
|
|
295
|
+
default:
|
|
296
|
+
if (!metadata.custom)
|
|
297
|
+
metadata.custom = {};
|
|
298
|
+
metadata.custom[keyword] = text;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Parse eXIf (EXIF) chunk
|
|
303
|
+
*/
|
|
304
|
+
parseExifChunk(data, metadata) {
|
|
305
|
+
if (data.length < 8)
|
|
306
|
+
return;
|
|
307
|
+
try {
|
|
308
|
+
const byteOrder = String.fromCharCode(data[0], data[1]);
|
|
309
|
+
const littleEndian = byteOrder === "II";
|
|
310
|
+
const ifd0Offset = littleEndian
|
|
311
|
+
? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
|
|
312
|
+
: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
|
|
313
|
+
if (ifd0Offset + 2 > data.length)
|
|
314
|
+
return;
|
|
315
|
+
const numEntries = littleEndian
|
|
316
|
+
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
317
|
+
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
318
|
+
for (let i = 0; i < numEntries; i++) {
|
|
319
|
+
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
320
|
+
if (entryOffset + 12 > data.length)
|
|
321
|
+
break;
|
|
322
|
+
const tag = littleEndian
|
|
323
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
324
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
325
|
+
if (tag === 0x0132) {
|
|
326
|
+
const valueOffset = littleEndian
|
|
327
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
328
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
329
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
330
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
331
|
+
if (valueOffset < data.length) {
|
|
332
|
+
const nullIndex = data.indexOf(0, valueOffset);
|
|
333
|
+
if (nullIndex > valueOffset) {
|
|
334
|
+
const dateStr = new TextDecoder().decode(data.slice(valueOffset, nullIndex));
|
|
335
|
+
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
336
|
+
if (match) {
|
|
337
|
+
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (_e) {
|
|
345
|
+
// Ignore EXIF parsing errors
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Create pHYs (physical pixel dimensions) chunk
|
|
350
|
+
*/
|
|
351
|
+
createPhysChunk(metadata) {
|
|
352
|
+
const chunk = new Uint8Array(9);
|
|
353
|
+
const dpiX = metadata.dpiX ?? 72;
|
|
354
|
+
const dpiY = metadata.dpiY ?? 72;
|
|
355
|
+
const pixelsPerMeterX = Math.round(dpiX * INCHES_PER_METER);
|
|
356
|
+
const pixelsPerMeterY = Math.round(dpiY * INCHES_PER_METER);
|
|
357
|
+
this.writeUint32(chunk, 0, pixelsPerMeterX);
|
|
358
|
+
this.writeUint32(chunk, 4, pixelsPerMeterY);
|
|
359
|
+
chunk[8] = 1;
|
|
360
|
+
return chunk;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Create tEXt (text) chunk
|
|
364
|
+
*/
|
|
365
|
+
createTextChunk(keyword, text) {
|
|
366
|
+
const keywordBytes = new TextEncoder().encode(keyword);
|
|
367
|
+
const textBytes = new TextEncoder().encode(text);
|
|
368
|
+
const chunk = new Uint8Array(keywordBytes.length + 1 + textBytes.length);
|
|
369
|
+
chunk.set(keywordBytes, 0);
|
|
370
|
+
chunk[keywordBytes.length] = 0;
|
|
371
|
+
chunk.set(textBytes, keywordBytes.length + 1);
|
|
372
|
+
return chunk;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Create eXIf (EXIF) chunk
|
|
376
|
+
*/
|
|
377
|
+
createExifChunk(metadata) {
|
|
378
|
+
const entries = [];
|
|
379
|
+
if (metadata.creationDate) {
|
|
380
|
+
const date = metadata.creationDate;
|
|
381
|
+
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`;
|
|
382
|
+
entries.push({
|
|
383
|
+
tag: 0x0132,
|
|
384
|
+
type: 2,
|
|
385
|
+
value: new TextEncoder().encode(dateStr),
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
if (entries.length === 0)
|
|
389
|
+
return null;
|
|
390
|
+
const exif = [];
|
|
391
|
+
exif.push(0x49, 0x49); // "II"
|
|
392
|
+
exif.push(0x2a, 0x00); // 42
|
|
393
|
+
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
394
|
+
exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
|
|
395
|
+
let dataOffset = 8 + 2 + entries.length * 12 + 4;
|
|
396
|
+
for (const entry of entries) {
|
|
397
|
+
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
398
|
+
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
399
|
+
const count = entry.value.length;
|
|
400
|
+
exif.push(count & 0xff, (count >> 8) & 0xff, (count >> 16) & 0xff, (count >> 24) & 0xff);
|
|
401
|
+
if (entry.value.length <= 4) {
|
|
402
|
+
for (let i = 0; i < 4; i++) {
|
|
403
|
+
exif.push(i < entry.value.length ? entry.value[i] : 0);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
exif.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
408
|
+
dataOffset += entry.value.length;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
412
|
+
for (const entry of entries) {
|
|
413
|
+
if (entry.value.length > 4) {
|
|
414
|
+
for (const byte of entry.value) {
|
|
415
|
+
exif.push(byte);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return new Uint8Array(exif);
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Concatenate multiple byte arrays into a single Uint8Array
|
|
423
|
+
*/
|
|
424
|
+
concatenateChunks(chunks) {
|
|
425
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.data.length, 0);
|
|
426
|
+
const result = new Uint8Array(totalLength);
|
|
427
|
+
let offset = 0;
|
|
428
|
+
for (const chunk of chunks) {
|
|
429
|
+
result.set(chunk.data, offset);
|
|
430
|
+
offset += chunk.data.length;
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Concatenate multiple Uint8Arrays into a single Uint8Array
|
|
436
|
+
*/
|
|
437
|
+
concatenateArrays(arrays) {
|
|
438
|
+
const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
439
|
+
const result = new Uint8Array(totalLength);
|
|
440
|
+
let offset = 0;
|
|
441
|
+
for (const arr of arrays) {
|
|
442
|
+
result.set(arr, offset);
|
|
443
|
+
offset += arr.length;
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Add metadata chunks to the chunks array
|
|
449
|
+
* Shared method to avoid duplication between PNG and APNG encoding
|
|
450
|
+
*/
|
|
451
|
+
addMetadataChunks(chunks, metadata) {
|
|
452
|
+
if (!metadata)
|
|
453
|
+
return;
|
|
454
|
+
// Add pHYs chunk for DPI information
|
|
455
|
+
if (metadata.dpiX !== undefined || metadata.dpiY !== undefined) {
|
|
456
|
+
const physChunk = this.createPhysChunk(metadata);
|
|
457
|
+
chunks.push(this.createChunk("pHYs", physChunk));
|
|
458
|
+
}
|
|
459
|
+
// Add tEXt chunks for standard metadata
|
|
460
|
+
if (metadata.title !== undefined) {
|
|
461
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Title", metadata.title)));
|
|
462
|
+
}
|
|
463
|
+
if (metadata.author !== undefined) {
|
|
464
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Author", metadata.author)));
|
|
465
|
+
}
|
|
466
|
+
if (metadata.description !== undefined) {
|
|
467
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Description", metadata.description)));
|
|
468
|
+
}
|
|
469
|
+
if (metadata.copyright !== undefined) {
|
|
470
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk("Copyright", metadata.copyright)));
|
|
471
|
+
}
|
|
472
|
+
// Add custom metadata fields
|
|
473
|
+
if (metadata.custom) {
|
|
474
|
+
for (const [key, value] of Object.entries(metadata.custom)) {
|
|
475
|
+
chunks.push(this.createChunk("tEXt", this.createTextChunk(key, String(value))));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// Add EXIF chunk for GPS data and creation date
|
|
479
|
+
if (metadata.latitude !== undefined || metadata.longitude !== undefined ||
|
|
480
|
+
metadata.creationDate !== undefined) {
|
|
481
|
+
const exifChunk = this.createExifChunk(metadata);
|
|
482
|
+
if (exifChunk) {
|
|
483
|
+
chunks.push(this.createChunk("eXIf", exifChunk));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -27,7 +27,6 @@ export declare class WebPFormat implements ImageFormat {
|
|
|
27
27
|
* @returns Encoded WebP image bytes
|
|
28
28
|
*/
|
|
29
29
|
encode(imageData: ImageData, options?: WebPEncodeOptions): Promise<Uint8Array>;
|
|
30
|
-
private readUint32LE;
|
|
31
30
|
private readUint24LE;
|
|
32
31
|
private decodeUsingRuntime;
|
|
33
32
|
private parseEXIF;
|
package/esm/src/formats/webp.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateImageDimensions } from "../utils/security.js";
|
|
2
|
+
import { readUint32LE } from "../utils/byte_utils.js";
|
|
2
3
|
// Default quality for WebP encoding when not specified
|
|
3
4
|
const DEFAULT_WEBP_QUALITY = 90;
|
|
4
5
|
/**
|
|
@@ -52,7 +53,7 @@ export class WebPFormat {
|
|
|
52
53
|
// Read all chunks to extract metadata
|
|
53
54
|
while (pos + 8 <= data.length) {
|
|
54
55
|
const chunkType = String.fromCharCode(data[pos], data[pos + 1], data[pos + 2], data[pos + 3]);
|
|
55
|
-
const chunkSize =
|
|
56
|
+
const chunkSize = readUint32LE(data, pos + 4);
|
|
56
57
|
pos += 8;
|
|
57
58
|
// Stop if we've gone past the end
|
|
58
59
|
if (pos + chunkSize > data.length)
|
|
@@ -74,7 +75,7 @@ export class WebPFormat {
|
|
|
74
75
|
else if (chunkType === "VP8L") {
|
|
75
76
|
// Lossless format - extract dimensions
|
|
76
77
|
if (chunkData.length >= 5 && chunkData[0] === 0x2f) {
|
|
77
|
-
const bits =
|
|
78
|
+
const bits = readUint32LE(chunkData, 1);
|
|
78
79
|
width = (bits & 0x3fff) + 1;
|
|
79
80
|
height = ((bits >> 14) & 0x3fff) + 1;
|
|
80
81
|
}
|
|
@@ -166,10 +167,6 @@ export class WebPFormat {
|
|
|
166
167
|
}
|
|
167
168
|
return encoded;
|
|
168
169
|
}
|
|
169
|
-
readUint32LE(data, offset) {
|
|
170
|
-
return data[offset] | (data[offset + 1] << 8) |
|
|
171
|
-
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
172
|
-
}
|
|
173
170
|
readUint24LE(data, offset) {
|
|
174
171
|
return data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16);
|
|
175
172
|
}
|
|
@@ -306,7 +303,7 @@ export class WebPFormat {
|
|
|
306
303
|
let pos = 12;
|
|
307
304
|
while (pos + 8 <= webpData.length) {
|
|
308
305
|
const chunkType = String.fromCharCode(webpData[pos], webpData[pos + 1], webpData[pos + 2], webpData[pos + 3]);
|
|
309
|
-
const chunkSize =
|
|
306
|
+
const chunkSize = readUint32LE(webpData, pos + 4);
|
|
310
307
|
// Don't copy existing EXIF/XMP chunks (we'll add new ones)
|
|
311
308
|
if (chunkType !== "EXIF" && chunkType !== "XMP ") {
|
|
312
309
|
const chunkEnd = pos + 8 + chunkSize + (chunkSize % 2);
|
package/esm/src/image.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { resizeBilinear, resizeNearest } from "./utils/resize.js";
|
|
2
2
|
import { adjustBrightness, adjustContrast, adjustExposure, adjustSaturation, composite, crop, fillRect, grayscale, invert, } from "./utils/image_processing.js";
|
|
3
3
|
import { PNGFormat } from "./formats/png.js";
|
|
4
|
+
import { APNGFormat } from "./formats/apng.js";
|
|
4
5
|
import { JPEGFormat } from "./formats/jpeg.js";
|
|
5
6
|
import { WebPFormat } from "./formats/webp.js";
|
|
6
7
|
import { GIFFormat } from "./formats/gif.js";
|
|
7
8
|
import { TIFFFormat } from "./formats/tiff.js";
|
|
8
9
|
import { BMPFormat } from "./formats/bmp.js";
|
|
10
|
+
import { ICOFormat } from "./formats/ico.js";
|
|
9
11
|
import { DNGFormat } from "./formats/dng.js";
|
|
10
12
|
import { PAMFormat } from "./formats/pam.js";
|
|
11
13
|
import { PCXFormat } from "./formats/pcx.js";
|
|
@@ -551,11 +553,13 @@ Object.defineProperty(Image, "formats", {
|
|
|
551
553
|
writable: true,
|
|
552
554
|
value: [
|
|
553
555
|
new PNGFormat(),
|
|
556
|
+
new APNGFormat(),
|
|
554
557
|
new JPEGFormat(),
|
|
555
558
|
new WebPFormat(),
|
|
556
559
|
new GIFFormat(),
|
|
557
560
|
new TIFFFormat(),
|
|
558
561
|
new BMPFormat(),
|
|
562
|
+
new ICOFormat(),
|
|
559
563
|
new DNGFormat(),
|
|
560
564
|
new PAMFormat(),
|
|
561
565
|
new PCXFormat(),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared byte-level read/write utilities for image formats
|
|
3
|
+
* These functions handle reading and writing multi-byte integers
|
|
4
|
+
* in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Read a 16-bit unsigned integer in little-endian format
|
|
8
|
+
*/
|
|
9
|
+
export declare function readUint16LE(data: Uint8Array, offset: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Read a 32-bit unsigned integer in little-endian format
|
|
12
|
+
*/
|
|
13
|
+
export declare function readUint32LE(data: Uint8Array, offset: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Read a 32-bit signed integer in little-endian format
|
|
16
|
+
*/
|
|
17
|
+
export declare function readInt32LE(data: Uint8Array, offset: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Write a 16-bit unsigned integer in little-endian format
|
|
20
|
+
*/
|
|
21
|
+
export declare function writeUint16LE(data: Uint8Array, offset: number, value: number): void;
|
|
22
|
+
/**
|
|
23
|
+
* Write a 32-bit unsigned integer in little-endian format
|
|
24
|
+
*/
|
|
25
|
+
export declare function writeUint32LE(data: Uint8Array, offset: number, value: number): void;
|
|
26
|
+
/**
|
|
27
|
+
* Write a 32-bit signed integer in little-endian format
|
|
28
|
+
*/
|
|
29
|
+
export declare function writeInt32LE(data: Uint8Array, offset: number, value: number): void;
|
|
30
|
+
//# sourceMappingURL=byte_utils.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared byte-level read/write utilities for image formats
|
|
3
|
+
* These functions handle reading and writing multi-byte integers
|
|
4
|
+
* in little-endian byte order, commonly used in BMP, ICO, GIF, and other formats.
|
|
5
|
+
*/
|
|
6
|
+
// Constants for signed/unsigned integer conversion
|
|
7
|
+
const INT32_MAX = 0x7fffffff;
|
|
8
|
+
const UINT32_RANGE = 0x100000000;
|
|
9
|
+
/**
|
|
10
|
+
* Read a 16-bit unsigned integer in little-endian format
|
|
11
|
+
*/
|
|
12
|
+
export function readUint16LE(data, offset) {
|
|
13
|
+
return data[offset] | (data[offset + 1] << 8);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Read a 32-bit unsigned integer in little-endian format
|
|
17
|
+
*/
|
|
18
|
+
export function readUint32LE(data, offset) {
|
|
19
|
+
return data[offset] | (data[offset + 1] << 8) |
|
|
20
|
+
(data[offset + 2] << 16) | (data[offset + 3] << 24);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Read a 32-bit signed integer in little-endian format
|
|
24
|
+
*/
|
|
25
|
+
export function readInt32LE(data, offset) {
|
|
26
|
+
const value = readUint32LE(data, offset);
|
|
27
|
+
return value > INT32_MAX ? value - UINT32_RANGE : value;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Write a 16-bit unsigned integer in little-endian format
|
|
31
|
+
*/
|
|
32
|
+
export function writeUint16LE(data, offset, value) {
|
|
33
|
+
data[offset] = value & 0xff;
|
|
34
|
+
data[offset + 1] = (value >>> 8) & 0xff;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Write a 32-bit unsigned integer in little-endian format
|
|
38
|
+
*/
|
|
39
|
+
export function writeUint32LE(data, offset, value) {
|
|
40
|
+
data[offset] = value & 0xff;
|
|
41
|
+
data[offset + 1] = (value >>> 8) & 0xff;
|
|
42
|
+
data[offset + 2] = (value >>> 16) & 0xff;
|
|
43
|
+
data[offset + 3] = (value >>> 24) & 0xff;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Write a 32-bit signed integer in little-endian format
|
|
47
|
+
*/
|
|
48
|
+
export function writeInt32LE(data, offset, value) {
|
|
49
|
+
writeUint32LE(data, offset, value < 0 ? value + UINT32_RANGE : value);
|
|
50
|
+
}
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
export declare class GIFEncoder {
|
|
6
6
|
private width;
|
|
7
7
|
private height;
|
|
8
|
-
private
|
|
9
|
-
constructor(width: number, height: number, data
|
|
8
|
+
private frames;
|
|
9
|
+
constructor(width: number, height: number, data?: Uint8Array);
|
|
10
|
+
addFrame(data: Uint8Array, delay?: number): void;
|
|
10
11
|
private writeBytes;
|
|
11
12
|
private writeUint16LE;
|
|
12
13
|
private writeString;
|