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,791 @@
1
+ import { TIFFLZWDecoder, TIFFLZWEncoder } from "../utils/tiff_lzw.js";
2
+ // Constants for unit conversions
3
+ const DEFAULT_DPI = 72;
4
+ /**
5
+ * TIFF format handler
6
+ * Implements pure-JS TIFF decoder for uncompressed and LZW-compressed RGB/RGBA images
7
+ * and encoder for uncompressed and LZW-compressed RGBA TIFFs. Falls back to ImageDecoder
8
+ * for other compressed TIFFs (JPEG, PackBits, etc.)
9
+ * Supports multi-page TIFF files.
10
+ */
11
+ export class TIFFFormat {
12
+ constructor() {
13
+ Object.defineProperty(this, "name", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: "tiff"
18
+ });
19
+ Object.defineProperty(this, "mimeType", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: "image/tiff"
24
+ });
25
+ }
26
+ supportsMultipleFrames() {
27
+ return true;
28
+ }
29
+ canDecode(data) {
30
+ // TIFF signature: "II" (little-endian) or "MM" (big-endian) followed by 42
31
+ return data.length >= 4 &&
32
+ ((data[0] === 0x49 && data[1] === 0x49 && data[2] === 0x2a &&
33
+ data[3] === 0x00) || // "II*\0"
34
+ (data[0] === 0x4d && data[1] === 0x4d && data[2] === 0x00 &&
35
+ data[3] === 0x2a) // "MM\0*"
36
+ );
37
+ }
38
+ async decode(data) {
39
+ if (!this.canDecode(data)) {
40
+ throw new Error("Invalid TIFF signature");
41
+ }
42
+ // Determine byte order
43
+ const isLittleEndian = data[0] === 0x49;
44
+ // Read IFD offset
45
+ const ifdOffset = this.readUint32(data, 4, isLittleEndian);
46
+ // Parse IFD to get image dimensions and metadata
47
+ const width = this.getIFDValue(data, ifdOffset, 0x0100, isLittleEndian); // ImageWidth tag
48
+ const height = this.getIFDValue(data, ifdOffset, 0x0101, isLittleEndian); // ImageHeight tag
49
+ if (!width || !height) {
50
+ throw new Error("Could not determine TIFF dimensions");
51
+ }
52
+ // Extract metadata from TIFF tags
53
+ const metadata = {};
54
+ // XResolution (0x011a) and YResolution (0x011b) for DPI
55
+ const xResOffset = this.getIFDValue(data, ifdOffset, 0x011a, isLittleEndian);
56
+ const yResOffset = this.getIFDValue(data, ifdOffset, 0x011b, isLittleEndian);
57
+ if (xResOffset && xResOffset < data.length - 8) {
58
+ const numerator = this.readUint32(data, xResOffset, isLittleEndian);
59
+ const denominator = this.readUint32(data, xResOffset + 4, isLittleEndian);
60
+ if (denominator > 0) {
61
+ metadata.dpiX = Math.round(numerator / denominator);
62
+ }
63
+ }
64
+ if (yResOffset && yResOffset < data.length - 8) {
65
+ const numerator = this.readUint32(data, yResOffset, isLittleEndian);
66
+ const denominator = this.readUint32(data, yResOffset + 4, isLittleEndian);
67
+ if (denominator > 0) {
68
+ metadata.dpiY = Math.round(numerator / denominator);
69
+ }
70
+ }
71
+ // Calculate physical dimensions if DPI is available
72
+ if (metadata.dpiX && metadata.dpiY) {
73
+ metadata.physicalWidth = width / metadata.dpiX;
74
+ metadata.physicalHeight = height / metadata.dpiY;
75
+ }
76
+ // ImageDescription (0x010e)
77
+ const descOffset = this.getIFDValue(data, ifdOffset, 0x010e, isLittleEndian);
78
+ if (descOffset && descOffset < data.length) {
79
+ metadata.description = this.readString(data, descOffset);
80
+ }
81
+ // Artist (0x013b)
82
+ const artistOffset = this.getIFDValue(data, ifdOffset, 0x013b, isLittleEndian);
83
+ if (artistOffset && artistOffset < data.length) {
84
+ metadata.author = this.readString(data, artistOffset);
85
+ }
86
+ // Copyright (0x8298)
87
+ const copyrightOffset = this.getIFDValue(data, ifdOffset, 0x8298, isLittleEndian);
88
+ if (copyrightOffset && copyrightOffset < data.length) {
89
+ metadata.copyright = this.readString(data, copyrightOffset);
90
+ }
91
+ // DateTime (0x0132)
92
+ const dateTimeOffset = this.getIFDValue(data, ifdOffset, 0x0132, isLittleEndian);
93
+ if (dateTimeOffset && dateTimeOffset < data.length) {
94
+ const dateStr = this.readString(data, dateTimeOffset);
95
+ const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
96
+ if (match) {
97
+ metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
98
+ }
99
+ }
100
+ // For a complete pure JS implementation, we'd need to handle:
101
+ // - Various compression schemes (LZW, JPEG, PackBits, etc.)
102
+ // - Different color spaces and bit depths
103
+ // - Strips and tiles
104
+ // - Multiple IFDs (multi-page TIFFs)
105
+ // This is very complex, so we'll use the runtime's decoder if available.
106
+ const rgba = await this.decodeUsingRuntime(data, width, height);
107
+ return {
108
+ width,
109
+ height,
110
+ data: rgba,
111
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
112
+ };
113
+ }
114
+ encode(imageData, options) {
115
+ const { width, height, data, metadata } = imageData;
116
+ const opts = options;
117
+ const compression = opts?.compression ?? "none";
118
+ // Prepare pixel data (compress if needed)
119
+ let pixelData;
120
+ let compressionCode;
121
+ if (compression === "lzw") {
122
+ // LZW compress the pixel data
123
+ const encoder = new TIFFLZWEncoder();
124
+ pixelData = encoder.compress(data);
125
+ compressionCode = 5;
126
+ }
127
+ else {
128
+ // Uncompressed
129
+ pixelData = data;
130
+ compressionCode = 1;
131
+ }
132
+ const result = [];
133
+ // Header (8 bytes)
134
+ // Little-endian byte order
135
+ result.push(0x49, 0x49); // "II"
136
+ result.push(0x2a, 0x00); // 42
137
+ // IFD offset (will be after header and pixel data)
138
+ const ifdOffset = 8 + pixelData.length;
139
+ this.writeUint32LE(result, ifdOffset);
140
+ // Pixel data
141
+ for (let i = 0; i < pixelData.length; i++) {
142
+ result.push(pixelData[i]);
143
+ }
144
+ // IFD (Image File Directory)
145
+ const ifdStart = result.length;
146
+ // Count number of entries (including metadata)
147
+ let numEntries = 11; // Base entries
148
+ if (metadata?.description)
149
+ numEntries++;
150
+ if (metadata?.author)
151
+ numEntries++;
152
+ if (metadata?.copyright)
153
+ numEntries++;
154
+ if (metadata?.creationDate)
155
+ numEntries++;
156
+ this.writeUint16LE(result, numEntries);
157
+ // Calculate offsets for variable-length data
158
+ let dataOffset = ifdStart + 2 + numEntries * 12 + 4;
159
+ // IFD entries (12 bytes each)
160
+ // ImageWidth (0x0100)
161
+ this.writeIFDEntry(result, 0x0100, 4, 1, width);
162
+ // ImageHeight (0x0101)
163
+ this.writeIFDEntry(result, 0x0101, 4, 1, height);
164
+ // BitsPerSample (0x0102) - 8 bits per channel
165
+ this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
166
+ dataOffset += 8; // 4 x 2-byte values
167
+ // Compression (0x0103) - 1 = uncompressed, 5 = LZW
168
+ this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
169
+ // PhotometricInterpretation (0x0106) - 2 = RGB
170
+ this.writeIFDEntry(result, 0x0106, 3, 1, 2);
171
+ // StripOffsets (0x0111)
172
+ this.writeIFDEntry(result, 0x0111, 4, 1, 8);
173
+ // SamplesPerPixel (0x0115) - 4 (RGBA)
174
+ this.writeIFDEntry(result, 0x0115, 3, 1, 4);
175
+ // RowsPerStrip (0x0116)
176
+ this.writeIFDEntry(result, 0x0116, 4, 1, height);
177
+ // StripByteCounts (0x0117)
178
+ this.writeIFDEntry(result, 0x0117, 4, 1, pixelData.length);
179
+ // XResolution (0x011a)
180
+ const xResOffset = dataOffset;
181
+ this.writeIFDEntry(result, 0x011a, 5, 1, xResOffset);
182
+ dataOffset += 8;
183
+ // YResolution (0x011b)
184
+ const yResOffset = dataOffset;
185
+ this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
186
+ dataOffset += 8;
187
+ // Optional metadata entries
188
+ if (metadata?.description) {
189
+ const descBytes = new TextEncoder().encode(metadata.description + "\0");
190
+ this.writeIFDEntry(result, 0x010e, 2, descBytes.length, dataOffset);
191
+ dataOffset += descBytes.length;
192
+ }
193
+ if (metadata?.author) {
194
+ const authorBytes = new TextEncoder().encode(metadata.author + "\0");
195
+ this.writeIFDEntry(result, 0x013b, 2, authorBytes.length, dataOffset);
196
+ dataOffset += authorBytes.length;
197
+ }
198
+ if (metadata?.copyright) {
199
+ const copyrightBytes = new TextEncoder().encode(metadata.copyright + "\0");
200
+ this.writeIFDEntry(result, 0x8298, 2, copyrightBytes.length, dataOffset);
201
+ dataOffset += copyrightBytes.length;
202
+ }
203
+ if (metadata?.creationDate) {
204
+ const date = metadata.creationDate;
205
+ 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`;
206
+ const dateBytes = new TextEncoder().encode(dateStr);
207
+ this.writeIFDEntry(result, 0x0132, 2, dateBytes.length, dataOffset);
208
+ dataOffset += dateBytes.length;
209
+ }
210
+ // Next IFD offset (0 = no more IFDs)
211
+ this.writeUint32LE(result, 0);
212
+ // Write variable-length data
213
+ // XResolution value (rational)
214
+ const dpiX = metadata?.dpiX ?? DEFAULT_DPI;
215
+ this.writeUint32LE(result, dpiX);
216
+ this.writeUint32LE(result, 1);
217
+ // YResolution value (rational)
218
+ const dpiY = metadata?.dpiY ?? DEFAULT_DPI;
219
+ this.writeUint32LE(result, dpiY);
220
+ this.writeUint32LE(result, 1);
221
+ // BitsPerSample values
222
+ this.writeUint16LE(result, 8);
223
+ this.writeUint16LE(result, 8);
224
+ this.writeUint16LE(result, 8);
225
+ this.writeUint16LE(result, 8);
226
+ // Write metadata strings
227
+ if (metadata?.description) {
228
+ const descBytes = new TextEncoder().encode(metadata.description + "\0");
229
+ for (const byte of descBytes) {
230
+ result.push(byte);
231
+ }
232
+ }
233
+ if (metadata?.author) {
234
+ const authorBytes = new TextEncoder().encode(metadata.author + "\0");
235
+ for (const byte of authorBytes) {
236
+ result.push(byte);
237
+ }
238
+ }
239
+ if (metadata?.copyright) {
240
+ const copyrightBytes = new TextEncoder().encode(metadata.copyright + "\0");
241
+ for (const byte of copyrightBytes) {
242
+ result.push(byte);
243
+ }
244
+ }
245
+ if (metadata?.creationDate) {
246
+ const date = metadata.creationDate;
247
+ 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`;
248
+ const dateBytes = new TextEncoder().encode(dateStr);
249
+ for (const byte of dateBytes) {
250
+ result.push(byte);
251
+ }
252
+ }
253
+ return Promise.resolve(new Uint8Array(result));
254
+ }
255
+ /**
256
+ * Decode all pages from a multi-page TIFF
257
+ */
258
+ async decodeFrames(data) {
259
+ if (!this.canDecode(data)) {
260
+ throw new Error("Invalid TIFF signature");
261
+ }
262
+ // Determine byte order
263
+ const isLittleEndian = data[0] === 0x49;
264
+ // Read first IFD offset
265
+ let ifdOffset = this.readUint32(data, 4, isLittleEndian);
266
+ const frames = [];
267
+ let globalMetadata;
268
+ // Loop through all IFDs
269
+ while (ifdOffset !== 0 && ifdOffset < data.length) {
270
+ try {
271
+ // Get dimensions for this page
272
+ const width = this.getIFDValue(data, ifdOffset, 0x0100, isLittleEndian);
273
+ const height = this.getIFDValue(data, ifdOffset, 0x0101, isLittleEndian);
274
+ if (!width || !height) {
275
+ throw new Error("Could not determine TIFF dimensions for page");
276
+ }
277
+ // Extract metadata for this page (only from first page for global metadata)
278
+ if (!globalMetadata) {
279
+ globalMetadata = this.extractMetadataFromIFD(data, ifdOffset, isLittleEndian);
280
+ }
281
+ // Decode the page
282
+ const rgba = await this.decodePage(data, ifdOffset, width, height);
283
+ frames.push({
284
+ width,
285
+ height,
286
+ data: rgba,
287
+ });
288
+ // Read next IFD offset
289
+ const numEntries = this.readUint16(data, ifdOffset, isLittleEndian);
290
+ const nextIFDOffsetPos = ifdOffset + 2 + numEntries * 12;
291
+ if (nextIFDOffsetPos + 3 >= data.length) {
292
+ break;
293
+ }
294
+ ifdOffset = this.readUint32(data, nextIFDOffsetPos, isLittleEndian);
295
+ }
296
+ catch (error) {
297
+ // If we fail to decode a page, we've likely hit corruption or unsupported features
298
+ // Stop processing but return frames successfully decoded so far
299
+ if (frames.length === 0) {
300
+ throw error; // No frames decoded - propagate the error
301
+ }
302
+ break;
303
+ }
304
+ }
305
+ if (frames.length === 0) {
306
+ throw new Error("No pages found in TIFF");
307
+ }
308
+ // Use dimensions from first frame as canvas dimensions
309
+ return {
310
+ width: frames[0].width,
311
+ height: frames[0].height,
312
+ frames,
313
+ metadata: globalMetadata && Object.keys(globalMetadata).length > 0
314
+ ? globalMetadata
315
+ : undefined,
316
+ };
317
+ }
318
+ /**
319
+ * Encode multi-page TIFF
320
+ */
321
+ encodeFrames(imageData, options) {
322
+ const opts = options;
323
+ const compression = opts?.compression ?? "none";
324
+ if (imageData.frames.length === 0) {
325
+ throw new Error("No frames to encode");
326
+ }
327
+ const result = [];
328
+ // Header (8 bytes)
329
+ // Little-endian byte order
330
+ result.push(0x49, 0x49); // "II"
331
+ result.push(0x2a, 0x00); // 42
332
+ // First IFD offset (will be calculated after writing all pixel data)
333
+ const firstIFDOffsetPos = result.length;
334
+ this.writeUint32LE(result, 0); // Placeholder
335
+ let currentOffset = 8;
336
+ const ifdOffsets = [];
337
+ const pixelDataOffsets = [];
338
+ // Write all pixel data first
339
+ for (const frame of imageData.frames) {
340
+ pixelDataOffsets.push(currentOffset);
341
+ let pixelData;
342
+ if (compression === "lzw") {
343
+ const encoder = new TIFFLZWEncoder();
344
+ pixelData = encoder.compress(frame.data);
345
+ }
346
+ else {
347
+ pixelData = frame.data;
348
+ }
349
+ for (let i = 0; i < pixelData.length; i++) {
350
+ result.push(pixelData[i]);
351
+ }
352
+ currentOffset += pixelData.length;
353
+ }
354
+ // Write IFDs
355
+ for (let i = 0; i < imageData.frames.length; i++) {
356
+ const frame = imageData.frames[i];
357
+ const isLastIFD = i === imageData.frames.length - 1;
358
+ ifdOffsets.push(currentOffset);
359
+ const ifdStart = result.length;
360
+ // Count number of entries (including metadata only for first page)
361
+ let numEntries = 11;
362
+ if (i === 0 && imageData.metadata) {
363
+ if (imageData.metadata.description)
364
+ numEntries++;
365
+ if (imageData.metadata.author)
366
+ numEntries++;
367
+ if (imageData.metadata.copyright)
368
+ numEntries++;
369
+ if (imageData.metadata.creationDate)
370
+ numEntries++;
371
+ }
372
+ this.writeUint16LE(result, numEntries);
373
+ // Calculate offsets for variable-length data
374
+ let dataOffset = ifdStart + 2 + numEntries * 12 + 4;
375
+ // IFD entries
376
+ this.writeIFDEntry(result, 0x0100, 4, 1, frame.width); // ImageWidth
377
+ this.writeIFDEntry(result, 0x0101, 4, 1, frame.height); // ImageHeight
378
+ // BitsPerSample
379
+ this.writeIFDEntry(result, 0x0102, 3, 4, dataOffset);
380
+ dataOffset += 8;
381
+ // Compression
382
+ const compressionCode = compression === "lzw" ? 5 : 1;
383
+ this.writeIFDEntry(result, 0x0103, 3, 1, compressionCode);
384
+ // PhotometricInterpretation
385
+ this.writeIFDEntry(result, 0x0106, 3, 1, 2);
386
+ // StripOffsets
387
+ this.writeIFDEntry(result, 0x0111, 4, 1, pixelDataOffsets[i]);
388
+ // SamplesPerPixel
389
+ this.writeIFDEntry(result, 0x0115, 3, 1, 4);
390
+ // RowsPerStrip
391
+ this.writeIFDEntry(result, 0x0116, 4, 1, frame.height);
392
+ // StripByteCounts
393
+ const pixelDataSize = compression === "lzw"
394
+ ? new TIFFLZWEncoder().compress(frame.data).length
395
+ : frame.data.length;
396
+ this.writeIFDEntry(result, 0x0117, 4, 1, pixelDataSize);
397
+ // XResolution
398
+ const xResOffset = dataOffset;
399
+ this.writeIFDEntry(result, 0x011a, 5, 1, xResOffset);
400
+ dataOffset += 8;
401
+ // YResolution
402
+ const yResOffset = dataOffset;
403
+ this.writeIFDEntry(result, 0x011b, 5, 1, yResOffset);
404
+ dataOffset += 8;
405
+ // Metadata (only for first page)
406
+ if (i === 0 && imageData.metadata) {
407
+ if (imageData.metadata.description) {
408
+ const descBytes = new TextEncoder().encode(imageData.metadata.description + "\0");
409
+ this.writeIFDEntry(result, 0x010e, 2, descBytes.length, dataOffset);
410
+ dataOffset += descBytes.length;
411
+ }
412
+ if (imageData.metadata.author) {
413
+ const authorBytes = new TextEncoder().encode(imageData.metadata.author + "\0");
414
+ this.writeIFDEntry(result, 0x013b, 2, authorBytes.length, dataOffset);
415
+ dataOffset += authorBytes.length;
416
+ }
417
+ if (imageData.metadata.copyright) {
418
+ const copyrightBytes = new TextEncoder().encode(imageData.metadata.copyright + "\0");
419
+ this.writeIFDEntry(result, 0x8298, 2, copyrightBytes.length, dataOffset);
420
+ dataOffset += copyrightBytes.length;
421
+ }
422
+ if (imageData.metadata.creationDate) {
423
+ const date = imageData.metadata.creationDate;
424
+ 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`;
425
+ const dateBytes = new TextEncoder().encode(dateStr);
426
+ this.writeIFDEntry(result, 0x0132, 2, dateBytes.length, dataOffset);
427
+ dataOffset += dateBytes.length;
428
+ }
429
+ }
430
+ // Next IFD offset
431
+ const nextIFDOffset = isLastIFD ? 0 : dataOffset;
432
+ this.writeUint32LE(result, nextIFDOffset);
433
+ currentOffset = dataOffset;
434
+ // Write variable-length data
435
+ // XResolution value (rational)
436
+ const dpiX = (i === 0 && imageData.metadata?.dpiX) ||
437
+ DEFAULT_DPI;
438
+ this.writeUint32LE(result, dpiX);
439
+ this.writeUint32LE(result, 1);
440
+ // YResolution value (rational)
441
+ const dpiY = (i === 0 && imageData.metadata?.dpiY) ||
442
+ DEFAULT_DPI;
443
+ this.writeUint32LE(result, dpiY);
444
+ this.writeUint32LE(result, 1);
445
+ // BitsPerSample values
446
+ this.writeUint16LE(result, 8);
447
+ this.writeUint16LE(result, 8);
448
+ this.writeUint16LE(result, 8);
449
+ this.writeUint16LE(result, 8);
450
+ // Write metadata strings (only for first page)
451
+ if (i === 0 && imageData.metadata) {
452
+ if (imageData.metadata.description) {
453
+ const descBytes = new TextEncoder().encode(imageData.metadata.description + "\0");
454
+ for (const byte of descBytes) {
455
+ result.push(byte);
456
+ }
457
+ }
458
+ if (imageData.metadata.author) {
459
+ const authorBytes = new TextEncoder().encode(imageData.metadata.author + "\0");
460
+ for (const byte of authorBytes) {
461
+ result.push(byte);
462
+ }
463
+ }
464
+ if (imageData.metadata.copyright) {
465
+ const copyrightBytes = new TextEncoder().encode(imageData.metadata.copyright + "\0");
466
+ for (const byte of copyrightBytes) {
467
+ result.push(byte);
468
+ }
469
+ }
470
+ if (imageData.metadata.creationDate) {
471
+ const date = imageData.metadata.creationDate;
472
+ 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`;
473
+ const dateBytes = new TextEncoder().encode(dateStr);
474
+ for (const byte of dateBytes) {
475
+ result.push(byte);
476
+ }
477
+ }
478
+ }
479
+ currentOffset = result.length;
480
+ }
481
+ // Write first IFD offset to header
482
+ const firstIFDOffset = ifdOffsets[0];
483
+ result[firstIFDOffsetPos] = firstIFDOffset & 0xff;
484
+ result[firstIFDOffsetPos + 1] = (firstIFDOffset >>> 8) & 0xff;
485
+ result[firstIFDOffsetPos + 2] = (firstIFDOffset >>> 16) & 0xff;
486
+ result[firstIFDOffsetPos + 3] = (firstIFDOffset >>> 24) & 0xff;
487
+ return Promise.resolve(new Uint8Array(result));
488
+ }
489
+ /**
490
+ * Decode a single page from TIFF given its IFD offset
491
+ */
492
+ async decodePage(data, ifdOffset, width, height) {
493
+ const isLittleEndian = data[0] === 0x49;
494
+ // Try pure JavaScript decoder first
495
+ try {
496
+ const pureJSResult = this.decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian);
497
+ if (pureJSResult) {
498
+ return pureJSResult;
499
+ }
500
+ }
501
+ catch (_error) {
502
+ // Pure JS decoder failed, fall through to ImageDecoder silently
503
+ }
504
+ // Fall back to runtime decoder
505
+ return await this.decodeUsingRuntime(data, width, height);
506
+ }
507
+ /**
508
+ * Extract metadata from an IFD
509
+ */
510
+ extractMetadataFromIFD(data, ifdOffset, isLittleEndian) {
511
+ const metadata = {};
512
+ // XResolution and YResolution
513
+ const xResOffset = this.getIFDValue(data, ifdOffset, 0x011a, isLittleEndian);
514
+ const yResOffset = this.getIFDValue(data, ifdOffset, 0x011b, isLittleEndian);
515
+ if (xResOffset && xResOffset < data.length - 8) {
516
+ const numerator = this.readUint32(data, xResOffset, isLittleEndian);
517
+ const denominator = this.readUint32(data, xResOffset + 4, isLittleEndian);
518
+ if (denominator > 0) {
519
+ metadata.dpiX = Math.round(numerator / denominator);
520
+ }
521
+ }
522
+ if (yResOffset && yResOffset < data.length - 8) {
523
+ const numerator = this.readUint32(data, yResOffset, isLittleEndian);
524
+ const denominator = this.readUint32(data, yResOffset + 4, isLittleEndian);
525
+ if (denominator > 0) {
526
+ metadata.dpiY = Math.round(numerator / denominator);
527
+ }
528
+ }
529
+ // ImageDescription
530
+ const descOffset = this.getIFDValue(data, ifdOffset, 0x010e, isLittleEndian);
531
+ if (descOffset && descOffset < data.length) {
532
+ metadata.description = this.readString(data, descOffset);
533
+ }
534
+ // Artist
535
+ const artistOffset = this.getIFDValue(data, ifdOffset, 0x013b, isLittleEndian);
536
+ if (artistOffset && artistOffset < data.length) {
537
+ metadata.author = this.readString(data, artistOffset);
538
+ }
539
+ // Copyright
540
+ const copyrightOffset = this.getIFDValue(data, ifdOffset, 0x8298, isLittleEndian);
541
+ if (copyrightOffset && copyrightOffset < data.length) {
542
+ metadata.copyright = this.readString(data, copyrightOffset);
543
+ }
544
+ // DateTime
545
+ const dateTimeOffset = this.getIFDValue(data, ifdOffset, 0x0132, isLittleEndian);
546
+ if (dateTimeOffset && dateTimeOffset < data.length) {
547
+ const dateStr = this.readString(data, dateTimeOffset);
548
+ const match = dateStr.match(/^(\d{4}):(\d{2}):(\d{2}) (\d{2}):(\d{2}):(\d{2})$/);
549
+ if (match) {
550
+ metadata.creationDate = new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(match[3]), parseInt(match[4]), parseInt(match[5]), parseInt(match[6]));
551
+ }
552
+ }
553
+ return metadata;
554
+ }
555
+ readUint16(data, offset, isLittleEndian) {
556
+ if (offset + 1 >= data.length) {
557
+ throw new Error("TIFF read error: offset out of bounds");
558
+ }
559
+ if (isLittleEndian) {
560
+ return data[offset] | (data[offset + 1] << 8);
561
+ }
562
+ else {
563
+ return (data[offset] << 8) | data[offset + 1];
564
+ }
565
+ }
566
+ readUint32(data, offset, isLittleEndian) {
567
+ if (offset + 3 >= data.length) {
568
+ throw new Error("TIFF read error: offset out of bounds");
569
+ }
570
+ if (isLittleEndian) {
571
+ return data[offset] | (data[offset + 1] << 8) |
572
+ (data[offset + 2] << 16) | (data[offset + 3] << 24);
573
+ }
574
+ else {
575
+ return (data[offset] << 24) | (data[offset + 1] << 16) |
576
+ (data[offset + 2] << 8) | data[offset + 3];
577
+ }
578
+ }
579
+ writeUint16LE(result, value) {
580
+ result.push(value & 0xff, (value >>> 8) & 0xff);
581
+ }
582
+ writeUint32LE(result, value) {
583
+ result.push(value & 0xff, (value >>> 8) & 0xff, (value >>> 16) & 0xff, (value >>> 24) & 0xff);
584
+ }
585
+ writeIFDEntry(result, tag, type, count, valueOrOffset) {
586
+ this.writeUint16LE(result, tag);
587
+ this.writeUint16LE(result, type);
588
+ this.writeUint32LE(result, count);
589
+ this.writeUint32LE(result, valueOrOffset);
590
+ }
591
+ getIFDValue(data, ifdOffset, tag, isLittleEndian) {
592
+ if (ifdOffset + 2 > data.length)
593
+ return null;
594
+ const numEntries = this.readUint16(data, ifdOffset, isLittleEndian);
595
+ let pos = ifdOffset + 2;
596
+ for (let i = 0; i < numEntries; i++) {
597
+ if (pos + 12 > data.length)
598
+ break;
599
+ const entryTag = this.readUint16(data, pos, isLittleEndian);
600
+ const entryType = this.readUint16(data, pos + 2, isLittleEndian);
601
+ const entryCount = this.readUint32(data, pos + 4, isLittleEndian);
602
+ const entryValue = this.readUint32(data, pos + 8, isLittleEndian);
603
+ if (entryTag === tag) {
604
+ // For SHORT/LONG types with count=1, value is stored directly
605
+ // For other types or count>1, this returns the offset to the actual data
606
+ // Callers should handle offsets appropriately based on the tag type
607
+ if ((entryType === 3 || entryType === 4) && entryCount === 1) {
608
+ return entryValue;
609
+ }
610
+ // Return the value/offset for other cases
611
+ return entryValue;
612
+ }
613
+ pos += 12;
614
+ }
615
+ return null;
616
+ }
617
+ async decodeUsingRuntime(data, width, height) {
618
+ // Try pure JavaScript decoder first for uncompressed TIFFs
619
+ try {
620
+ const pureJSResult = this.decodePureJS(data, width, height);
621
+ if (pureJSResult) {
622
+ return pureJSResult;
623
+ }
624
+ }
625
+ catch (_error) {
626
+ // Pure JS decoder failed, fall through to ImageDecoder silently
627
+ // This is expected for compressed TIFFs
628
+ }
629
+ // Try to use ImageDecoder API if available (Deno, modern browsers)
630
+ if (typeof ImageDecoder !== "undefined") {
631
+ try {
632
+ const decoder = new ImageDecoder({ data, type: "image/tiff" });
633
+ const result = await decoder.decode();
634
+ const bitmap = result.image;
635
+ // Create a canvas to extract pixel data
636
+ const canvas = new OffscreenCanvas(bitmap.displayWidth, bitmap.displayHeight);
637
+ const ctx = canvas.getContext("2d");
638
+ if (!ctx)
639
+ throw new Error("Could not get canvas context");
640
+ ctx.drawImage(bitmap, 0, 0);
641
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
642
+ bitmap.close();
643
+ return new Uint8Array(imageData.data.buffer);
644
+ }
645
+ catch (error) {
646
+ throw new Error(`TIFF decoding failed: ${error}`);
647
+ }
648
+ }
649
+ throw new Error("TIFF decoding requires uncompressed TIFF or ImageDecoder API support");
650
+ }
651
+ readString(data, offset) {
652
+ const endIndex = data.indexOf(0, offset);
653
+ if (endIndex === -1 || endIndex <= offset)
654
+ return "";
655
+ return new TextDecoder().decode(data.slice(offset, endIndex));
656
+ }
657
+ /**
658
+ * Pure JavaScript TIFF decoder for uncompressed and LZW-compressed RGB/RGBA images
659
+ * Returns null if the TIFF uses unsupported features
660
+ */
661
+ decodePureJS(data, width, height) {
662
+ // Validate minimum TIFF header size
663
+ if (data.length < 8) {
664
+ return null;
665
+ }
666
+ // Determine byte order
667
+ const isLittleEndian = data[0] === 0x49;
668
+ // Read IFD offset
669
+ const ifdOffset = this.readUint32(data, 4, isLittleEndian);
670
+ // Check compression
671
+ const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
672
+ if (compression !== 1 && compression !== 5) {
673
+ // Only support uncompressed (1) and LZW (5)
674
+ return null;
675
+ }
676
+ // Check photometric interpretation
677
+ const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
678
+ if (photometric !== 2) {
679
+ // Only support RGB (photometric = 2)
680
+ return null;
681
+ }
682
+ // Get samples per pixel
683
+ const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
684
+ if (!samplesPerPixel || (samplesPerPixel !== 3 && samplesPerPixel !== 4)) {
685
+ // Only support RGB (3) or RGBA (4)
686
+ return null;
687
+ }
688
+ // Get strip offset
689
+ const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
690
+ if (!stripOffset || stripOffset >= data.length) {
691
+ return null;
692
+ }
693
+ // Get strip byte counts to know how much compressed data to read
694
+ const stripByteCount = this.getIFDValue(data, ifdOffset, 0x0117, isLittleEndian);
695
+ if (!stripByteCount) {
696
+ return null;
697
+ }
698
+ // Read and decompress pixel data
699
+ let pixelData;
700
+ if (compression === 5) {
701
+ // LZW compressed
702
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
703
+ const decoder = new TIFFLZWDecoder(compressedData);
704
+ pixelData = decoder.decompress();
705
+ }
706
+ else {
707
+ // Uncompressed
708
+ pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
709
+ }
710
+ // Convert to RGBA
711
+ const rgba = new Uint8Array(width * height * 4);
712
+ let srcPos = 0;
713
+ for (let y = 0; y < height; y++) {
714
+ for (let x = 0; x < width; x++) {
715
+ const dstIdx = (y * width + x) * 4;
716
+ if (srcPos + samplesPerPixel > pixelData.length) {
717
+ return null; // Not enough data
718
+ }
719
+ // TIFF stores RGB(A) in order
720
+ rgba[dstIdx] = pixelData[srcPos++]; // R
721
+ rgba[dstIdx + 1] = pixelData[srcPos++]; // G
722
+ rgba[dstIdx + 2] = pixelData[srcPos++]; // B
723
+ rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
724
+ }
725
+ }
726
+ return rgba;
727
+ }
728
+ /**
729
+ * Pure JavaScript TIFF decoder for a specific IFD
730
+ * Returns null if the TIFF uses unsupported features
731
+ */
732
+ decodePureJSFromIFD(data, ifdOffset, width, height, isLittleEndian) {
733
+ // Check compression
734
+ const compression = this.getIFDValue(data, ifdOffset, 0x0103, isLittleEndian);
735
+ if (compression !== 1 && compression !== 5) {
736
+ // Only support uncompressed (1) and LZW (5)
737
+ return null;
738
+ }
739
+ // Check photometric interpretation
740
+ const photometric = this.getIFDValue(data, ifdOffset, 0x0106, isLittleEndian);
741
+ if (photometric !== 2) {
742
+ // Only support RGB (photometric = 2)
743
+ return null;
744
+ }
745
+ // Get samples per pixel
746
+ const samplesPerPixel = this.getIFDValue(data, ifdOffset, 0x0115, isLittleEndian);
747
+ if (!samplesPerPixel || (samplesPerPixel !== 3 && samplesPerPixel !== 4)) {
748
+ // Only support RGB (3) or RGBA (4)
749
+ return null;
750
+ }
751
+ // Get strip offset
752
+ const stripOffset = this.getIFDValue(data, ifdOffset, 0x0111, isLittleEndian);
753
+ if (!stripOffset || stripOffset >= data.length) {
754
+ return null;
755
+ }
756
+ // Get strip byte counts to know how much compressed data to read
757
+ const stripByteCount = this.getIFDValue(data, ifdOffset, 0x0117, isLittleEndian);
758
+ if (!stripByteCount) {
759
+ return null;
760
+ }
761
+ // Read and decompress pixel data
762
+ let pixelData;
763
+ if (compression === 5) {
764
+ // LZW compressed
765
+ const compressedData = data.slice(stripOffset, stripOffset + stripByteCount);
766
+ const decoder = new TIFFLZWDecoder(compressedData);
767
+ pixelData = decoder.decompress();
768
+ }
769
+ else {
770
+ // Uncompressed
771
+ pixelData = data.slice(stripOffset, stripOffset + stripByteCount);
772
+ }
773
+ // Convert to RGBA
774
+ const rgba = new Uint8Array(width * height * 4);
775
+ let srcPos = 0;
776
+ for (let y = 0; y < height; y++) {
777
+ for (let x = 0; x < width; x++) {
778
+ const dstIdx = (y * width + x) * 4;
779
+ if (srcPos + samplesPerPixel > pixelData.length) {
780
+ return null; // Not enough data
781
+ }
782
+ // TIFF stores RGB(A) in order
783
+ rgba[dstIdx] = pixelData[srcPos++]; // R
784
+ rgba[dstIdx + 1] = pixelData[srcPos++]; // G
785
+ rgba[dstIdx + 2] = pixelData[srcPos++]; // B
786
+ rgba[dstIdx + 3] = samplesPerPixel === 4 ? pixelData[srcPos++] : 255; // A
787
+ }
788
+ }
789
+ return rgba;
790
+ }
791
+ }