diff --git a/src/formats/tiff-decoder.ts b/src/formats/tiff-decoder.ts new file mode 100644 index 0000000..c5df074 --- /dev/null +++ b/src/formats/tiff-decoder.ts @@ -0,0 +1,191 @@ +/** @format */ + +import { FrameAnimation } from '../common/frame-animation'; +import { FrameType } from '../common/frame-type'; +import { MemoryImage } from '../common/memory-image'; +import { HdrImage } from '../hdr/hdr-image'; +import { Decoder } from './decoder'; +import { TiffImage } from './tiff/tiff-image'; +import { TiffInfo } from './tiff/tiff-info'; +import { InputBuffer } from './util/input-buffer'; + +export class TiffDecoder implements Decoder { + private static readonly TIFF_SIGNATURE = 42; + private static readonly TIFF_LITTLE_ENDIAN = 0x4949; + private static readonly TIFF_BIG_ENDIAN = 0x4d4d; + + private input!: InputBuffer; + + private _info: TiffInfo | undefined = undefined; + public get info(): TiffInfo | undefined { + return this._info; + } + + /** + * How many frames are available to be decoded. [startDecode] should have been called first. + * Non animated image files will have a single frame. + */ + public get numFrames(): number { + return this._info !== undefined ? this._info.images.length : 0; + } + + /** + * Read the TIFF header and IFD blocks. + */ + private readHeader(p: InputBuffer): TiffInfo | undefined { + const byteOrder = p.readUint16(); + if ( + byteOrder !== TiffDecoder.TIFF_LITTLE_ENDIAN && + byteOrder !== TiffDecoder.TIFF_BIG_ENDIAN + ) { + return undefined; + } + + let bigEndian = false; + if (byteOrder === TiffDecoder.TIFF_BIG_ENDIAN) { + p.bigEndian = true; + bigEndian = true; + } else { + p.bigEndian = false; + bigEndian = false; + } + + let signature = 0; + signature = p.readUint16(); + if (signature !== TiffDecoder.TIFF_SIGNATURE) { + return undefined; + } + + let offset = p.readUint32(); + + const p2 = InputBuffer.from(p); + p2.offset = offset; + + const images: TiffImage[] = []; + while (offset !== 0) { + let img: TiffImage | undefined = undefined; + try { + img = new TiffImage(p2); + if (!img.isValid) { + break; + } + } catch (error) { + break; + } + images.push(img); + + offset = p2.readUint32(); + if (offset !== 0) { + p2.offset = offset; + } + } + + return images.length > 0 + ? new TiffInfo({ + bigEndian: bigEndian, + signature: signature, + ifdOffset: offset, + images: images, + }) + : undefined; + } + + /** + * Is the given file a valid TIFF image? + */ + public isValidFile(bytes: Uint8Array): boolean { + const buffer = new InputBuffer({ + buffer: bytes, + }); + return this.readHeader(buffer) !== undefined; + } + + /** + * Validate the file is a TIFF image and get information about it. + * If the file is not a valid TIFF image, undefined is returned. + */ + public startDecode(bytes: Uint8Array): TiffInfo | undefined { + this.input = new InputBuffer({ + buffer: bytes, + }); + this._info = this.readHeader(this.input); + return this._info; + } + + /** + * Decode a single frame from the data stat was set with [startDecode]. + * If [frame] is out of the range of available frames, undefined is returned. + * Non animated image files will only have [frame] 0. An [AnimationFrame] + * is returned, which provides the image, and top-left coordinates of the + * image, as animated frames may only occupy a subset of the canvas. + */ + public decodeFrame(frame: number): MemoryImage | undefined { + if (this._info === undefined) { + return undefined; + } + + return this._info.images[frame].decode(this.input); + } + + public decodeHdrFrame(frame: number): HdrImage | undefined { + if (this._info === undefined) { + return undefined; + } + return this._info.images[frame].decodeHdr(this.input); + } + + /** + * Decode all of the frames from an animation. If the file is not an + * animation, a single frame animation is returned. If there was a problem + * decoding the file, undefined is returned. + */ + public decodeAnimation(bytes: Uint8Array): FrameAnimation | undefined { + if (this.startDecode(bytes) === undefined) { + return undefined; + } + + const animation = new FrameAnimation({ + width: this._info!.width, + height: this._info!.height, + frameType: FrameType.page, + }); + + for (let i = 0, len = this.numFrames; i < len; ++i) { + const image = this.decodeFrame(i); + animation.addFrame(image!); + } + + return animation; + } + + /** + * Decode the file and extract a single image from it. If the file is + * animated, the specified [frame] will be decoded. If there was a problem + * decoding the file, undefined is returned. + */ + public decodeImage(bytes: Uint8Array, frame = 0): MemoryImage | undefined { + this.input = new InputBuffer({ + buffer: bytes, + }); + + this._info = this.readHeader(this.input); + if (this._info === undefined) { + return undefined; + } + + return this._info.images[frame].decode(this.input); + } + + public decodeHdrImage(bytes: Uint8Array, frame = 0): HdrImage | undefined { + this.input = new InputBuffer({ + buffer: bytes, + }); + + this._info = this.readHeader(this.input); + if (this._info === undefined) { + return undefined; + } + + return this._info.images[frame].decodeHdr(this.input); + } +} diff --git a/src/formats/tiff-encoder.ts b/src/formats/tiff-encoder.ts new file mode 100644 index 0000000..505f247 --- /dev/null +++ b/src/formats/tiff-encoder.ts @@ -0,0 +1,190 @@ +/** @format */ + +import { FrameAnimation } from '../common/frame-animation'; +import { MemoryImage } from '../common/memory-image'; +import { HdrImage } from '../hdr/hdr-image'; +import { HdrSlice } from '../hdr/hdr-slice'; +import { Encoder } from './encoder'; +import { TiffEntry } from './tiff/tiff-entry'; +import { TiffImage } from './tiff/tiff-image'; +import { OutputBuffer } from './util/output-buffer'; + +/** + * Encode a TIFF image. + */ +export class TiffEncoder implements Encoder { + private static readonly LITTLE_ENDIAN = 0x4949; + private static readonly SIGNATURE = 42; + + private _supportsAnimation = false; + public get supportsAnimation(): boolean { + return this._supportsAnimation; + } + + private writeHeader(out: OutputBuffer): void { + // byteOrder + out.writeUint16(TiffEncoder.LITTLE_ENDIAN); + // TIFF signature + out.writeUint16(TiffEncoder.SIGNATURE); + // Offset to the start of the IFD tags + out.writeUint32(8); + } + + private writeImage(out: OutputBuffer, image: MemoryImage): void { + // number of IFD entries + out.writeUint16(11); + + this.writeEntryUint32(out, TiffImage.TAG_IMAGE_WIDTH, image.width); + this.writeEntryUint32(out, TiffImage.TAG_IMAGE_LENGTH, image.height); + this.writeEntryUint16(out, TiffImage.TAG_BITS_PER_SAMPLE, 8); + this.writeEntryUint16( + out, + TiffImage.TAG_COMPRESSION, + TiffImage.COMPRESSION_NONE + ); + this.writeEntryUint16( + out, + TiffImage.TAG_PHOTOMETRIC_INTERPRETATION, + TiffImage.PHOTOMETRIC_RGB + ); + this.writeEntryUint16(out, TiffImage.TAG_SAMPLES_PER_PIXEL, 4); + this.writeEntryUint16( + out, + TiffImage.TAG_SAMPLE_FORMAT, + TiffImage.FORMAT_UINT + ); + + this.writeEntryUint32(out, TiffImage.TAG_ROWS_PER_STRIP, image.height); + this.writeEntryUint16(out, TiffImage.TAG_PLANAR_CONFIGURATION, 1); + this.writeEntryUint32( + out, + TiffImage.TAG_STRIP_BYTE_COUNTS, + image.width * image.height * 4 + ); + this.writeEntryUint32(out, TiffImage.TAG_STRIP_OFFSETS, out.length + 4); + out.writeBytes(image.getBytes()); + } + + private writeHdrImage(out: OutputBuffer, image: HdrImage): void { + // number of IFD entries + out.writeUint16(11); + + this.writeEntryUint32(out, TiffImage.TAG_IMAGE_WIDTH, image.width); + this.writeEntryUint32(out, TiffImage.TAG_IMAGE_LENGTH, image.height); + this.writeEntryUint16( + out, + TiffImage.TAG_BITS_PER_SAMPLE, + image.bitsPerSample + ); + this.writeEntryUint16( + out, + TiffImage.TAG_COMPRESSION, + TiffImage.COMPRESSION_NONE + ); + this.writeEntryUint16( + out, + TiffImage.TAG_PHOTOMETRIC_INTERPRETATION, + image.numberOfChannels === 1 + ? TiffImage.PHOTOMETRIC_BLACKISZERO + : TiffImage.PHOTOMETRIC_RGB + ); + this.writeEntryUint16( + out, + TiffImage.TAG_SAMPLES_PER_PIXEL, + image.numberOfChannels + ); + this.writeEntryUint16( + out, + TiffImage.TAG_SAMPLE_FORMAT, + this.getSampleFormat(image) + ); + + const bytesPerSample = Math.trunc(image.bitsPerSample / 8); + const imageSize = + image.width * image.height * image.slices.size * bytesPerSample; + + this.writeEntryUint32(out, TiffImage.TAG_ROWS_PER_STRIP, image.height); + this.writeEntryUint16(out, TiffImage.TAG_PLANAR_CONFIGURATION, 1); + this.writeEntryUint32(out, TiffImage.TAG_STRIP_BYTE_COUNTS, imageSize); + this.writeEntryUint32(out, TiffImage.TAG_STRIP_OFFSETS, out.length + 4); + + const channels: Uint8Array[] = []; + if (image.blue !== undefined) { + // ? Why does this channel order working but not RGB? + channels.push(image.blue.getBytes()); + } + if (image.red !== undefined) { + channels.push(image.red.getBytes()); + } + if (image.green !== undefined) { + channels.push(image.green.getBytes()); + } + if (image.alpha !== undefined) { + channels.push(image.alpha.getBytes()); + } + if (image.depth !== undefined) { + channels.push(image.depth.getBytes()); + } + + for (let y = 0, pi = 0; y < image.height; ++y) { + for (let x = 0; x < image.width; ++x, pi += bytesPerSample) { + for (let c = 0; c < channels.length; ++c) { + const ch = channels[c]; + for (let b = 0; b < bytesPerSample; ++b) { + out.writeByte(ch[pi + b]); + } + } + } + } + } + + private getSampleFormat(image: HdrImage): number { + switch (image.sampleFormat) { + case HdrSlice.UINT: + return TiffImage.FORMAT_UINT; + case HdrSlice.INT: + return TiffImage.FORMAT_INT; + } + return TiffImage.FORMAT_FLOAT; + } + + private writeEntryUint16(out: OutputBuffer, tag: number, data: number): void { + out.writeUint16(tag); + out.writeUint16(TiffEntry.TYPE_SHORT); + // number of values + out.writeUint32(1); + out.writeUint16(data); + // pad to 4 bytes + out.writeUint16(0); + } + + private writeEntryUint32(out: OutputBuffer, tag: number, data: number): void { + out.writeUint16(tag); + out.writeUint16(TiffEntry.TYPE_LONG); + // number of values + out.writeUint32(1); + out.writeUint32(data); + } + + public encodeImage(image: MemoryImage): Uint8Array { + const out = new OutputBuffer(); + this.writeHeader(out); + this.writeImage(out, image); + // no offset to the next image + out.writeUint32(0); + return out.getBytes(); + } + + public encodeAnimation(_animation: FrameAnimation): Uint8Array | undefined { + return undefined; + } + + public encodeHdrImage(image: HdrImage): Uint8Array { + const out = new OutputBuffer(); + this.writeHeader(out); + this.writeHdrImage(out, image); + // no offset to the next image + out.writeUint32(0); + return out.getBytes(); + } +} diff --git a/src/formats/tiff/tiff-bit-reader.ts b/src/formats/tiff/tiff-bit-reader.ts new file mode 100644 index 0000000..7a54470 --- /dev/null +++ b/src/formats/tiff/tiff-bit-reader.ts @@ -0,0 +1,70 @@ +/** @format */ + +import { InputBuffer } from '../util/input-buffer'; + +export class TiffBitReader { + private static readonly BITMASK = [0, 1, 3, 7, 15, 31, 63, 127, 255]; + + private bitBuffer = 0; + + private bitPosition = 0; + + private input: InputBuffer; + + constructor(input: InputBuffer) { + this.input = input; + } + + /** + * Read a number of bits from the input stream. + */ + public readBits(numBits: number): number { + let nBits = numBits; + if (nBits === 0) { + return 0; + } + + if (this.bitPosition === 0) { + this.bitPosition = 8; + this.bitBuffer = this.input.readByte(); + } + + let value = 0; + + while (nBits > this.bitPosition) { + value = + (value << this.bitPosition) + + (this.bitBuffer & TiffBitReader.BITMASK[this.bitPosition]); + nBits -= this.bitPosition; + this.bitPosition = 8; + this.bitBuffer = this.input.readByte(); + } + + if (nBits > 0) { + if (this.bitPosition == 0) { + this.bitPosition = 8; + this.bitBuffer = this.input.readByte(); + } + + value = + (value << nBits) + + ((this.bitBuffer >> (this.bitPosition - nBits)) & + TiffBitReader.BITMASK[nBits]); + + this.bitPosition -= nBits; + } + + return value; + } + + public readByte() { + return this.readBits(8); + } + + /** + * Flush the rest of the bits in the buffer so the next read starts at the next byte. + */ + public flushByte() { + return (this.bitPosition = 0); + } +} diff --git a/src/formats/tiff/tiff-entry.ts b/src/formats/tiff/tiff-entry.ts new file mode 100644 index 0000000..65209d7 --- /dev/null +++ b/src/formats/tiff/tiff-entry.ts @@ -0,0 +1,201 @@ +/** @format */ + +import { ImageError } from '../../error/image-error'; +import { InputBuffer } from '../util/input-buffer'; +import { TiffImage } from './tiff-image'; + +export interface TiffEntryInitOptions { + tag: number; + type: number; + numValues: number; + p: InputBuffer; +} + +export class TiffEntry { + private static readonly SIZE_OF_TYPE: number[] = [ + // 0 = n/a + 0, + // 1 = byte + 1, + // 2 = ascii + 1, + // 3 = short + 2, + // 4 = long + 4, + // 5 = rational + 8, + // 6 = sbyte + 1, + // 7 = undefined + 1, + // 8 = sshort + 2, + // 9 = slong + 4, + // 10 = srational + 8, + // 11 = float + 4, + // 12 = double + 8, 0, + ]; + + public static readonly TYPE_BYTE = 1; + public static readonly TYPE_ASCII = 2; + public static readonly TYPE_SHORT = 3; + public static readonly TYPE_LONG = 4; + public static readonly TYPE_RATIONAL = 5; + public static readonly TYPE_SBYTE = 6; + public static readonly TYPE_UNDEFINED = 7; + public static readonly TYPE_SSHORT = 8; + public static readonly TYPE_SLONG = 9; + public static readonly TYPE_SRATIONAL = 10; + public static readonly TYPE_FLOAT = 11; + public static readonly TYPE_DOUBLE = 12; + + private _tag: number; + public get tag(): number { + return this._tag; + } + + private _type: number; + public get type(): number { + return this._type; + } + + private _numValues: number; + public get numValues(): number { + return this._numValues; + } + + private _valueOffset: number | undefined; + public get valueOffset(): number | undefined { + return this._valueOffset; + } + public set valueOffset(v: number | undefined) { + this._valueOffset = v; + } + + private _p: InputBuffer; + public get p(): InputBuffer { + return this._p; + } + + get isValid(): boolean { + return this._type < 13 && this._type > 0; + } + + get typeSize(): number { + return this.isValid ? TiffEntry.SIZE_OF_TYPE[this._type] : 0; + } + + get isString(): boolean { + return this._type === TiffEntry.TYPE_ASCII; + } + + constructor(options: TiffEntryInitOptions) { + this._tag = options.tag; + this._type = options.type; + this._numValues = options.numValues; + this._p = options.p; + } + + private readValueInternal(): number { + switch (this._type) { + case TiffEntry.TYPE_BYTE: + case TiffEntry.TYPE_ASCII: + return this._p.readByte(); + case TiffEntry.TYPE_SHORT: + return this._p.readUint16(); + case TiffEntry.TYPE_LONG: + return this._p.readUint32(); + case TiffEntry.TYPE_RATIONAL: { + const num = this._p.readUint32(); + const den = this._p.readUint32(); + if (den === 0) { + return 0; + } + return Math.trunc(num / den); + } + case TiffEntry.TYPE_SBYTE: + throw new ImageError('Unhandled value type: SBYTE'); + case TiffEntry.TYPE_UNDEFINED: + return this._p.readByte(); + case TiffEntry.TYPE_SSHORT: + throw new ImageError('Unhandled value type: SSHORT'); + case TiffEntry.TYPE_SLONG: + throw new ImageError('Unhandled value type: SLONG'); + case TiffEntry.TYPE_SRATIONAL: + throw new ImageError('Unhandled value type: SRATIONAL'); + case TiffEntry.TYPE_FLOAT: + throw new ImageError('Unhandled value type: FLOAT'); + case TiffEntry.TYPE_DOUBLE: + throw new ImageError('Unhandled value type: DOUBLE'); + } + return 0; + } + + public toString() { + if (TiffImage.TAG_NAME.has(this._tag)) { + return `${TiffImage.TAG_NAME.get(this._tag)}: $type $numValues`; + } + return `<${this._tag}>: ${this._type} ${this._numValues}`; + } + + public readValue(): number { + this._p.offset = this._valueOffset!; + return this.readValueInternal(); + } + + public readValues(): number[] { + this._p.offset = this._valueOffset!; + const values: number[] = []; + for (let i = 0; i < this._numValues; ++i) { + values.push(this.readValueInternal()); + } + return values; + } + + public readString(): string { + if (this._type !== TiffEntry.TYPE_ASCII) { + throw new ImageError('readString requires ASCII entity'); + } + // TODO: ASCII fields can contain multiple strings, separated with a NULL. + return String.fromCharCode(...this.readValues()); + } + + public read(): number[] { + this._p.offset = this._valueOffset!; + const values: number[] = []; + for (let i = 0; i < this._numValues; ++i) { + switch (this._type) { + case TiffEntry.TYPE_BYTE: + case TiffEntry.TYPE_ASCII: + values.push(this._p.readByte()); + break; + case TiffEntry.TYPE_SHORT: + values.push(this._p.readUint16()); + break; + case TiffEntry.TYPE_LONG: + values.push(this._p.readUint32()); + break; + case TiffEntry.TYPE_RATIONAL: { + const num = this._p.readUint32(); + const den = this._p.readUint32(); + if (den !== 0) { + values.push(num / den); + } + break; + } + case TiffEntry.TYPE_FLOAT: + values.push(this._p.readFloat32()); + break; + case TiffEntry.TYPE_DOUBLE: + values.push(this._p.readFloat64()); + break; + } + } + return values; + } +} diff --git a/src/formats/tiff/tiff-fax-decoder.ts b/src/formats/tiff/tiff-fax-decoder.ts new file mode 100644 index 0000000..ebb0d19 --- /dev/null +++ b/src/formats/tiff/tiff-fax-decoder.ts @@ -0,0 +1,1523 @@ +/** @format */ + +import { ImageError } from '../../error/image-error'; +import { InputBuffer } from '../util/input-buffer'; + +export interface TiffFaxDecoderInitOptions { + fillOrder: number; + width: number; + height: number; +} + +export class TiffFaxDecoder { + private static readonly TABLE1: number[] = [ + // 0 bits are left in first byte - SHOULD NOT HAPPEN + 0x00, + // 1 bits are left in first byte + 0x01, + // 2 bits are left in first byte + 0x03, + // 3 bits are left in first byte + 0x07, + // 4 bits are left in first byte + 0x0f, + // 5 bits are left in first byte + 0x1f, + // 6 bits are left in first byte + 0x3f, + // 7 bits are left in first byte + 0x7f, + // 8 bits are left in first byte + 0xff, + ]; + + private static readonly TABLE2: number[] = [ + // 0 + 0x00, + // 1 + 0x80, + // 2 + 0xc0, + // 3 + 0xe0, + // 4 + 0xf0, + // 5 + 0xf8, + // 6 + 0xfc, + // 7 + 0xfe, + // 8 + 0xff, + ]; + + /** + * Table to be used when fillOrder = 2, for flipping bytes. + */ + private static readonly FLIP_TABLE: number[] = [ + 0, -128, 64, -64, 32, -96, 96, -32, 16, -112, 80, -48, 48, -80, 112, -16, 8, + -120, 72, -56, 40, -88, 104, -24, 24, -104, 88, -40, 56, -72, 120, -8, 4, + -124, 68, -60, 36, -92, 100, -28, 20, -108, 84, -44, 52, -76, 116, -12, 12, + -116, 76, -52, 44, -84, 108, -20, 28, -100, 92, -36, 60, -68, 124, -4, 2, + -126, 66, -62, 34, -94, 98, -30, 18, -110, 82, -46, 50, -78, 114, -14, 10, + -118, 74, -54, 42, -86, 106, -22, 26, -102, 90, -38, 58, -70, 122, -6, 6, + -122, 70, -58, 38, -90, 102, -26, 22, -106, 86, -42, 54, -74, 118, -10, 14, + -114, 78, -50, 46, -82, 110, -18, 30, -98, 94, -34, 62, -66, 126, -2, 1, + -127, 65, -63, 33, -95, 97, -31, 17, -111, 81, -47, 49, -79, 113, -15, 9, + -119, 73, -55, 41, -87, 105, -23, 25, -103, 89, -39, 57, -71, 121, -7, 5, + -123, 69, -59, 37, -91, 101, -27, 21, -107, 85, -43, 53, -75, 117, -11, 13, + -115, 77, -51, 45, -83, 109, -19, 29, -99, 93, -35, 61, -67, 125, -3, 3, + -125, 67, -61, 35, -93, 99, -29, 19, -109, 83, -45, 51, -77, 115, -13, 11, + -117, 75, -53, 43, -85, 107, -21, 27, -101, 91, -37, 59, -69, 123, -5, 7, + -121, 71, -57, 39, -89, 103, -25, 23, -105, 87, -41, 55, -73, 119, -9, 15, + -113, 79, -49, 47, -81, 111, -17, 31, -97, 95, -33, 63, -65, 127, -1, + ]; + + /** + * The main 10 bit white runs lookup table + */ + private static readonly WHITE: number[] = [ + // 0 - 7 + 6430, 6400, 6400, 6400, 3225, 3225, 3225, 3225, + // 8 - 15 + 944, 944, 944, 944, 976, 976, 976, 976, + // 16 - 23 + 1456, 1456, 1456, 1456, 1488, 1488, 1488, 1488, + // 24 - 31 + 718, 718, 718, 718, 718, 718, 718, 718, + // 32 - 39 + 750, 750, 750, 750, 750, 750, 750, 750, + // 40 - 47 + 1520, 1520, 1520, 1520, 1552, 1552, 1552, 1552, + // 48 - 55 + 428, 428, 428, 428, 428, 428, 428, 428, + // 56 - 63 + 428, 428, 428, 428, 428, 428, 428, 428, + // 64 - 71 + 654, 654, 654, 654, 654, 654, 654, 654, + // 72 - 79 + 1072, 1072, 1072, 1072, 1104, 1104, 1104, 1104, + // 80 - 87 + 1136, 1136, 1136, 1136, 1168, 1168, 1168, 1168, + // 88 - 95 + 1200, 1200, 1200, 1200, 1232, 1232, 1232, 1232, + // 96 - 103 + 622, 622, 622, 622, 622, 622, 622, 622, + // 104 - 111 + 1008, 1008, 1008, 1008, 1040, 1040, 1040, 1040, + // 112 - 119 + 44, 44, 44, 44, 44, 44, 44, 44, + // 120 - 127 + 44, 44, 44, 44, 44, 44, 44, 44, + // 128 - 135 + 396, 396, 396, 396, 396, 396, 396, 396, + // 136 - 143 + 396, 396, 396, 396, 396, 396, 396, 396, + // 144 - 151 + 1712, 1712, 1712, 1712, 1744, 1744, 1744, 1744, + // 152 - 159 + 846, 846, 846, 846, 846, 846, 846, 846, + // 160 - 167 + 1264, 1264, 1264, 1264, 1296, 1296, 1296, 1296, + // 168 - 175 + 1328, 1328, 1328, 1328, 1360, 1360, 1360, 1360, + // 176 - 183 + 1392, 1392, 1392, 1392, 1424, 1424, 1424, 1424, + // 184 - 191 + 686, 686, 686, 686, 686, 686, 686, 686, + // 192 - 199 + 910, 910, 910, 910, 910, 910, 910, 910, + // 200 - 207 + 1968, 1968, 1968, 1968, 2000, 2000, 2000, 2000, + // 208 - 215 + 2032, 2032, 2032, 2032, 16, 16, 16, 16, + // 216 - 223 + 10257, 10257, 10257, 10257, 12305, 12305, 12305, 12305, + // 224 - 231 + 330, 330, 330, 330, 330, 330, 330, 330, + // 232 - 239 + 330, 330, 330, 330, 330, 330, 330, 330, + // 240 - 247 + 330, 330, 330, 330, 330, 330, 330, 330, + // 248 - 255 + 330, 330, 330, 330, 330, 330, 330, 330, + // 256 - 263 + 362, 362, 362, 362, 362, 362, 362, 362, + // 264 - 271 + 362, 362, 362, 362, 362, 362, 362, 362, + // 272 - 279 + 362, 362, 362, 362, 362, 362, 362, 362, + // 280 - 287 + 362, 362, 362, 362, 362, 362, 362, 362, + // 288 - 295 + 878, 878, 878, 878, 878, 878, 878, 878, + // 296 - 303 + 1904, 1904, 1904, 1904, 1936, 1936, 1936, 1936, + // 304 - 311 + -18413, -18413, -16365, -16365, -14317, -14317, -10221, -10221, + // 312 - 319 + 590, 590, 590, 590, 590, 590, 590, 590, + // 320 - 327 + 782, 782, 782, 782, 782, 782, 782, 782, + // 328 - 335 + 1584, 1584, 1584, 1584, 1616, 1616, 1616, 1616, + // 336 - 343 + 1648, 1648, 1648, 1648, 1680, 1680, 1680, 1680, + // 344 - 351 + 814, 814, 814, 814, 814, 814, 814, 814, + // 352 - 359 + 1776, 1776, 1776, 1776, 1808, 1808, 1808, 1808, + // 360 - 367 + 1840, 1840, 1840, 1840, 1872, 1872, 1872, 1872, + // 368 - 375 + 6157, 6157, 6157, 6157, 6157, 6157, 6157, 6157, + // 376 - 383 + 6157, 6157, 6157, 6157, 6157, 6157, 6157, 6157, + // 384 - 391 + -12275, -12275, -12275, -12275, -12275, -12275, -12275, -12275, + // 392 - 399 + -12275, -12275, -12275, -12275, -12275, -12275, -12275, -12275, + // 400 - 407 + 14353, 14353, 14353, 14353, 16401, 16401, 16401, 16401, + // 408 - 415 + 22547, 22547, 24595, 24595, 20497, 20497, 20497, 20497, + // 416 - 423 + 18449, 18449, 18449, 18449, 26643, 26643, 28691, 28691, + // 424 - 431 + 30739, 30739, -32749, -32749, -30701, -30701, -28653, -28653, + // 432 - 439 + -26605, -26605, -24557, -24557, -22509, -22509, -20461, -20461, + // 440 - 447 + 8207, 8207, 8207, 8207, 8207, 8207, 8207, 8207, + // 448 - 455 + 72, 72, 72, 72, 72, 72, 72, 72, + // 456 - 463 + 72, 72, 72, 72, 72, 72, 72, 72, + // 464 - 471 + 72, 72, 72, 72, 72, 72, 72, 72, + // 472 - 479 + 72, 72, 72, 72, 72, 72, 72, 72, + // 480 - 487 + 72, 72, 72, 72, 72, 72, 72, 72, + // 488 - 495 + 72, 72, 72, 72, 72, 72, 72, 72, + // 496 - 503 + 72, 72, 72, 72, 72, 72, 72, 72, + // 504 - 511 + 72, 72, 72, 72, 72, 72, 72, 72, + // 512 - 519 + 104, 104, 104, 104, 104, 104, 104, 104, + // 520 - 527 + 104, 104, 104, 104, 104, 104, 104, 104, + // 528 - 535 + 104, 104, 104, 104, 104, 104, 104, 104, + // 536 - 543 + 104, 104, 104, 104, 104, 104, 104, 104, + // 544 - 551 + 104, 104, 104, 104, 104, 104, 104, 104, + // 552 - 559 + 104, 104, 104, 104, 104, 104, 104, 104, + // 560 - 567 + 104, 104, 104, 104, 104, 104, 104, 104, + // 568 - 575 + 104, 104, 104, 104, 104, 104, 104, 104, + // 576 - 583 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 584 - 591 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 592 - 599 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 600 - 607 + 4107, 4107, 4107, 4107, 4107, 4107, 4107, 4107, + // 608 - 615 + 266, 266, 266, 266, 266, 266, 266, 266, + // 616 - 623 + 266, 266, 266, 266, 266, 266, 266, 266, + // 624 - 631 + 266, 266, 266, 266, 266, 266, 266, 266, + // 632 - 639 + 266, 266, 266, 266, 266, 266, 266, 266, + // 640 - 647 + 298, 298, 298, 298, 298, 298, 298, 298, + // 648 - 655 + 298, 298, 298, 298, 298, 298, 298, 298, + // 656 - 663 + 298, 298, 298, 298, 298, 298, 298, 298, + // 664 - 671 + 298, 298, 298, 298, 298, 298, 298, 298, + // 672 - 679 + 524, 524, 524, 524, 524, 524, 524, 524, + // 680 - 687 + 524, 524, 524, 524, 524, 524, 524, 524, + // 688 - 695 + 556, 556, 556, 556, 556, 556, 556, 556, + // 696 - 703 + 556, 556, 556, 556, 556, 556, 556, 556, + // 704 - 711 + 136, 136, 136, 136, 136, 136, 136, 136, + // 712 - 719 + 136, 136, 136, 136, 136, 136, 136, 136, + // 720 - 727 + 136, 136, 136, 136, 136, 136, 136, 136, + // 728 - 735 + 136, 136, 136, 136, 136, 136, 136, 136, + // 736 - 743 + 136, 136, 136, 136, 136, 136, 136, 136, + // 744 - 751 + 136, 136, 136, 136, 136, 136, 136, 136, + // 752 - 759 + 136, 136, 136, 136, 136, 136, 136, 136, + // 760 - 767 + 136, 136, 136, 136, 136, 136, 136, 136, + // 768 - 775 + 168, 168, 168, 168, 168, 168, 168, 168, + // 776 - 783 + 168, 168, 168, 168, 168, 168, 168, 168, + // 784 - 791 + 168, 168, 168, 168, 168, 168, 168, 168, + // 792 - 799 + 168, 168, 168, 168, 168, 168, 168, 168, + // 800 - 807 + 168, 168, 168, 168, 168, 168, 168, 168, + // 808 - 815 + 168, 168, 168, 168, 168, 168, 168, 168, + // 816 - 823 + 168, 168, 168, 168, 168, 168, 168, 168, + // 824 - 831 + 168, 168, 168, 168, 168, 168, 168, 168, + // 832 - 839 + 460, 460, 460, 460, 460, 460, 460, 460, + // 840 - 847 + 460, 460, 460, 460, 460, 460, 460, 460, + // 848 - 855 + 492, 492, 492, 492, 492, 492, 492, 492, + // 856 - 863 + 492, 492, 492, 492, 492, 492, 492, 492, + // 864 - 871 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 872 - 879 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 880 - 887 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 888 - 895 + 2059, 2059, 2059, 2059, 2059, 2059, 2059, 2059, + // 896 - 903 + 200, 200, 200, 200, 200, 200, 200, 200, + // 904 - 911 + 200, 200, 200, 200, 200, 200, 200, 200, + // 912 - 919 + 200, 200, 200, 200, 200, 200, 200, 200, + // 920 - 927 + 200, 200, 200, 200, 200, 200, 200, 200, + // 928 - 935 + 200, 200, 200, 200, 200, 200, 200, 200, + // 936 - 943 + 200, 200, 200, 200, 200, 200, 200, 200, + // 944 - 951 + 200, 200, 200, 200, 200, 200, 200, 200, + // 952 - 959 + 200, 200, 200, 200, 200, 200, 200, 200, + // 960 - 967 + 232, 232, 232, 232, 232, 232, 232, 232, + // 968 - 975 + 232, 232, 232, 232, 232, 232, 232, 232, + // 976 - 983 + 232, 232, 232, 232, 232, 232, 232, 232, + // 984 - 991 + 232, 232, 232, 232, 232, 232, 232, 232, + // 992 - 999 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1000 - 1007 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1008 - 1015 + 232, 232, 232, 232, 232, 232, 232, 232, + // 1016 - 1023 + 232, 232, 232, 232, 232, 232, 232, 232, + ]; + + /** + * Additional make up codes for both White and Black runs + */ + private static readonly ADDITIONAL_MAKEUP: number[] = [ + 28679, 28679, 31752, -32759, -31735, -30711, -29687, -28663, 29703, 29703, + 30727, 30727, -27639, -26615, -25591, -24567, + ]; + + /** + * Initial black run look up table, uses the first 4 bits of a code + */ + private static readonly INIT_BLACK: number[] = [ + // 0 - 7 + 3226, 6412, 200, 168, 38, 38, 134, 134, + // 8 - 15 + 100, 100, 100, 100, 68, 68, 68, 68, + ]; + + private static readonly TWO_BIT_BLACK: number[] = [292, 260, 226, 226]; + + /** + * Main black run table, using the last 9 bits of possible 13 bit code + */ + private static readonly BLACK: number[] = [ + // 0 - 7 + 62, 62, 30, 30, 0, 0, 0, 0, + // 8 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, + // 16 - 23 + 0, 0, 0, 0, 0, 0, 0, 0, + // 24 - 31 + 0, 0, 0, 0, 0, 0, 0, 0, + // 32 - 39 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 40 - 47 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 48 - 55 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 56 - 63 + 3225, 3225, 3225, 3225, 3225, 3225, 3225, 3225, + // 64 - 71 + 588, 588, 588, 588, 588, 588, 588, 588, + // 72 - 79 + 1680, 1680, 20499, 22547, 24595, 26643, 1776, 1776, + // 80 - 87 + 1808, 1808, -24557, -22509, -20461, -18413, 1904, 1904, + // 88 - 95 + 1936, 1936, -16365, -14317, 782, 782, 782, 782, + // 96 - 103 + 814, 814, 814, 814, -12269, -10221, 10257, 10257, + // 104 - 111 + 12305, 12305, 14353, 14353, 16403, 18451, 1712, 1712, + // 112 - 119 + 1744, 1744, 28691, 30739, -32749, -30701, -28653, -26605, + // 120 - 127 + 2061, 2061, 2061, 2061, 2061, 2061, 2061, 2061, + // 128 - 135 + 424, 424, 424, 424, 424, 424, 424, 424, + // 136 - 143 + 424, 424, 424, 424, 424, 424, 424, 424, + // 144 - 151 + 424, 424, 424, 424, 424, 424, 424, 424, + // 152 - 159 + 424, 424, 424, 424, 424, 424, 424, 424, + // 160 - 167 + 750, 750, 750, 750, 1616, 1616, 1648, 1648, + // 168 - 175 + 1424, 1424, 1456, 1456, 1488, 1488, 1520, 1520, + // 176 - 183 + 1840, 1840, 1872, 1872, 1968, 1968, 8209, 8209, + // 184 - 191 + 524, 524, 524, 524, 524, 524, 524, 524, + // 192 - 199 + 556, 556, 556, 556, 556, 556, 556, 556, + // 200 - 207 + 1552, 1552, 1584, 1584, 2000, 2000, 2032, 2032, + // 208 - 215 + 976, 976, 1008, 1008, 1040, 1040, 1072, 1072, + // 216 - 223 + 1296, 1296, 1328, 1328, 718, 718, 718, 718, + // 224 - 231 + 456, 456, 456, 456, 456, 456, 456, 456, + // 232 - 239 + 456, 456, 456, 456, 456, 456, 456, 456, + // 240 - 247 + 456, 456, 456, 456, 456, 456, 456, 456, + // 248 - 255 + 456, 456, 456, 456, 456, 456, 456, 456, + // 256 - 263 + 326, 326, 326, 326, 326, 326, 326, 326, + // 264 - 271 + 326, 326, 326, 326, 326, 326, 326, 326, + // 272 - 279 + 326, 326, 326, 326, 326, 326, 326, 326, + // 280 - 287 + 326, 326, 326, 326, 326, 326, 326, 326, + // 288 - 295 + 326, 326, 326, 326, 326, 326, 326, 326, + // 296 - 303 + 326, 326, 326, 326, 326, 326, 326, 326, + // 304 - 311 + 326, 326, 326, 326, 326, 326, 326, 326, + // 312 - 319 + 326, 326, 326, 326, 326, 326, 326, 326, + // 320 - 327 + 358, 358, 358, 358, 358, 358, 358, 358, + // 328 - 335 + 358, 358, 358, 358, 358, 358, 358, 358, + // 336 - 343 + 358, 358, 358, 358, 358, 358, 358, 358, + // 344 - 351 + 358, 358, 358, 358, 358, 358, 358, 358, + // 352 - 359 + 358, 358, 358, 358, 358, 358, 358, 358, + // 360 - 367 + 358, 358, 358, 358, 358, 358, 358, 358, + // 368 - 375 + 358, 358, 358, 358, 358, 358, 358, 358, + // 376 - 383 + 358, 358, 358, 358, 358, 358, 358, 358, + // 384 - 391 + 490, 490, 490, 490, 490, 490, 490, 490, + // 392 - 399 + 490, 490, 490, 490, 490, 490, 490, 490, + // 400 - 407 + 4113, 4113, 6161, 6161, 848, 848, 880, 880, + // 408 - 415 + 912, 912, 944, 944, 622, 622, 622, 622, + // 416 - 423 + 654, 654, 654, 654, 1104, 1104, 1136, 1136, + // 424 - 431 + 1168, 1168, 1200, 1200, 1232, 1232, 1264, 1264, + // 432 - 439 + 686, 686, 686, 686, 1360, 1360, 1392, 1392, + // 440 - 447 + 12, 12, 12, 12, 12, 12, 12, 12, + // 448 - 455 + 390, 390, 390, 390, 390, 390, 390, 390, + // 456 - 463 + 390, 390, 390, 390, 390, 390, 390, 390, + // 464 - 471 + 390, 390, 390, 390, 390, 390, 390, 390, + // 472 - 479 + 390, 390, 390, 390, 390, 390, 390, 390, + // 480 - 487 + 390, 390, 390, 390, 390, 390, 390, 390, + // 488 - 495 + 390, 390, 390, 390, 390, 390, 390, 390, + // 496 - 503 + 390, 390, 390, 390, 390, 390, 390, 390, + // 504 - 511 + 390, 390, 390, 390, 390, 390, 390, 390, + ]; + + private static readonly TWO_D_CODES: number[] = [ + // 0 - 7 + 80, 88, 23, 71, 30, 30, 62, 62, + // 8 - 15 + 4, 4, 4, 4, 4, 4, 4, 4, + // 16 - 23 + 11, 11, 11, 11, 11, 11, 11, 11, + // 24 - 31 + 11, 11, 11, 11, 11, 11, 11, 11, + // 32 - 39 + 35, 35, 35, 35, 35, 35, 35, 35, + // 40 - 47 + 35, 35, 35, 35, 35, 35, 35, 35, + // 48 - 55 + 51, 51, 51, 51, 51, 51, 51, 51, + // 56 - 63 + 51, 51, 51, 51, 51, 51, 51, 51, + // 64 - 71 + 41, 41, 41, 41, 41, 41, 41, 41, + // 72 - 79 + 41, 41, 41, 41, 41, 41, 41, 41, + // 80 - 87 + 41, 41, 41, 41, 41, 41, 41, 41, + // 88 - 95 + 41, 41, 41, 41, 41, 41, 41, 41, + // 96 - 103 + 41, 41, 41, 41, 41, 41, 41, 41, + // 104 - 111 + 41, 41, 41, 41, 41, 41, 41, 41, + // 112 - 119 + 41, 41, 41, 41, 41, 41, 41, 41, + // 120 - 127 + 41, 41, 41, 41, 41, 41, 41, 41, + ]; + + private _width: number; + public get width(): number { + return this._width; + } + + private _height: number; + public get height(): number { + return this._height; + } + + private _fillOrder: number; + public get fillOrder(): number { + return this._fillOrder; + } + + // Data structures needed to store changing elements for the previous + // and the current scanline + private changingElemSize = 0; + private prevChangingElems?: Array; + private currChangingElems?: Array; + private data!: InputBuffer; + private bitPointer = 0; + private bytePointer = 0; + + // Element at which to start search in getNextChangingElement + private lastChangingElement = 0; + private compression = 2; + + // Variables set by T4Options + // @ts-ignore + private uncompressedMode = 0; + private fillBits = 0; + private oneD = 0; + + constructor(options: TiffFaxDecoderInitOptions) { + this._fillOrder = options.fillOrder; + this._width = options.width; + this._height = options.height; + this.prevChangingElems = new Array(this._width); + this.prevChangingElems.fill(0); + this.currChangingElems = new Array(this._width); + this.currChangingElems.fill(0); + } + + private nextNBits(bitsToGet: number): number { + let b = 0; + let next = 0; + let next2next = 0; + const l = this.data.length - 1; + const bp = this.bytePointer; + + if (this._fillOrder === 1) { + b = this.data.getByte(bp); + + if (bp === l) { + next = 0x00; + next2next = 0x00; + } else if (bp + 1 === l) { + next = this.data.getByte(bp + 1); + next2next = 0x00; + } else { + next = this.data.getByte(bp + 1); + next2next = this.data.getByte(bp + 2); + } + } else if (this._fillOrder === 2) { + b = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp) & 0xff]; + + if (bp === l) { + next = 0x00; + next2next = 0x00; + } else if (bp + 1 === l) { + next = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp + 1) & 0xff]; + next2next = 0x00; + } else { + next = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp + 1) & 0xff]; + next2next = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp + 2) & 0xff]; + } + } else { + throw new ImageError('TIFFFaxDecoder7'); + } + + const bitsLeft = 8 - this.bitPointer; + let bitsFromNextByte = bitsToGet - bitsLeft; + let bitsFromNext2NextByte = 0; + if (bitsFromNextByte > 8) { + bitsFromNext2NextByte = bitsFromNextByte - 8; + bitsFromNextByte = 8; + } + + this.bytePointer = this.bytePointer! + 1; + + const i1 = (b & TiffFaxDecoder.TABLE1[bitsLeft]) << (bitsToGet - bitsLeft); + let i2 = + (next & TiffFaxDecoder.TABLE2[bitsFromNextByte]) >> + (8 - bitsFromNextByte); + + let i3 = 0; + if (bitsFromNext2NextByte !== 0) { + i2 <<= bitsFromNext2NextByte; + i3 = + (next2next & TiffFaxDecoder.TABLE2[bitsFromNext2NextByte]) >> + (8 - bitsFromNext2NextByte); + i2 |= i3; + this.bytePointer += 1; + this.bitPointer = bitsFromNext2NextByte; + } else { + if (bitsFromNextByte === 8) { + this.bitPointer = 0; + this.bytePointer += 1; + } else { + this.bitPointer = bitsFromNextByte; + } + } + + return i1 | i2; + } + + private nextLesserThan8Bits(bitsToGet: number): number { + let b = 0; + let next = 0; + const l = this.data.length - 1; + const bp = this.bytePointer; + + if (this._fillOrder === 1) { + b = this.data.getByte(bp); + if (bp === l) { + next = 0x00; + } else { + next = this.data.getByte(bp + 1); + } + } else if (this._fillOrder === 2) { + b = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp) & 0xff]; + if (bp === l) { + next = 0x00; + } else { + next = TiffFaxDecoder.FLIP_TABLE[this.data.getByte(bp + 1) & 0xff]; + } + } else { + throw new ImageError('TIFFFaxDecoder7'); + } + + const bitsLeft = 8 - this.bitPointer; + const bitsFromNextByte = bitsToGet - bitsLeft; + + const shift = bitsLeft - bitsToGet; + let i1 = 0; + let i2 = 0; + if (shift >= 0) { + i1 = (b & TiffFaxDecoder.TABLE1[bitsLeft]) >> shift; + this.bitPointer += bitsToGet; + if (this.bitPointer === 8) { + this.bitPointer = 0; + this.bytePointer += 1; + } + } else { + i1 = (b & TiffFaxDecoder.TABLE1[bitsLeft]) << -shift; + i2 = + (next & TiffFaxDecoder.TABLE2[bitsFromNextByte]) >> + (8 - bitsFromNextByte); + + i1 |= i2; + this.bytePointer += 1; + this.bitPointer = bitsFromNextByte; + } + + return i1; + } + + /** + * Move pointer backwards by given amount of bits + */ + private updatePointer(bitsToMoveBack: number): void { + const i = this.bitPointer - bitsToMoveBack; + + if (i < 0) { + this.bytePointer -= 1; + this.bitPointer = 8 + i; + } else { + this.bitPointer = i; + } + } + + /** + * Move to the next byte boundary + */ + private advancePointer(): boolean { + if (this.bitPointer !== 0) { + this.bytePointer += 1; + this.bitPointer = 0; + } + + return true; + } + + private setToBlack( + buffer: InputBuffer, + lineOffset: number, + bitOffset: number, + numBits: number + ): void { + let bitNum = 8 * lineOffset + bitOffset; + const lastBit = bitNum + numBits; + + let byteNum = bitNum >> 3; + + // Handle bits in first byte + const shift = bitNum & 0x7; + if (shift > 0) { + let maskVal = 1 << (7 - shift); + let val = buffer.getByte(byteNum); + while (maskVal > 0 && bitNum < lastBit) { + val |= maskVal; + maskVal >>= 1; + ++bitNum; + } + buffer.setByte(byteNum, val); + } + + // Fill in 8 bits at a time + byteNum = bitNum >> 3; + while (bitNum < lastBit - 7) { + buffer.setByte(byteNum++, 255); + bitNum += 8; + } + + // Fill in remaining bits + while (bitNum < lastBit) { + byteNum = bitNum >> 3; + buffer.setByte( + byteNum, + buffer.getByte(byteNum) | (1 << (7 - (bitNum & 0x7))) + ); + ++bitNum; + } + } + + private decodeNextScanline( + buffer: InputBuffer, + lineOffset: number, + bitOffset: number + ): void { + let offset = bitOffset; + let bits = 0; + let code = 0; + let isT = 0; + let current = 0; + let entry = 0; + let twoBits = 0; + let isWhite = true; + + // Initialize starting of the changing elements array + this.changingElemSize = 0; + + // While scanline not complete + while (offset < this._width) { + while (isWhite) { + // White run + current = this.nextNBits(10); + entry = TiffFaxDecoder.WHITE[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x0f; + + if (bits === 12) { + // Additional Make up code + // Get the next 2 bits + twoBits = this.nextLesserThan8Bits(2); + // Consolidate the 2 bits and last 2 bits into 4 bits + current = ((current << 2) & 0x000c) | twoBits; + entry = TiffFaxDecoder.ADDITIONAL_MAKEUP[current]; + // 3 bits 0000 0111 + bits = (entry >> 1) & 0x07; + // 12 bits + code = (entry >> 4) & 0x0fff; + // Skip white run + offset += code; + + this.updatePointer(4 - bits); + } else if (bits === 0) { + // ERROR + throw new ImageError('TIFFFaxDecoder0'); + } else if (bits === 15) { + // EOL + throw new ImageError('TIFFFaxDecoder1'); + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >> 5) & 0x07ff; + offset += code; + + this.updatePointer(10 - bits); + if (isT === 0) { + isWhite = false; + this.currChangingElems![this.changingElemSize++] = offset; + } + } + } + + // Check whether this run completed one width, if so + // advance to next byte boundary for compression = 2. + if (offset === this._width) { + if (this.compression === 2) { + this.advancePointer(); + } + break; + } + + while (isWhite === false) { + // Black run + current = this.nextLesserThan8Bits(4); + entry = TiffFaxDecoder.INIT_BLACK[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x000f; + code = (entry >> 5) & 0x07ff; + + if (code === 100) { + current = this.nextNBits(9); + entry = TiffFaxDecoder.BLACK[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x000f; + code = (entry >> 5) & 0x07ff; + + if (bits === 12) { + // Additional makeup codes + this.updatePointer(5); + current = this.nextLesserThan8Bits(4); + entry = TiffFaxDecoder.ADDITIONAL_MAKEUP[current]; + // 3 bits 0000 0111 + bits = (entry >> 1) & 0x07; + // 12 bits + code = (entry >> 4) & 0x0fff; + + this.setToBlack(buffer, lineOffset, offset, code); + offset += code; + + this.updatePointer(4 - bits); + } else if (bits === 15) { + // EOL code + throw new ImageError('TIFFFaxDecoder2'); + } else { + this.setToBlack(buffer, lineOffset, offset, code); + offset += code; + + this.updatePointer(9 - bits); + if (isT === 0) { + isWhite = true; + this.currChangingElems![this.changingElemSize++] = offset; + } + } + } else if (code === 200) { + // Is a Terminating code + current = this.nextLesserThan8Bits(2); + entry = TiffFaxDecoder.TWO_BIT_BLACK[current]; + code = (entry >> 5) & 0x07ff; + bits = (entry >> 1) & 0x0f; + + this.setToBlack(buffer, lineOffset, offset, code); + offset += code; + + this.updatePointer(2 - bits); + isWhite = true; + this.currChangingElems![this.changingElemSize++] = offset; + } else { + // Is a Terminating code + this.setToBlack(buffer, lineOffset, offset, code); + offset += code; + + this.updatePointer(4 - bits); + isWhite = true; + this.currChangingElems![this.changingElemSize++] = offset; + } + } + + // Check whether this run completed one width + if (offset === this._width) { + if (this.compression === 2) { + this.advancePointer(); + } + break; + } + } + + this.currChangingElems![this.changingElemSize++] = offset; + } + + private readEOL(): number { + if (this.fillBits === 0) { + if (this.nextNBits(12) !== 1) { + throw new ImageError('TIFFFaxDecoder6'); + } + } else if (this.fillBits === 1) { + // First EOL code word xxxx 0000 0000 0001 will occur + // As many fill bits will be present as required to make + // the EOL code of 12 bits end on a byte boundary. + const bitsLeft = 8 - this.bitPointer; + + if (this.nextNBits(bitsLeft) !== 0) { + throw new ImageError('TIFFFaxDecoder8'); + } + + // If the number of bitsLeft is less than 8, then to have a 12 + // bit EOL sequence, two more bytes are certainly going to be + // required. The first of them has to be all zeros, so ensure + // that. + if (bitsLeft < 4) { + if (this.nextNBits(8) !== 0) { + throw new ImageError('TIFFFaxDecoder8'); + } + } + + // There might be a random number of fill bytes with 0s, so + // loop till the EOL of 0000 0001 is found, as long as all + // the bytes preceding it are 0's. + let n = 0; + while ((n = this.nextNBits(8)) !== 1) { + // If not all zeros + if (n !== 0) { + throw new ImageError('TIFFFaxDecoder8'); + } + } + } + + // If one dimensional encoding mode, then always return 1 + if (this.oneD === 0) { + return 1; + } else { + // Otherwise for 2D encoding mode, + // The next one bit signifies 1D/2D encoding of next line. + return this.nextLesserThan8Bits(1); + } + } + + private getNextChangingElement( + a0: number | undefined, + isWhite: boolean, + ret: Array + ): void { + // Local copies of instance variables + const pce = this.prevChangingElems; + const ces = this.changingElemSize; + + // If the previous match was at an odd element, we still + // have to search the preceeding element. + // int start = lastChangingElement & ~0x1; + let start = this.lastChangingElement > 0 ? this.lastChangingElement - 1 : 0; + if (isWhite) { + // Search even numbered elements + start &= ~0x1; + } else { + // Search odd numbered elements + start |= 0x1; + } + + let i = start; + for (; i < ces; i += 2) { + const temp = pce![i]!; + if (temp > a0!) { + this.lastChangingElement = i; + ret[0] = temp; + break; + } + } + + if (i + 1 < ces) { + ret[1] = pce![i + 1]; + } + } + + /** + * Returns run length + */ + private decodeWhiteCodeWord(): number { + let current = 0; + let entry = 0; + let bits = 0; + let isT = 0; + let twoBits = 0; + let code = -1; + let runLength = 0; + let isWhite = true; + + while (isWhite) { + current = this.nextNBits(10); + entry = TiffFaxDecoder.WHITE[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x0f; + + if (bits === 12) { + // Additional Make up code + // Get the next 2 bits + twoBits = this.nextLesserThan8Bits(2); + // Consolidate the 2 new bits and last 2 bits into 4 bits + current = ((current << 2) & 0x000c) | twoBits; + entry = TiffFaxDecoder.ADDITIONAL_MAKEUP[current]; + // 3 bits 0000 0111 + bits = (entry >> 1) & 0x07; + // 12 bits + code = (entry >> 4) & 0x0fff; + runLength += code; + this.updatePointer(4 - bits); + } else if (bits === 0) { + // ERROR + throw new ImageError('TIFFFaxDecoder0'); + } else if (bits === 15) { + // EOL + throw new ImageError('TIFFFaxDecoder1'); + } else { + // 11 bits - 0000 0111 1111 1111 = 0x07ff + code = (entry >> 5) & 0x07ff; + runLength += code; + this.updatePointer(10 - bits); + if (isT === 0) { + isWhite = false; + } + } + } + + return runLength; + } + + /** + * Returns run length + */ + private decodeBlackCodeWord() { + let current = 0; + let entry = 0; + let bits = 0; + let isT = 0; + let code = -1; + let runLength = 0; + let isWhite = false; + + while (!isWhite) { + current = this.nextLesserThan8Bits(4); + entry = TiffFaxDecoder.INIT_BLACK[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x000f; + code = (entry >> 5) & 0x07ff; + + if (code === 100) { + current = this.nextNBits(9); + entry = TiffFaxDecoder.BLACK[current]; + + // Get the 3 fields from the entry + isT = entry & 0x0001; + bits = (entry >> 1) & 0x000f; + code = (entry >> 5) & 0x07ff; + + if (bits === 12) { + // Additional makeup codes + this.updatePointer(5); + current = this.nextLesserThan8Bits(4); + entry = TiffFaxDecoder.ADDITIONAL_MAKEUP[current]; + // 3 bits 0000 0111 + bits = (entry >> 1) & 0x07; + // 12 bits + code = (entry >> 4) & 0x0fff; + runLength += code; + + this.updatePointer(4 - bits); + } else if (bits === 15) { + // EOL code + throw new ImageError('TIFFFaxDecoder2'); + } else { + runLength += code; + this.updatePointer(9 - bits); + if (isT === 0) { + isWhite = true; + } + } + } else if (code === 200) { + // Is a Terminating code + current = this.nextLesserThan8Bits(2); + entry = TiffFaxDecoder.TWO_BIT_BLACK[current]; + code = (entry >> 5) & 0x07ff; + runLength += code; + bits = (entry >> 1) & 0x0f; + this.updatePointer(2 - bits); + isWhite = true; + } else { + // Is a Terminating code + runLength += code; + this.updatePointer(4 - bits); + isWhite = true; + } + } + + return runLength; + } + + /** + * One-dimensional decoding methods + */ + public decode1D( + out: InputBuffer, + compData: InputBuffer, + startX: number, + height: number + ): void { + this.data = compData; + this.bitPointer = 0; + this.bytePointer = 0; + + let lineOffset = 0; + const scanlineStride = Math.trunc((this._width + 7) / 8); + + for (let i = 0; i < height; i++) { + this.decodeNextScanline(out, lineOffset, startX); + lineOffset += scanlineStride; + } + } + + /** + * Two-dimensional decoding methods + */ + public decode2D( + out: InputBuffer, + compData: InputBuffer, + startX: number, + height: number, + tiffT4Options: number + ): void { + this.data = compData; + this.compression = 3; + + this.bitPointer = 0; + this.bytePointer = 0; + + const scanlineStride = Math.trunc((this._width + 7) / 8); + + let a0 = 0; + let a1 = 0; + let entry = 0; + let code = 0; + let bits = 0; + let isWhite = false; + let currIndex = 0; + let temp: Array | undefined = undefined; + + const b = new Array(2); + b.fill(0); + + // fillBits - dealt with this in readEOL + // 1D/2D encoding - dealt with this in readEOL + + // uncompressedMode - haven't dealt with this yet. + this.oneD = tiffT4Options & 0x01; + this.uncompressedMode = (tiffT4Options & 0x02) >> 1; + this.fillBits = (tiffT4Options & 0x04) >> 2; + + // The data must start with an EOL code + if (this.readEOL() !== 1) { + throw new ImageError('TIFFFaxDecoder3'); + } + + let lineOffset = 0; + let bitOffset = 0; + + // Then the 1D encoded scanline data will occur, changing elements + // array gets set. + this.decodeNextScanline(out, lineOffset, startX); + lineOffset += scanlineStride; + + for (let lines = 1; lines < height; lines++) { + // Every line must begin with an EOL followed by a bit which + // indicates whether the following scanline is 1D or 2D encoded. + if (this.readEOL() === 0) { + // 2D encoded scanline follows + + // Initialize previous scanlines changing elements, and + // initialize current scanline's changing elements array + temp = this.prevChangingElems; + this.prevChangingElems = this.currChangingElems; + this.currChangingElems = temp; + currIndex = 0; + + // a0 has to be set just before the start of this scanline. + a0 = -1; + isWhite = true; + bitOffset = startX; + + this.lastChangingElement = 0; + + while (bitOffset < this._width) { + // Get the next changing element + this.getNextChangingElement(a0, isWhite, b); + + const b1 = b[0]; + const b2 = b[1]; + + // Get the next seven bits + entry = this.nextLesserThan8Bits(7); + + // Run these through the 2DCodes table + entry = TiffFaxDecoder.TWO_D_CODES[entry] & 0xff; + + // Get the code and the number of bits used up + code = (entry & 0x78) >> 3; + bits = entry & 0x07; + + if (code === 0) { + if (!isWhite) { + this.setToBlack(out, lineOffset, bitOffset, b2 - bitOffset); + } + a0 = b2; + bitOffset = a0; + + // Set pointer to consume the correct number of bits. + this.updatePointer(7 - bits); + } else if (code === 1) { + // Horizontal + this.updatePointer(7 - bits); + + // identify the next 2 codes. + let number = 0; + if (isWhite) { + number = this.decodeWhiteCodeWord(); + bitOffset += number; + this.currChangingElems![currIndex++] = bitOffset; + + number = this.decodeBlackCodeWord(); + this.setToBlack(out, lineOffset, bitOffset, number); + bitOffset += number; + this.currChangingElems![currIndex++] = bitOffset; + } else { + number = this.decodeBlackCodeWord(); + this.setToBlack(out, lineOffset, bitOffset, number); + bitOffset += number; + this.currChangingElems![currIndex++] = bitOffset; + + number = this.decodeWhiteCodeWord(); + bitOffset += number; + this.currChangingElems![currIndex++] = bitOffset; + } + + a0 = bitOffset; + } else if (code <= 8) { + // Vertical + a1 = b1 + (code - 5); + + this.currChangingElems![currIndex++] = a1; + + // We write the current color till a1 - 1 pos, + // since a1 is where the next color starts + if (!isWhite) { + this.setToBlack(out, lineOffset, bitOffset, a1 - bitOffset); + } + a0 = a1; + bitOffset = a0; + isWhite = !isWhite; + + this.updatePointer(7 - bits); + } else { + throw new ImageError('TIFFFaxDecoder4'); + } + } + + // Add the changing element beyond the current scanline for the + // other color too + this.currChangingElems![currIndex++] = bitOffset; + this.changingElemSize = currIndex; + } else { + // 1D encoded scanline follows + this.decodeNextScanline(out, lineOffset, startX); + } + + lineOffset += scanlineStride; + } + } + + public decodeT6( + out: InputBuffer, + compData: InputBuffer, + startX: number, + height: number, + tiffT6Options: number + ): void { + this.data = compData; + this.compression = 4; + + this.bitPointer = 0; + this.bytePointer = 0; + + const scanlineStride = Math.trunc((this._width + 7) / 8); + + let a0 = 0; + let a1 = 0; + let b1 = 0; + let b2 = 0; + let entry = 0; + let code = 0; + let bits = 0; + let isWhite = false; + let currIndex = 0; + let temp: Array | undefined = undefined; + + // Return values from getNextChangingElement + const b = new Array(2); + b.fill(0); + + this.uncompressedMode = (tiffT6Options & 0x02) >> 1; + + // Local cached reference + let cce = this.currChangingElems!; + + // Assume invisible preceding row of all white pixels and insert + // both black and white changing elements beyond the end of this + // imaginary scanline. + this.changingElemSize = 0; + cce[this.changingElemSize++] = this._width; + cce[this.changingElemSize++] = this._width; + + let lineOffset = 0; + let bitOffset = 0; + + for (let lines = 0; lines < height; lines++) { + // a0 has to be set just before the start of the scanline. + a0 = -1; + isWhite = true; + + // Assign the changing elements of the previous scanline to + // prevChangingElems and start putting this new scanline's + // changing elements into the currChangingElems. + temp = this.prevChangingElems; + this.prevChangingElems = this.currChangingElems; + cce = (this.currChangingElems = temp)!; + currIndex = 0; + + // Start decoding the scanline at startX in the raster + bitOffset = startX; + + // Reset search start position for getNextChangingElement + this.lastChangingElement = 0; + + // Till one whole scanline is decoded + while (bitOffset < this._width) { + // Get the next changing element + this.getNextChangingElement(a0, isWhite, b); + b1 = b[0]; + b2 = b[1]; + + // Get the next seven bits + entry = this.nextLesserThan8Bits(7); + // Run these through the 2DCodes table + entry = TiffFaxDecoder.TWO_D_CODES[entry] & 0xff; + + // Get the code and the number of bits used up + code = (entry & 0x78) >> 3; + bits = entry & 0x07; + + if (code === 0) { + // Pass + // We always assume WhiteIsZero format for fax. + if (!isWhite) { + this.setToBlack(out, lineOffset, bitOffset, b2! - bitOffset); + } + a0 = b2; + bitOffset = a0; + + // Set pointer to only consume the correct number of bits. + this.updatePointer(7 - bits); + } else if (code === 1) { + // Horizontal + // Set pointer to only consume the correct number of bits. + this.updatePointer(7 - bits); + + // identify the next 2 alternating color codes. + let number = 0; + if (isWhite) { + // Following are white and black runs + number = this.decodeWhiteCodeWord(); + bitOffset += number; + cce[currIndex++] = bitOffset; + + number = this.decodeBlackCodeWord(); + this.setToBlack(out, lineOffset, bitOffset, number); + bitOffset += number; + cce[currIndex++] = bitOffset; + } else { + // First a black run and then a white run follows + number = this.decodeBlackCodeWord(); + this.setToBlack(out, lineOffset, bitOffset, number); + bitOffset += number; + cce[currIndex++] = bitOffset; + + number = this.decodeWhiteCodeWord(); + bitOffset += number; + cce[currIndex++] = bitOffset; + } + + a0 = bitOffset; + } else if (code <= 8) { + // Vertical + a1 = b1 + (code - 5); + cce[currIndex++] = a1; + + // We write the current color till a1 - 1 pos, + // since a1 is where the next color starts + if (!isWhite) { + this.setToBlack(out, lineOffset, bitOffset, a1 - bitOffset); + } + a0 = a1; + bitOffset = a0; + isWhite = !isWhite; + + this.updatePointer(7 - bits); + } else if (code === 11) { + if (this.nextLesserThan8Bits(3) !== 7) { + throw new ImageError('TIFFFaxDecoder5'); + } + + let zeros = 0; + let exit = false; + + while (!exit) { + while (this.nextLesserThan8Bits(1) !== 1) { + zeros++; + } + + if (zeros > 5) { + // Exit code + + // Zeros before exit code + zeros -= 6; + + if (!isWhite && zeros > 0) { + cce[currIndex++] = bitOffset; + } + + // Zeros before the exit code + bitOffset += zeros; + if (zeros > 0) { + // Some zeros have been written + isWhite = true; + } + + // Read in the bit which specifies the color of + // the following run + if (this.nextLesserThan8Bits(1) === 0) { + if (!isWhite) { + cce[currIndex++] = bitOffset; + } + isWhite = true; + } else { + if (isWhite) { + cce[currIndex++] = bitOffset; + } + isWhite = false; + } + + exit = true; + } + + if (zeros === 5) { + if (!isWhite) { + cce[currIndex++] = bitOffset; + } + bitOffset += zeros; + + // Last thing written was white + isWhite = true; + } else { + bitOffset += zeros; + + cce[currIndex++] = bitOffset; + this.setToBlack(out, lineOffset, bitOffset, 1); + ++bitOffset; + + // Last thing written was black + isWhite = false; + } + } + } else { + throw new ImageError(`TIFFFaxDecoder5 ${code}`); + } + } + + // Add the changing element beyond the current scanline for the + // other color too + cce[currIndex++] = bitOffset; + + // Number of changing elements in this scanline. + this.changingElemSize = currIndex; + + lineOffset += scanlineStride; + } + } +} diff --git a/src/formats/tiff/tiff-image.ts b/src/formats/tiff/tiff-image.ts new file mode 100644 index 0000000..6b165ea --- /dev/null +++ b/src/formats/tiff/tiff-image.ts @@ -0,0 +1,1243 @@ +/** @format */ + +import { inflate } from 'uzip'; +import { BitOperators } from '../../common/bit-operators'; +import { Clamp } from '../../common/clamp'; +import { ColorUtils } from '../../common/color-utils'; +import { MemoryImage } from '../../common/memory-image'; +import { ImageError } from '../../error/image-error'; +import { Half } from '../../hdr/half'; +import { HdrImage } from '../../hdr/hdr-image'; +import { HdrSlice } from '../../hdr/hdr-slice'; +import { JpegDecoder } from '../jpeg-decoder'; +import { InputBuffer } from '../util/input-buffer'; +import { TiffBitReader } from './tiff-bit-reader'; +import { TiffEntry } from './tiff-entry'; +import { TiffFaxDecoder } from './tiff-fax-decoder'; +import { LzwDecoder } from './tiff-lzw-decoder'; + +export class TiffImage { + // Compression types + public static readonly COMPRESSION_NONE = 1; + public static readonly COMPRESSION_CCITT_RLE = 2; + public static readonly COMPRESSION_CCITT_FAX3 = 3; + public static readonly COMPRESSION_CCITT_FAX4 = 4; + public static readonly COMPRESSION_LZW = 5; + public static readonly COMPRESSION_OLD_JPEG = 6; + public static readonly COMPRESSION_JPEG = 7; + public static readonly COMPRESSION_NEXT = 32766; + public static readonly COMPRESSION_CCITT_RLEW = 32771; + public static readonly COMPRESSION_PACKBITS = 32773; + public static readonly COMPRESSION_THUNDERSCAN = 32809; + public static readonly COMPRESSION_IT8CTPAD = 32895; + public static readonly COMPRESSION_IT8LW = 32896; + public static readonly COMPRESSION_IT8MP = 32897; + public static readonly COMPRESSION_IT8BL = 32898; + public static readonly COMPRESSION_PIXARFILM = 32908; + public static readonly COMPRESSION_PIXARLOG = 32909; + public static readonly COMPRESSION_DEFLATE = 32946; + public static readonly COMPRESSION_ZIP = 8; + public static readonly COMPRESSION_DCS = 32947; + public static readonly COMPRESSION_JBIG = 34661; + public static readonly COMPRESSION_SGILOG = 34676; + public static readonly COMPRESSION_SGILOG24 = 34677; + public static readonly COMPRESSION_JP2000 = 34712; + + // Photometric types + public static readonly PHOTOMETRIC_BLACKISZERO = 1; + public static readonly PHOTOMETRIC_RGB = 2; + + // Image types + public static readonly TYPE_UNSUPPORTED = -1; + public static readonly TYPE_BILEVEL = 0; + public static readonly TYPE_GRAY_4BIT = 1; + public static readonly TYPE_GRAY = 2; + public static readonly TYPE_GRAY_ALPHA = 3; + public static readonly TYPE_PALETTE = 4; + public static readonly TYPE_RGB = 5; + public static readonly TYPE_RGB_ALPHA = 6; + public static readonly TYPE_YCBCR_SUB = 7; + public static readonly TYPE_GENERIC = 8; + + // Sample Formats + public static readonly FORMAT_UINT = 1; + public static readonly FORMAT_INT = 2; + public static readonly FORMAT_FLOAT = 3; + + // Tag types + public static readonly TAG_ARTIST = 315; + public static readonly TAG_BITS_PER_SAMPLE = 258; + public static readonly TAG_CELL_LENGTH = 265; + public static readonly TAG_CELL_WIDTH = 264; + public static readonly TAG_COLOR_MAP = 320; + public static readonly TAG_COMPRESSION = 259; + public static readonly TAG_DATE_TIME = 306; + public static readonly TAG_EXIF_IFD = 34665; + public static readonly TAG_EXTRA_SAMPLES = 338; + public static readonly TAG_FILL_ORDER = 266; + public static readonly TAG_FREE_BYTE_COUNTS = 289; + public static readonly TAG_FREE_OFFSETS = 288; + public static readonly TAG_GRAY_RESPONSE_CURVE = 291; + public static readonly TAG_GRAY_RESPONSE_UNIT = 290; + public static readonly TAG_HOST_COMPUTER = 316; + public static readonly TAG_ICC_PROFILE = 34675; + public static readonly TAG_IMAGE_DESCRIPTION = 270; + public static readonly TAG_IMAGE_LENGTH = 257; + public static readonly TAG_IMAGE_WIDTH = 256; + public static readonly TAG_IPTC = 33723; + public static readonly TAG_MAKE = 271; + public static readonly TAG_MAX_SAMPLE_VALUE = 281; + public static readonly TAG_MIN_SAMPLE_VALUE = 280; + public static readonly TAG_MODEL = 272; + public static readonly TAG_NEW_SUBFILE_TYPE = 254; + public static readonly TAG_ORIENTATION = 274; + public static readonly TAG_PHOTOMETRIC_INTERPRETATION = 262; + public static readonly TAG_PHOTOSHOP = 34377; + public static readonly TAG_PLANAR_CONFIGURATION = 284; + public static readonly TAG_PREDICTOR = 317; + public static readonly TAG_RESOLUTION_UNIT = 296; + public static readonly TAG_ROWS_PER_STRIP = 278; + public static readonly TAG_SAMPLES_PER_PIXEL = 277; + public static readonly TAG_SOFTWARE = 305; + public static readonly TAG_STRIP_BYTE_COUNTS = 279; + public static readonly TAG_STRIP_OFFSETS = 273; + public static readonly TAG_SUBFILE_TYPE = 255; + public static readonly TAG_T4_OPTIONS = 292; + public static readonly TAG_T6_OPTIONS = 293; + public static readonly TAG_THRESHOLDING = 263; + public static readonly TAG_TILE_WIDTH = 322; + public static readonly TAG_TILE_LENGTH = 323; + public static readonly TAG_TILE_OFFSETS = 324; + public static readonly TAG_TILE_BYTE_COUNTS = 325; + public static readonly TAG_SAMPLE_FORMAT = 339; + public static readonly TAG_XMP = 700; + public static readonly TAG_X_RESOLUTION = 282; + public static readonly TAG_Y_RESOLUTION = 283; + public static readonly TAG_YCBCR_COEFFICIENTS = 529; + public static readonly TAG_YCBCR_SUBSAMPLING = 530; + public static readonly TAG_YCBCR_POSITIONING = 531; + + public static readonly TAG_NAME: Map = new Map< + number, + string + >([ + [TiffImage.TAG_ARTIST, 'artist'], + [TiffImage.TAG_BITS_PER_SAMPLE, 'bitsPerSample'], + [TiffImage.TAG_CELL_LENGTH, 'cellLength'], + [TiffImage.TAG_CELL_WIDTH, 'cellWidth'], + [TiffImage.TAG_COLOR_MAP, 'colorMap'], + [TiffImage.TAG_COMPRESSION, 'compression'], + [TiffImage.TAG_DATE_TIME, 'dateTime'], + [TiffImage.TAG_EXIF_IFD, 'exifIFD'], + [TiffImage.TAG_EXTRA_SAMPLES, 'extraSamples'], + [TiffImage.TAG_FILL_ORDER, 'fillOrder'], + [TiffImage.TAG_FREE_BYTE_COUNTS, 'freeByteCounts'], + [TiffImage.TAG_FREE_OFFSETS, 'freeOffsets'], + [TiffImage.TAG_GRAY_RESPONSE_CURVE, 'grayResponseCurve'], + [TiffImage.TAG_GRAY_RESPONSE_UNIT, 'grayResponseUnit'], + [TiffImage.TAG_HOST_COMPUTER, 'hostComputer'], + [TiffImage.TAG_ICC_PROFILE, 'iccProfile'], + [TiffImage.TAG_IMAGE_DESCRIPTION, 'imageDescription'], + [TiffImage.TAG_IMAGE_LENGTH, 'imageLength'], + [TiffImage.TAG_IMAGE_WIDTH, 'imageWidth'], + [TiffImage.TAG_IPTC, 'iptc'], + [TiffImage.TAG_MAKE, 'make'], + [TiffImage.TAG_MAX_SAMPLE_VALUE, 'maxSampleValue'], + [TiffImage.TAG_MIN_SAMPLE_VALUE, 'minSampleValue'], + [TiffImage.TAG_MODEL, 'model'], + [TiffImage.TAG_NEW_SUBFILE_TYPE, 'newSubfileType'], + [TiffImage.TAG_ORIENTATION, 'orientation'], + [TiffImage.TAG_PHOTOMETRIC_INTERPRETATION, 'photometricInterpretation'], + [TiffImage.TAG_PHOTOSHOP, 'photoshop'], + [TiffImage.TAG_PLANAR_CONFIGURATION, 'planarConfiguration'], + [TiffImage.TAG_PREDICTOR, 'predictor'], + [TiffImage.TAG_RESOLUTION_UNIT, 'resolutionUnit'], + [TiffImage.TAG_ROWS_PER_STRIP, 'rowsPerStrip'], + [TiffImage.TAG_SAMPLES_PER_PIXEL, 'samplesPerPixel'], + [TiffImage.TAG_SOFTWARE, 'software'], + [TiffImage.TAG_STRIP_BYTE_COUNTS, 'stripByteCounts'], + [TiffImage.TAG_STRIP_OFFSETS, 'stropOffsets'], + [TiffImage.TAG_SUBFILE_TYPE, 'subfileType'], + [TiffImage.TAG_T4_OPTIONS, 't4Options'], + [TiffImage.TAG_T6_OPTIONS, 't6Options'], + [TiffImage.TAG_THRESHOLDING, 'thresholding'], + [TiffImage.TAG_TILE_WIDTH, 'tileWidth'], + [TiffImage.TAG_TILE_LENGTH, 'tileLength'], + [TiffImage.TAG_TILE_OFFSETS, 'tileOffsets'], + [TiffImage.TAG_TILE_BYTE_COUNTS, 'tileByteCounts'], + [TiffImage.TAG_XMP, 'xmp'], + [TiffImage.TAG_X_RESOLUTION, 'xResolution'], + [TiffImage.TAG_Y_RESOLUTION, 'yResolution'], + [TiffImage.TAG_YCBCR_COEFFICIENTS, 'yCbCrCoefficients'], + [TiffImage.TAG_YCBCR_SUBSAMPLING, 'yCbCrSubsampling'], + [TiffImage.TAG_YCBCR_POSITIONING, 'yCbCrPositioning'], + [TiffImage.TAG_SAMPLE_FORMAT, 'sampleFormat'], + ]); + + private readonly _tags: Map = new Map(); + public get tags(): Map { + return this._tags; + } + + private readonly _width: number = 0; + public get width(): number { + return this._width; + } + + private readonly _height: number = 0; + public get height(): number { + return this._height; + } + + private _photometricType: number | undefined; + public get photometricType(): number | undefined { + return this._photometricType; + } + + private _compression = 1; + public get compression(): number { + return this._compression; + } + + private _bitsPerSample = 1; + public get bitsPerSample(): number { + return this._bitsPerSample; + } + + private _samplesPerPixel = 1; + public get samplesPerPixel(): number { + return this._samplesPerPixel; + } + + private _sampleFormat = TiffImage.FORMAT_UINT; + public get sampleFormat(): number { + return this._sampleFormat; + } + + private _imageType = TiffImage.TYPE_UNSUPPORTED; + public get imageType(): number { + return this._imageType; + } + + private _isWhiteZero = false; + public get isWhiteZero(): boolean { + return this._isWhiteZero; + } + + private _predictor = 1; + public get predictor(): number { + return this._predictor; + } + + private _chromaSubH = 0; + public get chromaSubH(): number { + return this._chromaSubH; + } + + private _chromaSubV = 0; + public get chromaSubV(): number { + return this._chromaSubV; + } + + private _tiled = false; + public get tiled(): boolean { + return this._tiled; + } + + private _tileWidth = 0; + public get tileWidth(): number { + return this._tileWidth; + } + + private _tileHeight = 0; + public get tileHeight(): number { + return this._tileHeight; + } + + private _tileOffsets: number[] | undefined; + public get tileOffsets(): number[] | undefined { + return this._tileOffsets; + } + + private _tileByteCounts: number[] | undefined; + public get tileByteCounts(): number[] | undefined { + return this._tileByteCounts; + } + + private _tilesX = 0; + public get tilesX(): number { + return this._tilesX; + } + + private _tilesY = 0; + public get tilesY(): number { + return this._tilesY; + } + + private _tileSize: number | undefined; + public get tileSize(): number | undefined { + return this._tileSize; + } + + private _fillOrder = 1; + public get fillOrder(): number { + return this._fillOrder; + } + + private _t4Options = 0; + public get t4Options(): number { + return this._t4Options; + } + + private _t6Options = 0; + public get t6Options(): number { + return this._t6Options; + } + + private _extraSamples: number | undefined; + public get extraSamples(): number | undefined { + return this._extraSamples; + } + + private _colorMap: number[] | undefined; + public get colorMap(): number[] | undefined { + return this._colorMap; + } + + // Starting index in the [colorMap] for the red channel. + private colorMapRed = 0; + + // Starting index in the [colorMap] for the green channel. + private colorMapGreen = 0; + + // Starting index in the [colorMap] for the blue channel. + private colorMapBlue = 0; + + private image?: MemoryImage; + + private hdrImage?: HdrImage; + + public get isValid(): boolean { + return this._width !== 0 && this._height !== 0; + } + + constructor(p: InputBuffer) { + const p3 = InputBuffer.from(p); + const numDirEntries = p.readUint16(); + for (let i = 0; i < numDirEntries; ++i) { + const tag = p.readUint16(); + const type = p.readUint16(); + const numValues = p.readUint32(); + const entry = new TiffEntry({ + tag: tag, + type: type, + numValues: numValues, + p: p3, + }); + + // The value for the tag is either stored in another location, + // or within the tag itself (if the size fits in 4 bytes). + // We're not reading the data here, just storing offsets. + if (entry.numValues * entry.typeSize > 4) { + entry.valueOffset = p.readUint32(); + } else { + entry.valueOffset = p.offset; + p.offset += 4; + } + + this._tags.set(entry.tag, entry); + + if (entry.tag === TiffImage.TAG_IMAGE_WIDTH) { + this._width = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_IMAGE_LENGTH) { + this._height = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_PHOTOMETRIC_INTERPRETATION) { + this._photometricType = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_COMPRESSION) { + this._compression = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_BITS_PER_SAMPLE) { + this._bitsPerSample = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_SAMPLES_PER_PIXEL) { + this._samplesPerPixel = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_PREDICTOR) { + this._predictor = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_SAMPLE_FORMAT) { + this._sampleFormat = entry.readValue(); + } else if (entry.tag === TiffImage.TAG_COLOR_MAP) { + this._colorMap = entry.readValues(); + this.colorMapRed = 0; + this.colorMapGreen = Math.trunc(this._colorMap.length / 3); + this.colorMapBlue = this.colorMapGreen * 2; + } + } + + if (this._width === 0 || this._height === 0) { + return; + } + + if (this._colorMap !== undefined && this._bitsPerSample === 8) { + for (let i = 0, len = this._colorMap.length; i < len; ++i) { + this._colorMap[i] >>= 8; + } + } + + if (this._photometricType === 0) { + this._isWhiteZero = true; + } + + if (this.hasTag(TiffImage.TAG_TILE_OFFSETS)) { + this._tiled = true; + // Image is in tiled format + this._tileWidth = this.readTag(TiffImage.TAG_TILE_WIDTH); + this._tileHeight = this.readTag(TiffImage.TAG_TILE_LENGTH); + this._tileOffsets = this.readTagList(TiffImage.TAG_TILE_OFFSETS); + this._tileByteCounts = this.readTagList(TiffImage.TAG_TILE_BYTE_COUNTS); + } else { + this._tiled = false; + + this._tileWidth = this.readTag(TiffImage.TAG_TILE_WIDTH, this._width); + if (!this.hasTag(TiffImage.TAG_ROWS_PER_STRIP)) { + this._tileHeight = this.readTag( + TiffImage.TAG_TILE_LENGTH, + this._height + ); + } else { + const l = this.readTag(TiffImage.TAG_ROWS_PER_STRIP); + let infinity = 1; + infinity = (infinity << 32) - 1; + if (l === infinity) { + // 2^32 - 1 (effectively infinity, entire image is 1 strip) + this._tileHeight = this._height; + } else { + this._tileHeight = l; + } + } + + this._tileOffsets = this.readTagList(TiffImage.TAG_STRIP_OFFSETS); + this._tileByteCounts = this.readTagList(TiffImage.TAG_STRIP_BYTE_COUNTS); + } + + // Calculate number of tiles and the tileSize in bytes + this._tilesX = Math.trunc( + (this._width + this._tileWidth - 1) / this._tileWidth + ); + this._tilesY = Math.trunc( + (this._height + this._tileHeight - 1) / this._tileHeight + ); + this._tileSize = this._tileWidth * this._tileHeight * this._samplesPerPixel; + + this._fillOrder = this.readTag(TiffImage.TAG_FILL_ORDER, 1); + this._t4Options = this.readTag(TiffImage.TAG_T4_OPTIONS); + this._t6Options = this.readTag(TiffImage.TAG_T6_OPTIONS); + this._extraSamples = this.readTag(TiffImage.TAG_EXTRA_SAMPLES); + + // Determine which kind of image we are dealing with. + switch (this._photometricType) { + // WhiteIsZero + case 0: + // BlackIsZero + // falls through + case 1: + if (this._bitsPerSample === 1 && this._samplesPerPixel === 1) { + this._imageType = TiffImage.TYPE_BILEVEL; + } else if (this._bitsPerSample === 4 && this._samplesPerPixel === 1) { + this._imageType = TiffImage.TYPE_GRAY_4BIT; + } else if (this._bitsPerSample % 8 === 0) { + if (this._samplesPerPixel === 1) { + this._imageType = TiffImage.TYPE_GRAY; + } else if (this._samplesPerPixel === 2) { + this._imageType = TiffImage.TYPE_GRAY_ALPHA; + } else { + this._imageType = TiffImage.TYPE_GENERIC; + } + } + break; + // RGB + case 2: + if (this._bitsPerSample % 8 === 0) { + if (this._samplesPerPixel === 3) { + this._imageType = TiffImage.TYPE_RGB; + } else if (this._samplesPerPixel === 4) { + this._imageType = TiffImage.TYPE_RGB_ALPHA; + } else { + this._imageType = TiffImage.TYPE_GENERIC; + } + } + break; + // RGB Palette + case 3: + if ( + this._samplesPerPixel === 1 && + (this._bitsPerSample === 4 || + this._bitsPerSample === 8 || + this._bitsPerSample === 16) + ) { + this._imageType = TiffImage.TYPE_PALETTE; + } + break; + // Transparency mask + case 4: + if (this._bitsPerSample === 1 && this._samplesPerPixel === 1) { + this._imageType = TiffImage.TYPE_BILEVEL; + } + break; + // YCbCr + case 6: + if ( + this._compression === TiffImage.COMPRESSION_JPEG && + this._bitsPerSample === 8 && + this._samplesPerPixel === 3 + ) { + this._imageType = TiffImage.TYPE_RGB; + } else { + if (this.hasTag(TiffImage.TAG_YCBCR_SUBSAMPLING)) { + const v = this._tags + .get(TiffImage.TAG_YCBCR_SUBSAMPLING)! + .readValues(); + this._chromaSubH = v[0]; + this._chromaSubV = v[1]; + } else { + this._chromaSubH = 2; + this._chromaSubV = 2; + } + + if (this._chromaSubH * this._chromaSubV === 1) { + this._imageType = TiffImage.TYPE_GENERIC; + } else if (this._bitsPerSample === 8 && this._samplesPerPixel === 3) { + this._imageType = TiffImage.TYPE_YCBCR_SUB; + } + } + break; + // Other including CMYK, CIE L*a*b*, unknown. + default: + if (this._bitsPerSample % 8 === 0) { + this._imageType = TiffImage.TYPE_GENERIC; + } + break; + } + } + + private readTag(type: number, defaultValue = 0): number { + if (!this.hasTag(type)) { + return defaultValue; + } + return this._tags.get(type)!.readValue(); + } + + private readTagList(type: number): number[] | undefined { + if (!this.hasTag(type)) { + return undefined; + } + return this._tags.get(type)!.readValues(); + } + + private decodeBilevelTile( + p: InputBuffer, + tileX: number, + tileY: number + ): void { + const tileIndex = tileY * this._tilesX + tileX; + p.offset = this._tileOffsets![tileIndex]; + + const outX = tileX * this._tileWidth; + const outY = tileY * this._tileHeight; + + const byteCount = this._tileByteCounts![tileIndex]; + + let bdata: InputBuffer | undefined = undefined; + if (this._compression === TiffImage.COMPRESSION_PACKBITS) { + // Since the decompressed data will still be packed + // 8 pixels into 1 byte, calculate bytesInThisTile + let bytesInThisTile = 0; + if (this._tileWidth % 8 === 0) { + bytesInThisTile = Math.trunc(this._tileWidth / 8) * this._tileHeight; + } else { + bytesInThisTile = + (Math.trunc(this._tileWidth / 8) + 1) * this._tileHeight; + } + bdata = new InputBuffer({ + buffer: new Uint8Array(this._tileWidth * this._tileHeight), + }); + this.decodePackbits(p, bytesInThisTile, bdata.buffer); + } else if (this._compression === TiffImage.COMPRESSION_LZW) { + bdata = new InputBuffer({ + buffer: new Uint8Array(this._tileWidth * this._tileHeight), + }); + + const decoder = new LzwDecoder(); + decoder.decode(InputBuffer.from(p, 0, byteCount), bdata.buffer); + + // Horizontal Differencing Predictor + if (this._predictor === 2) { + let count = 0; + for (let j = 0; j < this._height; j++) { + count = this._samplesPerPixel * (j * this._width + 1); + for ( + let i = this._samplesPerPixel; + i < this._width * this._samplesPerPixel; + i++ + ) { + const b = + bdata.getByte(count) + + bdata.getByte(count - this._samplesPerPixel); + bdata.setByte(count, b); + count++; + } + } + } + } else if (this._compression === TiffImage.COMPRESSION_CCITT_RLE) { + bdata = new InputBuffer({ + buffer: new Uint8Array(this._tileWidth * this._tileHeight), + }); + try { + const decoder = new TiffFaxDecoder({ + fillOrder: this._fillOrder, + width: this._tileWidth, + height: this._tileHeight, + }); + decoder.decode1D(bdata, p, 0, this._tileHeight); + } catch (_) { + // skip + } + } else if (this._compression === TiffImage.COMPRESSION_CCITT_FAX3) { + bdata = new InputBuffer({ + buffer: new Uint8Array(this._tileWidth * this._tileHeight), + }); + try { + const decoder = new TiffFaxDecoder({ + fillOrder: this._fillOrder, + width: this._tileWidth, + height: this._tileHeight, + }); + decoder.decode2D(bdata, p, 0, this._tileHeight, this._t4Options); + } catch (_) { + // skip + } + } else if (this._compression === TiffImage.COMPRESSION_CCITT_FAX4) { + bdata = new InputBuffer({ + buffer: new Uint8Array(this._tileWidth * this._tileHeight), + }); + try { + const decoder = new TiffFaxDecoder({ + fillOrder: this._fillOrder, + width: this._tileWidth, + height: this._tileHeight, + }); + decoder.decodeT6(bdata, p, 0, this._tileHeight, this._t6Options); + } catch (_) { + // skip + } + } else if (this._compression === TiffImage.COMPRESSION_ZIP) { + const data = p.toUint8Array(0, byteCount); + const outData = inflate(data); + bdata = new InputBuffer({ + buffer: outData, + }); + } else if (this._compression === TiffImage.COMPRESSION_DEFLATE) { + const data = p.toUint8Array(0, byteCount); + const outData = inflate(data); + bdata = new InputBuffer({ + buffer: outData, + }); + } else if (this._compression === TiffImage.COMPRESSION_NONE) { + bdata = p; + } else { + throw new ImageError( + `Unsupported Compression Type: ${this._compression}` + ); + } + + const br = new TiffBitReader(bdata); + const white = this._isWhiteZero ? 0xff000000 : 0xffffffff; + const black = this._isWhiteZero ? 0xffffffff : 0xff000000; + + const img = this.image!; + for (let y = 0, py = outY; y < this._tileHeight; ++y, ++py) { + for (let x = 0, px = outX; x < this._tileWidth; ++x, ++px) { + if (py >= img.height || px >= img.width) break; + if (br.readBits(1) === 0) { + img.setPixel(px, py, black); + } else { + img.setPixel(px, py, white); + } + } + br.flushByte(); + } + } + + private decodeTile(p: InputBuffer, tileX: number, tileY: number): void { + // Read the data, uncompressing as needed. There are four cases: + // bilevel, palette-RGB, 4-bit grayscale, and everything else. + if (this._imageType === TiffImage.TYPE_BILEVEL) { + this.decodeBilevelTile(p, tileX, tileY); + return; + } + + const tileIndex = tileY * this._tilesX + tileX; + p.offset = this._tileOffsets![tileIndex]; + + const outX = tileX * this._tileWidth; + const outY = tileY * this._tileHeight; + + const byteCount = this._tileByteCounts![tileIndex]; + let bytesInThisTile = + this._tileWidth * this._tileHeight * this._samplesPerPixel; + if (this._bitsPerSample === 16) { + bytesInThisTile *= 2; + } else if (this._bitsPerSample === 32) { + bytesInThisTile *= 4; + } + + let bdata: InputBuffer | undefined = undefined; + if ( + this._bitsPerSample === 8 || + this._bitsPerSample === 16 || + this._bitsPerSample === 32 || + this._bitsPerSample === 64 + ) { + if (this._compression === TiffImage.COMPRESSION_NONE) { + bdata = p; + } else if (this._compression === TiffImage.COMPRESSION_LZW) { + bdata = new InputBuffer({ + buffer: new Uint8Array(bytesInThisTile), + }); + const decoder = new LzwDecoder(); + try { + decoder.decode(InputBuffer.from(p, 0, byteCount), bdata.buffer); + } catch (e) { + console.error(e); + } + // Horizontal Differencing Predictor + if (this._predictor === 2) { + let count = 0; + for (let j = 0; j < this._tileHeight; j++) { + count = this._samplesPerPixel * (j * this._tileWidth + 1); + for ( + let i = this._samplesPerPixel, + len = this._tileWidth * this._samplesPerPixel; + i < len; + i++ + ) { + const b = + bdata.getByte(count) + + bdata.getByte(count - this._samplesPerPixel); + bdata.setByte(count, b); + count++; + } + } + } + } else if (this._compression === TiffImage.COMPRESSION_PACKBITS) { + bdata = new InputBuffer({ + buffer: new Uint8Array(bytesInThisTile), + }); + this.decodePackbits(p, bytesInThisTile, bdata.buffer); + } else if (this._compression === TiffImage.COMPRESSION_DEFLATE) { + const data = p.toUint8Array(0, byteCount); + const outData = inflate(data); + bdata = new InputBuffer({ + buffer: outData, + }); + } else if (this._compression === TiffImage.COMPRESSION_ZIP) { + const data = p.toUint8Array(0, byteCount); + const outData = inflate(data); + bdata = new InputBuffer({ + buffer: outData, + }); + } else if (this._compression === TiffImage.COMPRESSION_OLD_JPEG) { + this.image ??= new MemoryImage({ + width: this._width, + height: this._height, + }); + const data = p.toUint8Array(0, byteCount); + const tile = new JpegDecoder().decodeImage(data); + if (tile !== undefined) { + this.jpegToImage( + tile, + this.image, + outX, + outY, + this._tileWidth, + this._tileHeight + ); + } + if (this.hdrImage !== undefined) { + this.hdrImage = HdrImage.fromImage(this.image); + } + return; + } else { + throw new ImageError( + `Unsupported Compression Type: ${this._compression}` + ); + } + + for ( + let y = 0, py = outY; + y < this._tileHeight && py < this._height; + ++y, ++py + ) { + for ( + let x = 0, px = outX; + x < this._tileWidth && px < this._width; + ++x, ++px + ) { + if (this._samplesPerPixel === 1) { + if (this._sampleFormat === TiffImage.FORMAT_FLOAT) { + let sample = 0.0; + if (this._bitsPerSample === 32) { + sample = bdata.readFloat32(); + } else if (this._bitsPerSample === 64) { + sample = bdata.readFloat64(); + } else if (this._bitsPerSample === 16) { + sample = Half.halfToDouble(bdata.readUint16()); + } + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, sample); + } + if (this.image !== undefined) { + const gray = Clamp.clampInt255(sample * 255); + let c = 0; + if ( + this._photometricType === 3 && + this._colorMap !== undefined + ) { + c = ColorUtils.getColor( + this._colorMap[this.colorMapRed + gray], + this._colorMap[this.colorMapGreen + gray], + this._colorMap[this.colorMapBlue + gray] + ); + } else { + c = ColorUtils.getColor(gray, gray, gray); + } + this.image.setPixel(px, py, c); + } + } else { + let gray = 0; + if (this._bitsPerSample === 8) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + } else if (this._bitsPerSample === 16) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + } else if (this._bitsPerSample === 32) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + } + + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, gray); + } + + if (this.image !== undefined) { + gray = + this._bitsPerSample === 16 + ? gray >> 8 + : this._bitsPerSample === 32 + ? gray >> 24 + : gray; + if (this._photometricType === 0) { + gray = 255 - gray; + } + + let c = 0; + if ( + this._photometricType === 3 && + this._colorMap !== undefined + ) { + c = ColorUtils.getColor( + this._colorMap[this.colorMapRed + gray], + this._colorMap[this.colorMapGreen + gray], + this._colorMap[this.colorMapBlue + gray] + ); + } else { + c = ColorUtils.getColor(gray, gray, gray); + } + + this.image.setPixel(px, py, c); + } + } + } else if (this._samplesPerPixel === 2) { + let gray = 0; + let alpha = 0; + if (this._bitsPerSample === 8) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + alpha = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + } else if (this._bitsPerSample === 16) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + alpha = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + } else if (this._bitsPerSample === 32) { + gray = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + alpha = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + } + + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, gray); + this.hdrImage.setGreen(px, py, alpha); + } + + if (this.image !== undefined) { + gray = + this._bitsPerSample === 16 + ? gray >> 8 + : this._bitsPerSample === 32 + ? gray >> 24 + : gray; + alpha = + this._bitsPerSample === 16 + ? alpha >> 8 + : this._bitsPerSample === 32 + ? alpha >> 24 + : alpha; + const c = ColorUtils.getColor(gray, gray, gray, alpha); + this.image.setPixel(px, py, c); + } + } else if (this._samplesPerPixel === 3) { + if (this._sampleFormat === TiffImage.FORMAT_FLOAT) { + let r = 0.0; + let g = 0.0; + let b = 0.0; + if (this._bitsPerSample === 32) { + r = bdata.readFloat32(); + g = bdata.readFloat32(); + b = bdata.readFloat32(); + } else if (this._bitsPerSample === 64) { + r = bdata.readFloat64(); + g = bdata.readFloat64(); + b = bdata.readFloat64(); + } else if (this._bitsPerSample === 16) { + r = Half.halfToDouble(bdata.readUint16()); + g = Half.halfToDouble(bdata.readUint16()); + b = Half.halfToDouble(bdata.readUint16()); + } + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, r); + this.hdrImage.setGreen(px, py, g); + this.hdrImage.setBlue(px, py, b); + } + if (this.image !== undefined) { + const ri = Clamp.clampInt255(r * 255); + const gi = Clamp.clampInt255(g * 255); + const bi = Clamp.clampInt255(b * 255); + const c = ColorUtils.getColor(ri, gi, bi); + this.image.setPixel(px, py, c); + } + } else { + let r = 0; + let g = 0; + let b = 0; + if (this._bitsPerSample === 8) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + } else if (this._bitsPerSample === 16) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + } else if (this._bitsPerSample === 32) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + } + + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, r); + this.hdrImage.setGreen(px, py, g); + this.hdrImage.setBlue(px, py, b); + } + + if (this.image !== undefined) { + r = + this._bitsPerSample === 16 + ? r >> 8 + : this._bitsPerSample === 32 + ? r >> 24 + : r; + g = + this._bitsPerSample === 16 + ? g >> 8 + : this._bitsPerSample === 32 + ? g >> 24 + : g; + b = + this._bitsPerSample === 16 + ? b >> 8 + : this._bitsPerSample === 32 + ? b >> 24 + : b; + const c = ColorUtils.getColor(r, g, b); + this.image.setPixel(px, py, c); + } + } + } else if (this._samplesPerPixel >= 4) { + if (this._sampleFormat === TiffImage.FORMAT_FLOAT) { + let r = 0.0; + let g = 0.0; + let b = 0.0; + let a = 0.0; + if (this._bitsPerSample === 32) { + r = bdata.readFloat32(); + g = bdata.readFloat32(); + b = bdata.readFloat32(); + a = bdata.readFloat32(); + } else if (this._bitsPerSample === 64) { + r = bdata.readFloat64(); + g = bdata.readFloat64(); + b = bdata.readFloat64(); + a = bdata.readFloat64(); + } else if (this._bitsPerSample === 16) { + r = Half.halfToDouble(bdata.readUint16()); + g = Half.halfToDouble(bdata.readUint16()); + b = Half.halfToDouble(bdata.readUint16()); + a = Half.halfToDouble(bdata.readUint16()); + } + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, r); + this.hdrImage.setGreen(px, py, g); + this.hdrImage.setBlue(px, py, b); + this.hdrImage.setAlpha(px, py, a); + } + if (this.image !== undefined) { + const ri = Clamp.clampInt255(r * 255); + const gi = Clamp.clampInt255(g * 255); + const bi = Clamp.clampInt255(b * 255); + const ai = Clamp.clampInt255(a * 255); + const c = ColorUtils.getColor(ri, gi, bi, ai); + this.image.setPixel(px, py, c); + } + } else { + let r = 0; + let g = 0; + let b = 0; + let a = 0; + if (this._bitsPerSample === 8) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + a = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt8() + : bdata.readByte(); + } else if (this._bitsPerSample === 16) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + a = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt16() + : bdata.readUint16(); + } else if (this._bitsPerSample === 32) { + r = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + g = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + b = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + a = + this._sampleFormat === TiffImage.FORMAT_INT + ? bdata.readInt32() + : bdata.readUint32(); + } + + if (this.hdrImage !== undefined) { + this.hdrImage.setRed(px, py, r); + this.hdrImage.setGreen(px, py, g); + this.hdrImage.setBlue(px, py, b); + this.hdrImage.setAlpha(px, py, a); + } + + if (this.image !== undefined) { + r = + this._bitsPerSample === 16 + ? r >> 8 + : this._bitsPerSample === 32 + ? r >> 24 + : r; + g = + this._bitsPerSample === 16 + ? g >> 8 + : this._bitsPerSample === 32 + ? g >> 24 + : g; + b = + this._bitsPerSample === 16 + ? b >> 8 + : this._bitsPerSample === 32 + ? b >> 24 + : b; + a = + this._bitsPerSample === 16 + ? a >> 8 + : this._bitsPerSample === 32 + ? a >> 24 + : a; + const c = ColorUtils.getColor(r, g, b, a); + this.image.setPixel(px, py, c); + } + } + } + } + } + } else { + throw new ImageError(`Unsupported bitsPerSample: ${this._bitsPerSample}`); + } + } + + private jpegToImage( + tile: MemoryImage, + image: MemoryImage, + outX: number, + outY: number, + tileWidth: number, + tileHeight: number + ): void { + const width = tileWidth; + const height = tileHeight; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + image.setPixel(x + outX, y + outY, tile.getPixel(x, y)); + } + } + } + + /** + * Uncompress packbits compressed image data. + */ + private decodePackbits( + data: InputBuffer, + arraySize: number, + dst: Uint8Array + ): void { + let srcCount = 0; + let dstCount = 0; + + while (dstCount < arraySize) { + const b = BitOperators.toInt8(data.getByte(srcCount++)); + if (b >= 0 && b <= 127) { + // literal run packet + for (let i = 0; i < b + 1; ++i) { + dst[dstCount++] = data.getByte(srcCount++); + } + } else if (b <= -1 && b >= -127) { + // 2 byte encoded run packet + const repeat = data.getByte(srcCount++); + for (let i = 0; i < -b + 1; ++i) { + dst[dstCount++] = repeat; + } + } else { + // no-op packet. Do nothing + srcCount++; + } + } + } + + public decode(p: InputBuffer): MemoryImage { + this.image = new MemoryImage({ + width: this._width, + height: this._height, + }); + for (let tileY = 0, ti = 0; tileY < this._tilesY; ++tileY) { + for (let tileX = 0; tileX < this._tilesX; ++tileX, ++ti) { + this.decodeTile(p, tileX, tileY); + } + } + return this.image; + } + + public decodeHdr(p: InputBuffer): HdrImage { + this.hdrImage = HdrImage.create( + this._width, + this._height, + this._samplesPerPixel, + this._sampleFormat === TiffImage.FORMAT_UINT + ? HdrSlice.UINT + : this._sampleFormat === TiffImage.FORMAT_INT + ? HdrSlice.INT + : HdrSlice.FLOAT, + this._bitsPerSample + ); + for (let tileY = 0, ti = 0; tileY < this._tilesY; ++tileY) { + for (let tileX = 0; tileX < this._tilesX; ++tileX, ++ti) { + this.decodeTile(p, tileX, tileY); + } + } + return this.hdrImage; + } + + public hasTag(tag: number): boolean { + return this._tags.has(tag); + } +} diff --git a/src/formats/tiff/tiff-info.ts b/src/formats/tiff/tiff-info.ts new file mode 100644 index 0000000..543d268 --- /dev/null +++ b/src/formats/tiff/tiff-info.ts @@ -0,0 +1,63 @@ +/** @format */ + +import { DecodeInfo } from '../decode-info'; +import { TiffImage } from './tiff-image'; + +export interface TiffInfoInitOptions { + bigEndian: boolean; + signature: number; + ifdOffset: number; + images: TiffImage[]; +} + +export class TiffInfo implements DecodeInfo { + private _bigEndian: boolean; + public get bigEndian(): boolean { + return this._bigEndian; + } + + private _signature: number; + public get signature(): number { + return this._signature; + } + + private _ifdOffset: number; + public get ifdOffset(): number { + return this._ifdOffset; + } + + private _images: TiffImage[] = []; + public get images(): TiffImage[] { + return this._images; + } + + private _width = 0; + public get width(): number { + return this._width; + } + + private _height = 0; + public get height(): number { + return this._height; + } + + private _backgroundColor = 0xffffffff; + get backgroundColor(): number { + throw this._backgroundColor; + } + + public get numFrames(): number { + return this._images.length; + } + + constructor(options: TiffInfoInitOptions) { + this._bigEndian = options.bigEndian; + this._signature = options.signature; + this._ifdOffset = options.ifdOffset; + this._images = options.images; + if (this._images.length > 0) { + this._width = this._images[0].width; + this._height = this._images[0].height; + } + } +} diff --git a/src/formats/tiff/tiff-lzw-decoder.ts b/src/formats/tiff/tiff-lzw-decoder.ts new file mode 100644 index 0000000..8a23985 --- /dev/null +++ b/src/formats/tiff/tiff-lzw-decoder.ts @@ -0,0 +1,147 @@ +/** @format */ + +import { ImageError } from '../../error/image-error'; +import { InputBuffer } from '../util/input-buffer'; + +export class LzwDecoder { + private static readonly LZ_MAX_CODE = 4095; + private static readonly NO_SUCH_CODE = 4098; + private static readonly AND_TABLE: number[] = [511, 1023, 2047, 4095]; + + private readonly buffer = new Uint8Array(4096); + + private bitsToGet = 9; + private bytePointer = 0; + private nextData = 0; + private nextBits = 0; + private data!: Uint8Array; + private dataLength!: number; + private out!: Uint8Array; + private outPointer!: number; + private table!: Uint8Array; + private prefix!: Uint32Array; + private tableIndex?: number; + private bufferLength!: number; + + private addString(string: number, newString: number): void { + this.table[this.tableIndex!] = newString; + this.prefix[this.tableIndex!] = string; + this.tableIndex = this.tableIndex! + 1; + + if (this.tableIndex === 511) { + this.bitsToGet = 10; + } else if (this.tableIndex === 1023) { + this.bitsToGet = 11; + } else if (this.tableIndex === 2047) { + this.bitsToGet = 12; + } + } + + private getString(code: number): void { + this.bufferLength = 0; + let c = code; + this.buffer[this.bufferLength++] = this.table[c]; + c = this.prefix[c]; + while (c !== LzwDecoder.NO_SUCH_CODE) { + this.buffer[this.bufferLength++] = this.table[c]; + c = this.prefix[c]; + } + } + + /** + * Returns the next 9, 10, 11 or 12 bits + */ + private getNextCode(): number { + if (this.bytePointer >= this.dataLength) { + return 257; + } + + while (this.nextBits < this.bitsToGet) { + if (this.bytePointer >= this.dataLength) { + return 257; + } + this.nextData = + ((this.nextData << 8) + this.data[this.bytePointer++]) & 0xffffffff; + this.nextBits += 8; + } + + this.nextBits -= this.bitsToGet; + const code = + (this.nextData >> this.nextBits) & + LzwDecoder.AND_TABLE[this.bitsToGet - 9]; + + return code; + } + + /** + * Initialize the string table. + */ + private initializeStringTable(): void { + this.table = new Uint8Array(LzwDecoder.LZ_MAX_CODE + 1); + this.prefix = new Uint32Array(LzwDecoder.LZ_MAX_CODE + 1); + this.prefix.fill(LzwDecoder.NO_SUCH_CODE, 0, this.prefix.length); + + for (let i = 0; i < 256; i++) { + this.table[i] = i; + } + + this.bitsToGet = 9; + + this.tableIndex = 258; + } + + public decode(p: InputBuffer, out: Uint8Array): void { + this.out = out; + const outLen = out.length; + this.outPointer = 0; + this.data = p.buffer; + this.dataLength = this.data.length; + this.bytePointer = p.offset; + + if (this.data[0] === 0x00 && this.data[1] === 0x01) { + throw new ImageError('Invalid LZW Data'); + } + + this.initializeStringTable(); + + this.nextData = 0; + this.nextBits = 0; + + let oldCode = 0; + + let code = this.getNextCode(); + while (code !== 257 && this.outPointer < outLen) { + if (code === 256) { + this.initializeStringTable(); + code = this.getNextCode(); + this.bufferLength = 0; + if (code === 257) { + break; + } + + this.out[this.outPointer++] = code; + oldCode = code; + } else { + if (code < this.tableIndex!) { + this.getString(code); + for (let i = this.bufferLength - 1; i >= 0; --i) { + this.out[this.outPointer++] = this.buffer[i]; + } + this.addString(oldCode, this.buffer[this.bufferLength - 1]); + oldCode = code; + } else { + this.getString(oldCode); + for (let i = this.bufferLength - 1; i >= 0; --i) { + this.out[this.outPointer++] = this.buffer[i]; + } + this.out[this.outPointer++] = this.buffer[this.bufferLength - 1]; + this.addString(oldCode, this.buffer[this.bufferLength - 1]); + + oldCode = code; + } + } + + code = this.getNextCode(); + } + } +} diff --git a/src/index.ts b/src/index.ts index b4a2b57..dd31703 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,8 @@ import { PngDecoder } from './formats/png-decoder'; import { PngEncoder } from './formats/png-encoder'; import { TgaDecoder } from './formats/tga-decoder'; import { TgaEncoder } from './formats/tga-encoder'; +import { TiffDecoder } from './formats/tiff-decoder'; +import { TiffEncoder } from './formats/tiff-encoder'; // Export types from 'common' directory export { BitOperators } from './common/bit-operators'; @@ -84,6 +86,11 @@ export function findDecoderForData(data: TypedArray): Decoder | undefined { return gif; } + const tiff = new TiffDecoder(); + if (tiff.isValidFile(bytes)) { + return tiff; + } + const bmp = new BmpDecoder(); if (bmp.isValidFile(bytes)) { return bmp; @@ -148,6 +155,9 @@ export function getDecoderForNamedImage(name: string): Decoder | undefined { if (n.endsWith('.gif')) { return new GifDecoder(); } + if (n.endsWith('.tif') || n.endsWith('.tiff')) { + return new TiffDecoder(); + } if (n.endsWith('.bmp')) { return new BmpDecoder(); } @@ -355,6 +365,33 @@ export function encodeGifAnimation( }).encodeAnimation(animation); } +/** + * Decode a TIFF formatted image. + */ +export function decodeTiff(data: TypedArray): MemoryImage | undefined { + const dataUint8 = new Uint8Array(data); + return new TiffDecoder().decodeImage(dataUint8); +} + +/** + * Decode an multi-image (animated) TIFF file. If the tiff doesn't have + * multiple images, the animation will contain a single frame with the tiff's + * image. + */ +export function decodeTiffAnimation( + data: TypedArray +): FrameAnimation | undefined { + const dataUint8 = new Uint8Array(data); + return new TiffDecoder().decodeAnimation(dataUint8); +} + +/** + * Encode an image to the TIFF format. + */ +export function encodeTiff(image: MemoryImage): Uint8Array { + return new TiffEncoder().encodeImage(image); +} + /** * Decode a BMP formatted image. */