Skip to content

Commit

Permalink
feat: add support for alpha channel and compressed 16-bit images
Browse files Browse the repository at this point in the history
  • Loading branch information
targos committed Aug 21, 2020
1 parent 3e0a826 commit 5f2e612
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 76 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ recent web browsers and Node.js. You can transpile it with a tool like
### [TIFF standard](./TIFF6.pdf)

The library can currently decode greyscale and RGB images (8, 16 or 32 bits).
It does not support any compression algorithm yet.
It supports LZW compression and images with an additional alpha channel.

## API

Expand All @@ -47,6 +47,7 @@ The `data` property is a Typed Array containing the pixel data. It is a
- `width` - number of columns
- `height` - number of rows
- `bitsPerSample` - bit depth
- `alpha` - `true` if the image has an additional alpha channel
- `xResolution`
- `yResolution`
- `resolutionUnit`
Expand Down
Binary file added img/color-5x5-lzw.tif
Binary file not shown.
Binary file added img/color-alpha-2x2.tif
Binary file not shown.
Binary file added img/color-alpha-5x5-lzw.tif
Binary file not shown.
Binary file added img/color-alpha-5x5.tif
Binary file not shown.
124 changes: 98 additions & 26 deletions src/__tests__/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface TiffFile {
height: number;
bitsPerSample: number;
components: number;
alpha?: boolean;
}

const files: TiffFile[] = [
Expand All @@ -32,14 +33,14 @@ const files: TiffFile[] = [
bitsPerSample: 8,
components: 3,
},
// TODO: implement alpha channel support.
// {
// name: 'color8-alpha.tif',
// width: 800,
// height: 600,
// bitsPerSample: 8,
// components: 4,
// },
{
name: 'color8-alpha.tif',
width: 800,
height: 600,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color16.tif',
width: 160,
Expand Down Expand Up @@ -83,38 +84,109 @@ const files: TiffFile[] = [
bitsPerSample: 16,
components: 1,
},
{
name: 'color-5x5.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 3,
},
{
name: 'color-5x5-lzw.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 3,
},
{
name: 'color-alpha-2x2.tif',
width: 2,
height: 2,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color-alpha-5x5.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 4,
alpha: true,
},
{
name: 'color-alpha-5x5-lzw.tif',
width: 5,
height: 5,
bitsPerSample: 8,
components: 4,
alpha: true,
},
];
const cases = files.map((file) => [file, readImage(file.name)] as const);
const cases = files.map(
(file) => [file.name, file, readImage(file.name)] as const,
);

const stack = readImage('stack.tif');

test.each(cases)('should decode %s', (file, image) => {
test.each(cases)('should decode %s', (name, file, image) => {
const result = decode(image);
expect(result).toHaveLength(1);
const { data, bitsPerSample, width, height, components } = result[0];
const { data, bitsPerSample, width, height, components, alpha } = result[0];
expect(width).toBe(file.width);
expect(height).toBe(file.height);
expect(components).toBe(file.components);
expect(bitsPerSample).toBe(file.bitsPerSample);
expect(data).toHaveLength(file.width * file.height * file.components);
expect(alpha).toBe(file.alpha ? true : false);
});

test('should decode RGB 8bit', () => {
// prettier-ignore
const expectedRgb8BitData = Uint8Array.from([
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128, 128, 128, 128,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 255, 255, 255, 0, 255, 255, 255, 0, 128, 128, 128, 128, 128, 128,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0,
]);

test('should decode RGB 8bit data', () => {
const [result] = decode(readImage('color-5x5.tif'));
expect(result.width).toBe(5);
expect(result.height).toBe(5);
expect(result.bitsPerSample).toBe(8);
expect(result.components).toBe(3);
expect(result.data).toStrictEqual(
// prettier-ignore
Uint8Array.from([
255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0,
255, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 128, 128, 128, 128,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 255, 255, 255, 0, 255, 255, 255, 0, 128, 128, 128, 128, 128, 128,
0, 255, 255, 255, 0, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0,
]),
);
expect(result.data).toStrictEqual(expectedRgb8BitData);
});

test('should decode RGB 8bit data with LZW compression', () => {
const [result] = decode(readImage('color-5x5-lzw.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitData);
});

// prettier-ignore
const expectedRgb8BitAlphaData = Uint8Array.from([
0, 0, 0, 0, 0, 255, 0, 54, 0, 0, 255, 102, 0, 0, 0, 152, 0, 0, 0, 203,
255, 0, 0, 31, 0, 255, 0, 78, 0, 0, 255, 255, 128, 128, 128, 255, 128, 128, 128, 255,
255, 255, 255, 54, 255, 255, 255, 255, 255, 255, 255, 127, 255, 255, 255, 255, 255, 255, 255, 255,
0, 255, 255, 78, 255, 0, 255, 255, 255, 255, 0, 255, 128, 128, 128, 177, 128, 128, 128, 255,
0, 255, 255, 102, 255, 0, 255, 255, 255, 255, 0, 255, 0, 0, 0, 255, 0, 0, 0, 229
]);

test('should decode RGB 8bit data with pre-multiplied alpha', () => {
const [result] = decode(readImage('color-alpha-5x5.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitAlphaData);
});

test('should decode RGB 8bit data with pre-multiplied alpha and LZW compression', () => {
const [result] = decode(readImage('color-alpha-5x5-lzw.tif'));
expect(result.data).toStrictEqual(expectedRgb8BitAlphaData);
});

test('should decode RGB 8bit data with pre-multiplied alpha and lost precision', () => {
// prettier-ignore
const expectedData = Uint8Array.from([
255, 0, 0, 6, 255, 0, 0, 6,
128, 0, 0, 6, 128, 0, 0, 6,
]);
const [result] = decode(readImage('color-alpha-2x2.tif'));
expect(result.data).toStrictEqual(expectedData);
});

test('should decode with onlyFirst', () => {
Expand Down
28 changes: 17 additions & 11 deletions src/horizontalDifferencing.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,35 @@
// Section 14: Differencing Predictor (p. 64)

export function applyHorizontalDifferencing(
export function applyHorizontalDifferencing8Bit(
data: Uint8Array,
width: number,
components: number,
): void {
let i = 0;
while (i < data.length) {
for (let j = 1; j < width; j++) {
data[i + j] = (data[i + j] + data[i + j - 1]) & 255;
for (let j = components; j < width * components; j += components) {
for (let k = 0; k < components; k++) {
data[i + j + k] =
(data[i + j + k] + data[i + j - (components - k)]) & 255;
}
}
i += width;
i += width * components;
}
}

export function applyHorizontalDifferencingColor(
data: Uint8Array,
export function applyHorizontalDifferencing16Bit(
data: Uint16Array,
width: number,
components: number,
): void {
let i = 0;
while (i < data.length) {
for (let j = 3; j < width * 3; j += 3) {
data[i + j] = (data[i + j] + data[i + j - 3]) & 255;
data[i + j + 1] = (data[i + j + 1] + data[i + j - 2]) & 255;
data[i + j + 2] = (data[i + j + 2] + data[i + j - 1]) & 255;
for (let j = components; j < width * components; j += components) {
for (let k = 0; k < components; k++) {
data[i + j + k] =
(data[i + j + k] + data[i + j - (components - k)]) & 65535;
}
}
i += width * 3;
i += width * components;
}
}
39 changes: 27 additions & 12 deletions src/tiffDecoder.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { IOBuffer } from 'iobuffer';

import {
applyHorizontalDifferencing,
applyHorizontalDifferencingColor,
applyHorizontalDifferencing8Bit,
applyHorizontalDifferencing16Bit,
} from './horizontalDifferencing';
import IFD from './ifd';
import { getByteLength, readData } from './ifdValue';
Expand Down Expand Up @@ -178,6 +178,7 @@ export default class TIFFDecoder extends IOBuffer {
throw unsupported('image type', ifd.type);
}
this.applyPredictor(ifd);
this.convertAlpha(ifd);
if (ifd.type === 0) {
// WhiteIsZero: we invert the values
const bitDepth = ifd.bitsPerSample;
Expand Down Expand Up @@ -276,18 +277,20 @@ export default class TIFFDecoder extends IOBuffer {
}
case 2: {
if (bitDepth === 8) {
if (ifd.samplesPerPixel === 1) {
applyHorizontalDifferencing(ifd.data as Uint8Array, ifd.width);
} else if (ifd.samplesPerPixel === 3) {
applyHorizontalDifferencingColor(ifd.data as Uint8Array, ifd.width);
} else {
throw new Error(
'Horizontal differencing is only supported for images with 1 or 3 samples per pixel',
);
}
applyHorizontalDifferencing8Bit(
ifd.data as Uint8Array,
ifd.width,
ifd.components,
);
} else if (bitDepth === 16) {
applyHorizontalDifferencing16Bit(
ifd.data as Uint16Array,
ifd.width,
ifd.components,
);
} else {
throw new Error(
'Horizontal differencing is only supported for 8-bit images',
`Horizontal differencing is only supported for images with a bit depth of ${bitDepth}`,
);
}
break;
Expand All @@ -296,6 +299,18 @@ export default class TIFFDecoder extends IOBuffer {
throw new Error(`invalid predictor: ${ifd.predictor}`);
}
}

private convertAlpha(ifd: TiffIfd): void {
if (ifd.alpha && ifd.associatedAlpha) {
const { data, components, maxSampleValue } = ifd;
for (let i = 0; i < data.length; i += components) {
const alphaValue = data[i + components - 1];
for (let j = 0; j < components - 1; j++) {
data[i + j] = Math.round((data[i + j] * maxSampleValue) / alphaValue);
}
}
}
}
}

function getDataArray(
Expand Down
Loading

0 comments on commit 5f2e612

Please sign in to comment.