tiff
Version:
TIFF image decoder written entirely in JavaScript
402 lines • 15.1 kB
JavaScript
import { IOBuffer } from 'iobuffer';
import { guessStripByteCounts } from "./hacks.js";
import { applyHorizontalDifferencing16Bit, applyHorizontalDifferencing8Bit, } from "./horizontal_differencing.js";
import IFD from "./ifd.js";
import { getByteLength, readData } from "./ifd_value.js";
import { decompressLzw } from "./lzw.js";
import TiffIfd from "./tiff_ifd.js";
import { decompressZlib } from "./zlib.js";
const defaultOptions = {
ignoreImageData: false,
};
export default class TIFFDecoder extends IOBuffer {
_nextIFD;
constructor(data) {
super(data);
this._nextIFD = 0;
}
get isMultiPage() {
let c = 0;
this.decodeHeader();
while (this._nextIFD) {
c++;
this.decodeIFD({ ignoreImageData: true }, true);
if (c === 2) {
return true;
}
}
if (c === 1) {
return false;
}
throw unsupported('ifdCount', c);
}
get pageCount() {
let c = 0;
this.decodeHeader();
while (this._nextIFD) {
c++;
this.decodeIFD({ ignoreImageData: true }, true);
}
if (c > 0) {
return c;
}
throw unsupported('ifdCount', c);
}
decode(options = {}) {
const { pages } = options;
checkPages(pages);
const maxIndex = pages ? Math.max(...pages) : Infinity;
options = { ...defaultOptions, ...options };
const result = [];
this.decodeHeader();
let index = 0;
while (this._nextIFD) {
if (pages) {
if (pages.includes(index)) {
result.push(this.decodeIFD(options, true));
}
else {
this.decodeIFD({ ignoreImageData: true }, true);
}
if (index === maxIndex) {
break;
}
}
else {
result.push(this.decodeIFD(options, true));
}
index++;
}
if (index < maxIndex && maxIndex !== Infinity) {
throw new RangeError(`Index ${maxIndex} is out of bounds. The stack only contains ${index} images.`);
}
return result;
}
decodeHeader() {
// Byte offset
const value = this.readUint16();
if (value === 0x4949) {
this.setLittleEndian();
}
else if (value === 0x4d4d) {
this.setBigEndian();
}
else {
throw new Error(`invalid byte order: 0x${value.toString(16)}`);
}
// Magic number
if (this.readUint16() !== 42) {
throw new Error('not a TIFF file');
}
// Offset of the first IFD
this._nextIFD = this.readUint32();
}
decodeIFD(options, tiff) {
this.seek(this._nextIFD);
let ifd;
if (tiff) {
ifd = new TiffIfd();
}
else {
if (!options.kind) {
throw new Error(`kind is missing`);
}
ifd = new IFD(options.kind);
}
const numEntries = this.readUint16();
for (let i = 0; i < numEntries; i++) {
this.decodeIFDEntry(ifd);
}
if (!options.ignoreImageData) {
if (!(ifd instanceof TiffIfd)) {
throw new Error('must be a tiff ifd');
}
this.decodeImageData(ifd);
}
this._nextIFD = this.readUint32();
return ifd;
}
decodeIFDEntry(ifd) {
const offset = this.offset;
const tag = this.readUint16();
const type = this.readUint16();
const numValues = this.readUint32();
if (type < 1 || type > 12) {
this.skip(4); // unknown type, skip this value
return;
}
const valueByteLength = getByteLength(type, numValues);
if (valueByteLength > 4) {
this.seek(this.readUint32());
}
const value = readData(this, type, numValues);
ifd.fields.set(tag, value);
// Read sub-IFDs
if (tag === 0x8769 || tag === 0x8825) {
const currentOffset = this.offset;
let kind = 'exif';
if (tag === 0x8769) {
kind = 'exif';
}
else if (tag === 0x8825) {
kind = 'gps';
}
this._nextIFD = value;
ifd[kind] = this.decodeIFD({
kind,
ignoreImageData: true,
}, false);
this.offset = currentOffset;
}
// go to the next entry
this.seek(offset);
this.skip(12);
}
decodeImageData(ifd) {
const orientation = ifd.orientation;
if (orientation && orientation !== 1) {
throw unsupported('orientation', orientation);
}
switch (ifd.type) {
case 0: // WhiteIsZero
case 1: // BlackIsZero
case 2: // RGB
case 3: // Palette color
if (ifd.tiled) {
this.readTileData(ifd);
}
else {
this.readStripData(ifd);
}
break;
default:
throw unsupported('image type', ifd.type);
}
this.applyPredictor(ifd);
this.convertAlpha(ifd);
if (ifd.bitsPerSample === 1) {
this.split1BitData(ifd);
}
if (ifd.type === 0) {
// WhiteIsZero: we invert the values
const bitDepth = ifd.bitsPerSample;
const maxValue = 2 ** bitDepth - 1;
for (let i = 0; i < ifd.data.length; i++) {
ifd.data[i] = maxValue - ifd.data[i];
}
}
}
split1BitData(ifd) {
const { imageWidth, imageLength, samplesPerPixel } = ifd;
const data = new Uint8Array(imageLength * imageWidth * samplesPerPixel);
const bytesPerRow = Math.ceil((imageWidth * samplesPerPixel) / 8);
let dataIndex = 0;
for (let row = 0; row < imageLength; row++) {
const rowStartByte = row * bytesPerRow;
for (let col = 0; col < imageWidth * samplesPerPixel; col++) {
const byteIndex = rowStartByte + Math.floor(col / 8);
const bitIndex = 7 - (col % 8);
const bit = (ifd.data[byteIndex] >> bitIndex) & 1;
data[dataIndex++] = bit;
}
}
ifd.data = data;
}
static uncompress(data, compression = 1) {
switch (compression) {
// No compression, nothing to do
case 1: {
return data;
}
// LZW compression
case 5: {
return decompressLzw(data);
}
// Zlib and Deflate compressions. They are identical.
case 8:
case 32946: {
return decompressZlib(data);
}
case 2: // CCITT Group 3 1-Dimensional Modified Huffman run length encoding
throw unsupported('Compression', 'CCITT Group 3');
case 32773: // PackBits compression
throw unsupported('Compression', 'PackBits');
default:
throw unsupported('Compression', compression);
}
}
createSampleReader(sampleFormat, bitDepth, littleEndian) {
if (bitDepth === 8 || bitDepth === 1) {
return (data, index) => data.getUint8(index);
}
else if (bitDepth === 16) {
return (data, index) => data.getUint16(2 * index, littleEndian);
}
else if (bitDepth === 32 && sampleFormat === 3) {
return (data, index) => data.getFloat32(4 * index, littleEndian);
}
else if (bitDepth === 64 && sampleFormat === 3) {
return (data, index) => data.getFloat64(8 * index, littleEndian);
}
else {
throw unsupported('bitDepth', bitDepth);
}
}
readStripData(ifd) {
// General Image Dimensions
const width = ifd.width;
const height = ifd.height;
const size = ifd.bitsPerSample !== 1
? width * ifd.samplesPerPixel * height
: Math.ceil((width * ifd.samplesPerPixel) / 8) * height;
// Compressed Strip Layout
const stripOffsets = ifd.stripOffsets;
const stripByteCounts = ifd.stripByteCounts || guessStripByteCounts(ifd);
const littleEndian = this.isLittleEndian();
// For 1-bit images, calculate pixels per strip correctly
const stripLength = ifd.bitsPerSample !== 1
? width * ifd.samplesPerPixel * ifd.rowsPerStrip
: Math.ceil((width * ifd.samplesPerPixel) / 8) * ifd.rowsPerStrip;
const readSamples = this.createSampleReader(ifd.sampleFormat, ifd.bitsPerSample, littleEndian);
// Output Data Buffer
const output = getDataArray(size, ifd.bitsPerSample, ifd.sampleFormat);
// Iterate over Number of Strips
let start = 0;
for (let i = 0; i < stripOffsets.length; i++) {
// Extract Strip Data, Uncompress
const stripData = new DataView(this.buffer, this.byteOffset + stripOffsets[i], stripByteCounts[i]);
const uncompressed = TIFFDecoder.uncompress(stripData, ifd.compression);
// Last strip can be smaller
const length = Math.min(stripLength, size - start);
// Write Uncompressed Strip Data to Output (Linear Layout)
for (let index = 0; index < length; ++index) {
const value = readSamples(uncompressed, index);
output[start + index] = value;
}
start += length;
}
ifd.data = output;
// For 1-bit images, we need to convert the data to bits
}
readTileData(ifd) {
if (!ifd.tileWidth || !ifd.tileHeight) {
return;
}
const width = ifd.width;
const height = ifd.height;
const size = ifd.bitsPerSample !== 1
? width * height * ifd.samplesPerPixel
: Math.ceil((width * ifd.samplesPerPixel) / 8) * height;
const twidth = ifd.tileWidth;
const theight = ifd.tileHeight;
const nwidth = Math.ceil(width / twidth);
const nheight = Math.ceil(height / theight);
const tileOffsets = ifd.tileOffsets;
const tileByteCounts = ifd.tileByteCounts;
const littleEndian = this.isLittleEndian();
const readSamples = this.createSampleReader(ifd.sampleFormat, ifd.bitsPerSample, littleEndian);
const output = getDataArray(size, ifd.bitsPerSample, ifd.sampleFormat);
for (let nx = 0; nx < nwidth; ++nx) {
for (let ny = 0; ny < nheight; ++ny) {
const nind = ny * nwidth + nx;
const tileData = new DataView(this.buffer, this.byteOffset + tileOffsets[nind], tileByteCounts[nind]);
const uncompressed = TIFFDecoder.uncompress(tileData, ifd.compression);
if (ifd.bitsPerSample === 1) {
// For 1-bit: read sequentially by bytes
const bytesPerRow = Math.ceil(width / 8);
const tileBytesPerRow = Math.ceil(twidth / 8);
for (let ty = 0; ty < theight && ny * theight + ty < height; ty++) {
const iy = ny * theight + ty;
const srcStart = ty * tileBytesPerRow;
const dstStart = iy * bytesPerRow + Math.floor((nx * twidth) / 8);
// Copy the row of bytes from tile to output
const bytesToCopy = Math.min(tileBytesPerRow, bytesPerRow - Math.floor((nx * twidth) / 8));
for (let b = 0; b < bytesToCopy; b++) {
output[dstStart + b] = readSamples(uncompressed, srcStart + b);
}
}
}
else {
// For 8/16/32-bit: read by pixels
for (let ty = 0; ty < theight; ty++) {
for (let tx = 0; tx < twidth; tx++) {
const ix = nx * twidth + tx;
const iy = ny * theight + ty;
if (ix >= width || iy >= height)
continue;
const tilePixelIndex = ty * twidth + tx;
const value = readSamples(uncompressed, tilePixelIndex);
const outputPixelIndex = (iy * width + ix) * ifd.samplesPerPixel;
output[outputPixelIndex] = value;
}
}
}
}
}
ifd.data = output;
}
applyPredictor(ifd) {
const bitDepth = ifd.bitsPerSample;
switch (ifd.predictor) {
case 1: {
// No prediction scheme, nothing to do
break;
}
case 2: {
if (bitDepth === 8) {
applyHorizontalDifferencing8Bit(ifd.data, ifd.width, ifd.components);
}
else if (bitDepth === 16) {
applyHorizontalDifferencing16Bit(ifd.data, ifd.width, ifd.components);
}
else {
throw new Error(`Horizontal differencing is only supported for images with a bit depth of ${bitDepth}`);
}
break;
}
default:
throw new Error(`invalid predictor: ${ifd.predictor}`);
}
}
convertAlpha(ifd) {
if (ifd.alpha && ifd.associatedAlpha) {
const { data, components, maxSampleValue } = ifd;
for (let i = 0; i < data.length; i += components) {
const alphaValue = data[i + components - 1];
for (let j = 0; j < components - 1; j++) {
data[i + j] = Math.round((data[i + j] * maxSampleValue) / alphaValue);
}
}
}
}
}
function getDataArray(size, bitDepth, sampleFormat) {
if (bitDepth === 8 || bitDepth === 1) {
return new Uint8Array(size);
}
else if (bitDepth === 16) {
return new Uint16Array(size);
}
else if (bitDepth === 32 && sampleFormat === 3) {
return new Float32Array(size);
}
else if (bitDepth === 64 && sampleFormat === 3) {
return new Float64Array(size);
}
else {
throw unsupported('bit depth / sample format', `${bitDepth} / ${sampleFormat}`);
}
}
function unsupported(type, value) {
return new Error(`Unsupported ${type}: ${value}`);
}
function checkPages(pages) {
if (pages) {
for (const page of pages) {
if (page < 0 || !Number.isInteger(page)) {
throw new RangeError(`Index ${page} is invalid. Must be a positive integer.`);
}
}
}
}
//# sourceMappingURL=tiff_decoder.js.map