UNPKG

tiff

Version:

TIFF image decoder written entirely in JavaScript

402 lines 15.1 kB
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