自己動手寫壓縮軟件


   自己動手寫壓縮軟件   

 

                                                                                              作者:  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 

                                                       ( 今天情人節 ! 沒情人的孩子在家寫軟件 ! )

 

補充:

載入界面后的壓縮軟件截圖:

【 采用多線程技術避免界面凍結 】

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM