Date Modified Tags java

1. Introduction

两年前因为要在java项目中写一个解压缩的小功能,使用了7zip binding项目,并发了篇博客记录Java解压缩7z文件,没想到后来文章访问量不错,而且在谷歌和百度的搜索结果中,排名都很靠前,尽管更可能的原因是没人在这方面做东西。

前段时间做项目,又需要使用压缩解压功能,但是仔细调查后发现,这个项目早就不更新了(尽管项目页面写着Last Update: 2017-07-19,但是代码的Last modified是2015年),而且用起来问题还挺多的。故转而去7z官方项目找帮助,发现7zip官方有个LZMA SDK,但其Java部分从注释来看可能很久不更新了,不过由于代码相对简单,而且有样例example,因此决定使用它来开发。

2. 项目地址

我将官方的java部分提取出来,添加了自己的zipper。

项目地址: LZMA

写了个简单的样例,示例了如何将大文件分割成小块,并对每一块进行压缩,而不是整个文件压缩:

LZMA Example

3. 跑官方样例代码

官方样例中,LzmaAlone即是一个包含main函数的独立类。

代码很短。可以看到主要是对命令行参数的解析。其usage:

"\nUsage:  LZMA <e|d> [<switches>...] inputFile outputFile\n" +
  "  e: encode file\n" +
  "  d: decode file\n" +
  "  b: Benchmark\n" +
  "<Switches>\n" +
  // "  -a{N}:  set compression mode - [0, 1], default: 1 (max)\n" +
  "  -d{N}:  set dictionary - [0,28], default: 23 (8MB)\n" +
  "  -fb{N}: set number of fast bytes - [5, 273], default: 128\n" +
  "  -lc{N}: set number of literal context bits - [0, 8], default: 3\n" +
  "  -lp{N}: set number of literal pos bits - [0, 4], default: 0\n" +
  "  -pb{N}: set number of pos bits - [0, 4], default: 2\n" +
  "  -mf{MF_ID}: set Match Finder: [bt2, bt4], default: bt4\n" +
  "  -eos:   write End Of Stream marker\n"
  );

3.1 encode file part

if (params.Command == CommandLine.kEncode)
{
  Compression.LZMA.Encoder encoder = new Compression.LZMA.Encoder();
  if (!encoder.SetAlgorithm(params.Algorithm))
    throw new Exception("Incorrect compression mode");
  if (!encoder.SetDictionarySize(params.DictionarySize))
    throw new Exception("Incorrect dictionary size");
  if (!encoder.SetNumFastBytes(params.Fb))
    throw new Exception("Incorrect -fb value");
  if (!encoder.SetMatchFinder(params.MatchFinder))
    throw new Exception("Incorrect -mf value");
  if (!encoder.SetLcLpPb(params.Lc, params.Lp, params.Pb))
    throw new Exception("Incorrect -lc or -lp or -pb value");
  encoder.SetEndMarkerMode(eos);
  //写入5bytes配置参数
  encoder.WriteCoderProperties(outStream);


  long fileSize;
  if (eos)
    fileSize = -1;
  else
    fileSize = inFile.length();
  for (int i = 0; i < 8; i++)
    outStream.write((int)(fileSize >>> (8 * i)) & 0xFF);
  encoder.Code(inStream, outStream, -1, -1, null);
}

前面部分是在设置压缩的参数。

压缩算法部分

    public boolean SetAlgorithm(int algorithm)
    {
        /*
        _fastMode = (algorithm == 0);
        _maxMode = (algorithm >= 2);
        */
        return true;
    }

可能由于代码长时间未更新,java版本的代码,压缩算法也直接省略成了maxMode。随后将参数直接关联的设置内容写入了outputstream里,一共5bytes。

fileSize则是通过文件流的length()函数获得。并将其一个字节一个字节的写入outputstream。注意这里的类型是long,但是写入时,取的是int。这里是没有问题的。因为>>>是逻辑右移,不带符号,即使强行截断成int,由于只需要0xFF8个bit,因为不会出现精度不够的问题。

3.2 decode part

int propertiesSize = 5;
byte[] properties = new byte[propertiesSize];
if (inStream.read(properties, 0, propertiesSize) != propertiesSize)
  throw new Exception("input .lzma file is too short");
Compression.LZMA.Decoder decoder = new Compression.LZMA.Decoder();
if (!decoder.SetDecoderProperties(properties))
  throw new Exception("Incorrect stream properties");
long outSize = 0;
for (int i = 0; i < 8; i++)
{
  int v = inStream.read();
  if (v < 0)
    throw new Exception("Can't read stream size");
  outSize |= ((long)v) << (8 * i);
}
if (!decoder.Code(inStream, outStream, outSize))
  throw new Exception("Error in data stream");

前面5个字节读取的就是在encode时存入的压缩参数,解压时不必再指定,直接从压缩文件里读取即可。

接下来的for循环即是在读取之前存入的解压出来的原始文件的大小。原理同上。

4. 改造压缩

现在的需求是想完成自己的encode和decode函数,byte[]作为要压缩的内容,压缩的内容放到另一个byte[]里。这样,就可以适应非文件场景下的数据压缩解压了。

4.1 Zipper.javaencode内容:

/**
     * 压缩函数
     *
     * @param inputStream:  要压缩的数据,如果源数据是byte[],那么在外层要ByteArrayInputStream ins = new ByteArrayInputStream(yourbytes[], offset, effective_length)
     * @param outputStream: 压缩好的数据,可以通过outputs_bytes.toBytes变成byte数组
     * @param len:          inputs_bytes中的有效数据长度,用于写入压缩好的数据开头,解压时会先读出来这段大小,以便知道要解压的数据大小。注意数据类型
     */
public void encode(ByteArrayInputStream inputStream, ByteArrayOutputStream outputStream, long len) throws Exception {
  Compression.LZMA.Encoder encoder = new Compression.LZMA.Encoder();
  //设置压缩参数。默认即可
  if (!encoder.SetAlgorithm(2))
    throw new Exception("Incorrect compression mode");
  if (!encoder.SetDictionarySize(1 << 23))
    throw new Exception("Incorrect dictionary size");
  if (!encoder.SetNumFastBytes(128))
    throw new Exception("Incorrect -fb value");
  if (!encoder.SetMatchFinder(1))
    throw new Exception("Incorrect -mf value");
  if (!encoder.SetLcLpPb(3, 0, 2))
    throw new Exception("Incorrect -lc or -lp or -pb value");
  encoder.SetEndMarkerMode(false);
  //首先会有5bytes的参数信息被写入
  encoder.WriteCoderProperties(outputStream);
  //接下来8bytes是要压缩的数据的长度,在解压时将被读取。注意这里len是long类型,如果是int,则最大可表示2GB的数据,因此采用long,但是里面每个byte在存储的时候,使用int即可。
  for (int j = 0; j < 8; j++)
    //无符号右移
    outputStream.write((int) (len >>> (8 * j)) & 0xFF);
  // inSize、outSize以及progress参数可以这样设置不用理会
  encoder.Code(inputStream, outputStream, -1, -1, null);
}

4.2 测试读入文件并分割压缩部分

Zipper zipper = new Zipper();

//读取文件
File infile = new File("/home/find/ddown/aa/aa.pptx");
BufferedInputStream ins = new BufferedInputStream(new FileInputStream(infile));
BufferedOutputStream outs = new BufferedOutputStream(new FileOutputStream(new File("/home/find/ddown/aa/aa.lzma")));
// @todo 设置real_len为int,实际限制了每块的大小不能超过2GB
int real_len;
// 要将文件分割的文件块的大小
final int blockSize = 1024 << 3;
// 用来保存每块压缩大小
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < infile.length(); i += real_len) {
  //由于压缩可能会导致文件块变大,因此预开辟两倍空间存放,默认文件分块大小为8KB,即1024<<3
  byte[] inbytes = new byte[blockSize << 1];
  // @todo: 如果实际不是读到1024 × 8,除非到文件尾部,否则应该继续读取,直到读完1024*8长度的块。
  real_len = ins.read(inbytes, 0, blockSize);
  // @warning: 一定要注意,要以实际大小建stream!!!否则压缩时,会将实际有效数据后面的部分空数据也认为是有效的。!!!
  ByteArrayInputStream inputStream = new ByteArrayInputStream(inbytes, 0, real_len);
  ByteArrayOutputStream outputStream = new ByteArrayOutputStream(blockSize << 1);
  System.out.print("实际读取的字节数" + real_len);
  zipper.encode(inputStream, outputStream, (long) real_len);
  // ByteArrarInputStream.size()是指实际有效数据
  queue.offer(outputStream.size());
  System.out.println("压缩后大小" + (outputStream.size()));
  //将压缩好的数据写入压缩文件
  outs.write(outputStream.toByteArray());
}
System.out.println("encode end\n======================================\n");

zipper只是封装了一下encode。

在测试部分可以看到,将文件内容读取到byte[] inbytes里,用ByteArrayInputStream包装成流,传给encode函数。

注意新建了一个queue来存放每块压缩后的大小,因为测试里,是将文件分割成blocksize大小的数据进行压缩,然后写入文件,而解压缩部分则是读取这个文件,按照之前存放的压缩后的大小新建流,并对该块进行解压。

4.3 几个关键问题

  • 读入文件的read函数实际读取到的内容,跟传入的要求读到的长度可能不一样大小。
  • 在newByteArrayInputStream时,一定要设置大小为真实压缩数据块的大小,否则7zip原声的压缩函数会将超出真实有效数据大小的部分,也视为有效数据!导致input实际比你要求压缩的大,自然压缩结果就不正确了。
  • 每个文件块压缩时都要存入压缩的参数和解压后的文件大小。

5. 改造解压

5.1 解压代码

/**
     * 解压函数
     *
     * @param inputStream:  要解压的数据。要求同encode。
     * @param outputStream: 解压获得的数据。
     */
public void decode(ByteArrayInputStream inputStream, ByteArrayOutputStream outputStream) throws Exception {
  Compression.LZMA.Decoder decoder = new Compression.LZMA.Decoder();
  //先读取5bytes设置
  int propertiesSize = 5;
  byte[] properties = new byte[propertiesSize];
  if (inputStream.read(properties, 0, propertiesSize) != propertiesSize)
    throw new Exception("input .lzma file is too short");
  if (!decoder.SetDecoderProperties(properties))
    throw new Exception("Incorrect stream properties");
  long outSize = 0;
  // 读取8bytes的要解压出来的文件长度(单位bytes)
  for (int j = 0; j < 8; j++) {
    int v = inputStream.read();
    if (v < 0)
      throw new Exception("Can't read stream size");
    outSize |= ((long) v) << (8 * j);
  }
  if (!decoder.Code(inputStream, outputStream, outSize)) {
    throw new Exception("Error in data stream");
  }
  //@todo: 这里不应该只是打印,应该throw error
  if (outputStream.size() != outSize) {
    System.out.println("实际解压大小和记录不同 outputstream.size " + outputStream.size() + "\toutsize" + outSize);
  }
}

5.2 测试

Zipper zipper = new Zipper();
// decoder part
infile = new File("/home/find/ddown/aa/aa.lzma");
BufferedOutputStream o2 = new BufferedOutputStream(new FileOutputStream(new File("/home/find/ddown/aa/aa_extra.pptx")));
BufferedInputStream i2 = new BufferedInputStream(new FileInputStream(infile));
// 每个压缩块的大小都在queue里。一个一个压缩块的进行读取和解压
while (!queue.isEmpty()) {
  byte[] inbytes = new byte[blockSize << 1];
  real_len = i2.read(inbytes, 0, queue.peek());
  //@todo: 这里应该throw error
  if (real_len != queue.peek()) {
    System.out.println("读取的大小和队列里的大小(要读的大小)不同" + real_len + "\t" + queue.peek());
  }

  ByteArrayInputStream inputStream = new ByteArrayInputStream(inbytes, 0, queue.peek());
  ByteArrayOutputStream outputStream = new ByteArrayOutputStream(blockSize << 1);
  zipper.decode(inputStream, outputStream);
  o2.write(outputStream.toByteArray());

  queue.poll();
}
o2.flush();
o2.close();
i2.close();

文章版权归 FindHao 所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明作者 FindHao 和本文原始地址:
https://findhao.net/easycoding/2129.html

Comments