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.
- package/LICENSE +21 -0
- package/README.md +606 -0
- package/esm/mod.d.ts +33 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +31 -0
- package/esm/package.json +3 -0
- package/esm/src/formats/ascii.d.ts +27 -0
- package/esm/src/formats/ascii.d.ts.map +1 -0
- package/esm/src/formats/ascii.js +172 -0
- package/esm/src/formats/bmp.d.ts +19 -0
- package/esm/src/formats/bmp.d.ts.map +1 -0
- package/esm/src/formats/bmp.js +174 -0
- package/esm/src/formats/gif.d.ts +40 -0
- package/esm/src/formats/gif.d.ts.map +1 -0
- package/esm/src/formats/gif.js +385 -0
- package/esm/src/formats/jpeg.d.ts +18 -0
- package/esm/src/formats/jpeg.d.ts.map +1 -0
- package/esm/src/formats/jpeg.js +414 -0
- package/esm/src/formats/png.d.ts +33 -0
- package/esm/src/formats/png.d.ts.map +1 -0
- package/esm/src/formats/png.js +544 -0
- package/esm/src/formats/raw.d.ts +23 -0
- package/esm/src/formats/raw.d.ts.map +1 -0
- package/esm/src/formats/raw.js +98 -0
- package/esm/src/formats/tiff.d.ts +58 -0
- package/esm/src/formats/tiff.d.ts.map +1 -0
- package/esm/src/formats/tiff.js +791 -0
- package/esm/src/formats/webp.d.ts +22 -0
- package/esm/src/formats/webp.d.ts.map +1 -0
- package/esm/src/formats/webp.js +403 -0
- package/esm/src/image.d.ts +124 -0
- package/esm/src/image.d.ts.map +1 -0
- package/esm/src/image.js +320 -0
- package/esm/src/types.d.ts +167 -0
- package/esm/src/types.d.ts.map +1 -0
- package/esm/src/types.js +1 -0
- package/esm/src/utils/gif_decoder.d.ts +42 -0
- package/esm/src/utils/gif_decoder.d.ts.map +1 -0
- package/esm/src/utils/gif_decoder.js +374 -0
- package/esm/src/utils/gif_encoder.d.ts +29 -0
- package/esm/src/utils/gif_encoder.d.ts.map +1 -0
- package/esm/src/utils/gif_encoder.js +226 -0
- package/esm/src/utils/jpeg_decoder.d.ts +39 -0
- package/esm/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_decoder.js +580 -0
- package/esm/src/utils/jpeg_encoder.d.ts +33 -0
- package/esm/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/esm/src/utils/jpeg_encoder.js +1017 -0
- package/esm/src/utils/lzw.d.ts +43 -0
- package/esm/src/utils/lzw.d.ts.map +1 -0
- package/esm/src/utils/lzw.js +309 -0
- package/esm/src/utils/resize.d.ts +9 -0
- package/esm/src/utils/resize.d.ts.map +1 -0
- package/esm/src/utils/resize.js +52 -0
- package/esm/src/utils/tiff_lzw.d.ts +44 -0
- package/esm/src/utils/tiff_lzw.d.ts.map +1 -0
- package/esm/src/utils/tiff_lzw.js +306 -0
- package/esm/src/utils/webp_decoder.d.ts +39 -0
- package/esm/src/utils/webp_decoder.d.ts.map +1 -0
- package/esm/src/utils/webp_decoder.js +493 -0
- package/esm/src/utils/webp_encoder.d.ts +72 -0
- package/esm/src/utils/webp_encoder.d.ts.map +1 -0
- package/esm/src/utils/webp_encoder.js +627 -0
- package/package.json +41 -0
- package/script/mod.d.ts +33 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +43 -0
- package/script/package.json +3 -0
- package/script/src/formats/ascii.d.ts +27 -0
- package/script/src/formats/ascii.d.ts.map +1 -0
- package/script/src/formats/ascii.js +176 -0
- package/script/src/formats/bmp.d.ts +19 -0
- package/script/src/formats/bmp.d.ts.map +1 -0
- package/script/src/formats/bmp.js +178 -0
- package/script/src/formats/gif.d.ts +40 -0
- package/script/src/formats/gif.d.ts.map +1 -0
- package/script/src/formats/gif.js +389 -0
- package/script/src/formats/jpeg.d.ts +18 -0
- package/script/src/formats/jpeg.d.ts.map +1 -0
- package/script/src/formats/jpeg.js +451 -0
- package/script/src/formats/png.d.ts +33 -0
- package/script/src/formats/png.d.ts.map +1 -0
- package/script/src/formats/png.js +548 -0
- package/script/src/formats/raw.d.ts +23 -0
- package/script/src/formats/raw.d.ts.map +1 -0
- package/script/src/formats/raw.js +102 -0
- package/script/src/formats/tiff.d.ts +58 -0
- package/script/src/formats/tiff.d.ts.map +1 -0
- package/script/src/formats/tiff.js +795 -0
- package/script/src/formats/webp.d.ts +22 -0
- package/script/src/formats/webp.d.ts.map +1 -0
- package/script/src/formats/webp.js +440 -0
- package/script/src/image.d.ts +124 -0
- package/script/src/image.d.ts.map +1 -0
- package/script/src/image.js +324 -0
- package/script/src/types.d.ts +167 -0
- package/script/src/types.d.ts.map +1 -0
- package/script/src/types.js +2 -0
- package/script/src/utils/gif_decoder.d.ts +42 -0
- package/script/src/utils/gif_decoder.d.ts.map +1 -0
- package/script/src/utils/gif_decoder.js +378 -0
- package/script/src/utils/gif_encoder.d.ts +29 -0
- package/script/src/utils/gif_encoder.d.ts.map +1 -0
- package/script/src/utils/gif_encoder.js +230 -0
- package/script/src/utils/jpeg_decoder.d.ts +39 -0
- package/script/src/utils/jpeg_decoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_decoder.js +584 -0
- package/script/src/utils/jpeg_encoder.d.ts +33 -0
- package/script/src/utils/jpeg_encoder.d.ts.map +1 -0
- package/script/src/utils/jpeg_encoder.js +1021 -0
- package/script/src/utils/lzw.d.ts +43 -0
- package/script/src/utils/lzw.d.ts.map +1 -0
- package/script/src/utils/lzw.js +314 -0
- package/script/src/utils/resize.d.ts +9 -0
- package/script/src/utils/resize.d.ts.map +1 -0
- package/script/src/utils/resize.js +56 -0
- package/script/src/utils/tiff_lzw.d.ts +44 -0
- package/script/src/utils/tiff_lzw.d.ts.map +1 -0
- package/script/src/utils/tiff_lzw.js +311 -0
- package/script/src/utils/webp_decoder.d.ts +39 -0
- package/script/src/utils/webp_decoder.d.ts.map +1 -0
- package/script/src/utils/webp_decoder.js +497 -0
- package/script/src/utils/webp_encoder.d.ts +72 -0
- package/script/src/utils/webp_encoder.d.ts.map +1 -0
- 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;
|