自己動手寫壓縮軟件
作者: 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
( 今天情人節 ! 沒情人的孩子
在家寫軟件 ! )
補充:
載入界面后的壓縮軟件截圖:
【 采用多線程技術避免界面凍結 】