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