Skip to content

Commit

Permalink
fix: make LZW more robust
Browse files Browse the repository at this point in the history
While the algorithm to getNextCode was rewritten inspired by a java code
(and looks much better and faster) the main difference with the previous
implementation is this return 257 in case of OutOfBounds.

https://github.com/sugark/Tiffus/blob/master/src/org/eclipse/swt/internal/image/TIFFLZWDecoder.java#L238-L241

This change will prevent issues with probably corrupted files.
Some other implementations also allow to check for this:
https://gitlab.com/libtiff/libtiff/blob/master/libtiff/tif_lzw.c#L170-185
  • Loading branch information
targos committed Oct 31, 2021
1 parent 442a32f commit 72a6180
Show file tree
Hide file tree
Showing 9 changed files with 136 additions and 36 deletions.
Binary file added img/image-lzw.tif
Binary file not shown.
Binary file added img/image.tif
Binary file not shown.
1 change: 1 addition & 0 deletions src/__tests__/data/1.strip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
�8`@$�BaP�d6�DbQ8�V-�FcQ��v=�HdR9$�M'�JeR�d�]/�LfS9��m7�NgS���}?�PhT:%�G�RiT�e6�O�TjU:�V�W�VkU��v�_�XlV;%��g�ZmV�e��o�\nW;���w�^oW������
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/data/173.strip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
�8`@$�BaP�d6�DbQ8�V-�FcQ��v=�HdR9$�M'�JeR�d�]/�H��9�>f��À3��� 7���3�%�<�RiT���O�TjQ:t��,��j�:�v�_�Ukv %��1��趘e�/l�\f�*E��w�۩W��©4���׀_��F1��.M|�cn��NG-'�D0ќfI���X�������ژ�;%C��5��k'��������\�v���Bx�m���;�z�^���e< ���ז���^ΣU��y���W���Y��?wƗ��쥟��C��9C��/�+��AL��l�
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/data/174.strip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
�8`@$�BaP�d6�DbQ8�V-�FcQ��v=�HdR9$�M'�JeR�d�]/�G�9�Vf���gSxt�<�Ϧ�:%��Q�T�d^�M�TjRJ|F��˪��V�S�XlV;%��g�M���-v�:�Z�9޿t�^h�zU�'n�ܯQ�������U��rRk�C2��r�<Fg9b��'�MR�����=/Y"��4����=z���p{��˵��h�N�G�Qg�I2Θra� �J��swX��WS���:�.�r���y�]ṇ���c��W����~_���O
Expand Down
38 changes: 38 additions & 0 deletions src/__tests__/decode.lzw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { readFileSync } from 'fs';
import { join } from 'path';

import { decode } from '..';

// We can decompress images using 'convert' from imagemagick
// convert image-lzw.tif -define colorspace:auto-grayscale=false -type truecolor image.tif

describe('decode lzw', () => {
it('image', () => {
const lzwBuffer = readFileSync(join(__dirname, '../../img/image-lzw.tif'));
const imageLzw = decode(lzwBuffer);
const buffer = readFileSync(join(__dirname, '../../img/image.tif'));
const image = decode(buffer);
expect(
dataEqual(imageLzw[0].data as Uint8Array, image[0].data as Uint8Array),
).toBe(true);
});
it('color8', () => {
const lzwBuffer = readFileSync(join(__dirname, '../../img/color8-lzw.tif'));
const imageLzw = decode(lzwBuffer);
const buffer = readFileSync(join(__dirname, '../../img/color8.tif'));
const image = decode(buffer);
expect(
dataEqual(imageLzw[0].data as Uint8Array, image[0].data as Uint8Array),
).toBe(true);
});
});

function dataEqual(data1: Uint8Array, data2: Uint8Array): boolean {
if (data1.length !== data2.length) return false;
for (let i = 0; i < data1.length; i++) {
if (data1[i] !== data2[i]) {
return false;
}
}
return true;
}
7 changes: 7 additions & 0 deletions src/__tests__/decode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ const files: TiffFile[] = [
bitsPerSample: 8,
components: 3,
},
{
name: 'image-lzw.tif',
width: 2590,
height: 3062,
bitsPerSample: 8,
components: 3,
},
{
name: 'color8.tif',
width: 160,
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/lzw.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { readFileSync } from 'fs';
import { join } from 'path';

import { decompressLzw } from '../lzw';

describe('lzw', () => {
it('1', () => {
const buffer = readFileSync(join(__dirname, 'data/1.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
expect(
new Uint8Array(
result.buffer,
result.byteOffset,
result.byteLength,
).reduce((sum, current) => (sum += current), 0),
).toBe(675);
});
it('173', () => {
const buffer = readFileSync(join(__dirname, 'data/173.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
expect(
new Uint8Array(
result.buffer,
result.byteOffset,
result.byteLength,
).reduce((sum, current) => (sum += current), 0),
).toBe(38307);
});
it('174', () => {
const buffer = readFileSync(join(__dirname, 'data/174.strip'));
const result = decompressLzw(
new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength),
);
expect(result.byteLength).toBe(7770);
});
});
82 changes: 46 additions & 36 deletions src/lzw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,31 @@ const EOI_CODE = 257;
const TABLE_START = 258;
const MIN_BIT_LENGTH = 9;

const stringTable: number[][] = [];
for (let i = 0; i < 256; i++) {
stringTable.push([i]);
}
// Fill the table with dummy data.
// Elements at indices > 257 will be replaced during decompression.
const dummyString = [0];
for (let i = 256; i < 4096; i++) {
stringTable.push(dummyString);
let stringTable: number[][] = [];
function initializeStringTable() {
if (stringTable.length === 0) {
for (let i = 0; i < 256; i++) {
stringTable.push([i]);
}
// Fill the table with dummy data.
// Elements at indices > 257 will be replaced during decompression.
const dummyString: number[] = [];
for (let i = 256; i < 4096; i++) {
stringTable.push(dummyString);
}
}
}

const andTable = [511, 1023, 2047, 4095];
const bitJumps = [0, 0, 0, 0, 0, 0, 0, 0, 0, 511, 1023, 2047, 4095];

class LzwDecoder {
private stripArray: Uint8Array;
private currentBit: number;
private tableLength: number;
private currentBitLength: number;
private nextData = 0;
private nextBits = 0;
private bytePointer = 0;
private tableLength = TABLE_START;
private currentBitLength = MIN_BIT_LENGTH;
private outData: IOBuffer;

public constructor(data: DataView) {
Expand All @@ -30,10 +39,8 @@ class LzwDecoder {
data.byteOffset,
data.byteLength,
);
this.currentBit = 0;
this.tableLength = TABLE_START;
this.currentBitLength = MIN_BIT_LENGTH;
this.outData = new IOBuffer(data.byteLength);
this.initializeTable();
}

public decode(): DataView {
Expand Down Expand Up @@ -64,6 +71,7 @@ class LzwDecoder {
}
}
const outArray = this.outData.toArray();

return new DataView(
outArray.buffer,
outArray.byteOffset,
Expand All @@ -72,6 +80,7 @@ class LzwDecoder {
}

private initializeTable(): void {
initializeStringTable();
this.tableLength = TABLE_START;
this.currentBitLength = MIN_BIT_LENGTH;
}
Expand All @@ -92,38 +101,39 @@ class LzwDecoder {
private addStringToTable(string: number[]): void {
stringTable[this.tableLength++] = string;
if (stringTable.length > 4096) {
stringTable = [];
throw new Error(
'LZW decoding error. Please open an issue at https://github.com/image-js/tiff/issues/new/choose (include a test image).',
);
}
if (this.tableLength + 1 === 2 ** this.currentBitLength) {
if (this.tableLength === bitJumps[this.currentBitLength]) {
this.currentBitLength++;
}
}

private getNextCode(): number {
const d = this.currentBit % 8;
const a = this.currentBit >>> 3;
const de = 8 - d;
const ef = this.currentBit + this.currentBitLength - (a + 1) * 8;
let fg = 8 * (a + 2) - (this.currentBit + this.currentBitLength);
const dg = (a + 2) * 8 - this.currentBit;
fg = Math.max(0, fg);
let chunk1 = this.stripArray[a] & (2 ** (8 - d) - 1);
chunk1 <<= this.currentBitLength - de;
let chunks = chunk1;
if (a + 1 < this.stripArray.length) {
let chunk2 = this.stripArray[a + 1] >>> fg;
chunk2 <<= Math.max(0, this.currentBitLength - dg);
chunks += chunk2;
this.nextData =
(this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
this.nextBits += 8;

if (this.nextBits < this.currentBitLength) {
this.nextData =
(this.nextData << 8) | (this.stripArray[this.bytePointer++] & 0xff);
this.nextBits += 8;
}
if (ef > 8 && a + 2 < this.stripArray.length) {
const hi = (a + 3) * 8 - (this.currentBit + this.currentBitLength);
const chunk3 = this.stripArray[a + 2] >>> hi;
chunks += chunk3;

const code =
(this.nextData >> (this.nextBits - this.currentBitLength)) &
andTable[this.currentBitLength - 9];
this.nextBits -= this.currentBitLength;

// This should not really happen but is present in other codes as well.
// See: https://github.com/sugark/Tiffus/blob/15a60123813d1612f4ae9e4fab964f9f7d71cf63/src/org/eclipse/swt/internal/image/TIFFLZWDecoder.java
if (this.bytePointer > this.stripArray.length) {
return 257;
}
this.currentBit += this.currentBitLength;
return chunks;

return code;
}
}

Expand Down

0 comments on commit 72a6180

Please sign in to comment.