cross-image 0.2.1 → 0.2.3
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 +160 -32
- package/esm/mod.d.ts +2 -1
- package/esm/mod.js +2 -1
- package/esm/src/formats/jpeg.d.ts +12 -1
- package/esm/src/formats/jpeg.js +633 -4
- package/esm/src/formats/png_base.d.ts +8 -0
- package/esm/src/formats/png_base.js +176 -3
- package/esm/src/formats/ppm.d.ts +50 -0
- package/esm/src/formats/ppm.js +242 -0
- package/esm/src/formats/tiff.d.ts +10 -1
- package/esm/src/formats/tiff.js +194 -44
- package/esm/src/formats/webp.d.ts +9 -2
- package/esm/src/formats/webp.js +211 -62
- package/esm/src/image.d.ts +81 -0
- package/esm/src/image.js +282 -5
- package/esm/src/types.d.ts +41 -1
- package/esm/src/utils/image_processing.d.ts +98 -0
- package/esm/src/utils/image_processing.js +440 -0
- package/esm/src/utils/metadata/xmp.d.ts +52 -0
- package/esm/src/utils/metadata/xmp.js +325 -0
- package/esm/src/utils/resize.d.ts +4 -0
- package/esm/src/utils/resize.js +74 -0
- package/package.json +1 -1
- package/script/mod.d.ts +2 -1
- package/script/mod.js +4 -2
- package/script/src/formats/jpeg.d.ts +12 -1
- package/script/src/formats/jpeg.js +633 -4
- package/script/src/formats/png_base.d.ts +8 -0
- package/script/src/formats/png_base.js +176 -3
- package/script/src/formats/ppm.d.ts +50 -0
- package/script/src/formats/ppm.js +246 -0
- package/script/src/formats/tiff.d.ts +10 -1
- package/script/src/formats/tiff.js +194 -44
- package/script/src/formats/webp.d.ts +9 -2
- package/script/src/formats/webp.js +211 -62
- package/script/src/image.d.ts +81 -0
- package/script/src/image.js +280 -3
- package/script/src/types.d.ts +41 -1
- package/script/src/utils/image_processing.d.ts +98 -0
- package/script/src/utils/image_processing.js +451 -0
- package/script/src/utils/metadata/xmp.d.ts +52 -0
- package/script/src/utils/metadata/xmp.js +333 -0
- package/script/src/utils/resize.d.ts +4 -0
- package/script/src/utils/resize.js +75 -0
|
@@ -76,6 +76,8 @@ export declare abstract class PNGBase {
|
|
|
76
76
|
* Parse eXIf (EXIF) chunk
|
|
77
77
|
*/
|
|
78
78
|
protected parseExifChunk(data: Uint8Array, metadata: ImageMetadata): void;
|
|
79
|
+
protected parseGPSIFD(data: Uint8Array, gpsIfdOffset: number, littleEndian: boolean, metadata: ImageMetadata): void;
|
|
80
|
+
protected readRational(data: Uint8Array, offset: number, littleEndian: boolean): number;
|
|
79
81
|
/**
|
|
80
82
|
* Create pHYs (physical pixel dimensions) chunk
|
|
81
83
|
*/
|
|
@@ -88,6 +90,8 @@ export declare abstract class PNGBase {
|
|
|
88
90
|
* Create eXIf (EXIF) chunk
|
|
89
91
|
*/
|
|
90
92
|
protected createExifChunk(metadata: ImageMetadata): Uint8Array | null;
|
|
93
|
+
protected createGPSIFD(metadata: ImageMetadata, gpsIfdStart: number): number[];
|
|
94
|
+
protected writeRational(output: number[], numerator: number, denominator: number): void;
|
|
91
95
|
/**
|
|
92
96
|
* Concatenate multiple byte arrays into a single Uint8Array
|
|
93
97
|
*/
|
|
@@ -104,5 +108,9 @@ export declare abstract class PNGBase {
|
|
|
104
108
|
* Shared method to avoid duplication between PNG and APNG encoding
|
|
105
109
|
*/
|
|
106
110
|
protected addMetadataChunks(chunks: Uint8Array[], metadata: ImageMetadata | undefined): void;
|
|
111
|
+
/**
|
|
112
|
+
* Get the list of metadata fields supported by PNG format
|
|
113
|
+
*/
|
|
114
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
107
115
|
}
|
|
108
116
|
//# sourceMappingURL=png_base.d.ts.map
|
|
@@ -315,6 +315,7 @@ export class PNGBase {
|
|
|
315
315
|
const numEntries = littleEndian
|
|
316
316
|
? data[ifd0Offset] | (data[ifd0Offset + 1] << 8)
|
|
317
317
|
: (data[ifd0Offset] << 8) | data[ifd0Offset + 1];
|
|
318
|
+
let gpsIfdOffset = 0;
|
|
318
319
|
for (let i = 0; i < numEntries; i++) {
|
|
319
320
|
const entryOffset = ifd0Offset + 2 + i * 12;
|
|
320
321
|
if (entryOffset + 12 > data.length)
|
|
@@ -339,12 +340,96 @@ export class PNGBase {
|
|
|
339
340
|
}
|
|
340
341
|
}
|
|
341
342
|
}
|
|
343
|
+
// GPS IFD Pointer tag (0x8825)
|
|
344
|
+
if (tag === 0x8825) {
|
|
345
|
+
gpsIfdOffset = littleEndian
|
|
346
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
347
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
348
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
349
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// Parse GPS IFD if present
|
|
353
|
+
if (gpsIfdOffset > 0 && gpsIfdOffset + 2 <= data.length) {
|
|
354
|
+
this.parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata);
|
|
342
355
|
}
|
|
343
356
|
}
|
|
344
357
|
catch (_e) {
|
|
345
358
|
// Ignore EXIF parsing errors
|
|
346
359
|
}
|
|
347
360
|
}
|
|
361
|
+
parseGPSIFD(data, gpsIfdOffset, littleEndian, metadata) {
|
|
362
|
+
try {
|
|
363
|
+
const numEntries = littleEndian
|
|
364
|
+
? data[gpsIfdOffset] | (data[gpsIfdOffset + 1] << 8)
|
|
365
|
+
: (data[gpsIfdOffset] << 8) | data[gpsIfdOffset + 1];
|
|
366
|
+
let latRef = "";
|
|
367
|
+
let lonRef = "";
|
|
368
|
+
let latitude;
|
|
369
|
+
let longitude;
|
|
370
|
+
for (let i = 0; i < numEntries; i++) {
|
|
371
|
+
const entryOffset = gpsIfdOffset + 2 + i * 12;
|
|
372
|
+
if (entryOffset + 12 > data.length)
|
|
373
|
+
break;
|
|
374
|
+
const tag = littleEndian
|
|
375
|
+
? data[entryOffset] | (data[entryOffset + 1] << 8)
|
|
376
|
+
: (data[entryOffset] << 8) | data[entryOffset + 1];
|
|
377
|
+
const type = littleEndian
|
|
378
|
+
? data[entryOffset + 2] | (data[entryOffset + 3] << 8)
|
|
379
|
+
: (data[entryOffset + 2] << 8) | data[entryOffset + 3];
|
|
380
|
+
const valueOffset = littleEndian
|
|
381
|
+
? data[entryOffset + 8] | (data[entryOffset + 9] << 8) |
|
|
382
|
+
(data[entryOffset + 10] << 16) | (data[entryOffset + 11] << 24)
|
|
383
|
+
: (data[entryOffset + 8] << 24) | (data[entryOffset + 9] << 16) |
|
|
384
|
+
(data[entryOffset + 10] << 8) | data[entryOffset + 11];
|
|
385
|
+
// GPSLatitudeRef (0x0001)
|
|
386
|
+
if (tag === 0x0001 && type === 2) {
|
|
387
|
+
latRef = String.fromCharCode(data[entryOffset + 8]);
|
|
388
|
+
}
|
|
389
|
+
// GPSLatitude (0x0002)
|
|
390
|
+
if (tag === 0x0002 && type === 5 && valueOffset + 24 <= data.length) {
|
|
391
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
392
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
393
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
394
|
+
latitude = degrees + minutes / 60 + seconds / 3600;
|
|
395
|
+
}
|
|
396
|
+
// GPSLongitudeRef (0x0003)
|
|
397
|
+
if (tag === 0x0003 && type === 2) {
|
|
398
|
+
lonRef = String.fromCharCode(data[entryOffset + 8]);
|
|
399
|
+
}
|
|
400
|
+
// GPSLongitude (0x0004)
|
|
401
|
+
if (tag === 0x0004 && type === 5 && valueOffset + 24 <= data.length) {
|
|
402
|
+
const degrees = this.readRational(data, valueOffset, littleEndian);
|
|
403
|
+
const minutes = this.readRational(data, valueOffset + 8, littleEndian);
|
|
404
|
+
const seconds = this.readRational(data, valueOffset + 16, littleEndian);
|
|
405
|
+
longitude = degrees + minutes / 60 + seconds / 3600;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
// Apply hemisphere references
|
|
409
|
+
if (latitude !== undefined && latRef) {
|
|
410
|
+
metadata.latitude = latRef === "S" ? -latitude : latitude;
|
|
411
|
+
}
|
|
412
|
+
if (longitude !== undefined && lonRef) {
|
|
413
|
+
metadata.longitude = lonRef === "W" ? -longitude : longitude;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (_e) {
|
|
417
|
+
// Ignore GPS parsing errors
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
readRational(data, offset, littleEndian) {
|
|
421
|
+
const numerator = littleEndian
|
|
422
|
+
? data[offset] | (data[offset + 1] << 8) | (data[offset + 2] << 16) |
|
|
423
|
+
(data[offset + 3] << 24)
|
|
424
|
+
: (data[offset] << 24) | (data[offset + 1] << 16) |
|
|
425
|
+
(data[offset + 2] << 8) | data[offset + 3];
|
|
426
|
+
const denominator = littleEndian
|
|
427
|
+
? data[offset + 4] | (data[offset + 5] << 8) | (data[offset + 6] << 16) |
|
|
428
|
+
(data[offset + 7] << 24)
|
|
429
|
+
: (data[offset + 4] << 24) | (data[offset + 5] << 16) |
|
|
430
|
+
(data[offset + 6] << 8) | data[offset + 7];
|
|
431
|
+
return denominator !== 0 ? numerator / denominator : 0;
|
|
432
|
+
}
|
|
348
433
|
/**
|
|
349
434
|
* Create pHYs (physical pixel dimensions) chunk
|
|
350
435
|
*/
|
|
@@ -385,14 +470,19 @@ export class PNGBase {
|
|
|
385
470
|
value: new TextEncoder().encode(dateStr),
|
|
386
471
|
});
|
|
387
472
|
}
|
|
388
|
-
if
|
|
473
|
+
// Check if we have GPS data
|
|
474
|
+
const hasGPS = metadata.latitude !== undefined &&
|
|
475
|
+
metadata.longitude !== undefined;
|
|
476
|
+
if (entries.length === 0 && !hasGPS)
|
|
389
477
|
return null;
|
|
390
478
|
const exif = [];
|
|
391
479
|
exif.push(0x49, 0x49); // "II"
|
|
392
480
|
exif.push(0x2a, 0x00); // 42
|
|
393
481
|
exif.push(0x08, 0x00, 0x00, 0x00);
|
|
394
|
-
|
|
395
|
-
|
|
482
|
+
// Number of entries (add GPS IFD pointer if we have GPS data)
|
|
483
|
+
const ifd0Entries = entries.length + (hasGPS ? 1 : 0);
|
|
484
|
+
exif.push(ifd0Entries & 0xff, (ifd0Entries >> 8) & 0xff);
|
|
485
|
+
let dataOffset = 8 + 2 + ifd0Entries * 12 + 4;
|
|
396
486
|
for (const entry of entries) {
|
|
397
487
|
exif.push(entry.tag & 0xff, (entry.tag >> 8) & 0xff);
|
|
398
488
|
exif.push(entry.type & 0xff, (entry.type >> 8) & 0xff);
|
|
@@ -408,6 +498,16 @@ export class PNGBase {
|
|
|
408
498
|
dataOffset += entry.value.length;
|
|
409
499
|
}
|
|
410
500
|
}
|
|
501
|
+
// Add GPS IFD pointer if we have GPS data
|
|
502
|
+
let gpsIfdOffset = 0;
|
|
503
|
+
if (hasGPS) {
|
|
504
|
+
gpsIfdOffset = dataOffset;
|
|
505
|
+
// GPS IFD Pointer tag (0x8825), type 4 (LONG), count 1
|
|
506
|
+
exif.push(0x25, 0x88); // Tag
|
|
507
|
+
exif.push(0x04, 0x00); // Type
|
|
508
|
+
exif.push(0x01, 0x00, 0x00, 0x00); // Count
|
|
509
|
+
exif.push(gpsIfdOffset & 0xff, (gpsIfdOffset >> 8) & 0xff, (gpsIfdOffset >> 16) & 0xff, (gpsIfdOffset >> 24) & 0xff);
|
|
510
|
+
}
|
|
411
511
|
exif.push(0x00, 0x00, 0x00, 0x00);
|
|
412
512
|
for (const entry of entries) {
|
|
413
513
|
if (entry.value.length > 4) {
|
|
@@ -416,8 +516,65 @@ export class PNGBase {
|
|
|
416
516
|
}
|
|
417
517
|
}
|
|
418
518
|
}
|
|
519
|
+
// Add GPS IFD if we have GPS data
|
|
520
|
+
if (hasGPS) {
|
|
521
|
+
const gpsIfd = this.createGPSIFD(metadata, gpsIfdOffset);
|
|
522
|
+
for (const byte of gpsIfd) {
|
|
523
|
+
exif.push(byte);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
419
526
|
return new Uint8Array(exif);
|
|
420
527
|
}
|
|
528
|
+
createGPSIFD(metadata, gpsIfdStart) {
|
|
529
|
+
const gps = [];
|
|
530
|
+
const numEntries = 4;
|
|
531
|
+
gps.push(numEntries & 0xff, (numEntries >> 8) & 0xff);
|
|
532
|
+
const latitude = metadata.latitude;
|
|
533
|
+
const longitude = metadata.longitude;
|
|
534
|
+
const absLat = Math.abs(latitude);
|
|
535
|
+
const absLon = Math.abs(longitude);
|
|
536
|
+
const latDeg = Math.floor(absLat);
|
|
537
|
+
const latMin = Math.floor((absLat - latDeg) * 60);
|
|
538
|
+
const latSec = ((absLat - latDeg) * 60 - latMin) * 60;
|
|
539
|
+
const lonDeg = Math.floor(absLon);
|
|
540
|
+
const lonMin = Math.floor((absLon - lonDeg) * 60);
|
|
541
|
+
const lonSec = ((absLon - lonDeg) * 60 - lonMin) * 60;
|
|
542
|
+
let dataOffset = gpsIfdStart + 2 + numEntries * 12 + 4;
|
|
543
|
+
// Entry 1: GPSLatitudeRef
|
|
544
|
+
gps.push(0x01, 0x00);
|
|
545
|
+
gps.push(0x02, 0x00);
|
|
546
|
+
gps.push(0x02, 0x00, 0x00, 0x00);
|
|
547
|
+
gps.push(latitude >= 0 ? 78 : 83, 0x00, 0x00, 0x00);
|
|
548
|
+
// Entry 2: GPSLatitude
|
|
549
|
+
gps.push(0x02, 0x00);
|
|
550
|
+
gps.push(0x05, 0x00);
|
|
551
|
+
gps.push(0x03, 0x00, 0x00, 0x00);
|
|
552
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
553
|
+
dataOffset += 24;
|
|
554
|
+
// Entry 3: GPSLongitudeRef
|
|
555
|
+
gps.push(0x03, 0x00);
|
|
556
|
+
gps.push(0x02, 0x00);
|
|
557
|
+
gps.push(0x02, 0x00, 0x00, 0x00);
|
|
558
|
+
gps.push(longitude >= 0 ? 69 : 87, 0x00, 0x00, 0x00);
|
|
559
|
+
// Entry 4: GPSLongitude
|
|
560
|
+
gps.push(0x04, 0x00);
|
|
561
|
+
gps.push(0x05, 0x00);
|
|
562
|
+
gps.push(0x03, 0x00, 0x00, 0x00);
|
|
563
|
+
gps.push(dataOffset & 0xff, (dataOffset >> 8) & 0xff, (dataOffset >> 16) & 0xff, (dataOffset >> 24) & 0xff);
|
|
564
|
+
gps.push(0x00, 0x00, 0x00, 0x00);
|
|
565
|
+
// Write rationals
|
|
566
|
+
this.writeRational(gps, latDeg, 1);
|
|
567
|
+
this.writeRational(gps, latMin, 1);
|
|
568
|
+
this.writeRational(gps, Math.round(latSec * 1000000), 1000000);
|
|
569
|
+
this.writeRational(gps, lonDeg, 1);
|
|
570
|
+
this.writeRational(gps, lonMin, 1);
|
|
571
|
+
this.writeRational(gps, Math.round(lonSec * 1000000), 1000000);
|
|
572
|
+
return gps;
|
|
573
|
+
}
|
|
574
|
+
writeRational(output, numerator, denominator) {
|
|
575
|
+
output.push(numerator & 0xff, (numerator >> 8) & 0xff, (numerator >> 16) & 0xff, (numerator >> 24) & 0xff);
|
|
576
|
+
output.push(denominator & 0xff, (denominator >> 8) & 0xff, (denominator >> 16) & 0xff, (denominator >> 24) & 0xff);
|
|
577
|
+
}
|
|
421
578
|
/**
|
|
422
579
|
* Concatenate multiple byte arrays into a single Uint8Array
|
|
423
580
|
*/
|
|
@@ -484,4 +641,20 @@ export class PNGBase {
|
|
|
484
641
|
}
|
|
485
642
|
}
|
|
486
643
|
}
|
|
644
|
+
/**
|
|
645
|
+
* Get the list of metadata fields supported by PNG format
|
|
646
|
+
*/
|
|
647
|
+
getSupportedMetadata() {
|
|
648
|
+
return [
|
|
649
|
+
"creationDate", // eXIf chunk
|
|
650
|
+
"latitude", // eXIf chunk (GPS IFD)
|
|
651
|
+
"longitude", // eXIf chunk (GPS IFD)
|
|
652
|
+
"dpiX", // pHYs chunk
|
|
653
|
+
"dpiY", // pHYs chunk
|
|
654
|
+
"title", // tEXt chunk
|
|
655
|
+
"author", // tEXt chunk
|
|
656
|
+
"description", // tEXt chunk
|
|
657
|
+
"copyright", // tEXt chunk
|
|
658
|
+
];
|
|
659
|
+
}
|
|
487
660
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ImageData, ImageFormat } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* PPM format handler
|
|
4
|
+
* Implements the Netpbm PPM (Portable PixMap) format.
|
|
5
|
+
* This is a simple uncompressed RGB format supported by many image tools.
|
|
6
|
+
*
|
|
7
|
+
* Format structure:
|
|
8
|
+
* - P3 (ASCII format):
|
|
9
|
+
* P3
|
|
10
|
+
* <width> <height>
|
|
11
|
+
* <maxval>
|
|
12
|
+
* R G B R G B ... (space-separated decimal values)
|
|
13
|
+
*
|
|
14
|
+
* - P6 (Binary format):
|
|
15
|
+
* P6
|
|
16
|
+
* <width> <height>
|
|
17
|
+
* <maxval>
|
|
18
|
+
* RGB RGB RGB ... (binary byte data)
|
|
19
|
+
*/
|
|
20
|
+
export declare class PPMFormat implements ImageFormat {
|
|
21
|
+
/** Format name identifier */
|
|
22
|
+
readonly name = "ppm";
|
|
23
|
+
/** MIME type for PPM images */
|
|
24
|
+
readonly mimeType = "image/x-portable-pixmap";
|
|
25
|
+
/**
|
|
26
|
+
* Check if the given data is a PPM image
|
|
27
|
+
* @param data Raw image data to check
|
|
28
|
+
* @returns true if data has PPM signature (P3 or P6)
|
|
29
|
+
*/
|
|
30
|
+
canDecode(data: Uint8Array): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Decode PPM image data to RGBA
|
|
33
|
+
* Supports both P3 (ASCII) and P6 (binary) formats
|
|
34
|
+
* @param data Raw PPM image data
|
|
35
|
+
* @returns Decoded image data with RGBA pixels
|
|
36
|
+
*/
|
|
37
|
+
decode(data: Uint8Array): Promise<ImageData>;
|
|
38
|
+
/**
|
|
39
|
+
* Encode RGBA image data to PPM format (P6 binary)
|
|
40
|
+
* Note: Alpha channel is ignored as PPM doesn't support transparency
|
|
41
|
+
* @param imageData Image data to encode
|
|
42
|
+
* @returns Encoded PPM image bytes
|
|
43
|
+
*/
|
|
44
|
+
encode(imageData: ImageData): Promise<Uint8Array>;
|
|
45
|
+
/**
|
|
46
|
+
* Check if a byte is whitespace (space, tab, CR, LF)
|
|
47
|
+
*/
|
|
48
|
+
private isWhitespace;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=ppm.d.ts.map
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { validateImageDimensions } from "../utils/security.js";
|
|
2
|
+
/**
|
|
3
|
+
* PPM format handler
|
|
4
|
+
* Implements the Netpbm PPM (Portable PixMap) format.
|
|
5
|
+
* This is a simple uncompressed RGB format supported by many image tools.
|
|
6
|
+
*
|
|
7
|
+
* Format structure:
|
|
8
|
+
* - P3 (ASCII format):
|
|
9
|
+
* P3
|
|
10
|
+
* <width> <height>
|
|
11
|
+
* <maxval>
|
|
12
|
+
* R G B R G B ... (space-separated decimal values)
|
|
13
|
+
*
|
|
14
|
+
* - P6 (Binary format):
|
|
15
|
+
* P6
|
|
16
|
+
* <width> <height>
|
|
17
|
+
* <maxval>
|
|
18
|
+
* RGB RGB RGB ... (binary byte data)
|
|
19
|
+
*/
|
|
20
|
+
export class PPMFormat {
|
|
21
|
+
constructor() {
|
|
22
|
+
/** Format name identifier */
|
|
23
|
+
Object.defineProperty(this, "name", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: "ppm"
|
|
28
|
+
});
|
|
29
|
+
/** MIME type for PPM images */
|
|
30
|
+
Object.defineProperty(this, "mimeType", {
|
|
31
|
+
enumerable: true,
|
|
32
|
+
configurable: true,
|
|
33
|
+
writable: true,
|
|
34
|
+
value: "image/x-portable-pixmap"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if the given data is a PPM image
|
|
39
|
+
* @param data Raw image data to check
|
|
40
|
+
* @returns true if data has PPM signature (P3 or P6)
|
|
41
|
+
*/
|
|
42
|
+
canDecode(data) {
|
|
43
|
+
// Check if data has at least magic bytes
|
|
44
|
+
if (data.length < 3) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Check for P3 or P6 followed by whitespace
|
|
48
|
+
return data[0] === 0x50 && // P
|
|
49
|
+
(data[1] === 0x33 || data[1] === 0x36) && // 3 or 6
|
|
50
|
+
(data[2] === 0x0a || data[2] === 0x0d || data[2] === 0x20 ||
|
|
51
|
+
data[2] === 0x09); // \n, \r, space, or tab
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Decode PPM image data to RGBA
|
|
55
|
+
* Supports both P3 (ASCII) and P6 (binary) formats
|
|
56
|
+
* @param data Raw PPM image data
|
|
57
|
+
* @returns Decoded image data with RGBA pixels
|
|
58
|
+
*/
|
|
59
|
+
decode(data) {
|
|
60
|
+
if (!this.canDecode(data)) {
|
|
61
|
+
throw new Error("Invalid PPM signature");
|
|
62
|
+
}
|
|
63
|
+
const isBinary = data[1] === 0x36; // P6
|
|
64
|
+
// Parse header
|
|
65
|
+
let offset = 0;
|
|
66
|
+
let width = 0;
|
|
67
|
+
let height = 0;
|
|
68
|
+
let maxval = 0;
|
|
69
|
+
let headerValues = 0; // Track how many values we've parsed (need 3: width, height, maxval)
|
|
70
|
+
// Skip magic number and whitespace
|
|
71
|
+
offset = 2;
|
|
72
|
+
while (offset < data.length && this.isWhitespace(data[offset])) {
|
|
73
|
+
offset++;
|
|
74
|
+
}
|
|
75
|
+
// Parse header values (width, height, maxval)
|
|
76
|
+
while (headerValues < 3 && offset < data.length) {
|
|
77
|
+
// Skip comments (lines starting with #)
|
|
78
|
+
if (data[offset] === 0x23) { // #
|
|
79
|
+
// Skip until newline
|
|
80
|
+
while (offset < data.length && data[offset] !== 0x0a) {
|
|
81
|
+
offset++;
|
|
82
|
+
}
|
|
83
|
+
if (offset < data.length)
|
|
84
|
+
offset++; // Skip the newline
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Skip whitespace
|
|
88
|
+
while (offset < data.length && this.isWhitespace(data[offset])) {
|
|
89
|
+
offset++;
|
|
90
|
+
}
|
|
91
|
+
// Read number
|
|
92
|
+
let numStr = "";
|
|
93
|
+
while (offset < data.length &&
|
|
94
|
+
!this.isWhitespace(data[offset]) &&
|
|
95
|
+
data[offset] !== 0x23) {
|
|
96
|
+
numStr += String.fromCharCode(data[offset]);
|
|
97
|
+
offset++;
|
|
98
|
+
}
|
|
99
|
+
if (numStr) {
|
|
100
|
+
const num = parseInt(numStr, 10);
|
|
101
|
+
if (isNaN(num) || num <= 0) {
|
|
102
|
+
throw new Error(`Invalid PPM header value: ${numStr}`);
|
|
103
|
+
}
|
|
104
|
+
if (headerValues === 0) {
|
|
105
|
+
width = num;
|
|
106
|
+
}
|
|
107
|
+
else if (headerValues === 1) {
|
|
108
|
+
height = num;
|
|
109
|
+
}
|
|
110
|
+
else if (headerValues === 2) {
|
|
111
|
+
maxval = num;
|
|
112
|
+
}
|
|
113
|
+
headerValues++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (headerValues < 3) {
|
|
117
|
+
throw new Error("Incomplete PPM header");
|
|
118
|
+
}
|
|
119
|
+
// Skip single whitespace character after maxval (required by spec)
|
|
120
|
+
if (offset < data.length && this.isWhitespace(data[offset])) {
|
|
121
|
+
offset++;
|
|
122
|
+
}
|
|
123
|
+
// Validate dimensions
|
|
124
|
+
validateImageDimensions(width, height);
|
|
125
|
+
// Validate maxval
|
|
126
|
+
if (maxval > 255) {
|
|
127
|
+
throw new Error(`Unsupported PPM maxval: ${maxval}. Only maxval <= 255 is supported.`);
|
|
128
|
+
}
|
|
129
|
+
const pixelCount = width * height;
|
|
130
|
+
const rgba = new Uint8Array(pixelCount * 4);
|
|
131
|
+
if (isBinary) {
|
|
132
|
+
// P6: Binary format
|
|
133
|
+
const expectedDataLength = pixelCount * 3;
|
|
134
|
+
const actualDataLength = data.length - offset;
|
|
135
|
+
if (actualDataLength < expectedDataLength) {
|
|
136
|
+
throw new Error(`Invalid PPM data length: expected ${expectedDataLength}, got ${actualDataLength}`);
|
|
137
|
+
}
|
|
138
|
+
// Read RGB data and convert to RGBA
|
|
139
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
140
|
+
const srcIndex = offset + i * 3;
|
|
141
|
+
const dstIndex = i * 4;
|
|
142
|
+
rgba[dstIndex] = data[srcIndex]; // R
|
|
143
|
+
rgba[dstIndex + 1] = data[srcIndex + 1]; // G
|
|
144
|
+
rgba[dstIndex + 2] = data[srcIndex + 2]; // B
|
|
145
|
+
rgba[dstIndex + 3] = 255; // A (fully opaque)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// P3: ASCII format
|
|
150
|
+
let pixelIndex = 0;
|
|
151
|
+
while (pixelIndex < pixelCount * 3 && offset < data.length) {
|
|
152
|
+
// Skip whitespace and comments
|
|
153
|
+
while (offset < data.length) {
|
|
154
|
+
if (data[offset] === 0x23) { // #
|
|
155
|
+
// Skip comment
|
|
156
|
+
while (offset < data.length && data[offset] !== 0x0a) {
|
|
157
|
+
offset++;
|
|
158
|
+
}
|
|
159
|
+
if (offset < data.length)
|
|
160
|
+
offset++;
|
|
161
|
+
}
|
|
162
|
+
else if (this.isWhitespace(data[offset])) {
|
|
163
|
+
offset++;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Read number
|
|
170
|
+
let numStr = "";
|
|
171
|
+
while (offset < data.length &&
|
|
172
|
+
!this.isWhitespace(data[offset]) &&
|
|
173
|
+
data[offset] !== 0x23) {
|
|
174
|
+
numStr += String.fromCharCode(data[offset]);
|
|
175
|
+
offset++;
|
|
176
|
+
}
|
|
177
|
+
if (numStr) {
|
|
178
|
+
const value = parseInt(numStr, 10);
|
|
179
|
+
if (isNaN(value) || value < 0 || value > maxval) {
|
|
180
|
+
throw new Error(`Invalid PPM pixel value: ${numStr}`);
|
|
181
|
+
}
|
|
182
|
+
// Scale to 0-255 if needed
|
|
183
|
+
const scaledValue = maxval === 255 ? value : Math.round((value * 255) / maxval);
|
|
184
|
+
const component = pixelIndex % 3;
|
|
185
|
+
const rgbaIndex = Math.floor(pixelIndex / 3) * 4;
|
|
186
|
+
if (component === 0) {
|
|
187
|
+
rgba[rgbaIndex] = scaledValue; // R
|
|
188
|
+
}
|
|
189
|
+
else if (component === 1) {
|
|
190
|
+
rgba[rgbaIndex + 1] = scaledValue; // G
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
rgba[rgbaIndex + 2] = scaledValue; // B
|
|
194
|
+
rgba[rgbaIndex + 3] = 255; // A
|
|
195
|
+
}
|
|
196
|
+
pixelIndex++;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (pixelIndex < pixelCount * 3) {
|
|
200
|
+
throw new Error(`Incomplete PPM pixel data: expected ${pixelCount * 3} values, got ${pixelIndex}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return Promise.resolve({ width, height, data: rgba });
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Encode RGBA image data to PPM format (P6 binary)
|
|
207
|
+
* Note: Alpha channel is ignored as PPM doesn't support transparency
|
|
208
|
+
* @param imageData Image data to encode
|
|
209
|
+
* @returns Encoded PPM image bytes
|
|
210
|
+
*/
|
|
211
|
+
encode(imageData) {
|
|
212
|
+
const { width, height, data } = imageData;
|
|
213
|
+
// Validate input
|
|
214
|
+
if (data.length !== width * height * 4) {
|
|
215
|
+
throw new Error(`Data length mismatch: expected ${width * height * 4}, got ${data.length}`);
|
|
216
|
+
}
|
|
217
|
+
// Create header
|
|
218
|
+
const header = `P6\n${width} ${height}\n255\n`;
|
|
219
|
+
const encoder = new TextEncoder();
|
|
220
|
+
const headerBytes = encoder.encode(header);
|
|
221
|
+
// Create output buffer (header + RGB data)
|
|
222
|
+
const pixelCount = width * height;
|
|
223
|
+
const output = new Uint8Array(headerBytes.length + pixelCount * 3);
|
|
224
|
+
// Write header
|
|
225
|
+
output.set(headerBytes, 0);
|
|
226
|
+
// Write RGB pixel data (discard alpha channel)
|
|
227
|
+
let outputOffset = headerBytes.length;
|
|
228
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
229
|
+
const srcIndex = i * 4;
|
|
230
|
+
output[outputOffset++] = data[srcIndex]; // R
|
|
231
|
+
output[outputOffset++] = data[srcIndex + 1]; // G
|
|
232
|
+
output[outputOffset++] = data[srcIndex + 2]; // B
|
|
233
|
+
}
|
|
234
|
+
return Promise.resolve(output);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if a byte is whitespace (space, tab, CR, LF)
|
|
238
|
+
*/
|
|
239
|
+
isWhitespace(byte) {
|
|
240
|
+
return byte === 0x20 || byte === 0x09 || byte === 0x0a || byte === 0x0d;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type { ImageData, ImageFormat, MultiFrameImageData } from "../types.js";
|
|
1
|
+
import type { ImageData, ImageFormat, ImageMetadata, MultiFrameImageData } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Options for TIFF encoding
|
|
4
4
|
*/
|
|
5
5
|
export interface TIFFEncodeOptions {
|
|
6
6
|
/** Compression method: "none" for uncompressed (default), "lzw" for LZW compression */
|
|
7
7
|
compression?: "none" | "lzw";
|
|
8
|
+
/** Encode as grayscale instead of RGB/RGBA */
|
|
9
|
+
grayscale?: boolean;
|
|
10
|
+
/** Encode as RGB without alpha channel (ignored if grayscale is true) */
|
|
11
|
+
rgb?: boolean;
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* TIFF format handler
|
|
@@ -70,5 +74,10 @@ export declare class TIFFFormat implements ImageFormat {
|
|
|
70
74
|
* Returns null if the TIFF uses unsupported features
|
|
71
75
|
*/
|
|
72
76
|
private decodePureJSFromIFD;
|
|
77
|
+
/**
|
|
78
|
+
* Get list of metadata fields supported by TIFF format
|
|
79
|
+
* TIFF supports extensive EXIF metadata including GPS and InteropIFD
|
|
80
|
+
*/
|
|
81
|
+
getSupportedMetadata(): Array<keyof ImageMetadata>;
|
|
73
82
|
}
|
|
74
83
|
//# sourceMappingURL=tiff.d.ts.map
|