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