-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Added support for encoding and decoding TIFF format
- Loading branch information
1 parent
6fc0235
commit 6c1b408
Showing
9 changed files
with
3,665 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} |
Oops, something went wrong.