PHPで、GIFファイルのコメントデータを削除する


## 概要

GIFファイルのコメントデータを、PHPの標準関数だけで削除してみます。

GIFのComment Extension blockを探し、それを削除したファイルを生成します。
Windows版PHP 5.4.5での動作は確認できています。

なお、他の画像形式のコメントデータを削除したい場合、[関連記事](#%E9%96%A2%E9%80%A3%E8%A8%98%E4%BA%8B)を参照してください。

## 注意

私はバイナリデータ操作を行う処理の実装経験がありません。
このため、本投稿で紹介する処理はメモリ使用率・速度・パフォーマンスなどの点で劣っている可能性があり、実用的でない恐れがあります。

## コード

```php
/**
 * 変数定義
 */
//対象のGIFファイル
$filename     = 'example.gif';
//コメントデータを削除したGIFファイルのファイル名(作成するファイル名)
$out_filename = 'example.clean.gif';





/**
 * 関数定義
 */
/**
 * バイナリのデータを数値に変換する
 *
 * @param string $str 変換するバイナリデータ
 * @return int 変換された数値
 */
function bin2dec($str){
	$str=unpack('Cdec',$str);
	return $str['dec'];
}
/**
 * GIFのSub blockをファイルポインタから読み込み、連続したSub blockの全データを出力する
 * 出力されるデータには、各ブロックのBlock Sizeや最後のTerminatorも含まれる
 *
 * @param resource $handle fopen関数を使用して作成したファイルシステムポインタリソース
 * @return string Sub blockのバイナリデータ。各ブロックの長さやTerminatorも含まれる
 */
function subblock_read($handle){
	$data='';
	while(true){
		$data.=
		$length=fread($handle,1);
		if($length==="\x00"){
			break;
		}
		$length=bin2dec($length);
		$data.=fread($handle,$length);
	}
	return $data;
}





try{
	/**
	 * GIFファイルを展開
	 */
	$fp=fopen($filename,'rb');
	if(!$fp){
		throw new RuntimeException('ファイルの読み込みに失敗しました');
	}

	/**
	 * Headerの解析
	 */
	/* ファイルのシグネチャが一致するかチェック */
	if(($output=fread($fp,3))!=="\x47\x49\x46"){
		throw new RuntimeException('指定されたファイルはGIF形式ではありません');
	}
	/* バージョンが一致するかチェック */
	$output.=
	$version=fread($fp,3);
	if($version!=="\x38\x37\x61" && $version!=="\x38\x39\x61"){
		throw new RuntimeException('不正なバージョンのGIFです: '.$version);
	}

	/**
	 * Logical Screen Descriptorの解析
	 */
	$output.=fread($fp,4);
	$output.=
	$desc=fread($fp,1);
	$output.=fread($fp,2);
	$desc=bin2dec($desc);

	/**
	 * Global Color Tableの解析
	 */
	if($desc>>7 & 1){
		$output.=fread(
			$fp,
			pow(2,($desc & 7)+1)*3
		);
	}

	$is_end=false;
	while($blockHeader=fread($fp,1)){
		if($is_end){
			/**
			 * Trailer以降にもデータが存在する場合、
			 * 不正なGIFとしてエラーを出す。
			 */
			throw new RuntimeException('不正な構造のGIFファイルです。Trailer block以降にデータを検出しました');
		}
		if($blockHeader==="\x2c"){
			/**
			 * Image Descriptorの解析
			 */
			$blockHeader.=fread($fp,8);
			$blockHeader.=
			$desc=fread($fp,1);
			$desc=bin2dec($desc);

			/**
			 * Local Color Tableの解析
			 */
			if($desc>>7 & 1){
				$blockHeader.=fread(
					$fp,
					pow(2,($desc & 7)+1)*3
				);
			}

			/**
			 * Image Dataの解析
			 */
			$blockHeader.=fread($fp,1);
			$blockHeader.=subblock_read($fp);
		}elseif($blockHeader==="\x21"){
			$blockLabel=fread($fp,1);
			if($blockLabel==="\xfe"){
				/**
				 * Comment Extensionは飛ばす
				 */
				subblock_read($fp);
				continue;
			}elseif($blockLabel!=="\xf9" && $blockLabel!=="\x01" && $blockLabel!=="\xff"){
				/**
				 * Comment Extension以外のどの拡張ブロックでもない場合、例外を投げる
				 */
				throw new RuntimeException('不正な構造のGIFファイルです。仕様にない拡張ブロックを検出しました');
			}
			$blockHeader.=$blockLabel.subblock_read($fp);
		}elseif($blockHeader==="\x3b"){
			/**
			 * Trailerを検出した場合、終端フラグをセットする
			 */
			$is_end=true;
		}else{
			throw new RuntimeException('不正な構造のGIFファイルです。仕様にないブロックを検出しました');
		}
		$output.=$blockHeader;
	}
	if(!$is_end){
		/**
		 * Trailerを検出できなかった場合、
		 * 不正なGIFとしてエラーを出す。
		 */
		throw new RuntimeException('不正な構造のGIFファイルです。Trailer blockを検出できませんでした');
	}

	//ファイルを閉じる
	fclose($fp);

	/**
	 * ファイルを上書き保存
	 */
	file_put_contents($out_filename,$output);
}catch(RuntimeException $e){
	//ファイルを閉じる
	fclose($fp);
	echo $e->getMessage();
}
if(isset($fp)){
	//ファイルを閉じる
	fclose($fp);
}
```

## やっていること

GIFをブロック毎に解析し、Comment Extension blockを除く全てのデータを新しいファイルに書き込んでいます。
GIFのフォーマット表を作る過程で、拡張ブロックは先頭2バイト以降は全てSub blockなのだと気づいたため、
Comment Extension block以外の拡張ブロックは先頭2バイトの判定以外は略しています。
その他、いろいろ略しているため、読みにくいかもしれませんが勘弁を。

## 参考サイト

*  [GIFファイル形式 - Nekopps Webアプリ制作編集](http://www.nekopps.com/article/gif_format/)
*  [GIFフォーマットの詳細](http://www.tohoho-web.com/wwwgif.htm)
*  [GIF画像から情報を抜き取るチュートリアル PHPでバイナリプログラミングその3 : アシアルブログ](http://blog.asial.co.jp/820)

## 三大画像形式のコメントデータ削除に成功したから…

次は、~~私個人の~~当初の目的だったコメントデータへの携帯端末用再配布禁止命令`copy="NO"`、`kddi_copyright=on`の書き込み処理を書きます。

## 最後に

GIFの仕様は(PNGと比べると)多少ややこしいので、解析プログラムを自前で組む際はブロックの前後関係や長さなどに関する表を作るべきでしょう。
ちなみに私は、下記の表を作って本コードを組みました。

[GIFファイルフォーマット表](http://jsbin.com/silem)

## 関連記事

* [PHPで、PNGファイルのコメントデータを削除する - Qiita](http://qiita.com/sounisi5011/items/e685ed1501a3cd979a3c)
* [PHPで、JPEGファイルのコメントデータやExif情報を削除する - Qiita](http://qiita.com/sounisi5011/items/cc3336775d4208fbcabd)