Skip to content

Commit

Permalink
feat: Added support for encoding and decoding TIFF format
Browse files Browse the repository at this point in the history
  • Loading branch information
yegor-pelykh committed Oct 29, 2022
1 parent 6fc0235 commit 6c1b408
Show file tree
Hide file tree
Showing 9 changed files with 3,665 additions and 0 deletions.
191 changes: 191 additions & 0 deletions src/formats/tiff-decoder.ts
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);
}
}
190 changes: 190 additions & 0 deletions src/formats/tiff-encoder.ts
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();
}
}
Loading

0 comments on commit 6c1b408

Please sign in to comment.