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