cross-image 0.2.0 → 0.2.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/README.md +41 -28
- package/esm/mod.d.ts +4 -1
- package/esm/mod.js +4 -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/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +4 -0
- package/esm/src/formats/tiff.js +163 -44
- package/esm/src/formats/webp.d.ts +0 -1
- package/esm/src/formats/webp.js +4 -7
- package/esm/src/image.d.ts +30 -0
- package/esm/src/image.js +62 -1
- 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 +43 -0
- package/esm/src/utils/image_processing.js +230 -0
- package/package.json +1 -1
- package/script/mod.d.ts +4 -1
- package/script/mod.js +8 -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/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +4 -0
- package/script/src/formats/tiff.js +163 -44
- package/script/src/formats/webp.d.ts +0 -1
- package/script/src/formats/webp.js +4 -7
- package/script/src/image.d.ts +30 -0
- package/script/src/image.js +61 -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
- package/script/src/utils/image_processing.d.ts +43 -0
- package/script/src/utils/image_processing.js +235 -0
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PNGFormat = void 0;
|
|
4
4
|
const security_js_1 = require("../utils/security.js");
|
|
5
|
-
|
|
6
|
-
const INCHES_PER_METER = 39.3701;
|
|
5
|
+
const png_base_js_1 = require("./png_base.js");
|
|
7
6
|
/**
|
|
8
7
|
* PNG format handler
|
|
9
8
|
* Implements a pure JavaScript PNG decoder and encoder
|
|
10
9
|
*/
|
|
11
|
-
class PNGFormat {
|
|
10
|
+
class PNGFormat extends png_base_js_1.PNGBase {
|
|
12
11
|
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
13
|
/** Format name identifier */
|
|
14
14
|
Object.defineProperty(this, "name", {
|
|
15
15
|
enumerable: true,
|
|
@@ -134,435 +134,11 @@ class PNGFormat {
|
|
|
134
134
|
chunks.push(new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10])); // PNG signature
|
|
135
135
|
chunks.push(this.createChunk("IHDR", ihdr));
|
|
136
136
|
// Add metadata chunks if available
|
|
137
|
-
|
|
138
|
-
// Add pHYs chunk for DPI information
|
|
139
|
-
if (metadata.dpiX !== undefined || metadata.dpiY !== undefined) {
|
|
140
|
-
const physChunk = this.createPhysChunk(metadata);
|
|
141
|
-
chunks.push(this.createChunk("pHYs", physChunk));
|
|
142
|
-
}
|
|
143
|
-
// Add tEXt chunks for standard metadata
|
|
144
|
-
if (metadata.title !== undefined) {
|
|
145
|
-
chunks.push(this.createChunk("tEXt", this.createTextChunk("Title", metadata.title)));
|
|
146
|
-
}
|
|
147
|
-
if (metadata.author !== undefined) {
|
|
148
|
-
chunks.push(this.createChunk("tEXt", this.createTextChunk("Author", metadata.author)));
|
|
149
|
-
}
|
|
150
|
-
if (metadata.description !== undefined) {
|
|
151
|
-
chunks.push(this.createChunk("tEXt", this.createTextChunk("Description", metadata.description)));
|
|
152
|
-
}
|
|
153
|
-
if (metadata.copyright !== undefined) {
|
|
154
|
-
chunks.push(this.createChunk("tEXt", this.createTextChunk("Copyright", metadata.copyright)));
|
|
155
|
-
}
|
|
156
|
-
// Add custom metadata fields
|
|
157
|
-
if (metadata.custom) {
|
|
158
|
-
for (const [key, value] of Object.entries(metadata.custom)) {
|
|
159
|
-
chunks.push(this.createChunk("tEXt", this.createTextChunk(key, String(value))));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
// Add EXIF chunk for GPS data and creation date
|
|
163
|
-
if (metadata.latitude !== undefined || metadata.longitude !== undefined ||
|
|
164
|
-
metadata.creationDate !== undefined) {
|
|
165
|
-
const exifChunk = this.createExifChunk(metadata);
|
|
166
|
-
if (exifChunk) {
|
|
167
|
-
chunks.push(this.createChunk("eXIf", exifChunk));
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
137
|
+
this.addMetadataChunks(chunks, metadata);
|
|
171
138
|
chunks.push(this.createChunk("IDAT", compressed));
|
|
172
139
|
chunks.push(this.createChunk("IEND", new Uint8Array(0)));
|
|
173
140
|
// Concatenate all chunks
|
|
174
|
-
|
|
175
|
-
const result = new Uint8Array(totalLength);
|
|
176
|
-
let offset = 0;
|
|
177
|
-
for (const chunk of chunks) {
|
|
178
|
-
result.set(chunk, offset);
|
|
179
|
-
offset += chunk.length;
|
|
180
|
-
}
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
readUint32(data, offset) {
|
|
184
|
-
return (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
185
|
-
(data[offset + 2] << 8) | data[offset + 3];
|
|
186
|
-
}
|
|
187
|
-
writeUint32(data, offset, value) {
|
|
188
|
-
data[offset] = (value >>> 24) & 0xff;
|
|
189
|
-
data[offset + 1] = (value >>> 16) & 0xff;
|
|
190
|
-
data[offset + 2] = (value >>> 8) & 0xff;
|
|
191
|
-
data[offset + 3] = value & 0xff;
|
|
192
|
-
}
|
|
193
|
-
concatenateChunks(chunks) {
|
|
194
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.data.length, 0);
|
|
195
|
-
const result = new Uint8Array(totalLength);
|
|
196
|
-
let offset = 0;
|
|
197
|
-
for (const chunk of chunks) {
|
|
198
|
-
result.set(chunk.data, offset);
|
|
199
|
-
offset += chunk.data.length;
|
|
200
|
-
}
|
|
201
|
-
return result;
|
|
202
|
-
}
|
|
203
|
-
async inflate(data) {
|
|
204
|
-
// Use DecompressionStream API (available in Deno, Node 17+, and browsers)
|
|
205
|
-
const stream = new Response(data).body
|
|
206
|
-
.pipeThrough(new DecompressionStream("deflate"));
|
|
207
|
-
const decompressed = await new Response(stream).arrayBuffer();
|
|
208
|
-
return new Uint8Array(decompressed);
|
|
209
|
-
}
|
|
210
|
-
async deflate(data) {
|
|
211
|
-
// Use CompressionStream API (available in Deno, Node 17+, and browsers)
|
|
212
|
-
const stream = new Response(data).body
|
|
213
|
-
.pipeThrough(new CompressionStream("deflate"));
|
|
214
|
-
const compressed = await new Response(stream).arrayBuffer();
|
|
215
|
-
return new Uint8Array(compressed);
|
|
216
|
-
}
|
|
217
|
-
unfilterAndConvert(data, width, height, bitDepth, colorType) {
|
|
218
|
-
const rgba = new Uint8Array(width * height * 4);
|
|
219
|
-
const bytesPerPixel = this.getBytesPerPixel(colorType, bitDepth);
|
|
220
|
-
const scanlineLength = width * bytesPerPixel;
|
|
221
|
-
let dataPos = 0;
|
|
222
|
-
const scanlines = [];
|
|
223
|
-
for (let y = 0; y < height; y++) {
|
|
224
|
-
const filterType = data[dataPos++];
|
|
225
|
-
const scanline = new Uint8Array(scanlineLength);
|
|
226
|
-
for (let x = 0; x < scanlineLength; x++) {
|
|
227
|
-
scanline[x] = data[dataPos++];
|
|
228
|
-
}
|
|
229
|
-
this.unfilterScanline(scanline, y > 0 ? scanlines[y - 1] : null, filterType, bytesPerPixel);
|
|
230
|
-
scanlines.push(scanline);
|
|
231
|
-
// Convert to RGBA
|
|
232
|
-
for (let x = 0; x < width; x++) {
|
|
233
|
-
const outIdx = (y * width + x) * 4;
|
|
234
|
-
if (colorType === 6) { // RGBA
|
|
235
|
-
rgba[outIdx] = scanline[x * 4];
|
|
236
|
-
rgba[outIdx + 1] = scanline[x * 4 + 1];
|
|
237
|
-
rgba[outIdx + 2] = scanline[x * 4 + 2];
|
|
238
|
-
rgba[outIdx + 3] = scanline[x * 4 + 3];
|
|
239
|
-
}
|
|
240
|
-
else if (colorType === 2) { // RGB
|
|
241
|
-
rgba[outIdx] = scanline[x * 3];
|
|
242
|
-
rgba[outIdx + 1] = scanline[x * 3 + 1];
|
|
243
|
-
rgba[outIdx + 2] = scanline[x * 3 + 2];
|
|
244
|
-
rgba[outIdx + 3] = 255;
|
|
245
|
-
}
|
|
246
|
-
else if (colorType === 0) { // Grayscale
|
|
247
|
-
const gray = scanline[x];
|
|
248
|
-
rgba[outIdx] = gray;
|
|
249
|
-
rgba[outIdx + 1] = gray;
|
|
250
|
-
rgba[outIdx + 2] = gray;
|
|
251
|
-
rgba[outIdx + 3] = 255;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
throw new Error(`Unsupported PNG color type: ${colorType}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return rgba;
|
|
259
|
-
}
|
|
260
|
-
unfilterScanline(scanline, prevLine, filterType, bytesPerPixel) {
|
|
261
|
-
for (let x = 0; x < scanline.length; x++) {
|
|
262
|
-
const left = x >= bytesPerPixel ? scanline[x - bytesPerPixel] : 0;
|
|
263
|
-
const above = prevLine ? prevLine[x] : 0;
|
|
264
|
-
const upperLeft = (x >= bytesPerPixel && prevLine)
|
|
265
|
-
? prevLine[x - bytesPerPixel]
|
|
266
|
-
: 0;
|
|
267
|
-
switch (filterType) {
|
|
268
|
-
case 0: // None
|
|
269
|
-
break;
|
|
270
|
-
case 1: // Sub
|
|
271
|
-
scanline[x] = (scanline[x] + left) & 0xff;
|
|
272
|
-
break;
|
|
273
|
-
case 2: // Up
|
|
274
|
-
scanline[x] = (scanline[x] + above) & 0xff;
|
|
275
|
-
break;
|
|
276
|
-
case 3: // Average
|
|
277
|
-
scanline[x] = (scanline[x] + Math.floor((left + above) / 2)) & 0xff;
|
|
278
|
-
break;
|
|
279
|
-
case 4: // Paeth
|
|
280
|
-
scanline[x] =
|
|
281
|
-
(scanline[x] + this.paethPredictor(left, above, upperLeft)) & 0xff;
|
|
282
|
-
break;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
paethPredictor(a, b, c) {
|
|
287
|
-
const p = a + b - c;
|
|
288
|
-
const pa = Math.abs(p - a);
|
|
289
|
-
const pb = Math.abs(p - b);
|
|
290
|
-
const pc = Math.abs(p - c);
|
|
291
|
-
if (pa <= pb && pa <= pc)
|
|
292
|
-
return a;
|
|
293
|
-
if (pb <= pc)
|
|
294
|
-
return b;
|
|
295
|
-
return c;
|
|
296
|
-
}
|
|
297
|
-
filterData(data, width, height) {
|
|
298
|
-
// Use filter type 0 (None) for simplicity
|
|
299
|
-
const filtered = new Uint8Array(height * (1 + width * 4));
|
|
300
|
-
let pos = 0;
|
|
301
|
-
for (let y = 0; y < height; y++) {
|
|
302
|
-
filtered[pos++] = 0; // Filter type: None
|
|
303
|
-
for (let x = 0; x < width * 4; x++) {
|
|
304
|
-
filtered[pos++] = data[y * width * 4 + x];
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return filtered;
|
|
308
|
-
}
|
|
309
|
-
getBytesPerPixel(colorType, bitDepth) {
|
|
310
|
-
const bitsPerPixel = this.getBitsPerPixel(colorType, bitDepth);
|
|
311
|
-
return Math.ceil(bitsPerPixel / 8);
|
|
312
|
-
}
|
|
313
|
-
getBitsPerPixel(colorType, bitDepth) {
|
|
314
|
-
switch (colorType) {
|
|
315
|
-
case 0: // Grayscale
|
|
316
|
-
return bitDepth;
|
|
317
|
-
case 2: // RGB
|
|
318
|
-
return bitDepth * 3;
|
|
319
|
-
case 3: // Palette
|
|
320
|
-
return bitDepth;
|
|
321
|
-
case 4: // Grayscale + Alpha
|
|
322
|
-
return bitDepth * 2;
|
|
323
|
-
case 6: // RGBA
|
|
324
|
-
return bitDepth * 4;
|
|
325
|
-
default:
|
|
326
|
-
throw new Error(`Unknown color type: ${colorType}`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
createChunk(type, data) {
|
|
330
|
-
const chunk = new Uint8Array(12 + data.length);
|
|
331
|
-
this.writeUint32(chunk, 0, data.length);
|
|
332
|
-
chunk[4] = type.charCodeAt(0);
|
|
333
|
-
chunk[5] = type.charCodeAt(1);
|
|
334
|
-
chunk[6] = type.charCodeAt(2);
|
|
335
|
-
chunk[7] = type.charCodeAt(3);
|
|
336
|
-
chunk.set(data, 8);
|
|
337
|
-
const crc = this.crc32(chunk.slice(4, 8 + data.length));
|
|
338
|
-
this.writeUint32(chunk, 8 + data.length, crc);
|
|
339
|
-
return chunk;
|
|
340
|
-
}
|
|
341
|
-
crc32(data) {
|
|
342
|
-
let crc = 0xffffffff;
|
|
343
|
-
for (let i = 0; i < data.length; i++) {
|
|
344
|
-
crc ^= data[i];
|
|
345
|
-
for (let j = 0; j < 8; j++) {
|
|
346
|
-
crc = (crc & 1) ? (crc >>> 1) ^ 0xedb88320 : crc >>> 1;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return (crc ^ 0xffffffff) >>> 0;
|
|
350
|
-
}
|
|
351
|
-
// Metadata parsing methods
|
|
352
|
-
parsePhysChunk(data, metadata, width, height) {
|
|
353
|
-
if (data.length < 9)
|
|
354
|
-
return;
|
|
355
|
-
const pixelsPerUnitX = this.readUint32(data, 0);
|
|
356
|
-
const pixelsPerUnitY = this.readUint32(data, 4);
|
|
357
|
-
const unit = data[8]; // 0 = unknown, 1 = meter
|
|
358
|
-
if (unit === 1 && pixelsPerUnitX > 0 && pixelsPerUnitY > 0) {
|
|
359
|
-
// Convert pixels per meter to DPI
|
|
360
|
-
metadata.dpiX = Math.round(pixelsPerUnitX / INCHES_PER_METER);
|
|
361
|
-
metadata.dpiY = Math.round(pixelsPerUnitY / INCHES_PER_METER);
|
|
362
|
-
metadata.physicalWidth = width / metadata.dpiX;
|
|
363
|
-
metadata.physicalHeight = height / metadata.dpiY;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
parseTextChunk(data, metadata) {
|
|
367
|
-
// tEXt format: keyword\0text
|
|
368
|
-
const nullIndex = data.indexOf(0);
|
|
369
|
-
if (nullIndex === -1)
|
|
370
|
-
return;
|
|
371
|
-
const keyword = new TextDecoder().decode(data.slice(0, nullIndex));
|
|
372
|
-
const text = new TextDecoder().decode(data.slice(nullIndex + 1));
|
|
373
|
-
// Map standard keywords to metadata fields
|
|
374
|
-
switch (keyword.toLowerCase()) {
|
|
375
|
-
case "title":
|
|
376
|
-
metadata.title = text;
|
|
377
|
-
break;
|
|
378
|
-
case "author":
|
|
379
|
-
metadata.author = text;
|
|
380
|
-
break;
|
|
381
|
-
case "description":
|
|
382
|
-
metadata.description = text;
|
|
383
|
-
break;
|
|
384
|
-
case "copyright":
|
|
385
|
-
metadata.copyright = text;
|
|
386
|
-
break;
|
|
387
|
-
default:
|
|
388
|
-
// Store as custom metadata
|
|
389
|
-
if (!metadata.custom)
|
|
390
|
-
metadata.custom = {};
|
|
391
|
-
metadata.custom[keyword] = text;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
parseITxtChunk(data, metadata) {
|
|
395
|
-
// iTXt format: keyword\0compressed_flag\0compression_method\0language\0translated_keyword\0text
|
|
396
|
-
let pos = 0;
|
|
397
|
-
const nullIndex = data.indexOf(0, pos);
|
|
398
|
-
if (nullIndex === -1 || pos >= data.length)
|
|
399
|
-
return;
|
|
400
|
-
const keyword = new TextDecoder().decode(data.slice(pos, nullIndex));
|
|
401
|
-
pos = nullIndex + 1;
|
|
402
|
-
if (pos + 2 > data.length)
|
|
403
|
-
return; // Need at least 2 bytes for flags
|
|
404
|
-
const _compressionFlag = data[pos++];
|
|
405
|
-
const _compressionMethod = data[pos++];
|
|
406
|
-
const languageNullIndex = data.indexOf(0, pos);
|
|
407
|
-
if (languageNullIndex === -1 || pos >= data.length)
|
|
408
|
-
return;
|
|
409
|
-
pos = languageNullIndex + 1;
|
|
410
|
-
const translatedKeywordNullIndex = data.indexOf(0, pos);
|
|
411
|
-
if (translatedKeywordNullIndex === -1 || pos >= data.length)
|
|
412
|
-
return;
|
|
413
|
-
pos = translatedKeywordNullIndex + 1;
|
|
414
|
-
if (pos >= data.length)
|
|
415
|
-
return; // No text data
|
|
416
|
-
const text = new TextDecoder("utf-8").decode(data.slice(pos));
|
|
417
|
-
// Map to metadata fields (same as tEXt)
|
|
418
|
-
switch (keyword.toLowerCase()) {
|
|
419
|
-
case "title":
|
|
420
|
-
metadata.title = text;
|
|
421
|
-
break;
|
|
422
|
-
case "author":
|
|
423
|
-
metadata.author = text;
|
|
424
|
-
break;
|
|
425
|
-
case "description":
|
|
426
|
-
metadata.description = text;
|
|
427
|
-
break;
|
|
428
|
-
case "copyright":
|
|
429
|
-
metadata.copyright = text;
|
|
430
|
-
break;
|
|
431
|
-
default:
|
|
432
|
-
if (!metadata.custom)
|
|
433
|
-
metadata.custom = {};
|
|
434
|
-
metadata.custom[keyword] = text;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
parseExifChunk(data, metadata) {
|
|
438
|
-
// Basic EXIF parsing for GPS and date
|
|
439
|
-
// EXIF data starts with byte order marker
|
|
440
|
-
if (data.length < 8)
|
|
441
|
-
return;
|
|
442
|
-
try {
|
|
443
|
-
const byteOrder = String.fromCharCode(data[0], data[1]);
|
|
444
|
-
const littleEndian = byteOrder === "II";
|
|
445
|
-
// Skip to IFD0 offset
|
|
446
|
-
const ifd0Offset = littleEndian
|
|
447
|
-
? data[4] | (data[5] << 8) | (data[6] << 16) | (data[7] << 24)
|
|
448
|
-
: (data[4] << 24) | (data[5] << 16) | (data[6] << 8) | data[7];
|
|
449
|
-
if (ifd0Offset + 2 > data.length)
|
|
450
|
-
return;
|
|
451
|
-
// Read number of IFD entries with bounds check
|
|
452
|
-
const numEntries = littleEndian
|
|
453
|
-
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
454
|
-
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
455
|
-
// Parse IFD entries looking for GPS and DateTime tags
|
|
456
|
-
for (let i = 0; i < numEntries; i++) {
|
|
457
|
-
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
458
|
-
if (entryOffset + 12 > data.length)
|
|
459
|
-
break;
|
|
460
|
-
const tag = littleEndian
|
|
461
|
-
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
462
|
-
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
463
|
-
// DateTime tag (0x0132)
|
|
464
|
-
if (tag === 0x0132) {
|
|
465
|
-
const valueOffset = littleEndian
|
|
466
|
-
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
467
|
-
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
468
|
-
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
469
|
-
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
470
|
-
if (valueOffset < data.length) {
|
|
471
|
-
const nullIndex = data.indexOf(0, valueOffset);
|
|
472
|
-
if (nullIndex > valueOffset) {
|
|
473
|
-
const dateStr = new TextDecoder().decode(data.slice(valueOffset, nullIndex));
|
|
474
|
-
// Parse EXIF datetime format: "YYYY:MM:DD HH:MM:SS"
|
|
475
|
-
const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
|
|
476
|
-
if (match) {
|
|
477
|
-
metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
catch (_e) {
|
|
485
|
-
// Ignore EXIF parsing errors
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
createPhysChunk(metadata) {
|
|
489
|
-
const chunk = new Uint8Array(9);
|
|
490
|
-
// Default to 72 DPI if not specified
|
|
491
|
-
const dpiX = metadata.dpiX ?? 72;
|
|
492
|
-
const dpiY = metadata.dpiY ?? 72;
|
|
493
|
-
// Convert DPI to pixels per meter
|
|
494
|
-
const pixelsPerMeterX = Math.round(dpiX * INCHES_PER_METER);
|
|
495
|
-
const pixelsPerMeterY = Math.round(dpiY * INCHES_PER_METER);
|
|
496
|
-
this.writeUint32(chunk, 0, pixelsPerMeterX);
|
|
497
|
-
this.writeUint32(chunk, 4, pixelsPerMeterY);
|
|
498
|
-
chunk[8] = 1; // Unit is meters
|
|
499
|
-
return chunk;
|
|
500
|
-
}
|
|
501
|
-
createTextChunk(keyword, text) {
|
|
502
|
-
const keywordBytes = new TextEncoder().encode(keyword);
|
|
503
|
-
const textBytes = new TextEncoder().encode(text);
|
|
504
|
-
const chunk = new Uint8Array(keywordBytes.length + 1 + textBytes.length);
|
|
505
|
-
chunk.set(keywordBytes, 0);
|
|
506
|
-
chunk[keywordBytes.length] = 0; // Null separator
|
|
507
|
-
chunk.set(textBytes, keywordBytes.length + 1);
|
|
508
|
-
return chunk;
|
|
509
|
-
}
|
|
510
|
-
createExifChunk(metadata) {
|
|
511
|
-
// Create a minimal EXIF structure
|
|
512
|
-
const entries = [];
|
|
513
|
-
// Add DateTime if available
|
|
514
|
-
if (metadata.creationDate) {
|
|
515
|
-
const date = metadata.creationDate;
|
|
516
|
-
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`;
|
|
517
|
-
entries.push({
|
|
518
|
-
tag: 0x0132,
|
|
519
|
-
type: 2, // ASCII
|
|
520
|
-
value: new TextEncoder().encode(dateStr),
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
if (entries.length === 0)
|
|
524
|
-
return null;
|
|
525
|
-
// Build minimal EXIF structure
|
|
526
|
-
const exif = [];
|
|
527
|
-
// Byte order marker (little endian)
|
|
528
|
-
exif.push(0x49, 0x49); // "II"
|
|
529
|
-
exif.push(0x2a, 0x00); // 42
|
|
530
|
-
// Offset to IFD0 (8 bytes from start)
|
|
531
|
-
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
532
|
-
// Number of IFD entries
|
|
533
|
-
exif.push(entries.length & 0xff, (entries.length >> 8) & 0xff);
|
|
534
|
-
// Calculate data offset (after all entries)
|
|
535
|
-
let dataOffset = 8 + 2 + entries.length * 12 + 4;
|
|
536
|
-
for (const entry of entries) {
|
|
537
|
-
// Tag
|
|
538
|
-
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
539
|
-
// Type
|
|
540
|
-
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
541
|
-
// Count
|
|
542
|
-
const count = entry.value.length;
|
|
543
|
-
exif.push(count & 0xff, (count >> 8) & 0xff, (count >> 16) & 0xff, (count >> 24) & 0xff);
|
|
544
|
-
// Value/Offset
|
|
545
|
-
if (entry.value.length <= 4) {
|
|
546
|
-
for (let i = 0; i < 4; i++) {
|
|
547
|
-
exif.push(i < entry.value.length ? entry.value[i] : 0);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
else {
|
|
551
|
-
exif.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
552
|
-
dataOffset += entry.value.length;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
// Next IFD offset (0 = no more IFDs)
|
|
556
|
-
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
557
|
-
// Append data for entries that didn't fit in value field
|
|
558
|
-
for (const entry of entries) {
|
|
559
|
-
if (entry.value.length > 4) {
|
|
560
|
-
for (const byte of entry.value) {
|
|
561
|
-
exif.push(byte);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
return new Uint8Array(exif);
|
|
141
|
+
return this.concatenateArrays(chunks);
|
|
566
142
|
}
|
|
567
143
|
}
|
|
568
144
|
exports.PNGFormat = PNGFormat;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ImageMetadata } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Base class for PNG and APNG format handlers
|
|
4
|
+
* Contains shared utility methods for PNG chunk manipulation and metadata parsing
|
|
5
|
+
*/
|
|
6
|
+
export declare abstract class PNGBase {
|
|
7
|
+
/**
|
|
8
|
+
* Read a 32-bit unsigned integer (big-endian)
|
|
9
|
+
*/
|
|
10
|
+
protected readUint32(data: Uint8Array, offset: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Read a 16-bit unsigned integer (big-endian)
|
|
13
|
+
*/
|
|
14
|
+
protected readUint16(data: Uint8Array, offset: number): number;
|
|
15
|
+
/**
|
|
16
|
+
* Write a 32-bit unsigned integer (big-endian)
|
|
17
|
+
*/
|
|
18
|
+
protected writeUint32(data: Uint8Array, offset: number, value: number): void;
|
|
19
|
+
/**
|
|
20
|
+
* Write a 16-bit unsigned integer (big-endian)
|
|
21
|
+
*/
|
|
22
|
+
protected writeUint16(data: Uint8Array, offset: number, value: number): void;
|
|
23
|
+
/**
|
|
24
|
+
* Decompress PNG data using deflate
|
|
25
|
+
*/
|
|
26
|
+
protected inflate(data: Uint8Array): Promise<Uint8Array>;
|
|
27
|
+
/**
|
|
28
|
+
* Compress PNG data using deflate
|
|
29
|
+
*/
|
|
30
|
+
protected deflate(data: Uint8Array): Promise<Uint8Array>;
|
|
31
|
+
/**
|
|
32
|
+
* Unfilter PNG scanlines and convert to RGBA
|
|
33
|
+
*/
|
|
34
|
+
protected unfilterAndConvert(data: Uint8Array, width: number, height: number, bitDepth: number, colorType: number): Uint8Array;
|
|
35
|
+
/**
|
|
36
|
+
* Unfilter a single PNG scanline
|
|
37
|
+
*/
|
|
38
|
+
protected unfilterScanline(scanline: Uint8Array, prevLine: Uint8Array | null, filterType: number, bytesPerPixel: number): void;
|
|
39
|
+
/**
|
|
40
|
+
* Paeth predictor for PNG filtering
|
|
41
|
+
*/
|
|
42
|
+
protected paethPredictor(a: number, b: number, c: number): number;
|
|
43
|
+
/**
|
|
44
|
+
* Filter PNG data for encoding (using filter type 0 - None)
|
|
45
|
+
*/
|
|
46
|
+
protected filterData(data: Uint8Array, width: number, height: number): Uint8Array;
|
|
47
|
+
/**
|
|
48
|
+
* Get bytes per pixel for a given color type and bit depth
|
|
49
|
+
*/
|
|
50
|
+
protected getBytesPerPixel(colorType: number, bitDepth: number): number;
|
|
51
|
+
/**
|
|
52
|
+
* Get bits per pixel for a given color type and bit depth
|
|
53
|
+
*/
|
|
54
|
+
protected getBitsPerPixel(colorType: number, bitDepth: number): number;
|
|
55
|
+
/**
|
|
56
|
+
* Create a PNG chunk with length, type, data, and CRC
|
|
57
|
+
*/
|
|
58
|
+
protected createChunk(type: string, data: Uint8Array): Uint8Array;
|
|
59
|
+
/**
|
|
60
|
+
* Calculate CRC32 checksum
|
|
61
|
+
*/
|
|
62
|
+
protected crc32(data: Uint8Array): number;
|
|
63
|
+
/**
|
|
64
|
+
* Parse pHYs (physical pixel dimensions) chunk
|
|
65
|
+
*/
|
|
66
|
+
protected parsePhysChunk(data: Uint8Array, metadata: ImageMetadata, width: number, height: number): void;
|
|
67
|
+
/**
|
|
68
|
+
* Parse tEXt (text) chunk
|
|
69
|
+
*/
|
|
70
|
+
protected parseTextChunk(data: Uint8Array, metadata: ImageMetadata): void;
|
|
71
|
+
/**
|
|
72
|
+
* Parse iTXt (international text) chunk
|
|
73
|
+
*/
|
|
74
|
+
protected parseITxtChunk(data: Uint8Array, metadata: ImageMetadata): void;
|
|
75
|
+
/**
|
|
76
|
+
* Parse eXIf (EXIF) chunk
|
|
77
|
+
*/
|
|
78
|
+
protected parseExifChunk(data: Uint8Array, metadata: ImageMetadata): void;
|
|
79
|
+
/**
|
|
80
|
+
* Create pHYs (physical pixel dimensions) chunk
|
|
81
|
+
*/
|
|
82
|
+
protected createPhysChunk(metadata: ImageMetadata): Uint8Array;
|
|
83
|
+
/**
|
|
84
|
+
* Create tEXt (text) chunk
|
|
85
|
+
*/
|
|
86
|
+
protected createTextChunk(keyword: string, text: string): Uint8Array;
|
|
87
|
+
/**
|
|
88
|
+
* Create eXIf (EXIF) chunk
|
|
89
|
+
*/
|
|
90
|
+
protected createExifChunk(metadata: ImageMetadata): Uint8Array | null;
|
|
91
|
+
/**
|
|
92
|
+
* Concatenate multiple byte arrays into a single Uint8Array
|
|
93
|
+
*/
|
|
94
|
+
protected concatenateChunks(chunks: {
|
|
95
|
+
type: string;
|
|
96
|
+
data: Uint8Array;
|
|
97
|
+
}[]): Uint8Array;
|
|
98
|
+
/**
|
|
99
|
+
* Concatenate multiple Uint8Arrays into a single Uint8Array
|
|
100
|
+
*/
|
|
101
|
+
protected concatenateArrays(arrays: Uint8Array[]): Uint8Array;
|
|
102
|
+
/**
|
|
103
|
+
* Add metadata chunks to the chunks array
|
|
104
|
+
* Shared method to avoid duplication between PNG and APNG encoding
|
|
105
|
+
*/
|
|
106
|
+
protected addMetadataChunks(chunks: Uint8Array[], metadata: ImageMetadata | undefined): void;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=png_base.d.ts.map
|