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