|
この文章はC MAGAZINE 2002/11月号に掲載された以下の記事の原稿です。
【特集2】 オープンソースの圧縮ツール「yz2」
[Part1] yz2の概要と圧縮アルゴリズムの基礎知識
[Part2]
yz2の圧縮アルゴリズムと活用方法
校正される前の原稿ですので、紙面の情報と若干異なります。
バックナンバーも若干残っているようですのでこちら↓のページからご購入いただけます。
http://books.softbank.co.jp/bm_detail.asp?sku=1008200207
山崎 敏 (YAMAZAKI Satoshi)
プログラマ
yz2 は、私(山崎 敏)が開発したアーカイバ(圧縮ツール)です。ちょっとした圧縮率と、なかなかの圧縮速度を特徴とするオープンソースな圧縮ツールです。
記事の全体を半分に分け、前半に概要(とくに圧縮などに関する知識の少ない方に向けて)を書きます。そして、後半には詳細(ある程度プログラミングの知識のある方に向けて)を書きます。
・公開中のファイル
2002年8月現在、yz2の公開中の最新バージョンはalpha6です。
公開しているURLは
http://www.01-tec.com/yz2/index.html
です。ファイル名は
DeepFreezer2_alpha6_2002_02_21.yz1 (.exe)
yz2source_2002_02_21.yz1 (.zip)
の2つです。
DeepFreezer2_alpha6_2002_02_21.yz1 には実行プログラム群(DeepFreezer2,
yz2dlg.dll, yz2enc, yzedec)を梱包しています。
yz2source_2002_02_21.yz1には上記実行プログラム群の全ソースを含んだ開発環境を梱包しています。
前半の概要ではプログラム(主にDeepFreezer2_alpha6_2002_02_21.yz1 内のプログラム)について説明します。
後半の詳細ではソースファイル(主に yz2source_2002_02_21.yz1 内の開発環境やソースコードの内容、そして圧縮アルゴリズム)について説明します。
・肩書き
某誌では私の肩書きを「漁師プログラマ」としていますが、ここでは普通に「プログラマ」とします(深い意味はありません)。実は先日、長い間お世話になった会社を辞めまして、今は独立というか一人で仕事を探し歩いている状態です。
・数学が苦手
私は高校生のころから数学が苦手でした(プログラマの半数は数学が苦手といわれていますね。要するに数学とプログラミングは無関係であるということでしょう)。しかし、圧縮アルゴリズムについてはある程度理解できました。アルゴリズムの話になると数式などの表現を用いて説明されている場合が多いのですが、私は数学が苦手なのでそのような表現はいたしません。というかできません。なので、数学の苦手な方でも難なく読めると思います。逆に、数学の得意な方には物足りないかもしれません。その点はご了承ください。
ちなみに、プログラミングのセンスって、工作のセンスだと思うのですが、そんなことを思っているのは私だけでしょうか?
ここでは、主にDeepFreezer2_alpha6_2002_02_21.yz1 に梱包されている実行プログラム群(DeepFreezer2,
yz2dlg.dll, yz2enc, yzedec)について説明をします。その前に、少し基本的な話から始めます。
●yz2とはどんなものか?
・yz2 は圧縮アルゴリズムの形式名
yz2 とはアーカイバソフトの圧縮形式の形式名です。アーカイバとは主に圧縮ツールのことをいいます。LZH,ZIP
などと同じ種類のツールです。
アーカイバとはもともとは複数のファイルをひとつにまとめる tar のようなツールのことを呼んでいたと思うのですが「どうせまとめるならサイズを小さく圧縮しようよ」ということで付加機能として圧縮機能が付き、その後、圧縮機能がメインになってもアーカイバと呼ばれているようです。
yz2 は LZHやZIP とは違う新しい圧縮形式なので新しい名前を付けました。
単純に作者である私の名前(YAMAZAKI)から'y'と'z'をとって付けました。'z'は
zip という意味もこめてありますが深い意味はありません。'2'は単純にバージョン2(2つ目)という意味です。
圧縮形式名に自分の名前を付けるなんて、さぞかし気合が入っているのか?とか、会心の作なのか?と思われがちですが、そんなことはありません。とくに悩むくらいなら自分の名前を付けてしまえと単純な気持ちからでした。もちろん中途半端なモノには自分の名前を付けるつもりはありませんので自信作ではあります。
●どんなことができるのか?
yz2 は圧縮ツールです。他の圧縮ツールと同じようにファイルの圧縮ができます。圧縮ツールとは
Word や Excel などの文章データをバックアップしたりメールに添付して送るときに、ファイルのサイズを小さく圧縮することで保存媒体への負荷やネットワークへの負荷を減らすことができます。もっと簡単に説明すると、ある
Word の文書ファイルが 1.4Mバイトを超えてしまうと FD などに入らなくなります。そこで圧縮ツールをつかって半分ほどのサイズに圧縮するのです。すると
FD に入るようになり、他のPCとのやりとりができるわけです。
・どんなデータに対して効果的か?
yz2 は Word や Excel などの文章データや PowerPoint とかCADソフトなどのデータファイルに効果的です。高速に小さく圧縮できます。
さらには、画像であっても写真などの自然なデータではなくノイズの少ないキャプチャ画面やイラストのようなBMPファイルに対しても効果を発揮します。
C マガジンの読者の方には、開発環境(ソースコード)のバックアップなどにも向いていると思います。VisualC++
の開発環境などをフォルダごと圧縮するのです。簡単にしかも高速に圧縮することができます。私はそうして今までの開発環境を保存してきました。そうすることで何度か修羅場をくぐり抜けて来た記憶もあります。私の場合は、最新版を無くした時の被害を最小限に抑えるというメリットよりも、最新版の動作がなぜかおかしくなってしまって原因がつかめず難儀しているときに、一つ前(正しく動いているモノ)と比較することで原因が解明できるというメリットをよく感じました。
yz2 には DeepFreezer2 という簡易GUI(Windows用のインターフェース)も用意していますので、Windowsユーザの方は簡単に扱うことができると思います。DeepFreezzr2 についての詳細はあとで説明します。
・yz2の性能
以下にいろいろなデータに対して計測した平均値を図にします。LZHの性能を100として
ZIP と yz2 を比較しています。
「図 性能の比較」

圧縮率もよく、圧縮速度や解凍速度は約2倍ほど速いという結果になりました。どんな環境でも、どんなファイルでもこのような結果になるわけではありませんが、私の環境で、私の揃えたファイルではこのような結果が出たということです。揃えたファイルの中には圧縮のテストデータとして有名な「CanterburyCorpus」のファイルも含まれています。CanterburyCorpusのファイルはどちらかと言えば小さいファイルで、数も少ないので、CPU速度の遅いマシンでテストしないと大きな速度差が出にくいのが難点です。
もう少し具体的には、誰もが入手可能であり、比較的大きなファイルでためしてみることにします。以下のURLに
http://asia.microsoft.com/downloads/release.asp?ReleaseID=33706
DirectX 8.1 Software Development Kit (SDK)
DX81SDK_FULL.exe - 173,779 Kb
というファイルがあります。このファイルを展開すると、
DXSDK\samples\Multimedia\Direct3D
というフォルダがあり、中には 400個(全部で16,776,124バイト)のファイルがあります。このファイル群を圧縮すると、
「表 Direct3Dフォルダの圧縮率」
| 圧縮形式 |
圧縮後のサイズ |
圧縮処理時間 |
| LZH |
7,937,650 バイト |
14秒 |
| yz2 |
2,652,690 バイト |
10秒 |
と、このようにに大きくファイルサイズが変わります。極端な例かもしれませんが、このような結果になることもあるということです。
・もっと凝った使い方
yz2 には DLL もあります。yz2 の主な機能は DLL の中に入っています。この
DLL を呼び出すことで yz2 の機能を簡単に使うことができます。自作ツールへ組み込んだりすることで、ツールの機能を向上できます。DLL
なので開発言語は限定しません。DLL を呼び出すことのできる言語であれば容易に呼び出すことができるでしょう。
また yz2 はオープンソースです。すべてのソースの中身を自由に見ることも、改造することもできます。一部分の機能を切り出して再利用することもできます。私としては、より多くのプログラマにソースを見てもらい、たくさんの人に興味をもってもらえれば…と思っています。
ソースコードは VisualC++6.0 でコンパイル可能な状態になっていますが、Linux
などの gcc でもコンパイルできるように偏った書き方は一切していません。現在の
alpha6版では、gcc ではコンパイルできませんが、alpha4版ではコンパイルできていました。ですからalpha6でも比較的容易に
gcc に対応できると思っています。現在、時間を作っては少しずつ対応していますので、この記事が紙面に載るころにはalpha7かbeta1として公開されていると思います。
●圧縮について、もう少し詳しい説明
まずは、基本的なデータの圧縮方式についての概要から説明します。
・ロスレスとロッシー(Lossless and Lossy)
基本的な話をします。データの圧縮には大きく2種類の圧縮方式があります。1つは、不必要と思われるデータを省く方式。データを省くので、完全に元の状態には戻りません。画像や音声のデータに使われます。
もう1つは、データの内容を一切省かずに完全に圧縮する方式。データを省かないので圧縮後展開しても完全に元の状態に戻ります。
データを省くタイプは「ロッシー(Lossy)」や「非可逆」と呼ばれ、逆に、省かないタイプは「ロスレス(Lossless)」や「可逆」と呼ばれています。
ロッシータイプには画像や音声専用の JPEG や MP3 があります。人間の目や耳には判別できないような細かいデータを省いたり、加工したりして圧縮率を上げます。元のデータに戻すことよりも、少しでも圧縮率を上げて情報が人に伝わることを望む形式といえるでしょう。そのためロッシータイプの圧縮は1/20程度にまで小さく圧縮できます。
一方、ロスレスタイプはデータを元に戻すことが優先課題であり、1ビットたりともデータを失うことは許されない立場なのです。そのためどんなにがんばっても1/3程度までしか小さくなりません。より厳しい条件でがんばっているわけです。ロスレスタイプには、LZH
や ZIP があります。yz2 はLZH や ZIP と同じロスレスタイプです。
「表 データ圧縮の種類」
| ロッシー Lossy |
JPEG, MP3 など |
完全には元に戻らない |
| ロスレス Lossless |
LZH, ZIP など |
完全に元に戻る(yz2はこちら) |
・2段ロケット
データ圧縮のアルゴリズムは、一般的に2段ロケット形式です。一段目を「モデル化」。二段目を「符号化」と呼びます。ロスレスでもロッシーでも同じです。簡単に表すと
データ圧縮=モデル化+符号化
となります。
モデル化によってデータ列を数値列と頻度の確率モデルに変換します。たとえば英字+空白のみのデータであるなら0〜26の数値列とその数値の頻度を算出します。その数値列と頻度表を次の符号化に渡します。符号化では、受け取った頻度表にしたがって、数値列をビット列に変換します。このビット列が圧縮後のデータになるわけで、ファイルとして出力されるわけです。これらの2段の作業を行なうことでデータが圧縮されるのです。
逆に展開する場合は、まず、ビット列のデータを頻度表と照らし合わせて数値列に戻します。これを符号化の逆という意味で復号化と言います。その後、数字列からモデル化の逆を行い、元のデータに戻すわけです。この時、完全に元のデータに戻せるモデルなのかどうか?でロスレスかロッシーかに分かれます。モデル化によってロスレスかロッシーかが決まります。よって、符号化・復号化の部分はロスレスであってもロッシーであっても共通に使う部分なのです。これから圧縮アルゴリズムについて勉強してみたいと思われた方は、まずは、共通部分である符号・復号の部分から着手してみるとよいでしょう。
LZHやZIPとyz2のモデル化と符号化の違いを表にします。
「表」
| 形式名 |
モデル化 |
符号化 |
| LZH |
LZ77 |
Huffman |
| ZIP |
LZ77 |
Huffman |
| yz1 |
LZ77x256 |
Huffman |
| yz2 |
LZ77x256 |
RangeCoder |
yz2 はモデル化にLZ77形式の辞書を使い、さらにその辞書を512個持っています。単純に考えればそれだけ多くの単語を記憶できるということです。yz2 の最大の特徴は「使用する辞書を直前の文字によって切り替えている」ということです。さらに符号化にはRangeCoderを使いました。RangeCoderはHuffmanよりも符号化率も高く高速なので圧縮ツールには最適だと思います。
・yzの歴史
みなさんは DeepFreezer というアーカイバ(圧縮ツール)をご存知でしょうか?かなりマイナーな圧縮ツールなので、知らない人も多いと思います。この
DeepFreezer も私が開発したツールです。
「図 DeepFreezer実行画面」

DeepFreezer の圧縮形式は yz1 と言って yz2 の原型となった圧縮形式です。DeepFreezer
の Copyright は 1997 となっていますから、yz1 は 1997年(5年も前)のアルゴリズム(プログラム)なのです。実際には
DeepFreezerが完成するのに4年くらいかかっていますから、作りはじめたのは、もう10年ちかく前の話になります。時(刻?)の経つのは早いですね。
マイナーなDeepFreezerではありますが、使い勝手のよさは口コミで広がり、今では「Vector」(http://www.vector.co.jp/vpack/browse/person/an011682.html)での通算ダウンロード数は26,399を記録するほどになりました。複雑な機能がまったく無い、ユーザに配慮したシンプルな作りと手軽さが好評であると、私は設計方針に自信を深めています。世の中のソフトって、どうしてあんなにも複雑なモノが多いのですかね?私の設計方針は"Simple is Best."と"Keep it Simple, Stupid!"です。
DeepFreezer が公開された当時、ドラッグ&ドロップのみで圧縮と展開ができるツールは珍しかったのですが、いまではこの形式はすっかり定番になりました。
かれこれ10年くらい前の話になるでしょうか。ある本屋で『文書データ圧縮アルゴリズム入門−ハフマン符号・算術符号・LZ符号などをCで実現』(植松友彦 著/CQ出版/ISBN:4-7898-3672-X)という本(たいへん残念なことに、この本はすでに絶版になってしまっているようです。良い本なので、復刻してくれるとよいのですが…)を手にしたのでした。当時としては珍しく、付録としてソースコードの入った FD が付いていたのです。それまで、圧縮アルゴリズムに関しては専門外だった私ですが、この本のサンプルプログラムをいじり倒しているうちに一つの疑問が浮かんだのです。「辞書のサイズを増やしたらどうなるのだろう?」と。単純な疑問でした。当然サイズを増やすと圧縮率は上がります。ところが、ある程度以上増やしても変化がなくなってくるのです。逆に圧縮できなくなるのです。ある程度以上増やしても辞書に登録された単語が使われなければ圧縮したことにならないからです。なるほど…なかなか奥の深い世界です。こうなってくるとだんだん面白くなってきます。いろいろなアイデアが出てきて、あれもこれもと試してみたくなるわけです。ところが大抵のアイデアはアイデア倒れといいますか、うまく行きません。複雑になっただけでたいした効果もあがらないわけです。
そしてあるとき閃くのです。「そうだ!辞書を256個作ろう。そうすれば圧縮率も上がるし、検索速度もあがるぞ!」と。
これは成功でした。いままでの失敗を帳消しにできるくらいの成果が出ました。圧縮率も圧縮速度も上がったのです。このアルゴリズムがその後
DeepFreezerとして世に出て行くわけですが、それまでの期間が4年もかかりました。
そして yz1 はその後、H12の「未踏ソフトウェア創造事業」によってyz2 へ。さらにはH13の未踏ソフトでも更なる改良と
DLL 化を行ない、現在に至るのです。
このような貴重な体験はなかなかできるものではありませんが、もし yz2 のコードを読んで私と同じようにコードをいじり倒し、挫折感と達成感と閃きを体験する人が現れてくれれば…と期待しつつ、この記事を書いています。
・未踏ソフトってなに?
ここで少し未踏ソフトウェア創造事業について説明します。
IPA(情報処理振興事業協会、http://www.ipa.go.jp/)では経済産業省から補助金を受けて「優れた能力を有する個人又は数名のグループ(スーパークリエータ)による独創的なソフトウェア技術や事業アイディアを支援する」というプロジェクトを行なっているのです。
このプロジェクトの良いところは、主に個人のプログラマを対象としているところなのです。よいアイデアと実現能力さえあれば、誰でも応募できるところです(最低限の企画書・計画書を書く能力は必要とされます)。応募した案件が採択されれば、プログラムの開発費を負担してもらえます。そして、開発したプログラムは開発者の著作物になります。オープンソースとして公開してもよいし、商品として売っても良いのです。このあたりが今までになかったなかなか優れたプロジェクトだと思うのです。
記憶が曖昧なのですが、未踏ソフトプロジェクトは確か5ヵ年計画のプロジェクトであったはずです。今年(H14年度)が3回目なので、まだあと2回チャンスがあります。もしかしたら計画が延長されるかもしれません。この
yz2 によって未踏ソフトが一般に知られるようになり、さらに多くの人材(クリエータ?)が発掘されることを私も望んでいます。期間が延長されると日本のプログラマの質の向上にも(日本の経済の活性にも?)つながると思うので、ぜひ長く続いてほしいものです。
yz2 はこの未踏ソフトウェア創造事業によって開発されたソフトウェアなのです(基盤となる yz1 は個人で作りましたが)。未踏ソフトウェア創造事業がなければここまで完成しなかったでしょう。未踏ソフトウェア創造事業の関係者の皆様には紙面をお借りして心より感謝いたします。
応募は毎年春頃に行なわれています(詳しくはHPを確認してください)。もし「私のアイデアこそ未踏ソフトにふさわしい案件である!」という情熱をお持ちの方がいらっしゃるのなら、ぜひチャレンジしてみましょう。あなたならビル・ゲイツを超えられるかもしれません。
ちなみに、私はその対象から外れるのですが、年齢制限あり(30歳まで)の若者を対象とした「未踏ユース(Youth)」(http://www.ipa.go.jp/NBP/14nendo/esp/youth.html)というプロジェクトもあります。ご参考まで。自分はまだ若いと思う方は「いろいろチャレンジすること」を勧めます。また、自分はもう若くはないと思う方には「若い方にいろいろアドバイスすること」を求めます。
・GizmoI.Q.
yz2 を DLL 化するのあたり、この圧縮アルゴリズムが商用ソフトにも適用できることを実証することと、商用ソフトに適用されることで
yz2 の知名度を上げたいという思いから、個人的にお気に入りのソフトハウスであるElectricSheepさん(http://www.electricsheep.co.jp/)に協力してもらいました。ElectricSheepの次の作品である「Gizmo I.Q.(ギズモ・アイキュー)」に yz2 の DLL を使ってもらうことになったのです。この記事が紙面に載るころには発売されていると思います。
「図 Gizmo I.Q. 動作画面」

「Gizmo I.Q.」はプログラム記述タイプの対戦ゲームです。ロボットの動きをあらかじめプログラミングしておいて、2台のロボットを対戦させるゲームです。この時の対戦結果は2分間の動画データになるのですが、1試合で数メガバイトのデータになります。この対戦結果の動画データを
yz2 で数十キロバイトにまで圧縮するわけです。数十キロバイトであれば1メガのFD1枚に10試合以上の動画データを格納することができる計算になります。また、数十キロバイトのファイルであれば容易にメールに添付することができます。これが数メガバイトではそう簡単にはいきません。
具体的には3メガバイトの動画データが30キロバイト程度に圧縮されるので「動画データが1/100に!?」と驚く方もおられるかもしれません。実はちょっとしたトリックがありまして…、詳細は秘密です。ゲームならではの特殊なデータ構造ですが、それがyz2にピッタリなのです。なかなかの圧縮率と速度が実現できています。
yz2 が動画データを圧縮することで、ゲームとしての機動力を上げるイメージです。もちろん yz2 の DLL が無くてもゲームとして成立しますが、コミュニケーション能力を引き上げるところに yz2 が活躍しているわけです。
ゲームプログラム以外にもyz2が活躍できるシステムは存在すると思います。ぜひ、皆さんの周りにも適応できそうなシステムがないか探してみてください。金の卵が眠っているかもしれませんよ?
・yz2はオープンソースなんです。
yz2 は商用・非商用に関係なく、どなたでもソースコードを見て改良して、システムに組み込んだり販売したりすることができます。ライセンスなど一切ありません。自由に使っていただいてかまいません。じゃんじゃんコピーして儲けてください。ライセンス形式は FreeBSD としていますが、要するに著作権以外なんでもありです。
だた、コードの著作権は私にあります。万が一、じゃんじゃんコピーしてビルが建つほどのお金を稼いでしまった方には、伝家の宝刀「著作権」を行使して、ちょっぴり還元してもらうかもしれません。そんなビルを建ててしまいそうな方はあらかじめ連絡いただけると助かります。よろしくお願いします。
・無償にしてしまっていいのか?
当初は閃きで作りはじめた yz ですが、かなり長い時間と労力をこの yz1 と yz2 にかけてきました。本来であるなら、この時間と労力をご飯に変えて生きていかなければ…と考えるのが一般的な理論なのでしょう。しかし、私は何を思ったのか yz1 を「無償」「フリーソフト」として公開し、yz2 もオープンソースとして公開してしまったのです。
最初の理由は LZH や ZIP がすでに無償であり、ただでさえ乗り換えてくれる人が少ないこの圧縮の世界で
yz1 が有料となれば、さらに孤立してしまい誰も使ってくれなくなるという危険性があったからです。
yz2 のほうは、未踏ソフトというプロジェクトから充分(でもありませんが)お金を頂きましたから。でも、そのお金はもうありませんし、私は勢いで会社も辞めて独立してしまいました。さて、これからどうやってご飯を食べていけばいいのでしょうか?
GIF 形式のように、あるとき突然 yz2 が有料(ライセンス)に変わるのでしょうか?いえいえ yz2 がそれをしてしまったら、皆さんはいったい何を信じて圧縮ツールを使えば良いのでしょうか?とにかく yz2 は無償です。私?私は何か他の仕事をして生きていきますから、いいんです。気にしないでください。いや、ごめんなさい。正直に言います。圧縮とかプログラミングのコンサルトのような仕事がありましたらご一報ください。よろしくお願いします。
・オープンソースでご飯が食えるか?
勢いで、少し話が脱線します。とあるメーリングリストで「オープンソースで利益をあげるにはどうしたらよいか?」という話題が上がりました。長い間、議論は続いたのですが、結論は出ませんでした。というより「オープンソースそのものが利益を求める行為ではない」といわれ、結論としては「無理」となったようです。私は
Linux のような例もあるのだから、なにかしらの方法があるのではないか?と模索を続けていますが、どうやら「名声」は得られたとしても、すぐに「利益」は得られないようです。一番可能性の高い方法は「オープンソースにしたソフトウェアのマニュアルを売る」とか「コンサルトを行なう」ということのようです。もしくは「名前が売れるのでより高給な職場に転職できる」という二次的な可能性もあるかもしれません。いずれにしろ、ソースやプログラムそのものを直接売るという図式にはならないようです。
有効そうなアイデアの一つにWebブラウザの「オペラ」の方式があると思います。オペラのフリー版は「インターネット広告」が出るのです。オペラそのものはフリーで配布されてもオペラの作者はインターネット広告料がもらえるという仕組みのようです。なかなか賢い仕組みかもしれません。どうでしょう?DeepFreezer2 にインターネット広告が出るようになっても、みなさんは使いつづけてくださいますか?少し検討してみる価値はありそうです。
他にもなにか良いアイデアがありましたら教えていただけると助かります。
・特許に関する愚痴
まず、yz2 のアルゴリズムそのもので特許は取っていません。また、今後取るつもりもありません。
圧縮アルゴリズムには必ずと言っていいほど特許の問題がついてきます。なぜ、圧縮の世界ではこれほど特許について騒がれるのでしょうか?不思議に思えます。圧縮アルゴリズム以外にも、この世界にはたくさんのソフトウェアがあるのにもかかわらず、なぜか圧縮アルゴリズムだけがターゲットになっているかのように、つぎつぎと特許の問題が出てきます。なにかおかしいのではないでしょうか?
最近の有名な事件としては、米国 UNISYS 社がLZW特許でGIF使うソフトなどからライセンス料を請求したり(http://www.unisys.co.jp/LZW/)。また、Forgent Networks という会社が JPEG の特許を主張したり、現在においてもかなり混沌とした状況です。
私は思うのです。すべての特許を回避したプログラムは果たして作成可能なのだろうか?と。この世界に存在するすべてのプログラムはすべての特許に抵触していないと言い切れるのだろうか?と。
特許の最大の問題は、とくにフリーソフトなどの個人の開発者には、すべての特許を自力で検索して確認するだけのお金も時間も無いということなのです。だからと言って他人の特許を勝手に使って良いとは思ってはません。一番言いたいのは、誰もが思いつくような単純なアルゴリズムを「早い者勝ち」のような理屈で特許をとるのはいかがなものかと思うのです。恥ずかしいとは思わないのでしょうかね。
私には時間も資金も足りませんので、みなさんの力をお借りしたいと思います。もし、yz2のコードを読んでみて、特許にかかりそうなコードを見つけましたら、連絡ください。回避する方法を考えます。
それでも、圧縮アルゴリズムでとくに有名な特許は「木構造」と「ハッシュ」については回避しています。ハッシュとは、Stac 社が持つ特許で「LZ77の圧縮を高速化するために『ハッシュ』を使う」ところにあるそうです。yz2 では木構造もハッシュも使っていません。そろそろこれらの特許の期限が切れると思うのですが、このあたりも詳しい情報をお持ちの方がおられましたら、ぜひご連絡ください。
●使い方
最初にも書きましたが、2002年8月現在、公開中の最新バージョンはalpha6で、ファイルは
DeepFreezer2_alpha6_2002_02_21.yz1 (.exe)
yz2source_2002_02_21.yz1 (.zip)
の2つです。
プログラムを使うだけであればDeepFreezer2_alpha6_2002_02_21.yz1 のみで結構です。yz2source_2002_02_21.yz1はソースファイルなので、必要ありません。DeepFreezer2_alpha6_2002_02_21.yz1 には3つのプログラムと1つのDLLが入っています。
「表 プログラム一覧」
| プログラム名称 |
概要 |
| DeepFreezer2.exe |
Windows用のインターフェース |
| yz2dlg.dll |
DeepFreezer2から呼ばれるyz2のコア |
| yz2enc.exe |
コマンドラインからの圧縮プログラム |
| yz2dec.exe |
コマンドラインからの展開プログラム |
●動作環境
DeepFreezer2 は Windows2000 で動作確認を行なっています。他のWindowsでも問題なく動作するはずですが、今のところ未確認です。
●インストール方法
yz2 にはインストールプログラムはありません。展開したフォルダごと各自好きな位置に移動してお使いください。
・アンインストール
インストールと同様にアンインストールのプログラムも存在しません。フォルダごと削除してください。レジストリ情報は残さないので、きれいにアンインストールできます。
・DeepFreezer2の使い方
「図 DeepFreezer2の実行画面」

(1) フォルダごと圧縮するには
「フォルダを圧縮」ボタンを押し、圧縮したいフォルダを選択し「圧縮」ボタンを押します。"Normal
end" と表示されたら圧縮終了です。
(2) ファイルの圧縮するには
「ファイルを圧縮」ボタンを押し、圧縮したいファイルを選択し「開く」ボタンを押します。"Normal
end" と表示されたら圧縮終了です。
(3) 圧縮ファイルの展開するには
「圧縮ファイルを展開」ボタンを押し、圧縮された(*.yz2)ファイルを選択し「開く」ボタンを押します。"Normal end" と表示されたら展開終了です。
・yz2enc/yz2dec の使い方
まずコマンドプロンプトを開きます。そして、yz2enc,yz2decのあるディレクトリに移動するか、PATHを設定するか、もしくはフルパスで入力する必要があります。
コマンドプロンプト内で、
> yz2enc
のみを入力すると、英語で使い方が表示されます。
> yz2enc -j
と -j オプションを指定すると日本語で表示されます。
「図 yz2enc -j の実行画面」

ファイルやフォルダ(ディレクトリ)を圧縮する場合には単純に yz2enc の後ろにファイル名またはフォルダ名です。たとえば圧縮したいファイル名が
file.txt であった場合は
> yz2enc file.txt
と入力します。これで、圧縮されたファイルはfile.txtと同じディレクトリに
file.txt.yz2a6 というファイル名で出力されます。
圧縮したファイルを展開するには、yz2dec を使います。yz2dec の使い方も
yz2enc と同じように
> yz2dec -j
と入力することで、使い方が日本語で表示されます。
「図 yz2dec -j の実行画面」

展開したいファイルを単純に yz2dec の後ろに入力するだけです。たとえば展開したいファイル名が
file.txt.yz2a6 であった場合は
> yz2dec file.txt.yz2a6
と入力します。これで、展開されたファイルはfile.txt.yz2a6と同じディレクトリに出力されます。ただし、展開先に同じファイル名がある場合は展開内容とそのファイルの内容を比較するかどうか?をたずねてきます。比較する場合は
y を、しない場合は n を入力します。ちゃんと圧縮できているかどうか調べてみるときや、圧縮したファイルと圧縮前の内容が不一致になっていないか忘れてしまったときなどに使える機能です。ちなみに
DeepFreezer や DeepFreezer2 でも同じ機能が付いていて、同じように比較できます。
・yz2のメリット/デメリット
yz2のメリットとデメリットを少し書きます。
メリット
そこそこの圧縮率で、そこそこ速い
オープンソースである
難しいアルゴリズムは無い
デメリット
辞書のサイズが大きい(PDAやケータイのような小さいメモリサイズでは実装が厳しい)
最新の圧縮アルゴリズム(BlockSortingやPPM)にはかなわない
デメリットの1つである辞書のサイズですが、シェイプアップすることは可能です。単純に圧縮率を犠牲にしてサイズを減らすことも可能ですし、圧縮するデータに対して特化することでサイズを減らすこともできると思います。そして、やはり圧縮率では最新のアルゴリズムにはかなわないのですが、最新のアルゴリズムはそれなりの処理時間がかかる傾向もありますし、まだ「お手軽である」といえるレベルにまでは到達していないようです。もう少し時間をかければ実用に問題の無いレベルに達するでしょう。そういった意味から、yz2は次の圧縮アルゴリズムへの橋渡しの役目になれれば…と思っています。
ここからはプログラム内部の情報により詳しくふれて行きたいと思います。また、圧縮アルゴリズムの話についても詳しく解説したいと思います。具体的にはyz2source_2002_02_21.yz1
の中身の話が中心になります。
●開発環境
yz2はC++を使ってコーディングしています。メインのプログラム(yz2dlg.dll)はWindowsのVisualC++6.0で開発しています。GUI部分(DeepFreezer2)はC++Builder5を使っています。まずは、開発環境全体のディレクトリ構成がどうなっているのか説明します。
・ディレクトリ構成
yz2source_2002_02_21.yz1 の中身は以下のようになっています。
「図 yz2source_2002_02_21 ディレクトリ構成」

主に、DF2のフォルダと、yz2dlg15 のフォルダと、大きく2つに分かれています。DF2のフォルダには DeepFreezer2 の開発環境が入っています。yz2dlg15のフォルダには yz2dlg.dll, yz2enc.exe, yz2dec.exe の開発環境が入っています。yz2dlg15フォルダにはたくさんのフォルダがありますが、ソースコードが入っているのは CXL とyz2Fileの中とその中のyz2Code と RangeCode です。C++の拡張子(.cxxまたは.cpp)のファイルがすべてソースコードです。
まず、DF2 のディレクトリ構成を図に示します。
「図 DF2 ディレクトリ構成」

DeepFreezer2は C++Bulder5で開発しています。よって、DF2フォルダの中身はC++Builder5の開発環境です。CXLフォルダの中身や
yz2dlg.dll も見えますが、これらはyz2dlg15 フォルダからコピーしたものです。こちらが元ではありません。注意してください。
DeepFreezer2.bpr はC++Builder5のプロジェクトファイルです。これを起動すれば
C++Builder5 で DeepFreezer2.exe を作り直すことができます。
次に、yz2dlg15 のディレクトリ構成を図に示します。
「図 yz2dlg15 ディレクトリ構成」

yz2dlg15フォルダには、VisualC++6.0用のプロジェクトファイルが入っています。yz2dlg.dll,
yz2enc.exe, yz2dec.exe の開発環境です。これらのコンパイルを行なうには、ここにあるC++のソースと
CXL および yz2File フォルダ内のC++ソースを使用します。yz2encおよびyz2decはコマンドラインのプログラムなので、Windows以外のOSでもコンパイルできるような構成になっています。現状ではLinuxのgccでコンパイルできるように調整中です。Linuxなどのコマンドラインでのコンパイルには一般にmakefileを作りますが、yz2encとyz2decは作りが特殊なので、makefile
が無くてもコンパイルできます。具体的には
# gcc yz2enc.cxx -o yz2enc
と
# gcc yz2dec.cxx -o yz2dec
でコンパイルできます。ただし、alpha6ではまだ若干のエラーが出てしまいます。
「図 CXL ディレクトリ構成」

CXLとは私の作った.cxx形式のライブラリの名前です。ボーランドのKylixという開発環境には
CLX という名前のコンポーネントライブラリがありますが、それとは無関係です。偶然名前が似てしまいましたが、正直に言えば、私はKylixが発表される前から
CXL という名前でソースを公開していました。たまたまです。いやホントに。
CXL は主にOSによる環境の違いを吸収するために作ったクラス群です。yz2はこれらのクラスをアクセスすることで、OSによる環境の違いを意識しないで動くことができます。yz2の共通ルーチンとでも言いましょうか。現在、WindowsとLinuxの違いをこれらのクラスで吸収しています。他のOSに対応する場合にも、これらのクラスを変更・機能追加することで容易に対応できるように設計しています。実際に、容易に対応できるかどうかはやってみないとわかりませんが、設計方針としてはこのようになっています。
また、これらのコードにはユニットテストのコードが含まれており、容易にユニットテスト(単体テスト)が可能です。ユニットテストのコードを含むことでデグレード(変更時に新たに含まれてしまう誤り。デバッグの逆のエンバグという意味に似ている)を最小に抑えようというこころみでもあります。また、他の環境へ移植をするときのテスト項目としても使えます。さらには、ユニットテストのコードを読むことでそのクラスの使い方が解るようなリファレンスマニュアルの要素もあります。ユニットテストの方法は後で述べますが、まずはソースを読んでみてください。
次に、yz2File のディレクトリ構成を図に示します。
「図 yz2File ディレクトリ構成」

yz2File は主に、ファイルの入出力を管理する部分です。tar コマンドに相当するようなルーチン群です。yz2では複数のファイルをある程度まとめて圧縮するので、ファイルのまとめ処理をしています。私が今、一番直したいと思っている部分ですが、いろいろありまして…。
次に、yz2Code のディレクトリ構成を図に示します。
「図 yz2Code ディレクトリ構成」

yz2Code は、圧縮アルゴリズムと展開アルゴリズムの部分です。ファイルの入出力とは無関係にメモリ上のデータに対して圧縮を行なうルーチン群なので、ファイル入出力が不必要の場合はここから下のフォルダのみ利用することができます。また「データ圧縮=モデル化+符号化」の式でいうところの「モデル化」に相当する部分で、LZ77の辞書を持っているところです。
ゲームGizmoI.Q.はこのyz2Encodeとyz2Decodeの部分を直接利用しています。
次に、RangeCode のディレクトリ構成を図に示します。
「図 RangeCode ディレクトリ構成」

「データ圧縮=モデル化+符号化」の式でいうところの「符号化」に相当する部分で、RengeCodeのところです。頻度表を作る
Frequency4Tbl.cxx と RangeEncode.cxx, RangeDecode.cxx がメインのファイルです。この符号化部分だけでも独立していますから容易に使いまわすことができると思います。
これで、ソースファイルの説明はすべてです。
・ソース全体の特徴
これらのディレクトリ構成やソースファイル全体を見ていて鋭い方はお気づきかもしれませんが、includeフォルダやヘッダファイル(*.h)がありません。
このやり方は私のオリジナルなので賛否両論といいますか、どちらかといえば「なんでこんなことするんだ」という否定の意見が多いと思います。そこはそれ「人生これチャレンジの固まり」というか「こんなやり方もあるけど、どぉ?」という軽い気持ちで受け止めてください。
では、ヘッダファイルが無いことによるメリットとデメリットを簡単に説明します。まずメリットとしては、
- ファイル数が増えない:ヘッダファイルと本体ファイルの整合性を確認する必要がない
- ユニットテストのコードも一緒に書ける:テストや再テストが容易
の2つでしょうか。一方デメリットは、
- すべてインライン展開になってしまう
- 分割コンパイルができない
というところでしょう。インライン展開になってしまうとは言っても、必ずすべてのコードがインライン展開されるわけではなく、コンパイラの展開能力というか、インライン展開すべきかどうかの判断はコンパイラに任せています。よって、実行コードがやたらと巨大化するということはないと思っています(コンパイラの能力しだいです)。また、分割コンパイルができないとは言っても、最近のコンパイラは高速ですし、CPUも高性能です。全体をコンパイルして、1分も2分もかかってしまうことは無いと思います。
もう少し言うと、STLなども同じようにソースコードそのものをinclude して使いますし、コードはインライン展開され、分割コンパイルもできません。「テンプレートのコードだから仕方が無い」という見方もあるでしょうが「ヘッダファイルが無いことによるメリット」にも目を向けてみてはいただけないでしょうか?
・ユニットテスト(単体テスト)
ここで、ユニットテストについて少しふれておきたいと思います。私はシステム開発には「テストが重要である」と常に思っています。
XP(エクストリーム・プログラミング)などの文献を読んでも、テストファーストやユニットテストなどのテスティングの概念は重視されています。
yz2では以前からユニットテストのコードをyz2のコードの中に埋め込んできました。CXLのコードやyz2Codeのコードにはほとんどのコードにユニットテストのコードが入っています。ただ、まだすべてのコードに入っているという状態ではありません。それでも、これらのテストコードによってyz2の品質(動作の信頼性)を確保できていると私は思います。
ユニットテストのメリットは前にも書きましたが、デグレードしないことです。ソースコードに修正が入ることで、他の部分への悪影響(改悪)が常に心配されます。その心配をユニットテストによって取り除くわけです。XP的に言うなら「ユニットテストのコードがあるのでリファクタリングをしても問題はない」となるでしょう。とにかく、早期にバグの芽を摘み取ることは、システム開発にとってとても重要なことだと思います。
また、yz2のコードはユニットテストのコードが本体のソースに付属しています。このことにより、テストコードのみが行方不明になることを防いでいます。システム開発での掟(おきて)ではないのですが、よく「同じ事を二回書くな」と言われます。「コピー&ペーストをするな」とも言われます。これは「情報は一ヶ所で管理すべき」とか「同じ情報を複数箇所に分散するな」という意味であると私は解釈しています。複数箇所に分散すると、修正が発生した場合に複数箇所すべてに対して正確に修正する必要が出てくるにもかかわらず分散しているためにそれが困難になるということでしょう。マニュアルとソースコードにおいても不一致を発生させないためにはその情報ソースを分離しないことだと思います。同じように、ソースコードとテストコードも分離しないほうが良いと私は思っているのです。
さて、ここでyz2のユニットテストの仕方を見てみましょう。
たとえば、CXL内のSimpleDate.cxxのユニットテストをしてみましょう。SimpleDate.cxxは日付情報を管理するクラスです。"2002/08/25"のような文字列を作ったりします。
VisualC++6.0 の場合は、まずSimpleDate.cxxのファイルを開き、そこで一度「ビルド」をします。ビルドをしてもmain()関数が無いので「外部シンボル "_main" は未解決です」のようなエラーになります。エラーになっても気にしないでください。プロジェクトファイルを手早く作るためにビルドしたのですから。
SimpleDate.cxxでは255行目の
#ifdef __UnitTest__SimpleDate_cxx__
以降がユニットテストのコードで、そのすぐ下にmain()関数が書いてあります。ユニットテストのコードを展開するには__UnitTest__SimpleDate_cxx__をデファインする必要があります。先ほど作成したプロジェクトファイルの設定を変えます。「プロジェクト->設定->C/C++」の「プリプロセッサの定義」に", __UnitTest__SimpleDate_cxx__"の文字列を追加します。これでユニットテストのコードが展開できます。ここで再びビルドします。今度はエラーも無くプログラムが作られるので、あとはこれを実行するだけです。
「図 ユニットテスト実行結果」

ユニットテストを実行するとテスト結果が表示されます。SimpleDate.cxx では結果の自動判断まではしていないので、人が結果をチェックする必要はありますが、表示されていればある程度の動作確認はできていると思ってよいでしょう。本来ならば assert() 文などで自動的にチェックするのが理想だとは思います。
VisualC++以外のコンパイラでもコンパイル時にデファイン指定をすることでユニットテストのコンパイルは可能です。
# gcc SimpleDate.cxx -D__UnitTest__SimpleDate_cxx__
という形で、-D指定をすればユニットテスト用の a.out プログラムが出力されます。ここでa.out を実行すれば、VisualC++と同様にユニットテストの結果が表示されます。
●アルゴリズム
データ圧縮アルゴリズムは一般に2段ロケット方式であることは、前で説明しました。
yz2では「モデル化」にLZ77を改良した辞書と、「符号化」にRangeCoderを使っています。前段の「モデル化」では、LZ77の考え方をそのままに、辞書の構造を独自に改良し速度と圧縮率を上げています。また後段の「符号化」にRangeCoderを使うことで、こちらも速度と圧縮率を向上させています。
まずは、LZH,ZIP と yz2 のアルゴリズムを比較してみます。
「図 アルゴリズムの比較」

LZHやZIPでは、「モデル化」にはLZ77辞書を一つと、「符号化」にはHuffman符号を用いています。yz2では「モデル化」にLZ77辞書を複数(256個)持っています。そして「符号化」にはRangeCoderを用いています。
次に、LZ77辞書と、RangeCoderの解説をします。まずは、RangeCoderから説明し、その後にLZ77辞書について解説します。
・符号化
符号化のアルゴリズムではRangeCoderの解説をしますが、その前に、おさらいということで、まずは算術符号まで戻って解説します。
・算術符号
算術符号はHuffman符号などとは違って、文字に対してビットを割り当てるのではなく、文字列全体に対してビットを割り当てます。ですから「このビットパターンはAという文字」という「ビットパターン=文字」という対応にはなりません。
算術符号の詳細に触れる前に、まずは「少数点以下をビットに表すにはどうするか?」という話をしましょう。0.5(1/2)は2進数でどう表すでしょうか。0.1ですね。では0.25(1/4)は?0.01ですね。0.125(1/8)は?…これを図にしてみます。
「図 少数点以下のビット」
10進数 2進数 10進数 2進数 10進数 2進数
--- 1 = 1.0 --- 1 = 1.0 --- 1 = 1.0
| | |
| | -+- 0.875 = 0.111
| | |
| -+- 0.75 = 0.11 -+- 0.75 = 0.11
| | |
| | -+- 0.625 = 0.101
| | |
-+- 0.5 = 0.1 -+- 0.5 = 0.1 -+- 0.5 = 0.1
| | |
| | -+- 0.375 = 0.011
| | |
| -+- 0.25 = 0.01 -+- 0.25 = 0.01
| | |
| | -+- 0.125 = 0.001
| | |
-+- 0 = 0.0 -+- 0 = 0.0 -+- 0 = 0.0
|
この図からわかることは、1/2 までは少数点以下1ビット。1/4までは2ビット1/8までは3ビットと細かくなるにつれてビット数が増えているということです。「細かくなると桁が増える」というのは10進数でも2進数でも桁の増えかたは同じということです。
これをふまえて、算術符号の説明に移ります。
A,Bの2種類の文字しか出現しない文字列で、Aの頻度が2/3、Bの頻度が1/3という文字列があったとします。この場合、Huffman符号ではAもBも1ビットで表すしかありません。
たとえば"AAB"と言う文字列をHuffman符号化しても"110"(もしくは"001")と3ビット必要になるということです。
算術符号の考え方はこうです。まず、頻度を0〜1まで(1は含まない)の値で分けます。
「図 算術符号1」
--- 3/3 = 1.0
|
A |
|
-+- 1/3 = 0.333..
B |
-+- 0/3 = 0.0
|
この図の1/3〜3/3まで(3/3は含まない)はAを表します。0/3〜1/3まで(1/3は含まない)はBを表しています。数値と文字は相互に変換可能な状態です。
では、Aを表す数値の範囲内で、最小のビット数(最小桁数)になる2進数はなんでしょう?前の図を思い出してください(30秒経過…)。0.5(1/2)=0.1
ですね。1ビットで表せます。では、同じくBでは?(10秒経過…)0=0.0ですね。これも1ビットです。これが算術符号の基本的な考え方です。
では、この基本を応用して2文字の場合を考えてみましょう。"AA"だった場合の範囲を考えてみます。
「図 算術符号2」
--- 3/3 --- 9/9 = 1.0
| |
| A | ←ここ 3/4 = 0.75
A | |
| -+- 5/9 = 0.555..
| B |
-+- 1/3 -+- 3/9 = 0.333..
|
B |
-+- 0/3
|
"AA"は5/9〜9/9(9/9は含まない)になります。この範囲内の最小ビットの2進数は、0.75(3/4)=0.11です。2ビットで表せます。
同様に3文字"AAB"だったら
「図 算術符号3」
--- 9/9 --- 27/27 = 1.0
| |
| A |
AA | |
| -+- 19/27 = 0.703703..
| B | ←ここ 5/8 = 0.625
-+- 5/9 -+- 15/27 = 0.555..
|
AB |
-+- 3/9
B |
-+- 0/9
|
15/27〜19/27(19/27は含まない)なので0.625(5/8)=0.101になります。3ビットになります。どうでしょう?文字列とビット列の変換の考えかたを理解していただけたでしょうか?
「なんだ、3文字で3ビットならHuffman符号と変わらないじゃないか」とおっしゃる方もおられるでしょう。"AAB"では確かに3ビットです。では"BAA"ではどうでしょう?さらに"ABA"ではどうでしょう。ともに出現頻度はA=2/3,B=1/3の文字列です。
「図 算術符号4」
1 ---- 3/3 ----- 9/9 ----- 27/27 = 1.0
| | |
| | A |
| | --+-- 19/27 = 0.703703..
| | |
| A | B | ← 5/8 = 0.625
| | |
| --+-- 5/9 --+-- 15/27 = 0.555..
| | |
A | B | A | ← 1/2 = 0.5
| | |
| | --+-- 11/27 = 0.407407..
| | B |
--+-- 1/3 --+-- 3/9 --+-- 9/27 = 0.333..
| | |
B | A | A | ← 1/4 = 0.25
| | |
| | --+-- 5/27 = 0.185185..
| | B |
| --+-- 1/9 --+--
| B |
0 --+-- 0/3 --+-- 0/9
|
"BAA"は5/27〜9/27(9/27は含まない)なので、1/4(0.25)=0.01で2ビットです。"ABA"は11/27〜15/27(15/27は含まない)なので、1/2(0.5)=0.1で、なんと1ビットで表せます。どれも、Huffman符号よりもビット数を少なく変換できています。どうでしょう?算術符号が「文字ではなく文字列に対してビットを割り当てている」という意味を理解していただけたでしょうか。
ここで算術符号の解説は終わります。ではお待ちかねのRangeCoderの解説です
・RangeCoder(レンジコーダ)
RangeCoder については「Cマガジン2002/7月号」の奥村晴彦さんの特集記事「データ圧縮の基礎から応用まで」で詳しくふれられています。
RangeCoder は特許フリーでありながら算術符号に匹敵する符号化率と、Huffman符号よりも高速であることから最近注目されている符号化形式です。主に、桁上がりのある
MichelSchindler版と、桁上がりの無い DmitrySubotin版の二種類の形式が存在するようです。MichelSchindler版は桁上がりの処理があるために
DmitrySubotin版よりも若干速度が遅くなるようです。しかし、そのぶん符号化率が良くなるようです。
yz2 の RangeCoder では桁上がり(MichelSchindler)版を採用しています。
では、RangeCoderのアルゴリズムの説明します。
・RangeCoder符号化アルゴリズム
まずは符号化アルゴリズムから見ていきましょう。
最初に、Rangeを表す対応表(頻度表)を作ります。RangeCoderで重要なのは算術符号とは違い範囲を下の値(low)と幅(range)で表すことです。そして、このrangeの値で精度の調整をしているところも重要です。また、実数ではなく整数であることも大きな違いです。
「図 RangeCoder頻度表」
| 文字 |
頻度 |
f_range |
f_low |
備考 |
| A |
2 |
2 |
1 |
1 <= A < 3 |
| B |
1 |
1 |
0 |
0 <= B < 1 |
※母数は3です。
ここでは説明を簡単にするために精度を4ビットとします。算術符号でいう1.0の値がrangeの16(10進)に相当するという意味です。最低確保する精度は3ビットとします。rengeの値が7(10進)以下にならないように監視するという意味です。精度を4ビットとしたのでrangeの初期値は16になります。lowは0です。
「図 RangeCoder1」
--- 16 ← low+range 0+16 = 16(16は含まない)
|
A |
|
-+-
B |
-+- 0 ← low = 0
変数 |10進| 2進
range| 16 | 1,0000
low | 0 | 0000
|
ここまでが初期設定です。ここから符号化開始です。
最初にrange:16を母数3で割ります。整数で割るので小数点以下は切り捨てます。答えは5です。これに最初の文字 A の頻度(f_range:2, f_low:1)を加えます。rangeは置き換え 5*2 → 10 、lowは加えて 0+5*1 → 5 になります。図を見ながらイメージしてください。
「図 RangeCoder2」
--- 16
| ← low+range 5+10 = 15(15は含まない)
A | :
| :
-+- 5 ← low = 5
B |
-+- 0
変数 |10進| 2進
range| 10 | 1010
low | 5 | 0101
|
本来ならば、low+rangeは15ではなく16を指すべきなのですが、ここでは精度が4ビットと小さいので、誤差が早く出てしまいます。実際には精度を24ビットなどと大きくとることで、この誤差も少なくすることができます。
このあと、range:10 が最低確保する精度の3ビット(7) 以下にならないように注意しますが、ここでは10なのでそのままです。これを繰り返します。
次の文字もAです。range:10 を母数3で割り、小数点以下は切り捨てなので答えは3です。これに文字
A の頻度(f_range:2, f_low:1)を加えます。rangeは置き換え 3*2 → 6 。lowは加えて
5+3*1 → 8 になります。
「図 RangeCoder3」
--- 16
| ← low+range 8+6 = 14(14は含まない)
AA | :
| :
-+- 8 ← low = 8
AB |
-+- 5
B |
-+- 0
変数 |10進| 2進
range| 6 | 0110
low | 8 | 1000
|
ここで、rangeが3ビット(7) 以下になったので全体を底上げします。具体的には全部の数値を2倍(shift)します。
「図 RangeCoder4」
--- 32
| ← low+range 16+12 = 28(28は含まない)
AA | :
| :
-+- 16 ← low = 16
AB |
-+- 10
B |
-+- 0
変数 |10進| 2進
range| 12 | 1100
low | 16 |1,0000
shift=1
|
ここで、底上げしたビット数を覚えておきます。まだ1回です。
さて、また繰り返します。次の文字はBです。
range:12 を母数3で割ると答えは4です。これに文字 B の頻度(f_range:1, f_low:0)を加えます。rangeは置き換え
4*1 → 4 。lowは加えて 16+4*0 → 16 になります。
「図 RangeCoder5」
--- 32
|
AAA |
|
-+- 20 ← low+range 16+4 = 20(20は含まない)
AAB | :
-+- 16 ← low = 16
AB |
-+- 10
B |
-+- 0
変数 |10進| 2進
range| 4 | 0100
low | 16 | 1,0000
shift=1
|
ここでまた、rangeが3ビット(7) 以下になったので全体を底上げします。全部の数値を2倍します。
「図 RangeCoder6」
--- 64
|
AAA |
|
-+- 40 ← low+range 32+8 = 40(40は含まない)
AAB | :
-+- 32 ← low = 32
AB |
-+- 20
B |
-+- 0
変数 |10進| 2進
range| 8 | 1000
low | 32 |10,0000
shift=2
|
これで終わりです。
あとはlowにたまったビットを出力します。実際にはlowが桁あふれしないように、繰り返しの途中で出力していきます。(この出力のしかたに、桁上がりのある MichelSchindler版と、桁上がりの無い DmitrySubotin版の2種類のやり方があるようです。)
まず、shiftしたのは2ビットなので出力します。そして精度が4ビットで確保した制度が3ビットだったので間のワークビットの1ビットも出力します。これで3ビット出力することになります。lowの左から3ビット"100"が"AAB"の符号化後の値になります。
どうでしょう?算術符号と同じような処理を整数値とRangeを使って実現していることが解るでしょうか。
・RangeCoder復号化アルゴリズム
今度は復号化アルゴリズムを見てみましょう。具体的に"100"の3ビットを復号化してみます。処理としては符号化の逆を行なうだけです。
rangeの初期値は符号化と同じ16です。rangeは現在処理中の幅を示しています。そして、lowの初期値は左から3ビット"100"になります。この場合は、右に空きがありますが0を入れておきます。よってlowの初期値は8になります。lowは次の値を示しているのでlowの値から頻度表と照らし合わせることで、文字に戻せます。頻度表は符号化に使ったものをそのまま復号化でも使います。今回は符号化時に終端コードを入れなかったので、文字数が3文字であるという情報はあらかじめ伝わっているものとします。
「図 RangeCoder復号化1」
変数 |10進| 2進
range| 16 | 1,0000
low | 8 | 1000
--- 16
|
A |
| ← low = 8
-+- 5
B |
-+- 0
|
まず、range:16を3で割ります。答えは5です。
ここで次の文字を指しているlowを5で割って、頻度表と照らし合わせます。元の文字に戻すわけです。答えは1なので頻度表の中の1はAです。元の文字はAということになります。
A が解ったところで、符号化と同じように文字 A の頻度(f_range:2, f_low:1)を加えます。rangeは置き換え 5*2 → 10 です。lowは加えるのではなく引きます。 8-5*1 → 3 になります。
「図 RangeCoder復号化2」
変数 |10進| 2進
range| 10 | 1010
low | 3 | 0011
--- 10
|
A |
|
-+- 3 ← low = 3
B |
-+- 0
|
符号化と同じように、range:10 が3ビット(7) 以下にならないように注意します。ここでは10なのでそのままです。
これを符号化同様に繰り返します。
range:10 を3で割ると今度は3です。
ここで、lowを答えの3で割って、頻度表と照らし合わせます。答えは1です。頻度表の中の1はAです。
A が解ったところで、符号化と同じように文字Aの頻度(f_range:2, f_low:1)を加えます。rangeは置き換え 3*2 → 6 です。lowは引いて 3-3*1 → 0 になります。
「図 RangeCoder復号化3」
変数 |10進| 2進
range| 6 | 0110
low | 0 | 0000
--- 6
|
A |
|
-+- 2
B |
-+- 0 ← low = 0
|
符号化と同じように、range:6 が3ビット(7) 以下にならないように注意します。
下回ったので全体を底上げします。符号化と同様に2倍します。
このとき low には符号化されたビットが追加されるのですが、今回は3ビットすべて初期値のlowに入ってしまったので、もう入れるビットがありません。よって0を入れておきます。
「図 RangeCoder復号化4」
変数 |10進| 2進
range| 12 | 1100
low | 0 | 0000
--- 12
|
A |
|
-+- 4
B |
-+- 0 ← low = 0
|
さて、さらに繰り返します。
range:12 を3で割ると今度は4です。
ここでlowを答えの4で割って、頻度表と照らし合わせます。答えは0です。頻度表の中の0はBです。
B が解ったところで、符号化と同じように文字Bの頻度(f_range:1, f_low:0)を加えます。rangeは置き換え
4*1 → 4 です。lowは引いて 0-4*0 → 0 になります。…と繰り返すのですが、今回の復号はここで終わりです。3文字目がBとわかったので終わりです。これで、無事"100"を"AAB"に復号できました。
符号/復号ともにやっている計算は非常に単純です。高速に処理できる理由も理解できるかと思います。
具体的なソースコードの位置を示すと RangeEncode.cxx に符号化、RangeDecode.cxx
に復号化のコードが入っています。
本文で解説した、range と low は以下のメンバ変数です。
private : work_type m_width ; // 幅
private : work_type m_low ; // 幅の下の値
そして shift_Encode() メソッドにより、一回のループ(符号化)を行なっています。短いコードなので、本文と照らし合わせることで容易に理解できると思います。
ここでRangeCoderの解説は終わります。次はyz2の「モデル化」の部分LZ77辞書の解説です。
・モデル化
yz2ではモデル化にLZ77辞書を改良した独自の辞書を使っています。yz2の辞書の解説の前に、LZ77辞書から説明したいと思います。
・LZ77辞書
モデル化とは、一般に「データの特徴を捉える」ということをします。「データの特徴を捉えて変換しています」といわれても難しいかもしれません。データを変換するとどうして圧縮できるのか?というと、データの種類と量の関係になります。たとえば
4種類のデータが14個と、
7種類のデータが8個ではどちらのデータ量が少ないでしょうか?
4種類のデータは2ビットで表せるので、2×14=28ビット。
7種類のデータは3ビットで表せるので、3×8=24ビット
となり、後者のデータが少ないことになります。「データの種類が増えても量が減る」もしくは「データの量が増えても種類が減る」ように、全体とし少なくなる方向へ変換することが圧縮につながるのです。
たとえば、もっとも簡単なモデル化の例である「ランレングス」では「同じ文字の繰り返し」という特徴を捉えて変換しています。"AABBBCCCCDDDDD"
という4種類の文字データが14文字の文字列があった場合、これに繰り返しの意味である「かける(×)」という概念を取り入れて変換します。
と8個(7種類)のデータに変換します。これで
4種類(2ビット)×14=28ビットから、
7種類(3ビット)×8=24ビットと、28−24=4ビットのデータを圧縮することができました。
しかし、ランレングスの考え方で圧縮できるデータには限りがあります。単調にならんでいるようなデータは実際にはあまり存在しないからです。
LZ77辞書では「同じ文字列の繰り返し」という特徴を捉えて変換します。文字ではなくて「文字列」を捉えて変換するわけです。同じ文字列を「何文字前から何文字分」という概念で変換していくのです。たとえば"ABURAKATABURA"という13文字のデータでは"ABURA"という文字列が2回出てきます。この文字列を「何文字前から何文字分」で表すのです。
と10個(8種類)のデータに変換します。「8文字前, 5文字」とは「8文字前のから5文字分の文字列がここに入る」という意味です。
変換の結果、
6種類(3ビット)×13=39ビットのデータが、
8種類(3ビット)×10=30ビットへと、39−30=9ビット圧縮することができます。これがLZ77辞書の基本的な考え方です。
「辞書」とは「文字列を覚えている配列」といいますか、同じ文字列が現れたかどうか確認(比較)するために過去の文字列を覚えておく必要があるわけで、その比較のための文字列群の構造を辞書と呼んでいます。
LZ77では「処理中の文字列からn文字前」までを辞書の範囲としています。
すべての文字を辞書に入れることは難しいので、常に新しい文字を辞書の範囲として、古い文字は辞書の範囲から抜けていきます。
「図 辞書の範囲のイメージ図」
"[ABURA]KATABURA"
"A[BURAK]ATABURA"
"AB[URAKA]TABURA" |
このように、辞書の範囲がスライドするので、LZ77は「スライド辞書」とも言われます。
・yz2辞書の構造
いよいよ、ここからyz2の核心部分にふれたいと思います。
前にyz2は「使用する辞書を直前の文字によって切り替えている」と書きました。それを図にしてみます。
「図 yz2の辞書」

YzDictionary というクラスが1つで512個の文字列(の先頭アドレス)を覚えます。新しい文字列が優先され古い文字列は登録から破棄されます。この辞書がAで始まる文字列の辞書、Bで始まる文字列の辞書という具合に文字の種類分(256個)用意しています。この辞書の構造が最大の特徴です。実は、単純なことなんです。
この辞書によってモデル化された数値列は符号化であるRangeCoderに渡りビットに変換されます。
具体的なソースコードの位置はyzEncode.cxxの322行目のStrSearch()メソッドにて、新しい文字列が過去の辞書に一致するかどうかチェックされています。辞書にヒットした場合は0〜1023までの辞書番号として数値列化し、辞書にヒットしなかった場合は1024〜1024+255までの文字コードとして数値列化しています。辞書にヒットした場合でも、一致した文字列の長さによって0〜511と512〜1023のコードに分けていて、もし同じ長さなら0〜511で、違う長さなら512〜1023のコードとさらに文字列の長さも符号化するために数値として符号化へ渡しています。辞書でモデル化された数字列の意味を表にすると以下のようになります。具体的なコードの位置はyzEncode.cxxの502行目のforループの中で行なっています。
「表 モデル化後の数値列の意味」
| 数値列(符号化するコード) |
意味 |
| 0〜 511 |
ヒットした辞書番号(同じ文字の長さ) |
| 512〜1023 |
ヒットした辞書番号(異なる文字の長さ)+文字列長あり |
| 1024〜1279 |
文字コード(辞書にヒットしなかった) |
・なぜ圧縮率が高いのか
LZ77のように「何文字前から」というデータを符号化するのではなく、yz2では「Aの辞書の何個目に登録されている文字列」というデータを符号化しています。さらに「Aの辞書」という辞書の種類のデータは「一つ前の文字をみる」ことで、データとして送る必要がありません。要するに、"ABUR"まで展開されていたら、次はRの辞書から調べることに決まっているのです。1つ前のデータに次に使う辞書の情報も含まれているという考え方もできます。
・なぜ速度が速いのか
これは、単純にすべての辞書から単語を検索しなくてもよくなるからです。yz2の辞書には最大256×512の単語が登録されることになりますが、実際には前の文字によって1つの辞書に限定されますので、512個の文字列から一致する文字列を探せばよいことになります。実際のコードでは、1つの辞書の中の512個の単語を先頭の文字ごとにリンクリストに分割して管理していますから、シリアルに検索するよりも高速に検索できています。
●ファイルフォーマット
・エンディアンの話
リトルエンディアンとビックエンディアンの違いはご存知と思います。CPUによって、4バイトなどの複数バイトの値をメモリ上に格納する向きが異なるという話です。yz1
ではファイルのサイズなどの4バイト整数の情報をそのままバイナリとしてファイルに書き込んでいたので、エンディアンの問題が発生しました。基本をIntelのCPU(リトルエンディアン)とすることで、Intel以外のCPUで
yz1 のファイルを読み込む時にはエンディアンを変更する必要があるわけです。これが結構面倒な作業なんです。開発環境によって、というかCPUによって、コンパイル時の指定を変える必要があるからです。
int i = 1 ;
if ( *(char*)&i == 0 ){
// ビッグエンディアンの処理
...
}
といったコードを埋め込んで、自動的に切り替えてもよかったのですが、他にエンディアンとは別の問題が出てきました。それは2Gバイトを超えるファイルサイズをどうするか?と言う問題です。最近のOSは2Gバイトを超えるファイルサイズをサポートしています。2Gバイトを超える数を4バイトの整数では表現しきれません。はてさて…どうしたものか。
ここで出てきた考え方が「テキストで書く」でした。そうです。バイナリで出力するのではなくて、数字の文字列として出力するのです。こうすることで、エンディアンの問題も、2Gバイト超の問題も同時に解決できます。なんという素晴らしい考え方でしょうか(おおげさ)。「文字列で出したらバイト数が長くなって非効率だ」という意見もあるでしょうが、2G超を考えた場合、常に8バイトで出力することになります。8バイトあったら9,999,999(終端記号も含み7桁で約10MB)まで表現可能です。要するに10MB以下ならバイナリで出力するよりもテキストで出力したほうがバイト数は少なくなる計算です。もちろん、それ以上のサイズは増えてしまいますけど。10メガバイトを超えるファイルなんて、なかなか無いと思います。
・yz2形式
「図 yz2ファイルフォーマット」

yz2ファイルは大きく2つに分かれます。ヘッダ情報と圧縮データです。ヘッダ情報はすべてASCII文字列で、タブコード(\t)を区切り文字とし、改行コード(\n)までをヘッダ情報として認識しています。ヘッダ情報は可変長です。今後、指定されるオプションも増える可能性もあるので、現在のオプションに無い文字列は無視するようになっています。具体的なソースコードの位置はyz2EncFiles.cxxの1581行目からはじまっています。
「表 yz2ヘッダ情報 例」
yzA20600\t123\t4567\tpassFF\tbuff04\tppm\t\n…
| yzA20600\t |
識別コード。8バイト+\t固定長。ファイルyz2alpha6を表す。
正式版では yz0200の6バイトとなる予定。
展開可能なファイルなのかを先頭の4バイトで判断する。
残り2バイトはレビジョン情報。 |
| 123\t |
10進数。可変長。ファイルのヘッダー情報のサイズ。 |
| 4567\t |
10進数。可変長。データ展開後のサイズ。 |
以下オプションのため、オプション指定の無い場合は省略される。
| passFF\t |
パスワード付加オプション。
パスワード付きで圧縮した場合に付加されます。 |
| buff04\t |
バッファサイズ変更オプション。
通常8Mのバッファサイズを1M〜255Mまで指定できる。
8Mの場合は省略される。 |
| ppm\t |
PPMモード使用オプション。 |
| \n |
ヘッダ終端コード、この後ろにバイナリ形式で圧縮データが続く。 |
圧縮データの中身も大きく2つに分かれていて、ファイルのヘッダ情報と圧縮ファイルのパックです。ファイルのヘッダ情報もASCIIの文字列です。1ファイルにつき
file1.txt\n1234\n2002/01/01\n23:59:59\n
このような文字列がファイル数分入っています。ただし、ファイルのヘッダ情報は圧縮されているので、圧縮データの先頭から「ファイルのヘッダー情報のサイズ」分展開しないと読み取ることはできません。
圧縮ファイルのパックは、バッファサイズ(デフォルトでは8M)分のファイルを読み込みパックにしてに圧縮しています。そのパックがファイルがなくなるまで繰り返しています。1つのパックにいくつのファイルが入るかは、その時のファイルの大きさで計算されます。
・自己展開形式
「図 yz2自己解凍ファイル」

自己解凍ファイルのファイルフォーマットはいたって簡単で、先頭にyz2dec.exe
の実行ファイルがそのままあり、そのうしろに、yz2のファイルがそのまま付加されています。最後にyz2ファイルのサイズを知るために、後ろに"\n1234"という文字列でyz2のファイルサイズが付加されています。
●yz2をつかったプログラムの作り方
・DLLを使ってみる。
yz2の主な機能はほとんどyz2dlg.dllに入っています。yz2dlg.dllを呼び出すことで簡単にファイルを圧縮することができます。
まずは、DLLを使うための「お約束の操作」をします。VisualC++では最初に
::LoadLibrary( "yz2dlg.dll" ) ;
でハンドルを取得し、その後、
func = (func_ptr)::GetProcAddress( hDLL, "yz2" ) ;
にて、関数へのポインタを取得します。
DF2フォルダの中のmain.cppファイルが参考になります。main.cppの193行目のOnCreate()メソッドに実際のコードが書かれています。
「リスト main.cppの193行目」
void __fastcall TForm1_MainWindow::OnCreate(TObject *Sender)
{
hDLL = ::LoadLibrary( YZ2DLG_DLL ) ;
if ( hDLL != NULL )
{
func = (func_ptr)::GetProcAddress( hDLL, "yz2" ) ;
}
if ( func == 0 )
{
SpeedButton1->Enabled = false ;
SpeedButton2->Enabled = false ;
SpeedButton5->Enabled = false ;
Application->MessageBox(
"'" YZ2DLG_DLL "' が見つかりませんでした。\n"
"yz2形式の圧縮/展開処理は行えません。",
"DeepFreezer2 - Error",
MB_OK|MB_ICONEXCLAMATION
) ;
}
//
Caption = Caption + " (" __DATE__ ")" ;
}
|
次に取得した関数のポインタをコールすることで、圧縮/展開が行なえます。
yz2dlg.dllの具体的なインターフェースは2つあります。1つはダイアログを開くタイプのAPIで"yz2"です。もう1つはダイアログを開かないタイプの"yz2console"です。
yz2はダイアログを開くので、親ウィンドウのハンドルを渡す必要があります。yz2consoleはダイアログを開かないので、ウィンドウのハンドルを渡す必要はありませんが、標準入力や標準出力の変わりになる関数(get_func, put_func)を登録する必要があります。
「リスト yz2dlg.dll の公開API は二つ」
//-------------------------------------------------------------------
// 'yz2' dialog用インターフェース
extern "C" __declspec (dllexport)
int // out: 0:正常終了 / -1:キャンセル / -2:異常終了
yz2( // ---: -----------------
HWND in_h_wnd, // in : 親ウィンドウのハンドル
int in_decode_mode_on, // in : mode !0:展開decode / 0:圧縮encode
int in_ac, // in : 引数の個数
char * in_av[] // in : 引数(ファイル名+コマンド)
) ;
//-------------------------------------------------------------------
// 'yz2consolde' 端末用インターフェース
extern "C" __declspec (dllexport)
int // out: 0:正常終了 / -1:キャンセル / -2:異常終了
yz2console( // ---: -----------------
get_func in_str_get, // in : 標準入力からディレクトリ名称/yes-no入力
get_func in_pass_get, // in : 標準入力からパスワード入力
put_func in_out_put, // in : 標準出力関数
put_func in_err_put, // in : 標準エラー出力関数
int in_decode_mode_on, // in : mode !0:展開decode / 0:圧縮encode
int in_ac, // in : 引数の個数
char * in_av[] // in : 引数(ファイル名+コマンド)
) ;
typedef
int // out: 結果 1:OK / 0:NG
(*get_func)( // ---: -----------------
char * in_buff, // out: 入力バッファ
const int in_buff_size // in : 入力バッファサイズ
) ;
typedef
void // out: なし
(*put_func)( // ---: -----------------
const char * in_str // in : 出力文字列
) ;
|
yz2インターフェースを呼び出しているの実際のコードはmain.cppの264行目に書かれているEncode()メソッドやmain.cppの149行目に書かれているSpeedButton5Click()メソッドになります。具体的には、ac,
av といったファイル名とコマンドの引数を作成してyz2インターフェースを呼び出しています。
「リスト main.cppの264行目」
void TForm1_MainWindow::Encode( char* in_fname )
{
if ( func == 0 ) return ; // DLL 未実装
if ( ini.m_encode_each_time == true ) // その都度、指定する
{
if ( EncodeDirSelect() == false ) return ; // cancel
}
// 出力先ディレクトリ指定
char * out_dir = 0 ;
if ( ini.m_encode_out_mode == false ) // false:RegularFolder
{
std::string dir( "-d" ) ;
dir += ini.m_encode_folder.c_str() ;
out_dir = strdup( dir.c_str() ) ;
}
// パラメータ作成
char * av[] =
{
in_fname,
ini.m_detail_log_on?"--yz2dlg-detail-message-on":0,
// "--yz2dlg-auto-close-on",
"-j",
out_dir,
ini.m_self_extract_on?"--self-extract":0,
ini.m_password_on?"-p":0,
} ;
int ac = sizeof(av) / sizeof(char *) ;
// 圧縮処理
int rtn = (*func)(
Handle,
0, // in : mode !0:解凍decode / 0:圧縮encode
ac, av
) ;
// 後始末
if ( out_dir != 0 ) { free( out_dir ) ; out_dir = 0 ; }
}
|
「リスト main.cppの149行目」
void __fastcall TForm1_MainWindow::SpeedButton5Click(TObject *Sender)
{
if ( func == 0 ) return ; // DLL 未実装
bool r = OpenDialog2->Execute() ;
if ( r == false ) return ; // cancel
if ( ini.m_decode_each_time == true ) // その都度、指定する
{
if ( DecodeDirSelect() == false ) return ; // cancel
}
// 出力先ディレクトリ指定
char * out_dir = 0 ;
if ( ini.m_decode_out_mode == false ) // false:RegularFolder
{
std::string dir( "-d" ) ;
dir += ini.m_decode_folder.c_str() ;
out_dir = strdup( dir.c_str() ) ;
}
char * buff = strdup( OpenDialog2->FileName.c_str() ) ;
char * av[] =
{
buff,
ini.m_detail_log_on?"--yz2dlg-detail-message-on":0,
// "--yz2dlg-auto-close-on",
"-j",
out_dir,
} ;
int ac = sizeof(av) / sizeof(char *) ;
int rtn = (*func)(
Handle,
1, // in : mode !0:解凍decode / 0:圧縮encode
ac, av
) ;
// 後始末
if ( out_dir != 0 ) free( out_dir ) ;
if ( buff != 0 ) free( buff ) ;
} |
これらの実際のコードではわかりにくいかもしれませんので、もっと簡単な圧縮ファイル名を固定したプログラムを示します。こちらのほうが、必要最低限の処理しかしていないので参考になると思います。
「リスト yz2dlg.dll yz2コールサンプルプログラム」
#include "windows.h" // HWND
#include "stdio.h" // printf()
int main()
{
HMODULE hDLL = ::LoadLibrary( "yz2dlg.dll" ) ;
if ( hDLL == NULL )
{
puts( "yz2dlg.dll が見つかりません。" ) ;
return 1 ; // NG
}
puts( "yz2dlg.dll load ok." ) ;
typedef int (*func_ptr)(
HWND in_h_wnd, // in : 親ウィンドウのハンドル
int in_decode_mode_on, // in : mode !0:展開decode / 0:圧縮encode
int in_ac, // in : 引数の個数
char * in_av[] // in : 引数(ファイル名+コマンド)
) ;
func_ptr func = (func_ptr)::GetProcAddress( hDLL, "yz2" ) ;
if ( func )
{
char * e_av[] =
{
"C:/Documents and Settings/Administrator/デスクトップ/sample",
"-j",
// "-pAAAA",
} ;
int e_ac = sizeof(e_av) / sizeof(char *) ;
int e_r = (*func)(
HWND_DESKTOP,
0, // 0:圧縮encode
e_ac, e_av
) ;
printf( "encode end code : %d\n", e_r ) ;
char * d_av[] =
{
"C:/Documents and Settings/Administrator/デスクトップ/sample.yz2a6",
"-j",
} ;
int d_ac = sizeof(d_av) / sizeof(char *) ;
int d_r = (*func)(
HWND_DESKTOP,
1, // !0:展開decode
d_ac, d_av
) ;
printf( "decode end code : %d\n", d_r ) ;
}
::FreeLibrary( hDLL ) ;
return 0 ; // OK
} |
yz2, yz2console ともに引数にはファイル名とコマンドを文字列で渡すのです。そのコマンドの種類を以下に示します。
「表 圧縮コマンド」
| '-d' |
出力ディレクトリ指定 |
| '-dC:\tmp\' |
出力ディレクトリに 'C:\tmp\' を指定 |
| '-p' |
圧縮時のパスワード指定 |
| '-pAAAA' |
圧縮時のパスワードに 'AAAA' を指定 |
| '-y' |
YES/NO指定で常に YES |
| '-j' |
日本語表示 |
'-sfx'
'--self-extract' |
自己解凍形式で出力 |
「表 展開コマンド」
| '-d' |
出力ディレクトリ指定 |
| '-dC:\tmp\' |
出力ディレクトリに 'C:\tmp\' を指定 |
| '-p' |
展開時のパスワード指定 |
| '-pAAAA' |
展開時のパスワードに 'AAAA' を指定 |
| '-y' |
YES/NO指定で常に YES |
| '-t' |
解凍テスト |
| '-l' |
一覧表示のみ。解凍はしない。 |
| '-j' |
メッセージを日本語表示(指定無し時は英語) |
「表 ダイアログ関係のオプションコマンド」
| '--yz2dlg-detail-message-on' |
詳細ログの表示 |
| '--yz2dlg-auto-close-on' |
自動クローズ |
yz2console の使い方については、yz2enc.cxx, yz2dec.cxx のソースを参考にしてください。
・RangeCodeをつかってみる。
RangeCoderを直接利用することも可能です。一般に頻度表を作らないとRangeCoderのの威力が発揮されないので
RangeEncode.cxx 内のユニットテストのコードよりも、頻度表クラス(Frequency4Tbl)を用いてRengeCoderしている
FrequencyEncode.cxx ファイル内のユニットテストのコードが参考になるでしょう。test()関数ではunsigned
char の配列(vector)を受け取り符号化しています。符号化されたデータは OutEv
という出力クラスによってファイル"debug.out"に落とされます。
ファイルに出力したくない場合は、このOutEvクラスを独自に置き換えれば良い設計になっています。いま考えると、OutEvやInEvクラスのインターフェースはSTLの
ostreambuf_iterrator<char>やistreambuf_iterrator<char>を使うべきだったかも…と少し反省しています。
その後、InEvという入力クラスからファイル"debug.out"を読み込み復号化しています。復号化したデータは関数の引数の内容と比較され誤りがあると指摘します。FrequencyEncode.cxxのユニットテストは符号から復号にかけてプログラムにエラーがないか調べるような構造になっています。
「リスト FrequencyEncode.cxxの84行目」
void test(
const vector< unsigned char > & input_data
)
{
typedef RangeCode< 13, unsigned short, unsigned long > Range13Type ;
typedef Frequency4Tbl< Range13Type > Freq4Type ;
long input_size = input_data.size() ;
// 符号化
{
CXL::SystemTime sys_time ; // 時間を計る
{
OutEv debug_out( "debug.out" ) ; // 出力先ファイル
{
RangeEncode< OutEv, Range13Type > range_encode( debug_out ) ; // Range符号
FrequencyEncode< OutEv, Freq4Type > freq_enc( range_encode, 257 ) ; // 頻度テーブル
for ( int i=0 ; i<input_size ; i++ )
{
freq_enc.Encode( input_data[i] ) ;
}
freq_enc.Encode( 256 ) ;
} // <- このスコープで RangeCodeOutBuffer をフラッシュする
debug_out.m_write_bin_file.Close() ;
int encode_size = debug_out.m_write_bin_file.write_size_Get() ;
int per = (int)((double)encode_size*(double)10000/(double)input_size) ;
printf( "size: %d byte (%d.%02d%%) ", encode_size, per/100, per%100 ) ;
}
printf( "time: %s sec.\n", sys_time.str_Get() ) ;
}
// 復号化
{
std::vector< unsigned char > decode_data ;
CXL::SystemTime sys_time ; // 時間を計る
{
InEv debug_out( "debug.out" ) ; // 入力ファイル
RangeDecode< InEv, Range13Type > range_decode( debug_out ) ; // Range復号
FrequencyDecode< InEv, Freq4Type > freq_dec( range_decode, 257 ) ; // 頻度テーブル
for ( int i=0 ; i<input_size ; ++i )
{
int code = freq_dec.Decode() ;
if ( code >= 256 )
{
break ; // 終了
}
if ( code != (int)input_data[i] )
{
  | |