1. TIFF 文件对 LZW 的使用要求
TIFF 第6版标准文件在第13章节对 LZW 的使用有如下描述:
- 每个条带的数据独立压缩,每个条带的字典表是不同的。建议设置 RowsPerStrip 选项,以便每个条带在压缩前大约包含8K 字节,其主要目的就是希望条带足够小,以便压缩和未压缩的条带数据都能保存在内存中,即使是在小内容的机器上也能够保持几乎最佳的压缩比。
- TIFF 中使用可变长度的编码(variable-length code)实现 LZW 算法,最小长度为 9,最大长度为12。每个字节共占用8位,对应的整型范围为0~255。TIFF 文件中规定了256、257分别对应2个特殊字符 CLEAR_CODE 和 EOI_CODE。其中,CLEAR_CODE 是为了在压缩或者解压过程中当扩展字典容量超过2^12=4096时,告诉程序重新初始化字典表;每个条带的压缩结果都是以CLEAR_CODE开始。EOI_CODE 放在每个条带的压缩结果的末尾,表示当前条带的数据以压缩完成。
- 压缩结果以字节的方式写入,因此无论是小端字节序的文件还是大端字节序的文件,它们的压缩数据是相同的。
2. 需要提前了解的
3. 针对 TIFF 文件的 LZW 的实现
3.1 难点分析
针对 TIFF 文件的 LZW 的实现有2个实现难点,一个是如何有效管理编码,另一个是对于不定长的编码结果,如何有效的写入到输出结果并正确的转换。
3.1.1 如何有效管理编码
最基本的 LZW 实现程序只需要几十行代码,详细可查看另外一篇文章。真正的困难是如何有效管理编码表。穷举法会导致内存需求增大和程序执行缓慢。在商业 LZW 程序中通常使用几种方法来提升性能。举例来说,内存问题的出现是因为预先不知道每个字符串的编码是多长。大多数 LZW 程序利用编码表的冗余特性处理这种问题。例如,对于下面这篇文章中的编码示例,第2步编码256被定义为TO
、第12步编码265被定位以为TOB
,这个结果可以看做是编码256 + B
。依次类推,每个编码都可以表示为一个以前的编码加上一个新字符。
0102 | LZW 压缩算法
3.1.2 如何处理不定长编码结果
将不定长的编码结果转成字节存储是实现 LZW 算法压缩率的关键点之一。
举例来说,对于数据[256、7、258、8、8、258、6、6、257]
而言,如果按照 int 类型来存储,总共需要9*4=36个字节。如果换种方式来看,当前数据总最大的值为258,因此可以考虑使用9位二进制来表示,二进制转换结果为100000000,000000111,100000010,000001000,000001000,100000010,000000110,000000110,100000001
(逗号只是起到分隔作用),转换结果再转成字节存储,总共需要11个字节,对应的结果为-128(1000000)、1(00000001)、-32(11100000)、64(010000000)、-128(10000000)、68(01000100)、8(00001000)、12(00001100)、6(00000110)、-128(10000000)、-128(10000000)
(注意,负数是以补码的形式存储的)。
随着字典表的扩展,每个编码对应的位的长度也会增长,例如,对于256-511之间的数据可以用9位二进制来表示,而对于512-1023之间的数据就需要使用10位二进制来表示。对于这个思路的处理,可以使用位移运算再加上一些中间变量来实现。
3.2 压缩过程
3.2.1 数据编码
- 关键代码:
public void encode(final ByteBuffer byteBuffer) throws IOException {
if (!byteBuffer.hasRemaining()) return ;
// 写入CLEAR_CODE
this.writeCode(CLEAR_CODE);
this.parent = this.byte2int(byteBuffer.get());
while (byteBuffer.hasRemaining()) {
int current = this.byte2int(byteBuffer.get());
int child = children[this.parent];
if (child > 0) {
if (suffixes[child] == current) {
this.parent = child;
} else {
int sibling = child;
while (true) {
if (siblings[sibling] > 0) {
sibling = siblings[sibling];
if (suffixes[sibling] == current) {
this.parent = sibling;
break;
}
} else {
siblings[sibling] = (short) this.nextValidCode;
suffixes[this.nextValidCode] = (short) current;
this.writeCode(this.parent);
this.parent = current;
this.nextValidCode++;
break;
}
}
}
} else {
children[parent] = (short) nextValidCode;
suffixes[nextValidCode] = (short) current;
writeCode(parent);
parent = current;
nextValidCode++;
}
}
}
- 操作示例:输入数据:[7, 7, 7, 8, 8, 7, 7, 6, 6]
步骤1
基础数据:children=[](空数组)、suffixes=[](空数组)、siblings=[](空数组)、parent=7、nextValidCode = 258
执行过程:
-- current = 7
-- child = children[parent] = children[7] = 0(类似于判断Ω+λ是否在字典中中存在,如果等于0表示存在,如果不等于0表示不存在,则需要将Ω+λ写入到字典中)
-- child == 0
---- children[parent] = nextValidCode = 258
---- suffixes[nextValidCode] = current = 7 (和上一步一起执行了类似于将Ω+λ[77]写入到字典中的步骤)
---- parent = current = 7
---- nextValidCode += 1 = 259
---- 输出parent=7
步骤2
基础数据:children=[](7:258)、suffixes=[](258:7)、siblings=[](空数组)、parent=7、nextValidCode = 259
执行过程:
-- current = 7
-- child = children[parent] = children[7] = 258
-- child > 0
---- suffixes[child] = suffixes[258] = 7 == current(child[7] = 258、suffixes[258]=7类似于表示在字典中258编码对应的值是77)
------ parent = child = 258
步骤3
基础数据:children=[](7:258)、suffixes=[](258:7)、siblings=[](空数组)、parent=258、nextValidCode = 259
执行过程:
-- current = 8
-- child = children[parent] = children[258] = 0
-- child == 0
---- children[parent] = children[258] = nextValidCode = 259
---- suffixes[nextValidCode] = suffixes[259] = current = 8 (和上一步一起执行了类似于将Ω+λ[778]写入到字典中的步骤)
---- parent = current = 8
---- nextValidCode += 1 = 260
---- 输出parent=258
步骤4
基础数据:children=[](7:258、258:259)、suffixes=[](258:7、259:8)、siblings=[](空数组)、parent=8、nextValidCode = 260
执行过程:
-- current = 8
-- child = children[parent] = children[8] = 0
-- child == 0
---- children[parent] = children[8] = nextValidCode = 260
---- suffixes[nextValidCode] = suffixes[260] = current = 8 (和上一步一起执行了类似于将Ω+λ[88]写入到字典中的步骤)
---- parent = current = 8
---- nextValidCode += 1 = 261
---- 输出parent=8
步骤5
基础数据:children=[](7:258、8:260、258:259)、suffixes=[](258:7、259:8、260:8)、siblings=[](空数组)、parent=8、nextValidCode = 261
执行过程:
-- current = 7
-- child = children[parent] = children[8] = 260
-- child != 0
---- suffixes[child] = suffixes[260] = 8 != current = 7
------ sibling = child = 258
------ siblings[sibling] = siblings[258] == 0
-------- siblings[sibling] = siblings[258] = nextValidCode = 261
-------- suffixes[nextValidCode] = suffixes[261] = current = 7
-------- parent = current = 7
-------- nextValidCode += 1 = 262
-------- 输出parent=8
步骤6
基础数据:children=[](7:258、8:260、258:259)、suffixes=[](258:7、259:8、260:8、261:7)、siblings=[](260:261)、parent=7、nextValidCode = 262
执行过程:
-- current = 7
-- child = children[parent] = children[7] = 258
-- child != 0
---- suffixes[child] = suffixes[258] = 7 = current
------ parent = 258
步骤7
基础数据:children=[](7:258、8:260、258:259)、suffixes=[](258:7、259:8、260:8、261:7)、siblings=[](260:261)、parent=258、nextValidCode = 262
执行过程:
-- current = 6
-- child = children[parent] = children[258] = 259
-- child != 0
---- suffixes[child] = suffixes[259] = 8 != current = 6
------ sibling = child = 259
------ siblings[sibling] = siblings[259] = 0
-------- siblings[sibling] = siblings[259] = nextValidCode = 262
-------- suffixes[nextValidCode] = suffixes[262] = current = 6
-------- parent = current = 6
-------- nextValidCode += 1 = 263
-------- 输出parent=258
步骤8
基础数据:children=[](7:258、8:260、258:259)、suffixes=[](258:7、259:8、260:8、261:7、262:6)、siblings=[](259:262、260:261)、parent=6、nextValidCode = 263
执行过程:
-- current = 6
-- child = children[parent] = children[6] = 0
-- child == 0
---- children[parent] = children[6] = nextValidCode = 263
---- suffixes[nextValidCode] = suffixes[263] = current = 6
---- parent = current = 6
---- nextValidCode += 1 = 264
---- 输出parent=6
步骤9
基础数据:children=[](7:258、8:260、258:259)、suffixes=[](258:7、259:8、260:8、261:7、262:6)、siblings=[](259:262、260:261)、parent=6、nextValidCode = 263
执行过程:
-- 输出parent=6
3.2.2 不定长编码结果转字节
- 关键代码
bitPosOfNextCode:下一个编码结果中开始写入到字节的位的位置 bitsFromPreviousCode:上一个编码结果中需要写入到字节的内容
private void writeCode(final int code) throws IOException {
// 中间变量,将上一个编码结果中遗留的位和当前值合并
int var1 = (this.bitsFromPreviousCode << this.bitsPerCode) | (code & this.maxCode);
// 中间变量,上一个编码结果遗留下来的位的长度和当前编码结果对应的位的长度的总和
int var2 = this.bitPosOfNextCode + this.bitsPerCode;
while (var2 >= 8) {
// 如果合并后的位的长度大于8,则通过右移操作裁剪前8位数据
// 0xff的作用主要有2个:一个是将负数转为整数,另一个是对于var2是8的倍数时,最后一次循坏时,截取最后8位数据
int var3 = (var1 >> (var2 - 8)) & 0xff;
// 输出流写入int时,会自动转为byte
this.codeStream.write(var3);
var2 -= 8;
}
// 更新bitPosOfNextCode和bitsFromPreviousCode
this.bitPosOfNextCode = var2;
this.bitsFromPreviousCode = var1 & this.bitmaskFor(this.bitPosOfNextCode);
}
- 操作示例:输入数据[256, 7, 258, 8, 8, 258, 6, 6, 257],对应的输出结果为[-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128]
这里列出了前2步推导过程,剩下的步骤,这里不再赘述了,感兴趣的朋友可以自己推导一下
步骤1
基础数据:bitsFromPreviousCode=0、bitPosOfNextCode=0、maxCode=511
执行过程:
-- code = 256
-- var1 = (bitsFromPreviousCode << bitsPerCode) | (code & maxCode) = (0 << 9) | (256 & 511) = (0 << 9) | (100000000 & 111111111) = 256
-- var2 = bitPosOfNextCode + bitsPerCode = 0 + 9 = 9
-- var2 >= 8
---- var3 = (var1 >> (var2 - 8)) & 0xff = (256 >> 1) & 0xff = (100000000 >> 1) & 255 = 10000000 & 11111111 = 128
---- 写出128,输出流会自动转为byte,对应-128
---- var2 -= 8 = 1
-- bitPosOfNextCode = var2 = 1
-- bitsFromPreviousCode = var1 & bitmaskFor(1) = 256 & 0 = 100000000 & 0 = 0
步骤2
基础数据:bitsFromPreviousCode=0、bitPosOfNextCode=1、maxCode=511
执行过程:
-- code = 7
-- var1 = (bitsFromPreviousCode << bitsPerCode) | (code & maxCode) = (0 << 9) | (7 & 511) = 7
-- var2 = bitPosOfNextCode + bitsPerCode = 1 + 9 = 10
-- var2 >= 8
---- var3 = (var1 >> (var2 - 8)) & 0xff = (7 >> 2) & 255 = (111 >> 2) & 11111111 = 1 & 11111111 = 1
---- 写出1,输出流会自动转为byte,对应1
---- var2 -= 8 = 2
-- bitPosOfNextCode = var2 = 2
-- bitsFromPreviousCode = var1 & bitmaskFor(2) = 111 & 1 = 111 & 11 = 3
3.3 解压过程
3.3.1 数据解码
- 关键代码
public OutputStream decode(final InputStream inputStream) throws IOException {
try (FastByteArrayOutputStream byteArrayOutputStream = new FastByteArrayOutputStream()) {
// 初始化相关变量
this.initialize();
// 读取第1个编码结果,如果不是结束标识,则循环读取数据,直到遇到结束标识
int code = this.readLzwCode(inputStream);
while (code != EOI_CODE) {
if (code == CLEAR_CODE) {
// 如果遇到CLEAR_CODE
// 1. 重新初始化字典表及相关数据
this.initialize();
// 2. 读取后面的数据,直到遇到不是CLEAR_CODE和EOI_CODE的数据
code = this.readLzwCode(inputStream);
while (code == CLEAR_CODE) {
code = this.readLzwCode(inputStream);
}
if (code == EOI_CODE) {
break;
}
// 3. CLEAR_CODE后的第一个数据肯定在数据表中,直接写出
List<Integer> value = this.dictionary.get(code);
this.writeToOutput(value, byteArrayOutputStream);
this.previousCode = code;
} else {
// 如果在字典表中存在,则写出code对应的值,然后在字典表中按照<编码值:Ω+λ[0]>的方式增加新的值
List<Integer> value = this.dictionary.get(code);
if (value != null) {
this.writeToOutput(value, byteArrayOutputStream);
List<Integer> newValue = this.concat(this.dictionary.get(this.previousCode), value.get(0));
this.addToDictionary(newValue);
this.previousCode = code;
} else {
// 如果在字典表中不存在
// 1. 创建新的值并写出
List<Integer> previousValue = this.dictionary.get(this.previousCode);
List<Integer> newValue = this.concat(previousValue, previousValue.get(0));
this.writeToOutput(newValue, byteArrayOutputStream);
// 2. 在字典表中增加新的编码
this.addToDictionary(newValue);
this.previousCode = code;
}
}
code = this.readLzwCode(inputStream);
}
return byteArrayOutputStream;
}
}
- 操作示例:输入数据[-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128],输出结果应为[7, 7, 7, 8, 8, 7, 7, 6, 6]
这里列出了前3步推导过程,剩下的步骤,这里不再赘述了,感兴趣的朋友可以自己推导一下
步骤1
基础数据:dictionary={}、previousCode=0
执行过程:
-- code = 256(具体怎么得来的,可参考2.3.2)
-- code == CLEAR_CODE
---- 初始化字典表(dictionary添加记录,键值范围0~257,键对应的值与键值相同;bitsPerCode=MIN_BIT_SIZE=9;maxCode = 511)
---- code = 7
---- 写出字典中code对应的值,也就是[7]
---- previousCode = 7
-- code = 258
步骤2
基础数据:dictionary=<0-257>、previousCode=7、nextValidCode=258
执行过程:
-- code = 258
-- code != EOI_CODE != CLEAR_CODE
-- dictionary[code] = dictionary[258] = null
---- previousValue = dictionary[previouse] = dictionary[7] = [7]
---- newValue = previousValue + previousValue[0] = [77]
---- 写出值[77]
---- 字典中新增记录,键=258、值等于[77]
------ nextValidCode += 1 = 259 < maxCode = 511,不用改变bitsPerCode
---- previousValue = 258
-- code = 8
步骤3
基础数据:dictionary=<0-258>、previousCode=258、nextValidCode=259
执行过程:
-- code = 8
-- code != EOI_CODE != CLEAR_CODE
-- dictionary[code] = dictionary[8] = [8] != null
---- 写出值[8]
---- newValue = dictionary[previous] + dictionary[8][0] = [778]
---- 字典中新增记录,键=259、值等于[778]
------ nextValidCode += 1 = 260 < maxCode = 511,不用改变bitsPerCode
---- previousValue = 8
-- code = 8
3.3.2 字节转LZW编码结果
- 关键代码
private int readLzwCode(final InputStream inputStream) throws IOException {
int read = inputStream.read();
// 如果当前输入流没有可读数据,直接返回EOI_CODE
if (read < 0) {
return EOI_CODE;
}
// 将上一个字节遗留的结果左移8位并且与当前的值合并
int var1 = (this.remainValueOfPreviousByte << 8) | read;
// 已经读取的字节位数:上一个字节遗留的位数 + 当前字节的位数8
int var2 = this.bitsOfPreviousByte + 8;
// 如果已经读取的字节位数仍然小于编码结果对应的位数,则需要读取下一个值
// bitsPerCode最大值为12,下面的条件做多执行1次,所以不需要while循环
if (var2 < this.bitsPerCode) {
read = inputStream.read();
if (read < 0) {
return EOI_CODE;
}
// 已经读取的值左移8位并且与新读取的值合并
var1 = (var1 << 8) | read;
// 已经读取的字节位数+8
var2 += 8;
}
// 读取的值需要排除的位的个数
int var3 = var2 - this.bitsPerCode;
int code = (var1 >> var3) & this.bitmaskFor(this.bitsPerCode);
// 通过掩码获取已读取的字节中需要放到下一次获取编码结果的值
this.remainValueOfPreviousByte = var1 & this.bitmaskFor(var3);
this.bitsOfPreviousByte = var3;
return code;
}
- 操作示例:输入数据[-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128],对应的结果为[256, 7, 258, 8, 8, 258, 6, 6, 257]
这里列出了前2步推导过程,剩下的步骤,这里不再赘述了,感兴趣的朋友可以自己推导一下
步骤1
基础数据:bitsOfPreviousByte=0、remainValueOfPreviousByte、bitsPerCode=9
执行过程:
-- read = 128(10000000)
-- var1 = (remainValueOfPreviousByte << 8) | read = (0 << 8) | 128 = 128
-- var2 = bitsOfPreviousByte + 8 = 8
-- var2 < 9
---- read = 1(00000001)
---- var1 = (var1 << 8) | read = 32769(1000000000000001)
---- var2 += 8 = 16
-- var3 = var2 - bitsPerCode = 16 - 9 = 7
-- code = (var1 >> var3) & bitmaskFor(bitsPerCode) = (32769 >> 7) & 511 = 100000000 & 111111111 = 100000000 = 256
-- bitsOfPreviousByte = var3 = 7
-- remainValueOfPreviousByte = var1 & bitmaskFor(var3) = 32769 & 127 = 1000000000000001 & 1111111 = 1
步骤2
基础数据:bitsOfPreviousByte=7、remainValueOfPreviousByte=1、bitsPerCode=9
执行过程:
-- read = 224
-- var1 = (remainValueOfPreviousByte << 8) | read = (1 << 8) | 224 = 480
-- var2 = bitsOfPreviousByte + 8 = 15
-- var3 = var2 - bitsPerCode = 15 - 9 = 6
-- code = (var1 >> var3) & bitmaskFor(bitsPerCode) = (480 >> 6) & 511 = (111100000 >> 6) & 111111111 = 111 & 111111111 = 7
-- bitsOfPreviousByte = var3 = 6
-- remainValueOfPreviousByte = var1 & bitmaskFor(var3) = 480 & 63 = 111100000 & 111111 = 32
3.4 完整代码
3.4.1 压缩
package cn.funnymap.compression.lzw;
import cn.funnymap.compression.Encoder;
import org.springframework.util.FastByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class LZWEncoder implements Encoder {
// 常量初始化
private static final int CLEAR_CODE = 256;
private static final int EOI_CODE = 257;
private static final int MIN_BIT_SIZE = 9;
private static final int MAX_BIT_SIZE = 12;
private static final int MAX_TABLE_SIZE = 1 << MAX_BIT_SIZE;
// 初始化设置
private final OutputStream codeStream;
// 以下配置在编码过程中会使用到
private final short[] children = new short[MAX_TABLE_SIZE]; // 对应字典中存在的输入数据对应的键值
private final short[] suffixes = new short[MAX_TABLE_SIZE]; // Ω+λ的最后一个数值
private final short[] siblings = new short[MAX_TABLE_SIZE]; // Ω+λ的兄弟
private int bitsPerCode = MIN_BIT_SIZE; // 表示每一个编码结果的Bit的长度,最小值为9,最大值为12
private int nextValidCode = EOI_CODE + 1; // 字典中新增编码结果的值
private int maxCode = this.maxValueOf(bitsPerCode); // 当前Bit位数所能表示的最大值
// 以下配置在编码结果转字节时会使用到
private int bitPosOfNextCode = 0; // 下一个编码结果中开始写入到字节的位的位置
private int bitsFromPreviousCode = 0; // 上一个编码结果中需要写入到字节的内容
public LZWEncoder(final OutputStream codeStream) {
this.codeStream = codeStream;
}
@Override
public void encode(final ByteBuffer byteBuffer) throws IOException {
if (!byteBuffer.hasRemaining()) return ;
// 写入CLEAR_CODE
this.writeCode(CLEAR_CODE);
// 编码并写入输入数据
this.encodeBytes(byteBuffer);
// 写入EOI_CODE
this.writeCode(EOI_CODE);
// 如果最后剩余部分编码数据,通过写入0填充其他的位
if (this.bitPosOfNextCode > 0) {
this.writeCode(0);
}
}
@SuppressWarnings("java:S3776")
private void encodeBytes(final ByteBuffer byteBuffer) throws IOException {
// 编码过程
int parent = this.byte2int(byteBuffer.get());
while (byteBuffer.hasRemaining()) {
int current = this.byte2int(byteBuffer.get());
int child = this.children[parent];
if (child > 0) {
if (this.suffixes[child] == current) {
parent = child;
} else {
int sibling = child;
while (true) {
if (this.siblings[sibling] > 0) {
sibling = this.siblings[sibling];
if (this.suffixes[sibling] == current) {
parent = sibling;
break;
}
} else {
this.siblings[sibling] = (short) this.nextValidCode;
this.suffixes[this.nextValidCode] = (short) current;
this.writeCode(parent);
parent = current;
this.nextValidCode++;
this.increaseBitsPerCodeOrRestIfNeeded();
break;
}
}
}
} else {
this.children[parent] = (short) this.nextValidCode;
this.suffixes[this.nextValidCode] = (short) current;
writeCode(parent);
parent = current;
this.nextValidCode++;
this.increaseBitsPerCodeOrRestIfNeeded();
}
}
// 对于最后一个数据
this.writeCode(parent);
}
private int byte2int(byte byteValue) {
return byteValue & 0xff;
}
private void increaseBitsPerCodeOrRestIfNeeded() throws IOException {
if (this.nextValidCode >= this.maxCode) {
if (this.bitsPerCode == MAX_BIT_SIZE) {
// 输出流中添加 CLEAR_CODE 标志
this.writeCode(CLEAR_CODE);
// 重置 TABLE
this.resetTable();
} else {
this.bitsPerCode += 1;
this.maxCode = this.maxValueOf(bitsPerCode);
}
}
}
private void resetTable() {
Arrays.fill(this.suffixes, (short) 0);
Arrays.fill(this.children, (short) 0);
Arrays.fill(this.siblings, (short) 0);
this.bitsPerCode = MIN_BIT_SIZE;
this.maxCode = this.maxValueOf(this.bitsPerCode);
this.nextValidCode = EOI_CODE + 1;
}
/**
* 将编码结果写入输出流
*
* @param code 编码结果
* @throws IOException 输出流写入编码结果时抛出的异常
*/
private void writeCode(final int code) throws IOException {
// 中间变量,将上一个编码结果中遗留的位和当前值合并
int var1 = (this.bitsFromPreviousCode << this.bitsPerCode) | (code & this.maxCode);
// 中间变量,上一个编码结果遗留下来的位的长度和当前编码结果对应的位的长度的总和
int var2 = this.bitPosOfNextCode + this.bitsPerCode;
while (var2 >= 8) {
// 如果合并后的位的长度大于8,则通过右移操作裁剪前8位数据
// 0xff的作用主要有2个:一个是将负数转为整数,另一个是对于var2是8的倍数时,最后一次循坏时,截取最后8位数据
int var3 = (var1 >> (var2 - 8)) & 0xff;
// 输出流写入int时,会自动转为byte
this.codeStream.write(var3);
var2 -= 8;
}
// 更新bitPosOfNextCode和bitsFromPreviousCode
this.bitPosOfNextCode = var2;
this.bitsFromPreviousCode = var1 & this.bitmaskFor(this.bitPosOfNextCode);
}
/**
* 获取指定值对应的掩码
*
* @param value 具体的值
* @return 掩码
*/
private int bitmaskFor(int value) {
return this.maxValueOf(value);
}
/**
* 返回指定Bit位数的最大值
*
* @param bitsLength bit的位数
* @return 当前Bit位数能表达的最大值
*/
private int maxValueOf(int bitsLength) {
return (1 << bitsLength) - 1;
}
public static void main(String[] args) throws IOException {
FastByteArrayOutputStream outputStream = new FastByteArrayOutputStream();
LZWEncoder lzwEncoder = new LZWEncoder(outputStream);
// 1. writeCode方法测试
// 对应的输出结果为[-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128]
int[] exampleValues = new int[] {256, 7, 258, 8, 8, 258, 6, 6, 257};
for (int value : exampleValues) {
lzwEncoder.writeCode(value);
}
if (lzwEncoder.bitPosOfNextCode != 0) {
lzwEncoder.writeCode(0);
}
System.out.println(Arrays.toString(outputStream.toByteArray()));
// 2. encode方法测试
// 对应的输出结果为[-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128]
byte[] bytes = new byte[] {7, 7, 7, 8, 8, 7, 7, 6, 6};
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
lzwEncoder.encode(byteBuffer);
System.out.println(Arrays.toString(outputStream.toByteArray()));
}
}
3.4.2 解压
package cn.funnymap.compression.lzw;
import org.springframework.util.FastByteArrayOutputStream;
import java.io.*;
import java.util.*;
public class LZWDecoder {
// 常量初始化
private static final int CLEAR_CODE = 256;
private static final int EOI_CODE = 257;
private static final int MIN_BIT_SIZE = 9;
// 初始化设置
// 以下配置在解码过程中会使用到
private int nextValidCode = EOI_CODE + 1; // 字典中新增编码结果的值
private int bitsPerCode = MIN_BIT_SIZE; // 表示每一个编码结果的Bit的长度,最小值为9,最大值为12
private int maxCode = this.maxValueOf(bitsPerCode); // 当前Bit位数所能表示的最大值
private int previousCode = 0;
private Map<Integer, List<Integer>> dictionary; // 用于解码的字典表
// 以下配置在字节转编码结果时会使用到
private int bitsOfPreviousByte = 0; // 前一个字节中需要放入当前编码的位的个数
private int remainValueOfPreviousByte; // 前一个字节中需要放入当前编码的位对应的值
public OutputStream decode(final InputStream inputStream) throws IOException {
try (FastByteArrayOutputStream byteArrayOutputStream = new FastByteArrayOutputStream()) {
// 初始化相关变量
this.initialize();
// 读取第1个编码结果,如果不是结束标识,则循环读取数据,直到遇到结束标识
int code = this.readLzwCode(inputStream);
while (code != EOI_CODE) {
if (code == CLEAR_CODE) {
// 如果遇到CLEAR_CODE
// 1. 重新初始化字典表及相关数据
this.initialize();
// 2. 读取后面的数据,直到遇到不是CLEAR_CODE和EOI_CODE的数据
code = this.readLzwCode(inputStream);
while (code == CLEAR_CODE) {
code = this.readLzwCode(inputStream);
}
if (code == EOI_CODE) {
break;
}
// 3. CLEAR_CODE后的第一个数据肯定在数据表中,直接写出
List<Integer> value = this.dictionary.get(code);
this.writeToOutput(value, byteArrayOutputStream);
this.previousCode = code;
} else {
// 如果在字典表中存在,则写出code对应的值,然后在字典表中按照<编码值:Ω+λ[0]>的方式增加新的值
List<Integer> value = this.dictionary.get(code);
if (value != null) {
this.writeToOutput(value, byteArrayOutputStream);
List<Integer> newValue = this.concat(this.dictionary.get(this.previousCode), value.get(0));
this.addToDictionary(newValue);
this.previousCode = code;
} else {
// 如果在字典表中不存在
// 1. 创建新的值并写出
List<Integer> previousValue = this.dictionary.get(this.previousCode);
List<Integer> newValue = this.concat(previousValue, previousValue.get(0));
this.writeToOutput(newValue, byteArrayOutputStream);
// 2. 在字典表中增加新的编码
this.addToDictionary(newValue);
this.previousCode = code;
}
}
code = this.readLzwCode(inputStream);
}
return byteArrayOutputStream;
}
}
/**
* 初始化字典表及相关变量
*/
private void initialize() {
this.dictionary = new HashMap<>();
for (int i = 0; i < 258; i++) {
this.dictionary.put(i, Collections.singletonList(i));
}
this.bitsPerCode = MIN_BIT_SIZE;
this.maxCode = this.maxValueOf(this.bitsPerCode);
this.previousCode = 0;
}
private void writeToOutput(List<Integer> value, FastByteArrayOutputStream byteArrayOutputStream) throws IOException {
for (Integer integer : value) {
byteArrayOutputStream.write(integer);
}
}
private List<Integer> concat(List<Integer> integerList, Integer integer) {
List<Integer> newValue = new ArrayList<>(integerList);
newValue.add(integer);
return newValue;
}
private void addToDictionary(List<Integer> integerList) {
this.dictionary.put(this.nextValidCode, integerList);
this.nextValidCode += 1;
this.increaseBitsPerCodeOrRestIfNeeded();
}
private void increaseBitsPerCodeOrRestIfNeeded() {
if (this.nextValidCode >= this.maxCode) {
this.bitsPerCode += 1;
this.maxCode = this.maxValueOf(bitsPerCode);
}
}
/**
* 将输入流依次转为LZW编码结果
*
* @param inputStream 输入流
* @return LZW编码结果
* @throws IOException 输入流读取数据时的异常
*/
private int readLzwCode(final InputStream inputStream) throws IOException {
int read = inputStream.read();
// 如果当前输入流没有可读数据,直接返回EOI_CODE
if (read < 0) {
return EOI_CODE;
}
// 将上一个字节遗留的结果左移8位并且与当前的值合并
int var1 = (this.remainValueOfPreviousByte << 8) | read;
// 已经读取的字节位数:上一个字节遗留的位数 + 当前字节的位数8
int var2 = this.bitsOfPreviousByte + 8;
// 如果已经读取的字节位数仍然小于编码结果对应的位数,则需要读取下一个值
// bitsPerCode最大值为12,下面的条件做多执行1次,所以不需要while循环
if (var2 < this.bitsPerCode) {
read = inputStream.read();
if (read < 0) {
return EOI_CODE;
}
// 已经读取的值左移8位并且与新读取的值合并
var1 = (var1 << 8) | read;
// 已经读取的字节位数+8
var2 += 8;
}
// 读取的值需要排除的位的个数
int var3 = var2 - this.bitsPerCode;
int code = (var1 >> var3) & this.bitmaskFor(this.bitsPerCode);
// 通过掩码获取已读取的字节中需要放到下一次获取编码结果的值
this.remainValueOfPreviousByte = var1 & this.bitmaskFor(var3);
this.bitsOfPreviousByte = var3;
return code;
}
/**
* 获取指定值对应的掩码
*
* @param value 具体的值
* @return 掩码
*/
private int bitmaskFor(int value) {
return this.maxValueOf(value);
}
/**
* 返回指定Bit位数的最大值
*
* @param bitsLength bit的位数
* @return 当前Bit位数能表达的最大值
*/
private int maxValueOf(int bitsLength) {
return (1 << bitsLength) - 1;
}
public static void main(String[] args) throws IOException {
LZWDecoder decoder = new LZWDecoder();
byte[] bytes = new byte[]{-128, 1, -32, 64, -128, 68, 8, 12, 6, -128, -128};
InputStream inputStream = new ByteArrayInputStream(bytes);
OutputStream outputStream = decoder.decode(inputStream);
System.out.println(Arrays.toString(((FastByteArrayOutputStream) outputStream).toByteArray()));
}
}