ゲームプログラミング/画像ファイルの作成プログラム

概要

編集

もし、画像ファイルそのものをゲームから出力したい場合、

そういう出力機能がOSに用意されていればそれを使えばよいが、実は専用の出力機能がなくてもC言語でバイナリー・データファイルを作成・編集することによって、画像ファイルを作ることが原理的には可能である。

画像ファイルだけでなく音声ファイルなども同様にバイナリー・データの編集によって作成可能であるが、本ページでは画像ファイルを例に説明する。

ただし、それら画像フォーマットの、バイナリー・データでの記述の仕様を知る必要がある。かなり専門的な専門書や、もし出版されていなければネット検索などで、調べることになるだろう。

一応、バイナリー・データの直接編集をしなくても、windows にある MFC という[1]機能にある cimage クラスのsaveメソッドなどの機能によって、画像ファイルの出力作成が可能ではある。 しかし、それをどうDirectX形式またはDXライブラリ形式のC++のプログラムに組み込めばいいのか、現在では書籍が乏しく不明である(MFCの書式は、一般のC++プログラミングとは微妙に異なる)。だからMFCによる方法は、ゲームとしては2020年以降の現在では期待できないだろう。


バイナリー・データの読み書き

編集

C言語のプログラムで、バイナリー・データの読み書きをしたい場合、

fopen の引数で「r」(読み込み)とか「w」(書き込み)とか編集モードの指定がありますが、それに「b」をつけます。つまり「rb」や「wb」などの引数になります。

また、書き込む関数には fwrite を使う必要があります。 いっぽう、 fprintf では、テキストファイルに自動的に変換してしまいます。

fopenやfwriteの機能については、一般的な市販のC言語入門書に解説があるので、本ページではこれ以上は説明しない。


画像ファイルのバイナリー・データ

編集


 
バイナリエディタで見た場合のビットマップのバイナリー・データの配置
バージョンによって少々異なる場合もあるが、おおむね、こういう感じである。


たとえばビットマップ画像を作りたい場合、

いわゆる「ビットマップ構造体」の仕様で決められた様式で、バイナリー・データをファイルに書き込む。

すると、たいていのデスクトップ用OS(ウィンドウズも含む)では、ビットマップ形式(BMP)をサポートしているので、そのバイナリー・データを画像ファイルとして認識してくれるので、クリックなどをするとOSが画像表示などをしてくれる。

なお、ネットや専門書の解説では画像フォーマットの仕様を説明するためによくC言語の「構造体」の書式が説明されるが、しかし、その構造体を入力しても、画像は生成されない。


さて、たとえば何らかのビットマップ画像をバイナリエディタで読み込むと、ファイルの先頭は必ず「42 4D」という16進数である。この数字は、アスキーコードで翻訳すると「BM」になる。

この「42 4D」という冒頭の数字を識別子とすることで、ファイルの種類を認識している。

誤解しないでほしいが、けっして「BM」とバイナリー・データで書かれているのでなく(そもそも十六進数に「M」は無い)、書かれているのは あくまで「42 4D」である。

ネット検索で調べる場合は「42 4D」というキーワードを付け加えて「ビットマップ 42 4D」などで調べれば、ビットマップ画像のバイナリでの扱いに関する有益な情報が入手しやすいだろう。


たとえば、Windows用のフリーのバイナリエディタ stiring で3×5ピクセルの白色だけのビットマップファイルを見ると、下記のようになっている。

 ADDRESS   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
------------------------------------------------------------------------------
 00000000  42 4D 82 00 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
 00000010  00 00 05 00 00 00 03 00 00 00 01 00 04 00 00 00   ................ 
 00000020  00 00 0C 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00   ......ト...ト..... 
 00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
 00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
 00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
 00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
 00000070  00 00 FF FF FF 00 FF FF F0 00 FF FF F0 00 FF FF   ................ 
 00000080  F0 00  

stiring に限らず一般にバイナリエディタの画面の構成は、上記のようになっている。(なお、「ファイル」→「ダンプイメージの保存」などの操作で、上記のようなバイナリー・データのイメージをテキストエディタ(Windowsの場合は『ワードパッド』など)で見られる形式に変換して出力できる。)

バイナリー・データはそのままでは、テキスト表示できない。なので、それをテキスト化したり、見やすくするために書式を整えたりして、バイナリー・データの内容を出力したものをダンプ イメージという。

また、そのようなダンプイメージを出力することを、俗に(ぞくに)「ダンプする」などとIT業界では言う。


さて、このダンプイメージのうち、バイナリー・データの本体は、真ん中のブロックである、下記の

42 4D 82 00 00 00 00 00 00 00 76 00 00 00 28 00
00 00 05 00 00 00 03 00 00 00 01 00 04 00 00 00
00 00 0C 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80
00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80
00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF
00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF
00 00 FF FF FF 00 FF FF F0 00 FF FF F0 00 FF F
F0 00  

の部分である。


一番左のブロック、

BM........v...(. 
................ 
......ト...ト..... 
................ 
................ 
......タタタ....... 
................ 
................ 

は、ファイル内容のバイナリー・データをアスキーコードに変換して表示している。

このアスキーは、バイナリー・データのファイル内容ではなく、バイナリエディタ側がユーザーが内容を探しやすくする目的などのために表示している。


一番上の行の

00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
-------------------------------------------------------------------

は、単に何バイト目かをあらわしているだけである。これは、バイナリー・データのファイル内容ではなく、バイナリエディタ側がユーザーが見やすくするために表示している。


また、一番左の列の

 ADDRESS  
-----------
 00000000  
 00000010  
 00000020 
 00000030 
 00000040  
 00000050  
 00000060 
 00000070  
 00000080 

は、アドレスをあらわしている。


各ヘッダの名称


 
バイナリエディタで見た場合のビットマップのバイナリー・データの配置
バージョンによって少々異なる場合もあるが、おおむね、こういう感じである。


赤い背景色のブロックで表した部分を「BITMAP FILE HEADER」といい、これの長さは14バイトで固定である。「42 4D」で2バイト、同じように数えて12バイトの長さ。「BITMAP FILE HEADER」とは、つまり、「42 42」「画像のファイルサイズ」「予約領域」「予約領域」「画像の場所の指定」の部分である。

日本語では、「BITMAP FILE HEADER」のことを、ビットマップの「ファイルヘッダ」と言う場合もある。 つまり、ビットマップのファイルヘッダの長さは14バイトで固定である。


さて、青い背景色のブロックで表した部分を「BITMAP INFO HEADER」という。

つまり、「BITMAP INFO HEADER」とは、

「ヘッダサイズ」
「画像の横幅」「画像の高さ」「プレーン数」「色ビット数」「圧縮形式」
「画像データのサイズ」「ヨコ方向の解像度」「タテ方向の解像度」「パレット数」
「重要な色数」

の部分である。日本語では、「BITMAP INFO HEADER」のことを、ビットマップの「情報ヘッダ」と言う場合もある。

ビットマップ情報ヘッダの長さは40バイトで固定である。 バイナリー・データ本体の右上にあるビットマップ情報ヘッダの「28 00」と次の段の「00 00」は、ヘッダサイズに相当するが、28は十進数に直すと40である(16×2+8=40)。 リトルエンディアンなので、あとの00の3回くりかえしは無視していい。


各部の内容

さて、ビットマップ構造体の図を、ダンプイメージと照らし合わせてみよう。

 
(再掲)バイナリエディタで見た場合のビットマップのバイナリー・データの配置


 ADDRESS   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
------------------------------------------------------------------------------
 00000000  42 4D 82 00 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
 00000010  00 00 05 00 00 00 03 00 00 00 01 00 04 00 00 00   ................ 
 00000020  00 00 0C 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00   ......ト...ト..... 
 00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
 00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
 00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
 00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
 00000070  00 00 FF FF FF 00 FF FF F0 00 FF FF F0 00 FF FF   ................ 
 00000080  F0 00  


「42 4D」のあとに 「82 00 00 00」と4バイトぶんの数字が続くが、これはファイルサイズを表している。

なお、このファイルのサイズは130バイトである。16進数の「82」は 10進数で「130」である。算数で10進数の計算で 8×16+2=130 である。

読者のWindowsパソコンでもバージョンによるかもしれないが、『ペイント』などで3×5ピクセルの16色ビットマップ画像を作成すれば、おおむね、このあたりのファイルサイズになるだろう。

なお、ペイントのキャンバスサイズの変更は、左上のメニューアイコンから「プロパティ」を選ぶと「イメージのプロパティ」設定ウィンドウが開き、下のほうにキャンバス幅とキャンバス高さの指定欄があるので、そこで数値を入力してサイズを変形できる。


この「82 00 00 00」の意味は、10進数にすれば

130 + 2561×0 + 2562×0 + 2563×0

という意味である。こういう数え方を「リトル エンディアン」という。

なお「256」の由来は 16×16=256 である。

けっして、8200万バイトでないし、130万バイトでもないので、勘違いしないように。


次の、2つの 予約領域 は、ゼロで埋める決まりになっており、なので「00 00 」「00 00」である。


1段目アドレスのABCDの部分にある「 76 00 00 00」は、まず意味は10進数で118である。7×16+6=118 より。

で、「バイナリー・データの118バイト目から、画像データが始まります」と宣言している(ファイル先頭から画像デーマまでの「オフセット」という)。1バイトは256で、16×16=256なので16進数では2ケタぶんです。画像データの先頭のバイトが118バイト目であると、を指し示している。

なお、数え方は左上の「42 4D」ですでに2バイトである。 この調子で118バイト目を探すと

118÷16 = 7 あまり 6

なので、8行目(商7にプラス1)の7列目(あまり「6」+1)のバイト(6)目が、開始位置である。下図の太字にした部分のバイトである。

ADDRESS   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
------------------------------------------------------------------------------
00000000  42 4D 82 00 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
00000010  00 00 05 00 00 00 03 00 00 00 01 00 04 00 00 00   ................ 
00000020  00 00 0C 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00   ......ト...ト..... 
00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
00000070  00 00 FF FF FF 00 FF FF F0 00 FF FF F0 00 FF FF   ................ 
00000080  F0 00  


よくみると、この太字の部分からFが5個続き、「000」というのが3回数くりかえすので、合計でFが15回繰り返される。これは、この画像が横5×3であることに対応している。

さて、「画像の横幅」「画像の縦幅」に対応するのは「05 00 00 00」「 03 00 00 00」である。これは、横5ピクセル×縦3ピクセルという意味である。数値の数え方はリトルエンディアンである。


ビットマップ情報ヘッダの終わりから画像の開始までは、カラーパレットが16個、続いています。

上ダンプの太字の直前の「FF FF FF 00」とは、16番目のビットマップのカラーパレットです。カラーパレットは合計で16個あります。また、この16番目パレットを呼び出すのに、画像データ部分のF5回の繰り返しの部分では「F」でパレットを呼び出しています。


その直前のパレットの「FF FF 00 00」が15番目のパレットです。このパレットは「E」で呼び出されますが、今回の画像データでは使われてないです。



「プレーン数」と「色ビット数」の「01 00 」「04 00」は。プレーン数が十進数で1で、色ビット数が十進数で4という意味である。

「圧縮形式」とあるが、無圧縮にする場合はゼロにする。つまり「00 00 00 00」で無圧縮である。ビットマップは、仕様上では、実は圧縮も可能な仕様である。


解像度「C4 0E 00 00」は、印刷などをする際の1メートルあたりのピクセル数ですが、今回は印刷をしないので、気にしないことにします。


「パレット数」は、仕様上は策定されていますが、実際には利用されていません。マイクロソフトの『ペイント』アプリで作ったファイルですらも、ここはゼロのままの「00 00 00 00」です。

「重要な色数」も、仕様上は策定されていますが、やはり実際には利用されていません。これまたマイクロソフトの『ペイント』アプリで作ったファイルですらも、ここはゼロのままの「00 00 00 00」です。

なお、ビットマップの仕様を策定した起業は、昔のIBMとマイクロソフトです。


なお、マイクロソフトのサイトでは


typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;
  LONG  biWidth;
  LONG  biHeight;
  WORD  biPlanes;
  WORD  biBitCount;
  DWORD biCompression;
  DWORD biSizeImage;
  LONG  biXPelsPerMeter;
  LONG  biYPelsPerMeter;
  DWORD biClrUsed;
  DWORD biClrImportant;
} BITMAPINFOHEADER, *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

のようにC言語っぽいコードのようなモノが書いてあるが、これは単に構造を説明しているだけであり、このコードをコンパイラなどに入力しても何も起きない。

外部リンク: MSDN BITMAPINFOHEADER structure


実例

編集

背景が白色だと分かりづらいので、黄色にしてみましょう。


それを横5×縦3ピクセルにしたファイルのバイナリー・データダンプは、次のようになります。

 ADDRESS   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
------------------------------------------------------------------------------
 00000000  42 4D 82 00 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
 00000010  00 00 05 00 00 00 03 00 00 00 01 00 04 00 00 00   ................ 
 00000020  00 00 0C 00 00 00 00 00 00 00 00 00 00 00 00 00   ................ 
 00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
 00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
 00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
 00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
 00000070  00 00 FF FF FF 00 BB BB B0 00 BB BB B0 00 BB BB   ......ササー.ササー.ササ 
 00000080  B0 00  


「BB BB B0 00 」と3回繰り返すことで、パレットBでピクセルを塗るのを横5マスを3行続けていることが分かります。

末尾の「0」や「00」はおそらく改行のようなものでしょう。


また、キャンバスサイズを拡大し、横6×縦9ピクセルで黄色一色のビットマップ画像にすると、ダンプは下記のようになります。


 ADDRESS   00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   0123456789ABCDEF 
------------------------------------------------------------------------------
 00000000  42 4D 9A 00 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
 00000010  00 00 06 00 00 00 09 00 00 00 01 00 04 00 00 00   ................ 
 00000020  00 00 24 00 00 00 00 00 00 00 00 00 00 00 00 00   ..$............. 
 00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
 00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
 00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
 00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
 00000070  00 00 FF FF FF 00 BB BB BB 00 BB BB BB 00 BB BB   ......サササ.サササ.ササ 
 00000080  BB 00 BB BB BB 00 BB BB BB 00 BB BB BB 00 BB BB   サ.サササ.サササ.サササ.ササ 
 00000090  BB 00 BB BB BB 00 BB BB BB 00                     サ.サササ.サササ.      


よくみると後半「BB BB BB 00」が9回(※ この画像のタテのピクセル数が9である)、繰り返されています。

また、Bの個数も、6回ずつ(※ この画像の横幅は6である)です。

最後の「00」で、このことから、00バイトで改行を表すのだろう事が分かります。


とすると、パレット番号に「0」や「00」は使えないハズです。



なお、画像データの読み方は、バイナリー・データの情報が、画像では左下から右下に対応します。

なので、2色以上を使った画像で、調べてみましょう。

下記の画像は、ややサイズが大きい画像ですが(サイズが小さいとビューワーの性能によっては色ボケしたりして不正確になるので)、

幅30×高さ10 の画像です。白地を基調としておりますが、左上に赤い帯があります。下側に青い帯があります。右上に短く青い帯があります。


ADDRESS 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 0123456789ABCDEF


00000000  42 4D 16 01 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
00000010  00 00 1E 00 00 00 0A 00 00 00 01 00 04 00 00 00   ................ 
00000020  00 00 A0 00 00 00 00 00 00 00 00 00 00 00 00 00   ................ 
00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
00000070  00 00 FF FF FF 00 CC CC CC CC CC CC CC CC CC CC   ......フフフフフフフフフフ 
00000080  CC CC CC CC CC 00 6C CC CC CC CC CC CC CC CC CC   フフフフフ.lフフフフフフフフフ 
00000090  CC CC CC CC C6 00 FF FF FF FF FF FF FF FF FF FF   フフフフニ........... 
000000A0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000B0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000C0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000D0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000E0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000F0  FF FF FF FF FF 00 99 99 99 99 99 99 99 99 99 99   ......劔劔劔劔劔 
00000100  9F FF CC CF FF 00 99 99 99 99 99 99 99 99 99 99   ..フマ..劔劔劔劔劔 
00000110  9F FF 6C CF FF 00                                 ..lマ..           
 

太字にしたところから、画像の始まりです。ほぼパレット「C」が30回繰り返されています。2行目の最初が「6C」になってるのは、ペイントが勝手にそういう色に変えただけです。2行目は6のあと「C」が28回繰り返され、最後にパレット「6」番の色です。


パレットCは、斜線にした 「FF 00 00 00」の部分です。「赤色(256段階), 緑色(256段階), 青色(256段階)」の順番になっています。

なので、

「FF 00 00 00」なら、真っ赤な赤色、
「00 FF 00 00」なら、ま緑色、
「00 00 FF 00」なら、完全な青色

です。

「FF FF FF 00」なら、完全な白色、
「00 00 00 00」なら、完全な黒色です。


さて、幅30×高さ10 の画像は、画像で見ると、青い帯は下側にあります。しかしバイナリー・データでは、青い帯は、最初に書かれています。

このように、上下が反転しています。そういう仕様だからです。


Linux のxxdコマンド

Linux だと、このバイナリー・データのテキスト形式のダンプ内容を、バイナリー・データのビットマップ画像形式に変換できるコマンドがある。

xxd というコマンドを使えばいい。Fedora 31 では標準では、この xxd がついていない。なのでFedoraにこのコマンドをインストールするために vim-common をインストールする必要がある。Fedora ではvim-common に xxd が含まれている。

xxdはもともと、バイナリー・データを16進ダンプするためのコマンドであるが、このソフトウェアには、さらに16進ダンプされた内容のテキストをバイナリー・データに戻す機能もある。


引数に -r をつける事により、16進ダンプをバイナリー・データに戻せる。

たとえば、なんらかのテキストエディタ(Fedora標準の gedit でもよい)に、下記のテキスト(これはstirringで出力したテキストから、上2行の見出しを除去しただけの物である)をそのままコピーペーストしてテキストエディタに貼り付けし、普通に「test」などの名前で保存しよう。この段階では、ファイル「test」は、まだ単なるテキストファイルである。

00000000  42 4D 16 01 00 00 00 00 00 00 76 00 00 00 28 00   BM........v...(. 
00000010  00 00 1E 00 00 00 0A 00 00 00 01 00 04 00 00 00   ................ 
00000020  00 00 A0 00 00 00 00 00 00 00 00 00 00 00 00 00   ................ 
00000030  00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80   ................ 
00000040  00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80   ................ 
00000050  00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF   ......タタタ....... 
00000060  00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF   ................ 
00000070  00 00 FF FF FF 00 CC CC CC CC CC CC CC CC CC CC   ......フフフフフフフフフフ 
00000080  CC CC CC CC CC 00 6C CC CC CC CC CC CC CC CC CC   フフフフフ.lフフフフフフフフフ 
00000090  CC CC CC CC C6 00 FF FF FF FF FF FF FF FF FF FF   フフフフニ........... 
000000A0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000B0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000C0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000D0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000E0  FF FF FF FF FF 00 FF FF FF FF FF FF FF FF FF FF   ................ 
000000F0  FF FF FF FF FF 00 99 99 99 99 99 99 99 99 99 99   ......劔劔劔劔劔 
00000100  9F FF CC CF FF 00 99 99 99 99 99 99 99 99 99 99   ..フマ..劔劔劔劔劔 
00000110  9F FF 6C CF FF 00                                 ..lマ..           
 

だが、コマンド端末で

xxd -r test > testout

と入力して実行すると、なんと『testout』というファイル名で、画像ファイルが新規作成されている。


コード例

編集

言葉だけだと分かり辛いので、実際に下記コード例でC言語でバイナリー・データを書き込むことによって画像ファイルを作成してみましょう。

#include <stdio.h>

#pragma warning(disable : 4996)

int main() {
  FILE *fp1 = fopen("test1change.bmp", "wb");

  if (fp1 == NULL) {
    perror("ファイルを開けませんでした。");
    return 1;
  }
  printf("ファイルをオープンしました。\n");

  printf("バイナリファイルに書き込んでいます。 \n");

  char buf[] = {
      // 1 line
      0x42, 0x4d,    //
      0x82, 0, 0, 0, //ファイルサイズ
      0, 0,          //予約領域
      0, 0,          //予約領域
      0x76, 0, 0, 0, // ヘッダサイズ

      0x28, 0, 0, 0, //  情報ヘッダサイズを4バイトで指定

      // 2 line
      05, 00, 00, 00, // 画像の横幅
      03, 00, 00, 00, // 画像の高さ
      01, 00,         // プレーン数
      04, 00,         // 色ビット数
      00, 00, 00, 00, // 圧縮形式

      // 3 line
      0x0C, 0x00, 00, 00, // 画像データ部分のサイズ
                          // 12バイト(コード下部の画素の合計バイトと一致)
      0xC4, 0x0E, 00, 00, // 横方向の解像度
      0xC4, 0x0E, 00, 00, // 縦方向の解像度
      00, 00, 00, 00,     // パレット数
      00, 00, 00, 00,     // 重要な色数
      // ここから画像

      // カラーパレット
      00, 00, 00, 00,       // 0
      00, 00, 0x80, 00,     // 1
      00, 0x80, 00, 00,     // 2
      00, 0x80, 0x80, 00,   // 3
      0x80, 00, 00, 00,     // 4
      0x80, 00, 0x80, 00,   // 5
      0x80, 0x80, 00, 00,   // 6
      0x80, 0x80, 0x80, 00, // 7
      0xC0, 0xC0, 0xC0, 00, // 8
      00, 00, 0xFF, 00,     // 9 赤
      00, 0xFF, 00, 00,     // A 緑
      00, 0xFF, 0xFF, 00,   // B
      0xFF, 00, 00, 00,     // C 青
      0xFF, 00, 0xFF, 00,   // D 計算上は紫だが実際はピンク
      0xFF, 0xFF, 00, 00,   // E
      0xFF, 0xFF, 0xFF, 00, // F 白

      //画素
      0xBB, 0xBB, 0xB0, 00, // 4バイトなのは偶然
      0xBB, 0xBB, 0xB0, 00, 0xBB, 0xbb, 0xB0, 00,
  };
  fwrite(buf, sizeof *buf, sizeof buf / sizeof *buf, fp1);

  fclose(fp1);
  printf("ファイルをクローズしました。\n");
}

とりあえず、上記コードを実行すると、黄色いビットマップが作成されます。


もちろん、こうやって一個ずつ作成するのは非効率だし、今さらパレット数が限られる形式は古いビットマップは非効率ですが、とりあえず上記コードで実際にビットマップが作成できるのです。

後半部


  
  0xBB, 0xBB, 0xB0, 00, 
  0xBB, 0xBB, 0xB0, 00, 
  0xBB, 0xbb, 0xB0, 00,
    };

の英数字1文字が 5×3 のサイズの1ピクセルに対応していることが一目瞭然です。

実際、英数字のBを十六進で 0 から F の別の 英数字に変えると、コンパイル後のビットマップの対応する画素の色が変わります。

なお、画像は上下反転して表示されることに注意してください。

たとえば、元データが

0 1 0
0 0 0
1 0 1

の配置なら、表示画像は

1 0 1
0 0 0
0 1 0

のように上下逆に配置されます。

実際、色変えを試したところ、色と英数字の関係は

1 茶色 メロン色
2 深い緑(ビリジアン)
3 枯れ草色
4 深い青
5 紫
6 青と緑のまざったような色
7 暗い灰
8 黄色
9 サーモンピンク
A 明るい黄緑
B 黄色
C 青色
D ピンク
E 黒
F 白

でした。カラーパレットどおりです。

00, 00, 00, 00,      // パレット数

となっていますが、なぜか16個のパレットを使用できます。


さて、上記の黄色5×3のビットマップに、右端に青色を1列くわえて横6マスにしたい場合、やればいいことは単に

06, 00, 00, 00,      // 画像の横幅

のように横幅の数値を5から6に変えて、

 0xBB, 0xBB, 0xBC, 00, 
 0xBB, 0xBB, 0xBC, 00, 
 0xBB, 0xbb, 0xBC, 00,

のように青色に対応す「C」を一番右列の次に加えれば済みます。


なお、ビットマップにはいくつかの方式がありますが、 上述の例で示した方式は 4bitビットマップ

という方式です。


色ビット数の「04」に対応しています。


圧縮形式が「00」の場合、色ビット数の数値によって画像の形式が決まり、 色ビット数が「01」なら白黒2階調、「04」なら4bitカラー、「08」なら8bitカラーのように決まる仕様です

4bitビットマップでは、16色までしか使えません。

2010年以降の現在、一般のr=0~255,b=0~255,g=0~255の合計1677万色でよく使われているのは、これではなく24bitビットマップや32bitビットマップと呼ばれる方式です。


なお8bitビットマップは256色です。


重要色数から画素対応(イメージデータ)までの部分は、下記のように4個ずつ改行すれば16行になるので、

   00, 00, 00, 00,
   00, 00, 0x80, 00,
   00, 0x80,00, 00, 
   00, 0x80, 0x80, 00,
   0x80, 00, 00, 00,
   0x80, 00, 0x80, 00,
   0x80, 0x80,    00, 00, 
   0x80, 0x80, 0x80, 00,
   0xC0, 0xC0, 0xC0, 00,
   00, 00, 0xFF, 00,
   00, 0xFF,  00, 00,
   00, 0xFF, 0xFF, 00,
   0xFF, 00, 00, 00,
   0xFF, 00, 0xFF, 00,
   0xFF, 0xFF, 00, 00,
   0xFF, 0xFF, 0xFF, 00,  

これはカラーパレットです。

ただし、各行の数値は前から青、緑、赤の順番に対応しています。「RGB」の逆ですので、注意してください。

実際、たとえば、0からFまでの数字を使った画素情報

 0xde, 0xf0, 0xBC, 00, 
 0x78, 0x9a, 0xbc, 00, 
 0x12, 0x34, 0x56, 00,

に書き換えておいたあと、カラーパレットに相当する部分でたとえば 0xFF, 00, 00, 00, を 0x00, 00, 0xFF, 00, に書き換えたら、今まで青だったところが赤に変わります。


なお、ビットマップ形式は透明化をサポートしていません。

コードでは、たとえばパレットの6行目あたりに

0x80, 00, 0x80, 00,

RGB(コード青、緑、赤で並んでいますが)の色情報のあとにある 4列目の00 は、透明度でなければ不透明度でもなく、単なるゼロ・ゼロです。

実際、後述しますが24bitカラーになると、ゼロゼロはなくなり、RGB値だけを画素部分に直接記入しています。

ビットマップ構造体

編集

2bitカラー

編集

ビットマップの2bit カラーのモノクロ2諧調で下記のように5×3でキャンバスに色を塗ると、バイナリー・データは下記のようになります。

□□■□□
□□□■□
■□□□□


42 4D 4A 00 00 00 00 00 00 00 3E 00 00 00 28 00 00 00 05 00 00 00 03 00 00 00 01 00 01 00 00 00 00 00 0C 00 00 00 C4 0E 00 00 C4 0E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF 00 78 00 00 00 E8 00 00 00 D8 00 00 00


末尾の FF FF FF 00 78 00 00 00 E8 00 00 00 D8 00 00 00

の FF FF FF 00 の後が画素の情報です。

この意味は、 まず「78」の最初の7を 2進法に変換すれば

0111 です。

0が黒色、1が白色に相当し、

これで一番下の行の

■□□□

が確定します。


次の8は、2進数では「1000」です。1が白なので、

■□□□□■■■

となりますが、横幅が5までなので、6個目以上はカットされて、

■□□□□

となり、こうして一番下の行が確定します。 下から2行目や3行目の色も、1行目と同様に決まります。

もしカットされた横方向の6列目以降を見たいなら、バイナリエディタ2行目辺りにある画像の横幅の 05 を 08 に変えてみれば、見ることができ、確かに黒色になっています。

また、カラーパレットに相当する部分は

42 4D 4A 00 00 00 00 00 00 00
3E 00 00 00
28 00 00 00
05 00 00 00 // 横幅
03 00 00 00 // 縦幅
01 00 // 色数?。ここが1なので2bitカラーとして認識される
01 00 // 重要な色数?
00 00 00 00
0C 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
00 00 00 00
FF FF FF 00 78 00 00 00 E8 00 00 00 D8 00 00 00

のうちの

00 00 00 00 // 末尾 1つ
00 00 00 00 // これ
FF FF FF 00 78 00 00 00 E8 00 00 00 D8 00 00 00

のあたりです。

00 00
FF 00 00 FF
FF FF 00 00 78 00 00 00 E8 00 00 00 D8 00 00 00

のようにいじってみると、(白と黒ではなく)色が水色と青色のものに変わります。

FF 00 00 FF の最初の3ブロック FF 00 00 が、青FF, 緑00,赤00 を意味しています。「RGB」の逆ですので注意してください、

次のFF FF FF が、青FF,緑FF,赤FFを意味しています。

だから例えば紫色を使いたいなら、FF 00 FF を入力すればいいわけですが、実際には色解像度などの都合でピンク色が表示されます。

赤色の濃さをきめるブロックが本当に赤に対応しているかを確認するには、青と緑の部分を 00 に変えてみれば、確かに残りのブロックが赤色に対応していることがわかります。

24bitカラー

編集

5×3の黄色画像を24bitカラーで作ると、バイナリー・データは下記のようになります。

42 4D 66 00 00 00 00 00 00 00 36 00 00 00 28 00 00 00 05 00 00 00 03 00 00 00 01 00 18 00 00 00 00 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00 F2 FF 00


これは意味的には、

42 4D 
66 00 00 00 //ファイルサイズ 102バイト
00 00       //予約領域
00 00       //予約領域
36 00 00 00 // ヘッダサイズ
28 00 00 00 //  情報ヘッダサイズを4バイトで指定

05 00 00 00 // 横幅
03 00 00 00 // 縦幅
01 00       // プレーン数
18 00       // 十進数に直せば24(色ビット数)
00 00 00 00 // 圧縮形式

30 00 00 00 // 画像データ部分のサイズ 48 = 3色×縦3×横5 + 3
00 00 00 00 // 横方向の解像度
00 00 00 00 // 縦方向の解像度
00 00 00 00 // パレット数
00 00 00 00  // 重要な色数

// 画素
00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00

00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00 

00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00 F2 FF 
00

です。

24bitカラーでは、カラーパレットが無くなります。

色は、16進数の2ケタ×3原色ぶんで直接、指定する方式です。ただし、青,緑,赤の配置で並んでいます。

00 F2 FF 

青色 00,
緑色 F2, (十進にすれば約242)
赤色 FF,(十進で255)

の意味です。

だから、バイナリエディタを使って画素の最初の

00 F2 FF 

をたとえば FF 00 00 に変えれば、ビットマップ画像の左下が青く変わります。

PNGフォーマット

編集

例によって 横 5× 縦 3 の黄色画像を作ってバイナリー・データを調べてみましょう

結果は

89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 05 00 00 00 03 08 02 00 00 00 D4 54 52 AF 00 00 00 01 73 52 47 42 00 AE CE 1C E9 00 00 00 04 67 41 4D 41 00 00 B1 8F 0B FC 61 05 00 00 00 09 70 48 59 73 00 00 0E C3 00 00 0E C3 01 C7 6F A8 64 00 00 00 11 49 44 41 54 18 57 63 F8 FF 09 05 E1 E7 7F 62 00 00 CA 27 1D 20 62 9D 09 11 00 00 00 00 49 45 4E 44 AE 42 60 82

です。

これは意味は


89 50 4E 47 0D 0A 1A 0A // PNGで表すことを示す決まり文句/ 50 4E 47 がASCIIでPNGだが、直前の 89 によりPが別文字になることも

// イメージヘッダ (通常 合計25バイトまで)
00 00 00 0D // データ長さ 常に13
49 48 44 52 // ASCII でIHDR 

//種類によって長さが変わるので、CRCで終端チェック
00 00 00 05 // 画像の幅
00 00 00 03 // 画像の高さ
08 // ビット深度
02 // カラータイプ
00 // 圧縮手法. 現行では常に0で、デフレート圧縮(LZ77圧縮アルゴリズム)
00 // フィルター 普通0
00 // インターレースの有無。0なら無し。1であり。
D4 54 52 AF // CRC

// ここまでイメージヘッダ(バイトを数えると、きとんと25バイトある)

00 00 00 01 // データサイズ. 下記 sRGB なら「1」
73 52 47 42 // AcCIIで sRGB 
00          // レンダリング目的
AE CE 1C E9 // CRC

00 00 00 04 // データサイズ. gAMAなら 4
67 41 4D 41 // AcCIIで gAMA  ガンマ
00 00 B1 8F // ガンマ値
0B FC 61 05 // CRC

00 00 00 09 // データサイズ. pHYs なら 9
70 48 59 73 // AcCIIで pHYs
00 00 0E C3 // x軸上の単位あたりのピクセル数. 十進で 3779
00 00 0E C3 // y軸上の単位あたりのピクセル数. 十進で 3779
01 // 単位指示 1ならメートル
C7 6F A8 64 // CRC

// IDAT 
00 00 00 11 // 長さ。この場合、十進で17
49 44 41 54 // ASCII で IDAT
18 57 63 F8 FF 09 05 E1 E7 7F 62 00 00 CA 27 1D 20 // 長さで定義した17個
62 9D 09 11 // CRC

// 12バイトのイメージ終端
00 00 00 00 // 値は常に0
49 45 4E 44 // ASCII で IEND
AE 42 60 82 // 必ず 0xAE 0x42 0x60 0x82 . 終端をあらわす

数値はビッグエンディアンで並んでいます。

CRCは巡回冗長検査というものです。

PNGは圧縮ライブラリにオープンソースの圧縮伸張ライブラリ ZLIB を採用しています。なので、ZLIB を調べなければなりません。


PNGについて、企業 GREE の技術者ブログが詳しくわかりやすくまとまっています。 『PNG軽量化の減色と圧縮について | エンジニアブログ | GREE Engineering』

詳しくは、そちらを読んでください。当wikiよりも遥かにレベルの高い技術者がわかりやすく解説してくれています。

いちおう、当wiki側で念のためにある程度は技術解説しておくと(リンク先サイトが将来的に消失するような場合もあるので念のため)、 zlib自体は統一的に普及していますが、しかしPNGの圧縮はzlibだけによるものではなく、フィルタやパレットによる圧縮の効果もあります。

このため、どういったアルゴリズムを採用するかによって圧縮率が変わるので、実はPNGには統一的な圧縮方法がなく、ソフトウェアやアルゴリズムによって変わります。だから代表的なPNG編集可能ソフトウェアでは、さまざまなパターンの圧縮方法を試して、一番圧縮率の高かったものを採用するとのことです。

こういう複雑な仕様のため、個人でPNG編集ソフトウェアを作るのは、あまり現実的ではないでしょう。企業やベンチャーなどに任せましょう。


カラータイプを見て、パレットを使う方式かどうかを判別しています。パレットを使う方式の場合、 PLTE というチャンクがある。

カラータイプは、0,2,3,4,6 のいずれか。カラータイプ 3 ならパレットを使う方式。カラータイプが4または6のとき、透過情報をもつことができる。

 カラータイプ対応表
 カラータイプ 色深度 概要
0  1,2,4,8,16  グレースケール
2  8,16  カラー(トゥルーカラー)
3  1,2,4,8  パレット(インデックスカラー) + PLTE チャンク 必須
4  8,16  グレースケール + 透明(アルファ)
6  8,16  トゥルーカラー + 透明

ビット深度は、青1の強さが何ビットかということです。

8bitなら、0~255までの合計256段階ということです。

RGBと合計で三原色あるので、24ビットになります。


なので、バイナリー・データを見ると8bitですが、しかしマウス操作の右クリックなどで画像のプロパティを見ると 「ビットの深さ」が24ビットになっています(8×3=24 なので)。


なので、上記の黄色画像のファイル形式でなら フルカラー(約1670万色)を扱えます。PNG 24 は透過情報を持ちません。

上記、黄色画像は、後述するPNG 24 というものです。


PNG のビット深度には一般にrgbとアルファの合計で8bitと24bitと32bitのものがありますが、拡張子はどれも「.png」です。

通称、ビット深度がrgb+アルファで合計24bitのものは、PNG 24 などと言われます。PNG 24 はフルカラー(約1670万色)を扱えます。PNG 24 は透過情報を持ちません。

これに透過情報を持たせるには別途、外部の画像編集ソフトで透過情報を追加させてビット深度をrgb+アルファで合計32bitにさせた PNG 32 を作る必要があります。

ビット深度がrgb+アルファで合計8bitのものは256色程度までしか扱えず、PNG 8 などといわれます。 png 8 も png 24 も png 32 も、どれも拡張子は「.png」です。


なお、一原色あたりビット深度 16 のものは、つまり青一色でも 0 ~ 65535 の合計 65536 段階あるものです。

ビット深度は通常は 1, 2, 4, 8, 16 のいずれかです。カラータイプ 2 や 6などのトゥルーカラーが対応しているのはビット深度が 8 以上のものです。

なお実はPNGにはPNG 48 という色数が約280兆色 のものもあり、フォトショップなど一部の画像処理が対応しているようです。

※ 編集者が調査中でよく分かりませんので、PNG 48 については説明を省略します。


また、PNGには、画像の行ごとに「フィルタ」と言う処理があり・・・(調査中)


PNGはハフマン符号と言うのを使っています。これは、出現頻度の多い文字にほど、短い文字を割り当てるものです。ただし、1バイト単位で文字と見なして、それを符号化するようです。

ハフマン符号は、情報工学系の大学教科書などで一般的な『符号理論』のような科目の教科書で解説される、典型的な技術です。


PNGは、このほか LZSS という処理を使っています。

LZSS とハフマン符号を組み合わせたものが、PNGの採用している Deflate 圧縮です。

そして、このDeflate圧縮に補助的に色々なものをつけてライブラリ化したものが ZLIB です。


当ページは現状『ゲームプログラミング』をうたっています。

同人ゲームプログラミングなどでよくDXライブラリが使われますが、幸運なことに、DXライブラリの作者さまは圧縮にも興味を持っており、リンク先 『データ圧縮プログラムについてざっくり学ぶ』 のように圧縮についての技術解説をしたwebページを作ってくれています。


PNGはこのように複雑ですので、オープンソースのライブラリを使わずにPNGの読み書きソフトをゼロから作るのは非現実的です。zllbなどのオープンソースのライブラリを使って作ることになるでしょう。そのためには、windowsユーザーならwindowsでのライブラリの作り方やdllなどの作り方や使い方なども学ばなければならず、なかなか面倒そうです。

画像編集ソフトの企業の技術者でもない限り、PNGのフォーマットについては教養にとどめておいて、個人での編集ソフト製作はしないのが現実的かもしれません。


さて、PNGは解析には予備知識が膨大なのでプログラマーにはシンドイですが、しかし画像中での透過部分の保有に対応している画像フォーマットが、PNGやGIFなどの限られた画像フォーマットしかありません。(あとは、フォトショップなど一部の画像編集ソフトのフォーマットなので、そのソフトのユーザー以外には関係ない。)

ビットマップとJPEGは、透過部分の保有には対応していません。このため、画像中に透明部分の保有をしたい場合には、PNGがよく使われます。

なお、GIFはかつて特許問題があった等の事情があるためか、静止画にはGIFがあまり使われていないのが現状です。2004年にGIFの特許は切れましたが、しかしそれから10年以上経った2020年代の現在でも、静止画像にはGIFはあまり使われておらず、もっぱら動画において透過部分を保有したい場合にGIFが使われます。いわゆる「GIF動画」です。

そもそもの圧縮の理論について

編集

ランレングス圧縮

編集

よく圧縮の原理の説明で、「AAAAAAA」(Aが7個ある)を「A7」に置き換える、などと習います(なお「ランレングス圧縮」の原理)。高等学校でも、「ランレングス」という用語は習いませんが、こういう感じの原理を習います。

さて、上記「AAAAAAA」の説明では、都合よく続いている元の文字が、数字ではなく英字です。


ですが、もし「3333333」という数字の並びを圧縮するとき、どうするのかという問題があります。「37」とでも書くのでしょうか? しかしこれだと、もし三十七という内容の文字を表したい場合に困ります。

そう、つまり、実際には「A7」のような原理だけでは、圧縮できないのです。さらに必要な工夫として、圧縮後のどこからどこまでが文字の1単位であり、どこが反復回数なのかを、なんらかの方法で区別する必要性があるのです。

この問題を解決するための方法は単純であり、

必ず1バイト単位(十六進で2桁)で処理する。
必ず単位の文字に、反復回数を併記する。

という方法を使います。

たとえ1回しか現れない文字でも、反復回数を併記します。そのほうがアルゴリズムが簡単だからです。

具体例

「3」はアスキーコードで 0x03 です。これを7回続ける 3333333 なら、

0x03 0x07 です。


一方、「37」はアスキーコードで「0x03 0x07」です。1バイト単位で扱うので、「3」を1回、「7」を1回なので、

0x03 0x01 0x07 0x01

となります。

こうすることで、アスキー「3333333」とアスキー「37」を区別できます。


追記

なお、上記のような方法だと、反復回数は1バイト単位の最大数である FF までなので、つまり 255 回までしか反復を1フレーズでは処理できません。

だからもし「3」が262回続いたら、たとえば

0x03 0xFF 0x03 0x07

とでもなるでしょうか。圧縮後の記録が2バイトからバイトに増えましたが、しかしそれでも「FFFFF・・・」とFを262回記録する262バイトと比べたら大幅に圧縮できているので、意義のある圧縮です。


座学ばかりを聴いても頭に入らないでしょう。頭に入った気になるだけです。

実際に、ランレングス圧縮するコードを簡易的に作ってみましょう。

たとえば下記のテキストファイル "test.txt"

aaaaabbbbbbbbbbbbbb

をランレングス圧縮するコードを簡易的に書いてみましょう。簡易なので、色々と想定漏れがありますが、しかしこういうのは簡単な例からコードを作っていくのが勉強のコツです。いきなり色々なパターンを想定したものを書くと、挫折してしまいます。

例えば、下記のようになります。簡易的なものなので、2種類の文字までしか圧縮に対応していませんが、とりあえずこれでも実装時の方針はつかめます。 想定漏れはいろいろありますが、文句をつけるのではなく、読者がご自身で改良したコードを手元で自分で作っていってください。そうやってプログラミングは勉強していくものです。

1バイトの整数を宣言するには char 型を使います。(一方、int 型だと4バイトになるので、エラーの原因になりかねず危険です。)

また、ランレングスでは繰り返し回数は必ず0以上の正(ただし0も正とした)の数なので unsigned char 型で宣言するのが安全です。(unsigned をつけないと、繰り返し回数 128以上の結果が異なってしまうので。)


#include <stdio.h>

#pragma warning(disable : 4996)

int main() {
  FILE *fp1 = fopen("test2.txt", "rb");

  if (fp1 == NULL) {
    perror("ファイルを開けませんでした。\n");
    return 1;
  } else {
    printf("ファイルをオープンしました。\n");
  }

  unsigned char str1[70]; // 表示結果を短くするために数値を微妙に小さくした
  printf("バイナリー・データを読み取っています。\n"); // 「文字列」ではなくバイナリー・データ

  fread(str1, sizeof(unsigned char), 50, fp1);

  printf("ファイルに書いてあるバイナリー・データ\n");

  for (int i = 0; i < sizeof(str1) / sizeof(str1[0]); i = i + 1) {
    printf("%02x ", str1[i]); // 最低でも2桁を表示、の意味
  }

  printf("\n文字の繰り返しを検出しようとしています...\n");

  int bufRep[3]; // 繰り返し文字の回数を記録するカウンターをとりあえず3つ用意。
  int bufWord[3]; // 繰り返し文字の字を記録するバッファをとりあえず3つ用意。

  bufWord[0] = str1[0];
  bufRep[0] = 1;

  int repFlag = 1;

  char press1[70];  // 圧縮結果の保存用
  int preCount = 0; // 何文字目まで書き込んだかのカウンタ

  int temp = 0;
  int breakLoop = 0; // for から抜けるためのフラグ

  for (temp = 0; temp <= 12; temp = temp + 1) {

    if (repFlag == 1) {
      if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
        bufRep[0] = bufRep[0] + 1;
        repFlag = 1;
      } else if (str1[temp] != 0) {
        repFlag = 0;
        press1[0] = bufWord[0];
        press1[1] = bufRep[0];
        preCount = +2;
        breakLoop = 1;
        break;
      } else if (str1[temp] == 0) {
        breakLoop = 1;
        // ヌルが来たら文末などと判断し、他に何もせずに抜け出す
        break;
      } // else if

      if (breakLoop == 1) {
        break;
      }

    } // if
  }   // for

  printf("記録する文字1のバイナリー・データ: %02x \n",
         bufWord[0]); // 最低でも2桁を表示、の意味
  printf("その文字の現在の回数: %02x \n",
         bufRep[0]); // 最低でも2桁を表示、の意味

  printf("圧縮する文字1のバイナリー・データ: %02x ",
         press1[0]); // 最低でも2桁を表示、の意味
  printf("その文字の現在の回数: %02x ", press1[1]); // 最低でも2桁を表示、の意味

  printf("\n次の書き込みカウンター位置: %02x \n",
         preCount); // 最低でも2桁を表示、の意味

  printf("\n", preCount); // 最低でも2桁を表示、の意味



  int hokan = temp; // 次のforの開始番号用

  repFlag = 1; // 次のループ用に再セット

  bufWord[1] = str1[hokan + 1];
  bufRep[1] = 1;

  for (temp = hokan + 1; temp <= 255; temp = temp + 1) {
    if (repFlag == 1) {

      if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
        bufRep[1] = bufRep[1] + 1;
        repFlag = 1;

      } else if (str1[temp] != 0) {

        repFlag = 0;
        press1[preCount + 0] = bufWord[1];
        press1[preCount + 1] = bufRep[1];
         preCount = preCount+2;

        break;
      } else if (str1[temp] == 0) {
        // ヌルが来たら文末などと判断し、何もせずに抜け出す
        break;
      } // else if
    }   // if
  }     // for

  printf("記録する文字2のバイナリー・データ: %02x \n",
         bufWord[1]); // 最低でも2桁を表示、の意味
  printf("その文字の現在の回数: %02x \n",
         bufRep[1]); // 最低でも2桁を表示、の意味

  printf("圧縮する文字2のバイナリー・データ: %02x ",
         press1[2 * 1]); // 最低でも2桁を表示、の意味
  printf("その文字の現在の回数: %02x ",
         press1[2 * 1 + 1]); // 最低でも2桁を表示、の意味

  printf("\n次の書き込みカウンター位置: %02x \n",
         preCount); // 最低でも2桁を表示、の意味

  printf("\nランレングス的に圧縮のシミュレーション...\n");

  for (int i = 0; i < preCount +2; i = i + 1) {
    printf("%02x ", press1[i]); // 最低でも2桁を表示、の意味
  }


  fclose(fp1);
  printf("\nファイルをクローズしました。\n");
}

実行結果

ファイルをオープンしました。
バイナリー・データを読み取っています。
ファイルに書いてあるバイナリー・データ
61 61 61 61 61 62 62 62 62 62 62 62 62 62 62 62 62 62 62 0a 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
文字の繰り返しを検出しようとしています...
記録する文字1のバイナリー・データ: 61 
その文字の現在の回数: 05 
圧縮する文字1の機会語: 61 その文字の現在の回数: 05 
次の書き込みカウンター位置: 02 

記録する文字2のバイナリー・データ: 62 
その文字の現在の回数: 0e 
圧縮する文字2の機会語: 62 その文字の現在の回数: 0e 
次の書き込みカウンター位置: 02 

ランレングス的に圧縮のシミュレーション...
61 05 62 0e 
ファイルをクローズしました。


一方、もし

ファイルを開けませんでした。
: No such file or directory

と表示されたら、単に読み取り先のテキストファイルを作成し忘れているか、あるいはファイルのアクセス許可などのセキュリティ設定が不適切だろうと考えられますので、テキストファイル側を修正してください。


上記コードだけの簡易的なものでも、実際にコードを作るとなると、何時間も掛かってしまいます。

上記コードでは、本当はもっと短くできますが(たとえば文字aを読み取るブロックと文字bを読み取るブロックを分けているので、コードが長くなっている)、しかし読者がコードの構造を理解しやすいように、意図的に文字aと文字bを読み取るブロックを分けています。

なお、実際にコードを作る場合は、けっしていきなり for とかwhile のコードを書くのではなく、愚直にまずは if 文のコードで「文字が2回続いた場合」「文字が3回続いた場合」などと場合ワケをしていき、あとからそれら場合分けをしたいくつもあるif文ブロックをfor などの構造でまとめていくことで、上記のように雛形(ひながた)を作っていきます。

ランレングスは、符号の種類が少ない場合ほど、符号が連続しやすいので、圧縮効率が高くなります。なので、ファクシミリなどの白黒印刷で、よくランレングス圧縮が使われます。

説明の都合上、文字の圧縮で解説していますが、どちらかというとランレングスはある種の画像データの圧縮向きであり、文書データの圧縮には向かない手法です。

PackBits

編集

ランレングス圧縮の欠点として、

ABCDE

のような文字を圧縮すると、

A1B1C1D1E1

となり、逆に長くなってしまいます。

そこで、長さが2以上または3以上のときにだけ圧縮する手法が考えられますが、しかしこの問題は、どこが圧縮された文字で、圧縮されてない文字かの区別をつけるのが難しいことです。

そこで、圧縮する部分としない部分との区別を、負数で区別する方法が考えられました。

たとえば

AAAAABCDEEEE

5A-3BCD4E

のように処理する方法です。

負数の場合、その絶対値の大きさのぶんだけ、不連続の文字が続きます。

また、このような処理のため、文字より先に数値を付記する必要が生じます。

ただし、この方法だと、負数を保管するために負号付き整数を利用するため、 つまり 255文字ではなく127文字までしか長さを保管できません。

C言語では、unsigned をつけないchar型で宣言すれば、-128~127を保管できる1バイト整数型になります。


コード例としては、まずいきなりPackBitsのコードを書くのは難しいので、まずランレングスをきちんと作ろう。

読み取り対象のテキストファイル "test2.txt" を下記のようにすれば(aが5回連続、bとcとdは不連続、eは6回連続)、

aaaaabcdeeeeee

ランレングスのコードは、

// ※注: これはランレングスです。
#include <stdio.h>
#pragma warning(disable : 4996)

int main() {
    FILE *fp1 = fopen("test2.txt", "rb");

    if (fp1 == NULL) {
        perror("ファイルを開けませんでした。\n");
        return 1;
    } else {
        printf("ファイルをオープンしました。\n");
    }

    unsigned char str1[70]; // 表示結果を短くするために数値を微妙に小さくした
    printf("バイナリー・データを読み取っています。\n"); // 「文字列」ではなくバイナリー・データ

    fread(str1, sizeof *str1, sizeof str1/sizeof *str1 - 1, fp1);
    
     char str2[70]; // 表示結果を短くするために数値を微妙に小さくした
                
    int endCount = 0;
    for (int i = 0; i < sizeof(str1) / sizeof(str1[0]); i = i + 1) {
    
        if (str1[i] > 0 &&  str1[i] < 256  ) {
            str2[i] = str1[i] ;
        }
        else { endCount = i; 
        
         str2[i] = 0x00 ;
        
        break;  }
    }

    printf("\nファイルに書いてあるバイナリー・データ\n");

    for (int i = 0; i < sizeof(str1) / sizeof(str1[0]); i = i + 1) {
        printf("%02x ", str1[i]); // 最低でも2桁を表示、の意味
    }


    printf("\nファイルに書いてあるバイナリー・データ2\n");

    for (int i = 0; i < sizeof(str2) / sizeof(str2[0]); i = i + 1) {
        printf("%02x ", str2[i]); // 最低でも2桁を表示、の意味
    }


    printf("\n文字の繰り返しを検出しようとしています...\n");

    int bufRep
        [3]; // 繰り返し文字の回数を記録するカウンターをとりあえず3つ用意。
    int bufWord[3]; // 繰り返し文字の字を記録するバッファをとりあえず3つ用意

    bufWord[0] = str1[0];
    bufRep[0] = 1;

    int repFlag = 1;

    char press1[70];  // 圧縮結果の保存用
    int preCount = 0; // 何文字目まで書き込んだかのカウンタ

    int temp = 0;
    int breakLoop = 0; // for から抜けるためのフラグ

    for (temp = 0; temp <= 12; temp = temp + 1) {

        if (repFlag == 1) {
            if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
                bufRep[0] = bufRep[0] + 1;
                repFlag = 1;
            } else if (str1[temp] != 0) {
                repFlag = 0;
                press1[0] = bufWord[0];
                press1[1] = bufRep[0];
                preCount = +2;
                breakLoop = 1;
                break;
            } else if (str1[temp] == 0) {
                breakLoop = 1;
                // ヌルが来たら文末などと判断し、他に何もせずに抜け出す
                break;
            } // else if

            if (breakLoop == 1) {
                break;
            }

        } // if
    }     // for

    printf("記録する文字1のバイナリー・データ: %02x \n",
           bufWord[0]); // 最低でも2桁を表示、の意味
    printf("その文字の現在の回数: %02x \n",
           bufRep[0]); // 最低でも2桁を表示、の意味

    printf("圧縮する文字1のバイナリー・データ: %02x ",
           press1[0]); // 最低でも2桁を表示、の意味
    printf("その文字の現在の回数: %02x ",
           press1[1]); // 最低でも2桁を表示、の意味

    printf("\n次の書き込みカウンター位置: %02x \n",
           preCount); // 最低でも2桁を表示、の意味

    printf("\n", preCount); // 最低でも2桁を表示、の意味

    int hokan = temp; // 次のforの開始番号用

    int LoopNum = 1;

int count ;
    breakLoop = 0; // 下記 while のbreak フラグ用
    while (breakLoop == 0) {
           if(LoopNum == endCount ) {

                    repFlag = 0;
                    breakLoop = 1; 
                    break; 
            }
                    
                    
        hokan = temp;
        bufWord[LoopNum] = str1[hokan + 1];
        bufRep[LoopNum] = 1;

        press1[preCount + 0] = bufWord[LoopNum];
        press1[preCount + 1] = bufRep[LoopNum];

        repFlag = 1; // 次のループ用に再セット
        for (temp = hokan + 1; temp <= 255; temp = temp + 1) { 
        
        
        
        
        
            if (repFlag == 1) {

                if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
                    bufRep[LoopNum] = bufRep[LoopNum] + 1;
                    repFlag = 1;

                } else if (str1[temp] != 0) {

                    repFlag = 0;
                    press1[preCount + 0] = bufWord[LoopNum];
                    press1[preCount + 1] = bufRep[LoopNum];
                    preCount = preCount + 2;

                    break;
                } else if (str1[temp] == 0) {
                    breakLoop = 1; // ヌルが来たら文末などと判断し ループから break
                    
                    break;
                }

                else {
                    breakLoop = 1; // 無限ループ防止のため 想定外の自体はループから break

                    break;
                } // else if
            } else {
                breakLoop = 1; // 無限ループ防止のため 想定外の自体はループから break

                break;
            }

            // if
        } // for

        printf("LoopNum: %02x \n", LoopNum); // 最低でも2桁を表示、

        printf("記録する文字%dのバイナリー・データ: %02x \n", LoopNum,
               bufWord[LoopNum]); // 最低でも2桁を表示、の意味
        printf("その文字の現在の回数: %02x \n",
               bufRep[LoopNum]); // 最低でも2桁を表示、の意味

        printf("圧縮する文字%dのバイナリー・データ: %02x ", LoopNum,
               press1[2 * LoopNum]); // 最低でも2桁を表示、の意味
        printf("その文字の現在の回数: %02x ",
               press1[2 * LoopNum + 1]); // 最低でも2桁を表示、の意味

        printf("\n次の書き込みカウンター位置: %02x \n",
               preCount); // 最低でも2桁を表示、の意味

        LoopNum = LoopNum + 1;
    } // while

    printf("\nランレングス的に圧縮のシミュレーション...\n");

    for (int i = 0; i < preCount + 0; i = i + 1) {
        printf("%02x ", press1[i]); // 最低でも2桁を表示、の意味
    }

 printf("\nファイルに書いてあるバイナリー・データ1\n");

    for (int i = 0; i < endCount; i = i + 1) {
        printf("%02x ", str1[i]); // 最低でも2桁を表示、の意味
    }
    
    
    printf("\nファイルに書いてあるバイナリー・データ2\n");

    for (int i = 0; i < endCount; i = i + 1) {
        printf("%02x ", str2[i]); // 最低でも2桁を表示、の意味
    }

       fclose(fp1);
    printf("\nファイルをクローズしました。\n");
}

実験結果

ファイルをオープンしました。
バイナリー・データを読み取っています。

ファイルに書いてあるバイナリー・データ
61 61 61 61 61 62 63 64 65 65 65 65 65 65 00 00 00 00 00 00 00 00 00 00 00 00 00
 00 00 00 00 00 98 55 49 00 00 00 00 00 b4 00 69 fe fe 07 00 00 20 1c 40 00 00 0
0 00 00 73 8b b1 ce 00 00 00 00 c0 f1 4b 00 00 00
ファイルに書いてあるバイナリー・データ2
61 61 61 61 61 62 63 64 65 65 65 65 65 65 00 00 10 3a 30 00 00 00 00 00 00 00 00
 00 00 00 00 00 00 00 00 00 00 00 00 00 11 00 01 10 fffffffe 07 00 00 00 00 00 0
0 00 00 00 00 ffffffca 12 ffffff82 77 00 00 00 00 40 15 40 00 00 00
文字の繰り返しを検出しようとしています...
記録する文字1のバイナリー・データ: 61
その文字の現在の回数: 05
圧縮する文字1のバイナリー・データ: 61 その文字の現在の回数: 05
次の書き込みカウンター位置: 02

LoopNum: 01
記録する文字1のバイナリー・データ: 62
その文字の現在の回数: 01
圧縮する文字1のバイナリー・データ: 62 その文字の現在の回数: 01
次の書き込みカウンター位置: 04
LoopNum: 02
記録する文字2のバイナリー・データ: 63
その文字の現在の回数: 01
圧縮する文字2のバイナリー・データ: 63 その文字の現在の回数: 01
次の書き込みカウンター位置: 06
LoopNum: 03
記録する文字3のバイナリー・データ: 64
その文字の現在の回数: 01
圧縮する文字3のバイナリー・データ: 64 その文字の現在の回数: 01
次の書き込みカウンター位置: 08
LoopNum: 04
記録する文字4のバイナリー・データ: 65
その文字の現在の回数: 06
圧縮する文字4のバイナリー・データ: 65 その文字の現在の回数: 06
次の書き込みカウンター位置: 0a
LoopNum: 05
記録する文字5のバイナリー・データ: 00
その文字の現在の回数: 01
圧縮する文字5のバイナリー・データ: 00 その文字の現在の回数: 01
次の書き込みカウンター位置: 0a

ランレングス的に圧縮のシミュレーション...
61 05 62 01 63 01 64 01 65 06
ファイルに書いてあるバイナリー・データ1
61 61 61 61 61 62 63 64 65 65 65 65 65 65
ファイルに書いてあるバイナリー・データ2
01 00 00 00 06 00 00 00 01 00 00 00 65 65
ファイルをクローズしました。

のような実行結果になる。

あとはこのコードを、PackBitsになるように改良していけばいいだけである。


上記コードでバイナリー・データを格納する配列を2種類(str1 と str2)用意している理由は、なぜか最後に書き込みしたバイナリー・データを格納している配列が書き換わってしまうバグ的な現象があるため、それを波及させないように2つ配列を用意しているという理由にもとづく。(「バイナリー・データ1」と「バイナリー・データ2」の結果がなぜか違っている。)


PackBits

さて、我々が知りたいのはランレングスではなく、Pack bitsのコードである。それでは、そのコードを作ろう。

とりあえず、読者の簡便のため圧縮したい文字列を再掲する。

aaaaabcdeeeeee

このコードを、下記のコードで圧縮しよう。 下記のように、長いコードになったが、とりあえず下記のコードで、実際に英数字だけの文字列なら、実験の結果、いくつか圧縮できる。

なお、コードをなるべく短くするために、前コードにあったデバッグ用メッセージの printf のいくつかは削除した。


コード例

#include <stdio.h>
#pragma warning(disable : 4996)

int main() {
    FILE *fp1 = fopen("test2.txt", "rb");

    if (fp1 == NULL) {
        perror("ファイルを開けませんでした。\n");
        return 1;
    } else {
        printf("ファイルをオープンしました。\n");
    }

    unsigned char str1[70]; // 表示結果を短くするために数値を微妙に小さくした
    printf("バイナリー・データを読み取っています。\n"); // 「文字列」ではなくバイナリー・データ

    fread(str1, sizeof(unsigned char), 150, fp1);

    char str2[70]; // 表示結果を短くするために数値を微妙に小さくした

    int endCount;
    for (int i = 0; i < sizeof(str1) / sizeof(str1[0]); i = i + 1) {

        if (str1[i] > 30 && str1[i] < 128) {
            str2[i] = str1[i];
        } else {
            endCount = i;

            str2[i] = 0x00;

            break;
        }
    }

    printf("\nファイルに書いてあるバイナリー・データ\n");

    for (int i = 0; i < sizeof(str1) / sizeof(str1[0]); i = i + 1) {
        printf("%02x ", str1[i]); // 最低でも2桁を表示、の意味
    }

    printf("\nファイルに書いてあるバイナリー・データ2\n");

    for (int i = 0; i < sizeof(str2) / sizeof(str2[0]); i = i + 1) {
        printf("%02x ", str2[i]); // 最低でも2桁を表示、の意味
    }

    printf("\n文字の繰り返しを検出しようとしています...\n");

    int bufRep
        [3]; // 繰り返し文字の回数を記録するカウンターをとりあえず3つ用意。
    int bufWord[3]; // 繰り返し文字の字を記録するバッファをとりあえず3つ用意

    bufWord[0] = str1[0];
    bufRep[0] = 1;

    int repFlag = 1;

    char press1[70];  // 圧縮結果の保存用
    int preCount = 0; // 何文字目まで書き込んだかのカウンタ

    int temp = 0;
    int breakLoop = 0; // for から抜けるためのフラグ

    for (temp = 0; temp <= 12; temp = temp + 1) {

        if (repFlag == 1) {
            if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
                bufRep[0] = bufRep[0] + 1;
                repFlag = 1;
            } else if (str1[temp] != 0) {
                repFlag = 0;
                press1[0] = bufWord[0];
                press1[1] = bufRep[0];
                preCount = +2;
                breakLoop = 1;
                break;
            } else if (str1[temp] == 0) {
                breakLoop = 1;
                // ヌルが来たら文末などと判断し、他に何もせずに抜け出す
                break;
            } // else if

            if (breakLoop == 1) {
                break;
            }

        } // if
    }     // for

    int hokan = temp; // 次のforの開始番号用

    int LoopNum = 1;

    int count;
    breakLoop = 0; // 下記 while のbreak フラグ用
    while (breakLoop == 0) {
        if (LoopNum == endCount) {

            repFlag = 0;
            breakLoop = 1;
            break;
        }

        hokan = temp;

        if (str1[hokan + 1] > 30 && str1[hokan + 1] < 128) { // 制御文字などを除外するためのif
            bufWord[LoopNum] = str1[hokan + 1];
            bufRep[LoopNum] = 1;
        } else {
            bufWord[LoopNum] = 0;
            bufRep[LoopNum] = 0;
        }

        press1[preCount + 0] = bufWord[LoopNum];
        press1[preCount + 1] = bufRep[LoopNum];

        repFlag = 1; // 次のループ用に再セット
        for (temp = hokan + 1; temp <= 255; temp = temp + 1) {

            if (repFlag == 1 && str1[temp] > 31 && str1[temp] < 256) { // 31以上などの条件は制御文字などを読み取らないようにするため

                if (str1[temp + 1] == str1[temp] && str1[temp] != 0) {
                    bufRep[LoopNum] = bufRep[LoopNum] + 1;
                    repFlag = 1;

                } else if (str1[temp] != 0) {

                    repFlag = 0;
                    press1[preCount + 0] = bufWord[LoopNum];
                    press1[preCount + 1] = bufRep[LoopNum];
                    preCount = preCount + 2;

                    break;
                } else if (str1[temp] == 0) {
                    breakLoop =
                        1; // ヌルが来たら文末などと判断し ループから break

                    break;
                }

                else {
                    breakLoop = 1; // 無限ループ防止のため
                                  // 想定外の自体はループから break

                    break;
                } // else if
            } else {
                breakLoop =
                    1; // 無限ループ防止のため 想定外の自体はループから break

                break;
            }

            // if
        } // for

        LoopNum = LoopNum + 1;
    } // while

    printf("\nランレングス的に圧縮のシミュレーション...\n");

    for (int i = 0; i < preCount + 0; i = i + 1) {
        printf("%02x ", press1[i]); // 最低でも2桁を表示、の意味
    }

    printf("\nファイルに書いてあるバイナリー・データ1\n");

    for (int i = 0; i < endCount; i = i + 1) {
        printf("%02x ", str1[i]); // 最低でも2桁を表示、の意味
    }

    printf("\nファイルに書いてあるバイナリー・データ2\n");

    for (int i = 0; i < endCount; i = i + 1) {
        printf("%02x ", str2[i]); // 最低でも2桁を表示、の意味
    }

    int press2[100];

    for (int i = 0; i < preCount + 0; i = i + 1) {
        press2[i] = press1[i];
    }

    printf("\nランレングス的に圧縮のシミュレーション redo...\n");

    for (int i = 0; i < preCount + 0; i = i + 1) {
        printf("%02x ", press2[i]); // 最低でも2桁を表示、の意味
    }

    int pack0[50];

    for (int i = 0; i < preCount + 0; i = i + 1) {

        if (i % 2 == 0) {

            pack0[i + 1] = press1[i];
            // pCount = pCount + 1;
        }

        if (i % 2 == 1) {

            pack0[i - 1] = press1[i];
        }
    }

    printf("\n pack0 半製品(回数と読み取り文字の入れ替え)...\n");
    for (int i = 0; i < preCount; i = i + 1) {
        printf("%02x ", pack0[i]); // 最低でも2桁を表示、の意味
    }

    char pack1[50];
    int pCount = 0;
    int skipCount = 0;

    char minusCount = 0;

    int minStart = 0;

    for (int i = 0; i < preCount + 0; i = i + 1) {

        if (i % 2 == 1) {

            pack1[pCount] = pack0[i];
            pCount = pCount + 1;
        }

        // 偶数版は回数のはず
        if (i % 2 == 0) {

            // 回数1のときのみ発動
            if (pack0[i] == 1 && minStart == 0) {
                // pCount = pCount + 1;
                minStart = 1;
                minusCount = minusCount - 1;

                int j = i;
                int k = 0;
                for (k = 0; k <= 15; k = k + 1) {
                    if (pack0[j + 2 * k] == pack0[j + 2 + 2 * k]) {
                        minusCount = minusCount - 1;
                        skipCount = skipCount + 1;
                    } else {
                        break;
                    }
                }

                pack1[pCount] = minusCount;

                minusCount = 0; // 代入し終わったので 0 に戻す

                pCount = pCount + 1;
            }

            if (pack0[i] >= 2 && pack0[i] < 256) {
                minStart = 0;
                minusCount = 0;

                pack1[pCount] = pack0[i];
                pCount = pCount + 1;
            }
        }
    }

    printf("\n pack1 ほぼ目標品(回数1を負数に)...\n");
    for (int i = 0; i <= pCount; i = i + 1) {
        printf("%02x ", pack1[i]); // 最低でも2桁を表示、の意味
    }

    fclose(fp1);
    printf("\nファイルをクローズしました。\n");
}

実験結果

ファイルをオープンしました。
バイナリー・データを読み取っています。

ファイルに書いてあるバイナリー・データ
61 61 61 61 61 62 63 64 65 65 65 65 65 65 00 00 00 00 00 00 00 00 00 00 ca 12 64
 77 00 00 00 00 40 15 40 00 00 00 00 00 a3 c5 80 44 fe 07 00 00 02 00 00 00 00 0
0 00 00 1f 31 40 00 00 00 00 00 00 00 00 00 00 00
ファイルに書いてあるバイナリー・データ2
61 61 61 61 61 62 63 64 65 65 65 65 65 65 00 00 01 00 00 00 00 00 00 00 11 00 01
 10 00 00 00 00 00 00 00 00 00 00 00 00 ffffffca 12 64 77 00 00 00 00 10 3a 60 0
0 00 00 00 00 65 ffffffc9 ffffff94 fffffffe fffffffe 07 00 00 10 3a 60 00 00 00

文字の繰り返しを検出しようとしています...

ランレングス的に圧縮のシミュレーション...
61 05 62 01 63 01 64 01 65 06
ファイルに書いてあるバイナリー・データ1
61 61 61 61 61 62 63 64 65 65 65 65 65 65
ファイルに書いてあるバイナリー・データ2
01 00 00 00 06 00 00 00 00 00 00 00 65 65
ランレングス的に圧縮のシミュレーション redo...
61 05 62 01 63 01 64 01 65 06
 pack0 半製品(回数と読み取り文字の入れ替え)...
05 61 01 62 01 63 01 64 06 65
 pack1 ほぼ目標品(回数1を負数に)...
05 61 fffffffd 62 63 64 06 65 00
ファイルをクローズしました。
(※ Windows7 上のgcc で実行した結果.)

さて、実行結果中にある文字列 fffffffd が一見すると負数に見えないが、しかしこれが負数の -3 のことである。

なぜなら、16進数で8桁 である数

ffff ffff 

に1を足すと

0000 0000

になるので、ffff ffffはつまり-1のことである

同様に

ffff fffe とは -2 のこと。

よって

ffff fffd とは -3 のことである。


もっとも char型の1バイト(16進数で2桁のはず)の整数を宣言したのに、なぜ負数のときに 16進数で8桁の数が表示されるか疑問であるが、実行環境がそういう実装になっているので、妥協してもらいたい(Windows7 上のgcc で実行した結果である)。

ASCII文字の英文の圧縮の原理

編集

ある種の画像はランレングスで効果的に圧縮できそうです。

しかしテキストファイルでは、普通、同じ文字が長々と反復しているということはありません。

なので、普通のテキストファイルを圧縮してしまうと、むしろ長くなってしまいます。

たとえば

hello を圧縮すると、

h 0x68
e 0x65
l 0x6c
o 0x6f

なので、無圧縮なら

0x68 0x65 0x6c 0x6c 0x6f

である一方、「圧縮」すると回数が付記されるので

0x68 0x01 0x65 0x01 0x6c 0x02 0x6f 0x01

とむしろ増えてしまいます。


そこで改善案として、 続いていない文字は圧縮しないよう改良したモデルが既に考えられているのですが、問題点は、あるバイト情報が反復回数であるのか元文字のASCIIなのかをどう区別するか、という問題です。


さて、ASCII コードには十進で 127 (0x7f)までしかありません。

そこで、テキストファイルの圧縮を扱う際、128以上255以下の数字を、圧縮の開始記号として区別する方法もあります。

とりあえず、説明の簡単のため 0x80 を開始記号としたとしましょう(実際に採用されているほう恣意とは違う)。

たとえば helllllllo という小文字エル l が7回続いているヘロロロロロロローは、

0x68 0x65 0x6c 0x80 0x07 0x6f

となり、たった 6 バイトで表せます。なお、4バイト目の「0x80」が開始記号であり、その次の「0x07」が反復回数です。


さて、たった1個しかない開始記号に、1バイトも使うのは、人間にはわかりやすいですが、しかし機械的には冗長すぎて無駄です。現代では 1 バイトは 8ビット(十進で255、十六進でFF)です。

開始記号には1ビットか2ビットを使えば十分そうです。

アスキーは0x7f までですが、

7は二進数で 0111 ですので、つまりアスキーコードでは文字のビットの最初1ケタ目は必ず最初は0になります。

つまりアスキーでは最初1ビットに「1」は不使用です。

これに注目すれば、「1」をビット単位での開始記号とするアイデアが思いつきます。


そこで、先頭1ビットが「1」なら開始記号とする、という方式が採用されています。この方式では、残り7ビットを反復回数に利用できます。すると、おおよそ127回(256÷2 - 1 = 127)までの反復を処理できます。 256÷2 - 1の -1のぶんをしない方式でも、128回までの反復を処理できます。


英文の半角英数の直接入力によるテキストファイルは、これで効率的に圧縮できるでしょう。

ただし、英語以外の文字は別問題です。

また、テキスト以外の画像や音声なども、別問題です。

あくまで上記の方法は、ASCIIコードだけで表せる文字だけを使ったテキストファイルの場合のみの事例です。



同じデータが3個以上続いていたら圧縮する方式

連続していない1文字を圧縮しても、かえってバイトが増えるだけです。


AAのように二文字だけ連続している単語なら、(圧縮方法にも寄りますが)圧縮してもしなくても変わらないか、むしろ少し増えるリスクもあります。

そこで、反復回数がたとえば3回以上など、一定回数の反復があった場合にだけ圧縮するという方式も提案されています。

最低でも、長さに1文字、距離に1文字を使うわけですから、なので2文字よりも多い、3文字以上を圧縮しないと、効果がありません。


ABCABCFのようなの

単純に反復を見るだけだと、ABCABCFのように、ABCが2回以上続いているようなものは圧縮できません。

タイトルでは説明の簡単のため2回だけの反復にしていますが、もしこれが ABCABCABCABCABCF のようにABCが5回続いているものだとすると、

これを圧縮できないのは困ります。

そこで、こういうのを圧縮できるようにした方式として LZSS という方式が考えられています。


ABCDEABCDK を ABCDE[5,4]K

のように圧縮する方式です。

最初の5は、何文字戻った位置が繰り返しの起点かを示す数値です。

そして「4」は、繰り返しの単語の長さです。


ABCDEABCDABCDK の場合、

まず、前から圧縮していき、

ABCDE[5,4]ABCDK

のようになります。

そして、さらに圧縮されてない別の部分について

ABCDEABCD[9,4]K

のようになります。なお、ABCDEABCD[4,4]Kのようにはしません。なぜそうかと言われても、そういう慣習なので、それに従ってください。


そしてともかく、圧縮されてない部分どうしをあわせた

ABCDE[5,4][9,4]K

が最終的に保存されます。


これなら、複合するときに、まず最初のカッコから複合していき、

ABCDEABCD[9,4]K

が中間的に複合されます。さらに続けて

ABCDEABCDABCDK

が複合され、元の文字列が得られます。


ABABABABKのように繰り返しがある場合(ABが4回続いたあとにK)、カッコ内の数値の大小関係を [小、大] にして、

AB[2,4]K

のように表します。です。

こうすることで、単純な繰り返しにも対応できます。

また、それぞれの文字の冒頭に、1bitのフラグをつけて、圧縮しているものかどうかを判断します。 アスキー文字を1バイト(8ビット)で表した場合、先頭1ビットは必ず0なので、つまり先頭1ビットが「1」の場合なら次の数ビットを制御用のビットとして認識することができます。

これをもちいて上記 [2,4] などの情報を検出します。

制御文字には3ビットを使い、0x100 なら終端ビットである。

一致長さは0x101 ~ 0x11D (十進で257~285に相当)で制御されるが、しかしこのままではdeflate圧縮での繰り返しの仕様である3から258までの数をあわらせない。そこで拡張ビット(extra bit)という手法により、

拡張ビットのあとに補完で追加されるビットの長さは一定ではなく、基準値によって異なり、下記のように定められている。基本的に参照元の基準値が大きいほど、表す一致長さも大きくなるので拡張ビット数も大きくなるが(最大で5)、しかし例外的に最後の0x11Dは表す一致長さが1通りしかないので拡張ビット数が0である。

長さ符号の拡張ビットの対応表
 値 一致長さ 拡張ビット数
0x101  3  0
0x102  4  0
0x103  5  0
中略    
0x109  11 ~ 12  1
0x10A  13 ~ 14  1
中略    
0x11B  195 ~ 226  5
0x11C  227 ~ 257  5
0x11D  258  0


このように下段の大きい数値ほど拡張ビット数も増えている理由は、使用頻度の高いと考えられている短い一致長さほど、保存に要する容量も小さくすることで、より効率的に圧縮できる、というアイデアである。なお、このようなアイデア(使用頻度の高いものにほど短い符号を割りえてることで容量節約するアイデア)を「ハフマン符号」という。

数学書などでの「ハフマン符号」の説明では、確率論などを使って厳密に 使用頻度 が定式化されたりするが、しかし本ページで語るような実際の応用では、そんなに厳密に確率計算する必要はない。実際、上記の対応表も、別に確率方程式などから算出されたわけではないだろうし、表のどこにも確率計算は無い。


拡張ビットでは、表現できる値からの不足分の数値を表記する。

たとえば基準値 0x10A では 13~14の表現範囲だが、これで14を表現したい場合、

13に1をプラスすれば1になるので、

0x10A 1

のように表現される。0x10Aの拡張ビット数は「1」と決まっているので、10A の次の「1」と書かれた1ビットを読み取った時点で、自動的に拡張ビットの終わりだと判断される。


さて一方、(一致長さの符号ではなく)距離符号の拡張ビットの対応表は、別に定められている。下記のように 0x00 ~ 0x1D の距離で 1 ~ 32768 の距離に対応させています。

距離符号の拡張ビットの対応表
 値 距離 拡張ビット数
0x00  1  0
0x01  2  0
中略    
0x04  5 ~ 6  1
0x05  7 ~ 8  1
中略    
0x1B  12289 ~ 16384  12
0x1C  16385 ~ 24576  13
0x1D  24577 ~ 32768  13
※ 0x1B と 0x1C とで拡張ビット数が違うのは誤差ではなく仕様。

Deflate 圧縮における LZSS 方式では、

一致長さ、距離

の順番でそれを表す。

たとえば一致長が4、距離が5なら、

まず表現「4」は0x102で拡張ビット数0なので、

まず、0x102 が先にくることが決まり、拡張ビットはその直後には無い事が決まり、直後はすぐに距離が来ることが決まる。

つまり、

0x102

となる。


そして、距離の表現「5」に対応するのは、 0x04 が5~6で拡張ビット数1だから、5+0により0が拡張ビットの値になるので、

0x04 0

が来ることが決まる。

そして、

一致長さ、距離

の順序なのだから、

0x102 0x04 0

が来る。


ABCDEABCDK の場合、まずアスキーコードに変換して、

0x41 0x42 0x43 0x44 0x45 0x41 0x42 0x43 0x44 0x4B

である。

距離や一致長さの単位はバイト単位(十六進で2ケタ)で測定するので、

0x41 0x42 0x43 0x44 0x45(一致長さ4,距離5)0x4B

となる。

そして、いくつか前の段落で説明したように、

0x41 0x42 0x43 0x44 0x45 0x102 0x04 0 0x4B

となる。

最後に終端記号 0x100 がつくので、

0x41 0x42 0x43 0x44 0x45 0x102 0x04 0 0x4B 0x100

となる。これで圧縮がひとまず完了。これがLZSS圧縮の原理である。


参考サイト

拡張ビットの一覧

編集
長さ符号の拡張ビットの対応表 (全部)
 値 一致長さ 拡張ビット数
0x101 (257)  3  0
0x102 (258)  4  0
0x103 (259)  5  0
0x (260)  6  0
0x (261)  7  0
0x (262)  8  0
0x (263)  9  0
0x (264)  10  0
0x (265)  11~12  1
0x (266)  13~14  1
0x (267)  15~16  1
0x (268)  17~18  1
0x (269)  19~21  2
0x (270)  23~26  2
0x (271)  27~30  2
0x (272)  31~34  2
0x (273)  35~42  3
0x (274)  43~50  3
0x (275)  51~58  3
0x (276)  59~66  3
0x (277)  67~82  4
0x (278)  83~98  4
0x (279)  99~114  4
0x (280)  115~130  4
0x (281)  131~162  5
0x (282)  163~194  5
0x11B (283)  195 ~ 226  5
0x11C (284)  227 ~ 257  5
0x11D (285)  258  0


距離符号の拡張ビットの対応表 (全部)
 値 距離 拡張ビット数
0x00 (0)  1  0
0x01 (1)  2  0
0x (2)  3  0
0x (3)  4  0
0x (4)  5,6  1
0x05 (5)  7 ~ 8  1
0x (6)  9~12  2
0x (7)  13~16  2
0x (8)  17~24  3
0x (9)  25~32  3
0x (10)  33~48  4
0x (11)  49~64  4
0x (12)  65~96  5
0x (13)  97~128  5
0x (14)  129~192  6
0x (15)  193~256  6
0x (16)  257~384  7
0x (17)  385~512  7
0x (18)  513~768  8
0x (19)  769~1024  8
0x (20)  1025~1536  9
0x (21)  1537~2048  9
0x (22)  2049~3072  10
0x (23)  3073~4096  10
0x (24)  4097~6144  11
0x (25)  6145~8192  11
0x (26)  3193~12288  12
0x1B (27)  12289 ~ 16384  12
0x1C (28)  16385 ~ 24576  13
0x1D (29)  24577 ~ 32768  13
※ 0x1B と 0x1C とで拡張ビット数が違うのは誤差ではなく仕様。
  1. ^ 『MFCでのBITMAP作成について』2013/12/20 08:46 2022年1月1日に確認.