您的位置:首页 > 其它

自己动手写压缩软件

2012-06-07 00:23 260 查看
自己动手写压缩软件

作者: huzy

【 源码下载 : http://sourceforge.net/projects/hzyzip/

咳咳 !!!

首先,有点小激动,(*^_^*),写了两天两夜再加一个清晨的压缩软件“成功”通过啦!

压缩了一曲劲爆的 MV ,再解压,然后边听边写 …… 如有笔误,纯属激动!!!

打这个“歪主意”很久了,就是没动手,前些天被偶那亲爱的哥哥给激了下,所以决心“玩玩”。

经过偶的“高速 CPU ”规划了下,首先得准备好 Huffman 算法(算法是偶的强项,过去一年多,偶吃饱了就干这个,

所以小小 Huffman 不成问题);然后得测试一下读取所有格式的文件,以 ASCII 码方式读取

(这个是偶哥哥提示的,其实偶也知道,可就是想歪了,一直没到这个点上);

最后就是把这两个idea 合成在一起,听起来似乎很简单哦……动手玩玩!

整体的构想:

1. 按 ASCII 码读取文件,统计文件中每个ASCII码值对应字符的个数,作为权值,然后进行 Huffman 编码;



2. 然后将目标文件中的每个 ASCII 码字符用对应的 Huffman 编码字符串替换,替换后再将 '0' '1' 字符串转化为二进制流,

再将二进制流依次分割成8位的若干小片段,最后将每8位二进制转化为对应大小的整数,即为新的 ASCII 码值;

(参看函数:bool Huffman::condensingFile(char sourceFile[], char targetFile[], HCNode *HC))

嗯 ~~ 大概你没看太明白……

所以我涂鸦了个“思路图”,看看 ~ ~



上面的图中所示数据是我在调试程序的时候 copy 下来的,认真的你有没有发现……

呵呵!源文件中的头 10 个ASCII 码压缩后变成了 8 个ASCII 码了 ! (*^_^*)效果来了!

也许你还发现了第一个 ASCII 码对应的 Huffman 编码长度为 14,也就是说一个字节的数据被

“压缩”成了近两个字节(14 / 8 = 1.75) !

是否文件不但没有被压缩反而会被扩张呢?咳咳!实践加理论证明:不会。

为了便于解压,每次都会保存目标文件对应的 Huffman 树;

3. 压缩流程想好了,接下来是解压,首先从压缩文件对应的 Huffman 树文件开始,构建一棵 Huffman 树,

再就是压缩的逆操作:

以 ASCII 码形式读取压缩文件 ==> 转化成二进制字符串 ==> 二叉搜索 Huffman 树与二进制字符串匹配

==> 锁定叶子节点得到节点中保存的 ASCII 码 ==> 写入文件即得到解压文件。

首先测试以 ASCII 码方式读文件:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
FILE  *fp;
FILE  *fcopy;

/*
**  如果: ch 是 unsigned char 型的,那么 ch = fgetc(fp), ch 将不可能为 EOF;
**  如果: ch 是 char 型的,那么 ch = fgetc(fp), 当 ch = EOF 结束时,
**  可能没有读完文件就已终止!
**  所以: ch 应该设为 int 型 。
*/
//   unsigned char  ch;      // wrong ! can't be EOF !
//   char ch;
int  ch;                 // right !

   int   i, len ;
   int   count = 0;
   char  fileName[100];
   char  copyFile[100];
   char  postfix[] = "_copy";

   printf("> Input fileName : ");
   scanf("%s",fileName);

fp = (FILE*)fopen(fileName,"rb");
if( !fp )
{
printf("can't open the file .\n");
     return 0;
}

   strcpy(copyFile, fileName);

   len = strlen(fileName);

   for(i=len; i>0; i--)
     if(fileName[i] == '.')
       break;
   strcpy(&File[i], postfix);
   len = strlen(postfix);
   strcpy(&File[i+len], &fileName[i]);

fcopy = fopen(copyFile,"wb");   // copy

while( (ch = fgetc(fp)) != EOF )
{
fputc(ch,fcopy);
if((++count)%20 == 0)
printf("\n");
printf("%d ",ch);
}
printf("\n total : %d \n",count);
fclose(fp);
fclose(fcopy);
return 0;
}


输入目标文件路径名后就看到整版的数字啦,如下图所示:

【源代码参看 readfile_test 文件夹】



然后就是测试Huffman 算法:



/*=============================================================*/
/*                                                             */
/*                         Huffman 编码                        */
/*                                                             */
/*=============================================================*/

#ifndef HUFFMAN_CODE_STRUCT_H
#define HUFFMAN_CODE_STRUCT_H

/*=============================================================*/
#define INFINITY 1000000               // 自定义“无穷大”

/* 数据结构 */
typedef struct
{
unsigned int weight;
unsigned int parent;
unsigned int lchild;
unsigned int rchild;
}HTNode,*HuffmanTree;

typedef char **HuffmanCode;

/*=============================================================*/
/* 函数声明 */
void HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n);
void Select(HuffmanTree *tree, int n, int *s1, int *s2);
int  Min(HuffmanTree tree, int n);

/*=============================================================*/
// 从哈弗曼树的 n 个结点中选出权值最小的结点

int Min(HuffmanTree tree, int n)
{
unsigned int min = INFINITY;
int flag;
int i;

for(i=1; i<=n; i++)
if(tree[i].weight<min && tree[i].parent==0)
{
min = tree[i].weight;
flag = i;
}

tree[flag].parent = 1;
return flag;
}

/*=============================================================*/
// 在哈弗曼树的 n 个结点中选出权值最小的两个结点,记录其序号s1,s2

void Select(HuffmanTree *tree, int n, int *s1, int *s2)
{
int temp;

*s1 = Min(*tree,n);
*s2 = Min(*tree,n);

//    if(s1 > s2)                                // attention !

if( (*tree)[*s1].weight > (*tree)[*s2].weight )
{
temp = *s1;
*s1  = *s2;
*s2  = temp;
}
}

/*=============================================================*/

void HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n)
// HT 为二级指针,双向传值
{
char  *cd;
int i;
int s1, s2;
int go;
int cdlen;
int m = 2*n-1;
HuffmanTree p;

if( n <= 1 )
return ;
/*--------------------------------------------------------*/
// 1>. 初始化

//*HT = (HuffmanTree)malloc((m+1)*sizeof(HuffmanTree)); //

*HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode)); // 0 号单元不用
p = *HT + 1;

for(i=1; i<=n; i++, p++, w++)
{
(*p).parent = 0;
(*p).lchild = 0;
(*p).rchild = 0;
(*p).weight = *w;
}
for(i=n+1; i<=m; i++, p++)
{
p->parent = 0;
p->rchild = 0;
p->lchild = 0;
p->weight = 0;
}
/*--------------------------------------------------------*/
// 2>. 构建树

for(i=n+1; i<=m; i++)           // i<=m
{
Select(HT, i-1, &s1, &s2);

(*HT)[s1].parent = i;
(*HT)[s2].parent = i;

(*HT)[i].lchild = s1;
(*HT)[i].rchild = s2;

(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
/*--------------------------------------------------------*/
// 3>. 求 HF 编码                ( 从根结点到叶子结点求取 )

*HC = (HuffmanCode)malloc( (n+1)*sizeof(char *) );
cd  = (char *)malloc( n*sizeof(char) );             // 编码暂存串

if( !cd )
{
printf("> failure \n");
return ;
}
cdlen = 0;
go  = m;                                   // 从根结点开始
for(i=1; i<=m; i++)      // 利用 weight 来做左右孩子遍历的标记
(*HT)[i].weight = 0;

while( go )
{
if( 0 == (*HT)[go].weight )             // 左右孩子都未遍历
{
(*HT)[go].weight = 1;                   // 标为左访问
if( (*HT)[go].lchild != 0 )                // 左孩子存在
{
go = (*HT)[go].lchild;
cd[cdlen++] = '0';
}
else                                // 左孩子不存在
{
if( 0 == (*HT)[go].rchild )          // 右孩子不存在
{
(*HC)[go] = (char *)malloc( (cdlen+1) * sizeof(char) );
cd[cdlen] = '\0';
strcpy( (*HC)[go], cd );
}
}
}
else
{
if( 1 == (*HT)[go].weight )          // 左孩子已经遍历
{
(*HT)[go].weight = 2;              // 标为右访问
if( (*HT)[go].rchild != 0 )
{
go = (*HT)[go].rchild;
cd[cdlen++] = '1';
}
}
else                         // 左右孩子都已经遍历
{
go = (*HT)[go].parent;          // 退回到双亲结点
-- cdlen;
}
}
}
}
/*=============================================================*/
#endif           // 预编译结束


最后测试了一下我的大名(hu zhen yang)和今天的日期(2011.8.6)组成的叶子节点和权值,

得到每个字符串的对应编码,如下图所示:

【源代码参看 Huffman 文件夹】



偶特别的喜欢用 C 语言写程序,虽然偶的 C++ 学得特别认真,看了很多 C++ 写的代码,

偶在MFC下面也是用C++风格来写的,

可一旦要偶自己来封装个类,偶就不愿了,改不了 C 这行当。

不过这次偶可是认真筹划,用 C++ 自己封装了两个类(*^_^*),不是很有技术含量,但还勉强过意得去啦!

…… 【预编译和宏定义略】

class Huffman
{
public:
Huffman();
Huffman(Map *mapArray, int countLeaf);
~Huffman();

bool createFromFile(char *InFileName, char postfix[]);
bool writeToFile(char *OutFileName, char *postfix);
bool CodingFromTree();  // 二叉遍历已有的 Huffman 树获取编码

void setInfo(Map *mapArray, int countLeaf);
void HuffmanCoding();

HuffmanCode getHFcode();
int  getLeafCount();

/*===================================================================*/
public:
bool condensingFile(char sourceFile[], char targetFile[], HCNode *HC);
bool expandingFile(char sourceFile[], char targetFile[]);

protected:
int  BStringToInt(char str[], int str_len);
void IntToBString(int k, char str[], int str_len);
/*===================================================================*/

protected:
int  Min(HuffmanTree tree,int n);
void Select(HuffmanTree *tree, int n, int *s1, int *s2);

private:
int         m_countLeaf;                         // 叶子数
Map      *m_pMapArray;               // 叶子权值数组指针

HuffmanCode  HC;
HuffmanTree  HT;
};

……

class Zip
{
public:
Zip(char *fileName, bool flag);
Zip();
~Zip();

bool createZipFromFile(char *fileName, bool flag);
bool countMapArray();
void condenseFile();                  // 压缩文件(进行编码)
void expandeFile();                              // 解压文件
bool saveHuffmanTree(char  fileName[], char *postfix);
bool loadHuffmanTree(char   fileName[], char postfix[]);

/*================================================*/
void printMapArray();
void printHuffmanCode();
long totalByte();                           // 返回文件的大小
/*================================================*/
protected:
bool openFile();
HuffmanCode getHFcode();                        // 获取编码

private:
char  m_fileName[256];
Map   m_mapArray[256];
long  m_totalByte;
int   m_leafCount;                            // 有效叶子数

Huffman  m_huffmanProc;
HuffmanCode  m_code;
};

……


好不容易写完,兴奋的测试起来,结果首次测试,就满文件的乱码(如下图所示)……

是偶邪恶了?还好让偶看到了一点点希望,那一串串“======================”证明还没“邪”多远!



经过认真排查,终于发现问题出在解压时,搜索 Huffman 树,匹配成功的情况下二进制流未回退一步,更正代码截图如下所示:



修改后再测试截图如下:



最左边是源文件,中间是压缩后再解压的文件,哈哈,兴奋!

好了,再来看看怎么压缩文件和解压文件的:

/*=============================================================*/
// 从目标文件到压缩文件,按 Huffman 编码 ( HC )压缩并存储

const int BUF_LEN  = 960;
const int BUF_LEN2 = BUF_LEN + 40;
const int STR_LEN  = 8;                    // str 的长度固定为 8
const int STR_LEN2 = STR_LEN + 2;

bool Huffman::condensingFile(char sourceFile[], char targetFile[], HCNode *HC)
{
FILE *sfp = fopen(sourceFile,"rb");
if( !sfp )   return false;
FILE *tfp = fopen(targetFile,"wb");
if( !tfp )   return false;

int i, j, k;
int len, pos;
int ch, key;
int res_len = 0;

char str[STR_LEN2];                  // 比 STR_LEN 大一点
char temp[BUF_LEN2];               // 比 BUF_LEN 大一点

/*
** 关于已取得的 Huffman 编码表 HC
** 建立一个 ascii 码到 HC 数组下标的映射表 !!!
** 如果每次都遍历匹配会降低压缩效率。
*/
int asciiMap[256];
for(i=0; i<m_countLeaf; i++)
{
asciiMap[ HC[i+1].ascii ] = i+1;            // 0 号单元未用
}

while( (ch = fgetc(sfp)) != EOF )
{
len = strlen( HC[ asciiMap[ch] ].code );

for(i=0; i<len; i++)
temp[ res_len++ ] = HC[ asciiMap[ch] ].code[i]; // 按 Huffman 编码转化

if( res_len >= BUF_LEN )    // 长度达到 BUF_LEN 就处理
{
pos = 0;
k = 0;
while( pos <= BUF_LEN )
{
str[ k++ ] = temp[pos++];
//if( k == STR_LEN - 1 )   // wrong !
if( k == STR_LEN )
{
k = 0;
key = BStringToInt(str,STR_LEN);

fputc(key, tfp);
}
}
for(i=BUF_LEN, j=0; i<res_len; i++,j++) // 把未处理完的字符前移
temp[j] = temp[i];

res_len = j;
}
}
if( res_len > 0 )    // res_len < BUF_LEN  ( 960 )
{
pos = 0;
k = 0;
while( pos < res_len )
{
str[ k++ ] = temp[pos++];
//if( k == STR_LEN - 1 )       // wrong !
if( k == STR_LEN )
{
k = 0;
key = BStringToInt(str, STR_LEN);

fputc(key, tfp);
}
}
if( k > 0 )      // k < STR_LEN    ( 8 )
{
/*
** 对整个文件最后一个字符的处理:
*/
//key = BStringToInt(str, k);       // 不足八位,高位补零
key = BStringToInt(str, STR_LEN); // 不足八位,地位补零
fputc(key, tfp);
}
}
fclose(sfp);
fclose(tfp);

return true;
}

/*=============================================================*/
// 从压缩文件到目标文件,解压并存储

bool Huffman::expandingFile(char sourceFile[], char targetFile[])
{
FILE *sfp = fopen(sourceFile,"rb");        // 源文件(压缩文件)
if( !sfp )   return false;
FILE *tfp = fopen(targetFile,"wb"); // 目标文件(即将被解压后的文件)
if( !tfp )   return false;

int ch;
int i, j, rear;
int r, r_pre;
int pos;
int res_len = 0;
char key[STR_LEN2];       // 10
char temp[BUF_LEN2];      // 1000

while( (ch = fgetc(sfp)) != EOF )
{
IntToBString(ch, key, STR_LEN);

for(i=0; i<STR_LEN; i++)
temp[res_len++] = key[i];

if(res_len >= BUF_LEN)     // 长度达到 BUF_LEN 就处理
{
pos = 0;
r = m_countLeaf * 2 - 1;                        // 根
r_pre = r;

while( pos <= BUF_LEN )
{
if( r == 0 )             // r=0, r_pre 指向叶子结点
{
/*
** 当 r == 0 时,表示 r 的前一个结点是 Huffman 树的叶子结点;
** 然而,还多进行了一次 pos ++ 操作;应该回退一位。
** 故: 应该在找到叶子结点时  pos -- 。
*/

pos -- ;                  // very important !!!

rear = pos;         // 记录串中已处理的位置
r = m_countLeaf * 2 - 1;                // 根
//fputc(r_pre, tfp);                 // wrong !
fputc(HT[r_pre].ascii, tfp);              // !!!
}

r_pre = r;
temp[ pos ] == '0' ? r = HT[r].lchild : r = HT[r].rchild;
pos ++;
}
for( i=rear,j=0; i<res_len; i++,j++ ) // 把未处理完的字符前移
temp[j] = temp[i];

res_len = j;
}
}
if( res_len > 0 )        // res_len < BUF_LEN   ( 960 )
{
pos = 0;
r = m_countLeaf * 2 - 1;
r_pre = r;

while( pos < res_len )
{
if( r == 0 )
{
pos -- ;                     // very important !!!

rear = pos;
r = m_countLeaf * 2 - 1;
//fputc(r_pre, tfp);                      // wrong !
fputc(HT[r_pre].ascii, tfp);
}

r_pre = r;
temp[ pos++ ] == '0' ? r = HT[r].lchild : r = HT[r].rchild;
}
// 如果还有未处理的,省略。因为写入时最后一个字节采用了地位补零的方式。
}
fclose(sfp);
fclose(tfp);
return true;
}
/*=============================================================*/


接下来,我又测试了 BMP , jpg 文件,Map4 文件:



下图是一部 491M 大小的电影的 Huffman 编码表部分截图。





关键错误排查:

1. 当我测试全篇只有一个ASII码值的字符文件时,程序崩溃了!

原因很简单:

Huffman编码至少得需要两个节点才能编码。

对策:

方案1> 对文件遍历,对上述情况直接“跳出”,不予编码,记录该ASCII码和字符数量,简单快捷,压缩比最大。

方案2> 我再添加一个任意的ASCII码值,并且令其权值为0,

这时与文件中的那个ASCII码值就凑成了两个,就可以编码了!

我的处理方法:

为了适应整个软件的通用性,即压缩后产生两个文件,一个“资源文件”和一个“编码文件”,我采用的时方案2。

【补充: 对于空文件,直接跳出,因为对空文件压缩毫无意义。 】

2.在我随机的改变了文件大小的情况下,测试解压,发现在解压后的文件的尾部出现了乱码:



原因:

参看上文图解“压缩映射表”,我将每 8 位二进制码组成一个小片段,很显然在大多数情况下全文的二进制流的大小不会恰好是 8 的整数倍 !

而我的处理方法是将全文件的最后一个不足 8 位的二进制片段补 0 成为 8 位,而解压时,

很有可能补上的 0 恰好构成了一个编码,导致解压出了多余的字符。所以就有可能出现了上图中所示的乱码。

对策:

在压缩时,统计整个文件的大小 Count,并将文件的总字节数Count写入编码文件。在压缩时就只解压出 Count 个字节,

多余部分是无效的 0 ,予以略去。

性能比较与软件扩展:

好了!该“臭美”一下了!

与专业的 zip 压缩软件“比拼”!我的软件压缩速度居然比 zip 快 !呵呵……不过压缩比就逊色多了, 同一部电影,

我的压缩后还有 490 M,而 zip 压缩后只有 477 M;而且解压速度也差了很多,zip 解压 491 M的电影只须22 秒,

我的却要将近 3 分钟,小小打击了……不过我知道时间消耗在哪了:我的解压采用的是每次从 Huffman 树的根节点搜索,

这种方式无疑会更耗时。



虽然在效率上比不了 zip 等专业压缩软件,但是我可以换换角度——把它做成小型文件的加密软件 !

各种细节处理与技巧运用,参考源码文件,偶注解得还算详细 ! (*^_^*)

huzy

2011.8.6



今天情人节 ! 没情人的孩子

在家写软件 ! )

补充:

载入界面后的压缩软件截图:

【 采用多线程技术避免界面冻结 】

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: