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