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