GIF画像から情報を抜き取るチュートリアル PHPでバイナリプログラミングその3

こんにちは、久保田です。

これまでの記事では、PHPでバイナリを扱うための基本的な事柄を扱ってきました。第一回では、PHPではバイナリをどのように扱えばいいのか、第二回では、そもそもバイナリの反対のテキストとは何か、ということについて記述しました。今回の記事では、例として実際にPHPでGIF画像をどのように解釈していくかを解説します。

バイナリを扱うコードを書く際の手順

ではGIF画像をPHPで扱う前に、まずは一般的にバイナリを扱うコードを書く際の手順の例を以下に大雑把に示します。

1. フォーマットに関する資料を集める
2. 資料からフォーマットを理解する
3. プログラミング言語で実際に扱ってみる

バイナリファイルはその種類によって各々が違ったフォーマットを持っています。GIF画像とPNG画像にしても同じ画像ですが、当然ながら内部のフォーマットは全く違います。従って、自分がこれから扱いたいバイナリのフォーマットに関する資料を集めます。

フォーマットによっては英語の資料しかない場合もあるのですが、 GIFはよく利用される画像フォーマットであり、かつ内部構造もそれなりにシンプルでわかりやすく、日本語の資料も比較的多く存在します。

資料をある程度集めたら、その資料を読み込んでそのバイナリがどういったフォーマットを持つのかについて理解します。この時、適当な例となるバイナリを用意してバイナリエディタで中身を確認しつつやると理解しやすいです。

フォーマットを理解すれば、あとは実際にコードを書いて、目的に応じてバイナリを読み込んだり書きだす処理を記述します。

GIF画像のフォーマットを理解する

では早速GIF画像のフォーマットについて説明していきます。以下では、GIF画像のフォーマットについてその概要を示しています。


GIFの基本構造
-------------------------
Header(13~?バイト)
-------------------------
Block(?バイト)
-------------------------
...
-------------------------
Trailer(1Byte) - "\x3b"
-------------------------

GIF画像のフォーマットは、複数のセクションによって構成されています。基本構造としては、まずHeaderがあり、その後に任意の数のBlockがあり、最後にTrailerがあって終りとなります。フォーマットの末尾を表すTrailerを除いて各セクションの大きさは決まっておらず、パースしながらそのセクションの大きさは決定されます。従って、GIF画像の内部データを読み込む際には、GIFフォーマットではバイナリを上から順に読んでいく必要があります。

Headerセクション

Headerには、GIF画像の大きさ、カラーテーブル、色数などのイメージに関する基本的な情報が以下のように格納されています。
以下に概要を図で表します。


-------------------------------------------
シグネチャ(3バイト) - 'GIF'
-------------------------------------------
バージョン(3バイト) - '87a' か '89a'
-------------------------------------------
画像の横幅(2バイト)
-------------------------------------------
画像の縦幅(2バイト)
-------------------------------------------
各種フラグ(1バイト)
 - カラーテーブルがあるか(1ビット)
 - カラーレゾリューション(3ビット)
   (この値 + 1)がカラーテーブルの
   インデックスを表すために必要なビット数
 - カラーテーブル内がソート済みか(1ビット)
   ソート済みである場合、最もよく使う
   色がカラーテーブルの最初に来るように
   ソートされる
 - カラーテーブルのサイズ(3ビット)
   (2の(この値 + 1)乗 * 3) がサイズとなる
-------------------------------------------
背景色のインデックス(1バイト)
  カラーテーブルの何番目の色を
  背景色として使うか
-------------------------------------------
ピクセルのアスペクト比(1バイト)
-------------------------------------------
グローバルカラーテーブル
(0~255 * 3 バイト)
-------------------------------------------

シグネチャはこのバイナリファイルがGIF画像であることを表します。バージョンは、GIFのバージョンが格納されます。今のところ"87a"か"89a"のみです。アニメーションGIFが利用出来るのは"89a"のバージョンです。次に画像の横幅縦幅が格納されます。リトルエンディアンで格納されているので注意です。その後に各種フラグや背景色のインデックス、ピクセルのアスペクト比などが格納されます。カラーテーブルの大きさは、各種フラグの1バイト内に含まれています。カラーテーブルがヘッダに含まれていない場合、ヘッダのサイズは13バイトとなります。

とりあえずHeaderセクションを実際にバイナリエディタで覗いて確認してみましょう。サンプルとなる画像を以下に用意しました。これのヘッダをバイナリエディタで読んでみましょう。使うバイナリエディタはなんでもいいのですが、筆者はBzを使っています。

まず最初にシグネチャ"GIF"とバージョン"89a"があることがわかります。

その次には画像の横幅を表す"\x2c\x01"があります。これはリトルエンディアンで格納されているのでひっくりかえして 0x012c という数値を表します。0x012cを10進数に直すと300、つまり画像の横幅の大きさである300pxということです。次の縦幅を表す2バイトも同様です。

その次には各種フラグを格納する"\xf2"があります。0xf2 を2進数に直すと 11110010 となります。最初の1ビットはヘッダにカラーテーブルを持つか、次の3ビットは色深度、次の1ビットはカラーテーブル内の色がソート済みかどうか、次の3ビットはカラーテーブルのサイズを表します。従ってこの画像の場合は以下のような情報を読み取ることができます。

・カラーテーブルあり
・色深度 7 + 1
・カラーテーブルはソートされていない
・2の(7 + 1)乗 * 3 = 255 * 3 = 765

というような感じでバイナリのデータを読み進めていくことにします。

ヘッダのフォーマットを理解したところで、実際にこれを扱うPHPの関数を書いてみると以下のようになります。画像を文字列として読み込み、unpack関数を使ってPHPの型に変換します。コード自体は非常に単純で難しいところは無いと思います。


<?php
/**
 * gif画像のヘッダを読み込む
 *
 * @param resource $fp
 * @return array
 */
function readGifHeader($fp)
{
    $signature = fread($fp, 3);
    if ($signature !== 'GIF') {
        throw new RuntimeException('Invalid gif signature: ' . $signature);
    }
    $version = fread($fp, 3);
    if ($version !== '89a'  & & $version !== '87a') {
        throw new RuntimeException('Invalid gif version: ' . $version);
    }
    $info = unpack('vwidth/vheight/Cdescription/CcolorIndex/CpixelAspectRatio', fread($fp, 7));
    $desc = $info['description'];
    $width = $info['width'];
    $height = $info['height'];
    $backgroundColorIndex = $info['colorIndex'];
    $pixelAspectRatio = $info['pixelAspectRatio'];
    $hasColorTable = !!($desc >> 7  & 1);
    $colorResolution = ($desc >> 4  & 7) + 1;
    $colorTableIsSorted = !!($desc >> 3  & 1);
    $colorTableSize = pow(2, 1 + ($desc  & 7)) * 3;
    $colorTableBinary = $hasColorTable
        ? fread($fp, $colorTableSize)
        : '';
    // カラーテーブル
    $colorTable = array_chunk(array_values(unpack('C*', $colorTableBinary)), 3);
    return compact(
        'signature',            // シグネチャ
        'version',              // バージョン
        'width',                // 横幅
        'height',               // 縦幅
        'backgroundColorIndex', // 背景色
        'pixelAspectRatio',     // ピクセルのアスペクト比
        'hasColorTable',        // カラーテーブルがあるか
        'colorResolution',      // カラーテーブルのインデックスを表現するのに必要なビット数
        'colorTableIsSorted',   // カラーテーブルがよく使う順にソートされているか
        'colorTable'            // カラーテーブル
    );
}

これを上で挙げたサンプルのGIF画像に適用してダンプすると、以下のようになります。バイナリエディタから読んだ情報と一緒であることがわかると思います。


array(10) {
  ["signature"]=>
  string(3) "GIF"
  ["version"]=>
  string(3) "89a"
  ["width"]=>
  int(300)
  ["height"]=>
  int(300)
  ["backgroundColorIndex"]=>
  int(0)
  ["pixelAspectRatio"]=>
  int(0)
  ["hasColorTable"]=>
  bool(true)
  ["colorResolution"]=>
  int(8)
  ["colorTableIsSorted"]=>
  bool(false)
  ["colorTable"]=>
  array(8) {
    [0]=>
    array(3) {
      [0]=>
      int(201)
      [1]=>
      int(255)
      [2]=>
      int(171)
    }
    --- 中略 ---
    [7]=>
    array(3) {
      [0]=>
      int(0)
      [1]=>
      int(0)
      [2]=>
      int(0)
    }
  }
}

Blockセクション

上では、GIFのヘッダのフォーマットを理解し、それを扱うコードを書いてきました。
記事を書いてきてだんだん面倒になってきたというのもあるのですが、ヘッダ以降のブロックのパースなどは上でやった手順で自分でもやってみましょう。

GIFフォーマットに関する資料も以下に代表的なものをリストにしておきます。

GIFフォーマットの詳細 - わかりやすい日本語の資料
GIF ファイルフォーマット
GIF Official Specifications() - GIF87A, GIF89Aの仕様書

また、バイナリの中身を覗く際のバイナリエディタも色々ありますので好みのものを選びましょう。

Stirling
Bz
xedit

結論

この記事では、バイナリを扱うコードを書く際の手順をGIF画像を例に解説しました。

バイナリを扱うのは、バイナリ特有の事情さえ把握していれば誰でもできる簡単なものです。もし何かバイナリを弄ったりしたい場合は、何らかのツールを使ったり、誰かが作った拡張ライブラリを使うだけでなく、自分でコードを書くことも選択肢の一つとして検討することをおすすめします。