// Minimal decoder for the baseline TIFFs the broker produces (uncompressed, // single-channel, contiguous — see preview/JFJochTIFF.cpp). Handles 8/16/32/64-bit // samples in unsigned / signed / IEEE-float formats and both byte orders. Kept // dependency-free on purpose: the format is fully under our control, so a full // TIFF library would be overkill. export type SampleFormat = "uint" | "int" | "float"; export type SampleArray = Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array; export type DecodedImage = { width: number; height: number; bits: number; format: SampleFormat; data: SampleArray; // length = width * height, row-major, top-left origin }; // Bytes per TIFF field type (index = type code). const TYPE_SIZE: Record = { 1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8, }; const platformLittleEndian = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1; function makeSampleArray(buf: ArrayBuffer, bits: number, format: SampleFormat): SampleArray { if (format === "float") { if (bits === 32) return new Float32Array(buf); if (bits === 64) return new Float64Array(buf); } else if (format === "int") { if (bits === 8) return new Int8Array(buf); if (bits === 16) return new Int16Array(buf); if (bits === 32) return new Int32Array(buf); } else { if (bits === 8) return new Uint8Array(buf); if (bits === 16) return new Uint16Array(buf); if (bits === 32) return new Uint32Array(buf); } throw new Error(`Unsupported TIFF sample: ${bits}-bit ${format}`); } // In-place byte swap so a foreign-endian file can be viewed as a native typed array. function byteSwap(bytes: Uint8Array, wordSize: number) { if (wordSize < 2) return; for (let i = 0; i + wordSize <= bytes.length; i += wordSize) for (let a = 0, b = wordSize - 1; a < b; a++, b--) { const t = bytes[i + a]; bytes[i + a] = bytes[i + b]; bytes[i + b] = t; } } export function decodeTiff(buffer: ArrayBuffer): DecodedImage { const dv = new DataView(buffer); const bo = dv.getUint16(0, false); const little = bo === 0x4949; // "II" if (!little && bo !== 0x4d4d) throw new Error("Not a TIFF (bad byte-order mark)"); if (dv.getUint16(2, little) !== 42) throw new Error("Not a TIFF (bad magic)"); const readVals = (type: number, count: number, offset: number): number[] => { const size = TYPE_SIZE[type] ?? 0; const total = size * count; const base = total <= 4 ? offset : dv.getUint32(offset, little); // inline vs pointer const out: number[] = []; for (let i = 0; i < count; i++) { const p = base + i * size; switch (type) { case 3: out.push(dv.getUint16(p, little)); break; // SHORT case 4: out.push(dv.getUint32(p, little)); break; // LONG case 1: case 6: case 7: out.push(dv.getUint8(p)); break; // BYTE case 8: out.push(dv.getInt16(p, little)); break; // SSHORT case 9: out.push(dv.getInt32(p, little)); break; // SLONG default: out.push(dv.getUint32(p, little)); break; } } return out; }; const ifd = dv.getUint32(4, little); const n = dv.getUint16(ifd, little); const tags = new Map(); for (let i = 0; i < n; i++) { const e = ifd + 2 + i * 12; tags.set(dv.getUint16(e, little), readVals(dv.getUint16(e + 2, little), dv.getUint32(e + 4, little), e + 8)); } const tag = (id: number, def?: number) => (tags.get(id) ? tags.get(id)![0] : def); const width = tag(256)!; const height = tag(257)!; const bits = tag(258, 8)!; const samplesPerPixel = tag(277, 1)!; const compression = tag(259, 1)!; const sf = tag(339, 1)!; // 1=uint 2=int 3=float const format: SampleFormat = sf === 3 ? "float" : sf === 2 ? "int" : "uint"; const stripOffsets = tags.get(273) ?? []; const stripCounts = tags.get(279) ?? []; if (compression !== 1) throw new Error("Only uncompressed TIFF is supported"); if (samplesPerPixel !== 1) throw new Error("Only single-channel TIFF is supported"); if (!width || !height || !stripOffsets.length) throw new Error("Malformed TIFF (missing size/strips)"); // Concatenate strips into one contiguous, word-aligned buffer. const bytesPerSample = bits / 8; const totalBytes = width * height * bytesPerSample; const out = new Uint8Array(totalBytes); let pos = 0; for (let i = 0; i < stripOffsets.length; i++) { const len = stripCounts[i] ?? (totalBytes - pos); out.set(new Uint8Array(buffer, stripOffsets[i], len), pos); pos += len; } if (little !== platformLittleEndian) byteSwap(out, bytesPerSample); return { width, height, bits, format, data: makeSampleArray(out.buffer, bits, format) }; }