用7Zip的LZMA SDK压缩解压数据

最近在研究7zip的LZMA SDK,虽然很久以前曾在本站写过LZMA SDK的简单介绍,不过当时只是走马观花地扫了一下,这次由于一个项目需要,不得不仔细研究了一把。不知道7zip的那帮弟兄太忙还是不喜欢写使用手册, 翻遍整个SDK也没找到一份完整的使用说明,只有两个可怜的7zC.txt和lzma.txt可以参考,真是郁闷-_-

准备工作

首先是去http://www.7-zip.org/sdk.html下载lzma sdk包(我下载的是9.11 beta版本),我们需要SDK中的下面这些文件:

  • sdk\C\Alloc.c
  • sdk\C\LzFind.c
  • sdk\C\LzFindMt.c
  • sdk\C\LzmaDec.c
  • sdk\C\LzmaEnc.c
  • sdk\C\Threads.c
  • sdk\CPP\7zip\Common\CWrappers.cpp
  • sdk\CPP\7zip\Common\FileStreams.cpp
  • sdk\CPP\7zip\Common\StreamUtils.cpp
  • sdk\CPP\7zip\Compress\LzmaDecoder.cpp
  • sdk\CPP\7zip\Compress\LzmaEncoder.cpp
  • sdk\CPP\Common\StringConvert.cpp
  • sdk\CPP\Windows\FileIO.cpp

当然,还有头文件
我是在Windows下使用的,没在Unix下试过,从头文件定义上看,如果在Unix下,只要把sdk\CPP\Windows\FileIO.cpp改成sdk\CPP\Common\C_FileIO.cpp就可以了。

解压LZMA文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include "sdk/CPP/Common/MyInitGuid.h" //定义相关UUID
#include "sdk/CPP/7zip/Compress/LzmaDecoder.h"
#include "sdk/CPP/7zip/Common/FileStreams.h"
#include "sdk/CPP/Windows/NtCheck.h" //WinNT版本定义
int main(int argc, char* argv[])
{
NCompress::NLzma::CDecoder dec;
CInFileStream InStm;
COutFileStream OutStm;

if(argc <= 2){
std::cout << "用法:" << std::endl;
std::cout << " unLzma LZMA文件名 解压后的文件名" << std::endl;
return 0;
}
// 打开LZMA文件
if(!InStm.Open(argv[1])) return -1;
// 建立解压文件
OutStm.Create(argv[2], true);
// 读出LZMA压缩属性数据(5字节)
const UInt32 kPropertiesSize = 5;
BYTE properties[kPropertiesSize];
InStm.Read(properties, kPropertiesSize, 0);
// 设置LZMA压缩属性
if(!SUCCEEDED(dec.SetDecoderProperties2(properties, kPropertiesSize)))
return -1;
// 读出数据大小,注意字节存放顺序
UInt64 size = 0;
for(int i=0; i<8; i++)
{
BYTE b;
InStm.Read(&b, sizeof(b), NULL);
size |= ((UInt64)b) << (8*i);
}
// 解压到OutStm中
dec.Code(&InStm, &OutStm, 0, &size, NULL);

return 0;
}

要想正确编译这段代码,必须把本文开头的那堆文件加入到项目或makefile里一起编译。另外,在Windows下,还得链接uuid库,否则会说找不到IID_IUnknown
IID_IUnknown? 很眼熟?嗯~~搞过COM编程的人一定会感到亲切,这个SDK的C++封装借鉴了COM的思想,到处都充斥着IUnknown接口,以至于它还顺便实现了一套用于非Windows系统的COM头文件以及COM的智能指针,VARIANT封装等东东。
最后,友情提供一个测试源:到MinGW的 sourceforge网站上http://sourceforge.net/projects/mingw/files/去随便找个小的lzma来测试。当然,也可以直接跳到本文后面的LZMA压缩部分,用它来生成一个LZMA文件。

在内存中解压LZMA

看看NCompress::NLzma::CDecoder类的Code方法:

1
2
3
4
STDMETHODIMP CDecoder::Code(ISequentialInStream *inStream,
ISequentialOutStream *outStream,
const UInt64 * /* inSize */, const UInt64 *outSize,
ICompressProgressInfo *progress);

我们只要实现ISequentialInStreamISequentialOutStream就可以自定义数据来源了。
下面是我实现的一个内存流,仅演示目的,更好的方案是使用现成的SHCreateMemStream Windows API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
class CMemStream
:public IInStream,
public IOutStream,
public IStreamGetSize,
public CMyUnknownImp // LZMA SDK提供的IUnknown计数实现
{
public:
// LZMA SDK提供的实现IUnknown三个方法的宏,
MY_UNKNOWN_IMP3(IInStream, IOutStream, IStreamGetSize);
// 读数据
STDMETHOD(Read)(void *data, UInt32 size, UInt32 *processedSize)
{
size_t readed = size;
if(m_offset > m_data.size())
readed = 0;
else
{
if(m_offset + size > m_data.size())
readed = m_data.size() - m_offset;

char *pdata = static_cast<char*>(data);

std::copy(m_data.begin()+m_offset, m_data.begin()+m_offset+readed, pdata);
m_offset += readed;
}
if(processedSize) *processedSize = readed;
return S_OK;
}
// 定位数据
STDMETHOD(Seek)(Int64 offset, UInt32 seekOrigin, UInt64 *newPosition)
{
switch(seekOrigin)
{
case SEEK_END:
m_offset = m_data.size();
m_offset = (m_offset>=offset? m_offset-offset: 0);
break;
case SEEK_CUR:
m_offset += offset;
break;
case SEEK_SET:
m_offset = offset;
default:
break;
}
if(newPosition) *newPosition = m_offset;
return S_OK;
}
// 取得大小
STDMETHOD(GetSize)(UInt64 *size)
{
if(size) *size = m_data.size();
return S_OK;
}
// 写数据
STDMETHOD(Write)(const void *data, UInt32 size, UInt32 *processedSize)
{
// 准备足够的空间
if(m_offset + size > m_data.size()) m_data.resize( m_offset + size );
// 复制数据
const char *pdata = static_cast<const char*>(data);
std::copy(pdata, pdata+size, m_data.begin()+m_offset);
m_offset += size;
if(processedSize)*processedSize = size;
return S_OK;
}
// 直接设置大小
STDMETHOD(SetSize)(Int64 newSize)
{
m_data.resize(newSize);
return S_OK;
}

//默认构造
CMemStream()
:m_offset(0)
{ }
//输入一串内存数据
template <class InputIterator>
CMemStream(InputIterator begin, InputIterator end)
:m_data(begin, end), m_offset(0)
{ }
//析构
virtual ~CMemStream() {}
//得到流中的数据
std::vector<char>::iterator begin(){
return m_data.begin();
}
std::vector<char>::iterator end(){
return m_data.end();
}
std::vector<char>::const_iterator begin() const{
return m_data.begin();
}
std::vector<char>::const_iterator end() const{
return m_data.end();
}
protected:
std::vector<char> m_data; //使用vector作为内存流底层存储方案
size_t m_offset;
};

这里的CMyUnknownImpMY_UNKNOWN_IMP3是LZMA SDK“友情提供”的简化COM编程的东东,CMyUnknownImp里定义了一个整型数据,用于引用记数。宏MY_UNKNOWN_IMP3则实现了 IUnknwonAddRef,ReleaseRefQueryInterface三大方法,后面的数字3指的是可以放几个参数(即实现了几个接口),还有MY_UNKNOWN_IMPMY_UNKNOWN_IMP1MY_UNKNOWN_IMP2MY_UNKNOWN_IMP5可供选 择。

下面的程序把LZMA文件解压到我们的内存流中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <vector>
#include <iterator>
#include "sdk/CPP/Common/MyInitGuid.h"
#include "sdk/CPP/7zip/Compress/LzmaDecoder.h"
#include "sdk/CPP/7zip/Common/FileStreams.h"
#include "sdk/CPP/Windows/NtCheck.h"

// CMemStream,内存的输入输出流接口
class CMemStream
:public IInStream,
public IOutStream,
public IStreamGetSize,
public CMyUnknownImp
{
public:
...
};

int main(int argc, char* argv[])
{
NCompress::NLzma::CDecoder dec;
CInFileStream InStm;
CMemStream OutStm;

if(argc <= 1){
std::cout << "用法:" << std::endl;
std::cout << " unLzma LZMA文件名" << std::endl;
return 0;
}
// 打开LZMA文件
if(!InStm.Open(argv[1])) return -1;

// 读出LZMA压缩属性数据(5字节)
const UInt32 kPropertiesSize = 5;
BYTE properties[kPropertiesSize];
InStm.Read(properties, kPropertiesSize, 0);
// 设置LZMA压缩属性
if(!SUCCEEDED(dec.SetDecoderProperties2(properties, kPropertiesSize)))
return -1;
// 读出数据大小
UInt64 size = 0;
for(int i=0; i<8; i++)
{
BYTE b;
InStm.Read(&b, sizeof(b), NULL);
size |= ((UInt64)b) << (8*i);
}
// 解压到OutStm中
dec.Code(&InStm, &OutStm, 0, &size, NULL);
// 打印出来瞧瞧
// 如果是二进制数据的LZMA包,可以考虑把数据保存出来和之前解压的文件对比
// 当然,如果你相信我的话,就不用这句了,放心吧,一定是正确的
std::copy(OutStm.begin(), OutStm.end(), std::ostream_iterator<char>(std::cout));
return 0;
}

ICompressProgressInfo接口

ICompressProgressInfo接口用于得到解压、压缩的过程信息,用它来更新进度条比较合适。
下面是我实现的ICompressProgressInfo接口,只是简单地向控制台输出百分比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CProgressInfoShow
: public ICompressProgressInfo,
public CMyUnknownImp
{
public:
MY_UNKNOWN_IMP1(ICompressProgressInfo);

STDMETHOD(SetRatioInfo)(const UInt64 *inSize, const UInt64 *outSize)
{
std::cout << *inSize * 100.0 / m_TotalSize << std::endl;
return S_OK; //如果返回其它则中断
}

CProgressInfoShow(UInt64 inTotalSize)
:m_TotalSize(inTotalSize)
{}
private:
UInt64 m_TotalSize;
};

修改一下我们前面的代码,在解压前弄一个CProgressInfoShow实例:

1
2
3
4
5
// 解压到OutStm中
UInt64 totalsize;
InStm.GetSize(&totalsize);
CProgressInfoShow pi( totalsize );
dec.Code(&InStm, &OutStm, 0, &size, &pi);

压缩LZMA数据

压缩过程和解压相差不大,只是这次用的是NCompress::NLzma::CEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

#include "sdk/CPP/Common/MyInitGuid.h" //定义相关UUID
#include "sdk/CPP/7zip/Compress/LzmaEncoder.h"
#include "sdk/CPP/7zip/Common/FileStreams.h"
#include "sdk/CPP/Windows/NtCheck.h" //WinNT版本定义

int main(int argc, char* argv[])
{
NCompress::NLzma::CEncoder enc;
CInFileStream InStm;
COutFileStream OutStm;

if(argc <= 2){
std::cout << "用法:" << std::endl;
std::cout << " Lzma 源文件名 输出文件名" << std::endl;
return 0;
}
// 打开源文件
if(!InStm.Open(argv[1])) return -1;
// 建立压缩文件
OutStm.Create(argv[2], true);
//使用默认压缩参数
enc.SetCoderProperties(NULL,NULL,0);
//向LZMA文件写入压缩参数
enc.WriteCoderProperties(&OutStm);
// 取得源文件大小
UInt64 size;
InStm.GetSize(&size);
// 向LZMA文件写入源大小,注意字节顺序
for(int i=0; i<8; i++)
{
BYTE b = BYTE(size >> (8*i));
OutStm.Write(&b, sizeof(b), NULL);
}
// 压缩
enc.Code(&InStm, &OutStm, NULL, NULL, NULL);

return 0;
}

如果你装了7-zip的话,可以打开我们压缩的lzma文件。

根据这段代码,可以对LZMA文件格式有个大致的了解: 压缩参数(5字节) + 源大小(8字节) + 压缩数据。头上的五个字节保存了LZMA压缩相关的参数,比如字典大小、压缩模式、压缩线程等。本例中的enc.SetCoderProperties(NULL,NULL,0);就是用来设置这些参数的,它的原型是:

1
2
STDMETHODIMP CEncoder::SetCoderProperties(const PROPID *propIDs,
const PROPVARIANT *coderProps, UInt32 numProps)
  • propIDs 是PROPID数组,这个数组中的每个PROPID指定一种LZMA压缩参数。
  • coderProps 是PROPVARIANT数组,保存对应PROPID的LZMA压缩参数数值。
  • numProps 指出数组大小。

对于LZMA算法来说,可以接受的PROPID(NCoderPropID名空间下)见下表:

PROPID 描述 类型 取值范围 默认值
kDictionarySize 字典大小 VT_UI4 2^12 ~ 2^30 2^24 (16M)
kAlgorithm 压缩模式 VT_UI4 0或1 1(最大压缩)
kEndMarker 是否有流结束标记 VT_BOOL VARIANT_TRUE VARIANT_FALSE VARIANT_FALSE
kNumThreads 工作线程数 VT_UI4 依系统而定 依系统而定
kPosStateBits set number of pos bits VT_UI4 0~4 2
kLitContextBits set number of literal context bits VT_UI4 0~8 3
kLitPosBits set number of literal pos bits VT_UI4 0~4 0
kNumFastBytes set number of fast bytes VT_UI4 5~273 128
kMatchFinder set Match Finder VT_BSTR bt2, bt3, bt4, hc4 bt4
kMatchFinderCycles set number of cycles for match finder VT_UI4

注:线程数量可以依据NWindows::NSystem::GetNumberOfProcessors()取得的CPU内核数确定,后的6项是英文描述,因为在下实在不知道它们的具体含义,惭愧~~

下面的例子设置了二个线程并行压缩、加入流结束标记、字典大小为1M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int main(int argc, char* argv[])
{
NCompress::NLzma::CEncoder enc;
CInFileStream InStm;
COutFileStream OutStm;

if(argc <= 2){
std::cout << "用法:" << std::endl;
std::cout << " Lzma 源文件名 输出文件名" << std::endl;
return 0;
}
// 打开源文件
if(!InStm.Open(argv[1])) return -1;
// 建立压缩文件
OutStm.Create(argv[2], true);

PROPID propIDs[] = {
NCoderPropID::kDictionarySize,
NCoderPropID::kEndMarker,
NCoderPropID::kNumThreads
};
PROPVARIANT props[3];
props[0].vt = VT_UI4;
props[0].ulVal = 1<<20; //2^20 = 1M
props[1].vt = VT_BOOL;
props[1].boolVal = VARIANT_TRUE; //加入结束标记
props[2].vt = VT_UI4;
props[2].ulVal = 2; //双线程并行压缩

enc.SetCoderProperties(propIDs,props,3);
enc.WriteCoderProperties(&OutStm); //向LZMA文件写入压缩参数
// 因为有了流结束标记,文件大小信息已经不重要了,直接写个UInt64(-1)就行了。
UInt64 size = UInt64(-1);
OutStm.Write(&size, 8, NULL);
// 压缩
enc.Code(&InStm, &OutStm, NULL, NULL, NULL);

return 0;
}

其它压缩算法

打开SDK文件夹的CPP\7zip\Compress目录,会发现除了Lzma的算法以外,还有Lzma2和Ppmd算法。想试试 吗?它们的C++接口差不多,只要把上面的例子小小的修改一下就可以了。
下面的例子使用Lzma2算法压缩一个文件,只需改一个头文件和一个名空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>

#include "sdk/CPP/Common/MyInitGuid.h" //定义相关UUID
#include "sdk/CPP/7zip/Compress/Lzma2Encoder.h"
#include "sdk/CPP/7zip/Common/FileStreams.h"
#include "sdk/CPP/Windows/NtCheck.h" //WinNT版本定义

int main(int argc, char* argv[])
{
NCompress::NLzma2::CEncoder enc;
CInFileStream InStm;
COutFileStream OutStm;

if(argc <= 2){
std::cout << "用法:" << std::endl;
std::cout << " Lzma2 源文件名 输出文件名" << std::endl;
return 0;
}
// 打开源文件
if(!InStm.Open(argv[1])) return -1;
// 建立压缩文件
OutStm.Create(argv[2], true);

enc.SetCoderProperties(NULL,NULL,0); //使用默认压缩参数
enc.WriteCoderProperties(&OutStm); //向LZMA文件写入压缩参数
// 取得源文件大小
UInt64 size;

InStm.GetSize(&size);
// 向LZMA文件写入源大小,注意字节顺序
for(int i=0; i<8; i++)
{
BYTE b = BYTE(size >> (8*i));
OutStm.Write(&b, sizeof(b), NULL);
}

// 压缩
enc.Code(&InStm, &OutStm, NULL, NULL, NULL);

return 0;
}

要编译通过,除了项目中加入本文开头的那些文件以外,还要加入CPP\7zip\Compress\Lzma2Encoder.cppC\MtCoder.c文件。