Skip to content

Commit

Permalink
added support for tiled images
Browse files Browse the repository at this point in the history
  • Loading branch information
weigert committed Mar 5, 2025
1 parent 03f58db commit c66a49f
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 105 deletions.
241 changes: 136 additions & 105 deletions src/tiffDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ export default class TIFFDecoder extends IOBuffer {
case 1: // BlackIsZero
case 2: // RGB
case 3: // Palette color
this.readStripData(ifd);
if(ifd.tiled){
this.readTileData(ifd);

Check warning on line 197 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L197

Added line #L197 was not covered by tests
} else {
this.readStripData(ifd);
}
break;
default:
throw unsupported('image type', ifd.type);
Expand All @@ -210,88 +214,166 @@ export default class TIFFDecoder extends IOBuffer {
}
}

private static uncompress(data: DataView, compression = 1): DataView {

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);

Check warning on line 238 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L233-L238

Added lines #L233 - L238 were not covered by tests
}

}

private readStripData(ifd: TiffIfd): void {

// General Image Dimensions
const width = ifd.width;
const height = ifd.height;

const bitDepth = ifd.bitsPerSample;
const sampleFormat = ifd.sampleFormat;
const size = width * height * ifd.samplesPerPixel;
const data = getDataArray(size, bitDepth, sampleFormat);

const rowsPerStrip = ifd.rowsPerStrip;
const maxPixels = rowsPerStrip * width * ifd.samplesPerPixel;
// Compressed Strip Layout
// Note: Strips are Row-Major
const stripOffsets = ifd.stripOffsets;
const stripByteCounts = ifd.stripByteCounts || guessStripByteCounts(ifd);
const littleEndian = this.isLittleEndian()
const stripLength = width * ifd.rowsPerStrip * ifd.samplesPerPixel;

let remainingPixels = size;
let pixel = 0;
// 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 = remainingPixels > maxPixels ? maxPixels : remainingPixels;
remainingPixels -= length;
const length = Math.min(stripLength, size - start);

let dataToFill = stripData;
// Write Uncompressed Strip Data to Output (Linear Layout)
for(let index = 0; index < length; ++index){

const value = this.sampleValue(uncompressed, index, ifd.sampleFormat, ifd.bitsPerSample, littleEndian)
output[start + index] = value;

switch (ifd.compression) {
case 1: {
// No compression, nothing to do
break;
}
case 5: {
// LZW compression
dataToFill = decompressLzw(stripData);
break;
}
case 8:
case 32946: {
// Zlib and Deflate compressions. They are identical.
dataToFill = decompressZlib(stripData);
break;
}
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', ifd.compression);
}

pixel = this.fillUncompressed(
bitDepth,
sampleFormat,
data,
dataToFill,
pixel,
length,
);
start += length;

}

ifd.data = data;
ifd.data = output;
}

private fillUncompressed(
bitDepth: number,
private readTileData(ifd: TiffIfd): void {

Check warning on line 290 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L290

Added line #L290 was not covered by tests

if(!ifd.tileWidth || !ifd.tileHeight){
return;

Check warning on line 293 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L293

Added line #L293 was not covered by tests
}

// General Image Dimensions
const width = ifd.width;
const height = ifd.height;
const size = width * height * ifd.samplesPerPixel;

Check warning on line 299 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L297-L299

Added lines #L297 - L299 were not covered by tests

// Tile Dimensions, Counts
const twidth = ifd.tileWidth;
const theight = ifd.tileHeight;
const nwidth = Math.floor((width + twidth - 1) / twidth);
const nheight = Math.floor((height + theight - 1) / theight);

Check warning on line 305 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L302-L305

Added lines #L302 - L305 were not covered by tests

// Compressed Tile Layout
const tileOffsets = ifd.tileOffsets;
const tileByteCounts = ifd.tileByteCounts;
const littleEndian = this.isLittleEndian()

Check warning on line 310 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L308-L310

Added lines #L308 - L310 were not covered by tests

// Output Data Buffer
const output = getDataArray(size, ifd.bitsPerSample, ifd.sampleFormat);

Check warning on line 313 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L313

Added line #L313 was not covered by tests

// Iterate over Set of Tiles
for(let nx = 0; nx < nwidth; ++nx){
for(let ny = 0; ny < nheight; ++ny){

Check warning on line 317 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L316-L317

Added lines #L316 - L317 were not covered by tests

// Note: TIFF Orders Tiles Row-Major,
// including the tile interiors.
const nind = ny * nwidth + nx;

Check warning on line 321 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L321

Added line #L321 was not covered by tests

// Extract and Decompress Tile Data
const tileData = new DataView(

Check warning on line 324 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L324

Added line #L324 was not covered by tests
this.buffer,
tileOffsets[nind],
tileByteCounts[nind],
);
const uncompressed = TIFFDecoder.uncompress(tileData, ifd.compression)

Check warning on line 329 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L329

Added line #L329 was not covered by tests

// Write Uncompressed Tile Data to Output
for(let tx = 0; tx < twidth; ++tx){
for(let ty = 0; ty < theight; ++ty){

Check warning on line 333 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L332-L333

Added lines #L332 - L333 were not covered by tests

const ix = nx * twidth + tx;
const iy = ny * theight + ty;

Check warning on line 336 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L335-L336

Added lines #L335 - L336 were not covered by tests
if(ix >= width) continue;
if(iy >= height) continue;

const index = (ty * twidth + tx);
const value = this.sampleValue(uncompressed, index, ifd.sampleFormat, ifd.bitsPerSample, littleEndian);

Check warning on line 341 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L340-L341

Added lines #L340 - L341 were not covered by tests

const indexOut = (iy * width + ix);
output[indexOut] = value;

Check warning on line 344 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L343-L344

Added lines #L343 - L344 were not covered by tests

}
}
}
}

ifd.data = output;

Check warning on line 351 in src/tiffDecoder.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffDecoder.ts#L351

Added line #L351 was not covered by tests
}

//! sampleValue retrieves a single, typed value
//! from a DataView while considering the format,
//! bitDepth and endianness.
//!
//! As this is called once per iteration, it would make
//! sense to convert this to a switch or if statement
//! over an enumerator instead of a parameter.
//!
private sampleValue (
data: DataView,
index: number,
sampleFormat: number,
data: DataArray,
stripData: DataView,
pixel: number,
length: number,
bitDepth: number,
littleEndian: boolean
): number {
if (bitDepth === 8) {
return fill8bit(data, stripData, pixel, length);
return data.getUint8(index);
} else if (bitDepth === 16) {
return fill16bit(data, stripData, pixel, length, this.isLittleEndian());
return data.getUint16(2*index, littleEndian);
} else if (bitDepth === 32 && sampleFormat === 3) {
return fillFloat32(data, stripData, pixel, length, this.isLittleEndian());
return data.getFloat32(4*index, littleEndian);
} else if (bitDepth === 64 && sampleFormat === 3) {
return fillFloat64(data, stripData, pixel, length, this.isLittleEndian());
return data.getFloat64(8*index, littleEndian);
} else {
throw unsupported('bitDepth', bitDepth);
}
Expand Down Expand Up @@ -363,57 +445,6 @@ function getDataArray(
}
}

function fill8bit(
dataTo: DataArray,
dataFrom: DataView,
index: number,
length: number,
): number {
for (let i = 0; i < length; i++) {
dataTo[index++] = dataFrom.getUint8(i);
}
return index;
}

function fill16bit(
dataTo: DataArray,
dataFrom: DataView,
index: number,
length: number,
littleEndian: boolean,
): number {
for (let i = 0; i < length * 2; i += 2) {
dataTo[index++] = dataFrom.getUint16(i, littleEndian);
}
return index;
}

function fillFloat32(
dataTo: DataArray,
dataFrom: DataView,
index: number,
length: number,
littleEndian: boolean,
): number {
for (let i = 0; i < length * 4; i += 4) {
dataTo[index++] = dataFrom.getFloat32(i, littleEndian);
}
return index;
}

function fillFloat64(
dataTo: DataArray,
dataFrom: DataView,
index: number,
length: number,
littleEndian: boolean,
): number {
for (let i = 0; i < length * 8; i += 8) {
dataTo[index++] = dataFrom.getFloat64(i, littleEndian);
}
return index;
}

function unsupported(type: string, value: any): Error {

Check warning on line 448 in src/tiffDecoder.ts

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

Unexpected any. Specify a different type
return new Error(`Unsupported ${type}: ${value}`);
}
Expand All @@ -427,4 +458,4 @@ function checkPages(pages: number[] | undefined) {
}
}
}
}
}
18 changes: 18 additions & 0 deletions src/tiffIfd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ export default class TiffIfd extends Ifd {
}
return palette;
}
public get tileWidth(): number | undefined {
return this.get('TileWidth');
}
public get tileHeight(): number | undefined {
return this.get('TileLength');

Check warning on line 154 in src/tiffIfd.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffIfd.ts#L153-L154

Added lines #L153 - L154 were not covered by tests
}
public get tileOffsets(): number[] {
return alwaysArray(this.get('TileOffsets'));

Check warning on line 157 in src/tiffIfd.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffIfd.ts#L156-L157

Added lines #L156 - L157 were not covered by tests
}
public get tileByteCounts(): number[] {
return alwaysArray(this.get('TileByteCounts'));

Check warning on line 160 in src/tiffIfd.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffIfd.ts#L159-L160

Added lines #L159 - L160 were not covered by tests
}
public get tiled(): boolean {
return (this.tileWidth !== undefined)
&& (this.tileHeight !== undefined)
&& (this.tileOffsets !== undefined)
&& (this.tileByteCounts !== undefined);

Check warning on line 166 in src/tiffIfd.ts

View check run for this annotation

Codecov / codecov/patch

src/tiffIfd.ts#L164-L166

Added lines #L164 - L166 were not covered by tests
}
}

function alwaysArray(value: number | number[]): number[] {
Expand Down

0 comments on commit c66a49f

Please sign in to comment.