自己动手写压缩软件

   自己动手写压缩软件   

 

                                                                                              作者:  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 码压缩后变成了 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++ 写的代码,

偶在MFC下面也是用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.在我随机的改变了文件大小的情况下,测试解压,发现在解压后的文件的尾部出现了乱码:

 

 

原因:

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

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

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

对策:

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

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

 

性能比较与软件扩展:

 

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

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

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

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

这种方式无疑会更耗时。

 

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

 

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

                                                    huzy 

                                                              2011.8.6 

                                                       ( 今天情人节 ! 没情人的孩子在家写软件 ! )

 

补充:

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

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

 

 

 

 

 

 

posted @ 2012-06-07 00:23  hp+y  Views(6463)  Comments(1Edit  收藏  举报