カテゴリー別アーカイブ: EXIFviewer

Conceptual:WEBサービスページの共通部分をtwigテンプレート化してみる

twigの実践編というわけでもないが、これまでConceptualのページに載せていたWEBサービスページの共通部分(ヘッダー、広告、フッターなど)をtwigテンプレートとして抽出する作業を行ったので、その過程など紹介してみようと思う。

共通部分の抽出

公開しているプログラムのうち、OnlyCSViewEXIFviewerについてはページ構造がほぼ同じだ。現在の2プログラムの画面を並べてみると、下の画像のような感じになる。共通部分と書いてある所はほぼソースが同じであり、真ん中の白い部分(コントローラ部分)とスクリプトの部分だけが違うということになる。

共通部分を持つ2ページ

共通部分を持つ2ページ

テンプレートエンジンで共通部分を抽出する場合、Smarty2.xのような”閉じhead前切り”パラダイムのものだと、ヘッダー部分、メイン部分、フッター部分と必要ファイルが3つになってしまうだろう。今回はテンプレートの継承機能のあるTwigを用いるため、ヘッダーとフッターを含んだ基礎部分のテンプレート(base.html.twig)にメイン部分に対応するブロックを作り、子テンプレートでブロック部分をオーバーライドするという、2ファイルでの解決を図ることにする。

ロジックとビューの分離

加えて、今回手を入れるプログラムは相当昔に作ったものなので、PHP入門書の最初のサンプルにありがちな、ロジックとビューが混在している状態になっている。テンプレートエンジンらしく、これの分離も同時に行ってみたい。

EXIFviewer手入れ前のソース

ロジックとビューの分離の説明に丁度良かったので、今回例として出すのはEXIFviewerの方にする。

 
//手入れ前index.php(1ファイル)
 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="Description" content="EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。">
<meta name="Keywords" content="PHP,EXIF,WEBサービス,JPEG,画像,位置情報">
<link rel="stylesheet" href="base.css" type="text/css">
.
.
<script>
$(function(){
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
<title>EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス</title>
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script type="text/javascript">
 
//adsenseコード
 
</script>
</div>
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
<?php
if(!empty($_POST["path"])){
echo '<img src="' . $_POST["path"] . '">';
}
?>
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p><input type="submit" value="表示" class="btn btn-primary"></p>
</form>
</div>
<div id="tablearea">
<?php
if(!empty($_POST["blob"])){
if($exif = exif_read_data($_POST["blob"],NULL,true)){
echo "<h2>EXIF情報</h2>";
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . ' ' .  htmlentities($valname,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name . $valname,ENT_QUOTES,"UTF-8") . '">' . "$vval</td></tr>";
	}
	} else {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name,ENT_QUOTES,"UTF-8") . '">' . htmlentities($val,ENT_QUOTES,"UTF-8") . "</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
?>
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>

いわゆる、初回アクセス時にはPOSTの値(”blob”と”path”)が空であるため例外処理として初期画面を吐いているタイプのページだ。1ファイルでまとまってくれているので扱い易いが、echo命令によってテーブルを構成するhtmlタグの細切れを吐いている部分は一見して訳が分からない。html修正時にミスも起こり易いだろう。

EXIFviewer手入れ後のソース

index.phpにはテンプレートの呼び出しとロジックを押し込み、base.html.twigとindex.html.twigの2ファイルに値だけ渡す

 
//index.php
 
require_once("Twig/Autoloader.php");
Twig_Autoloader::register();
$loader = new Twig_Loader_Filesystem("twigtmp/");
$twig = new Twig_Environment($loader, array("cache" => "cache/"));
$blob = "";
$exif = array();
if(isset($_POST["blob"])){
$blob = $_POST["blob"];
}
if(!empty($_POST["path"])){
$exif = exif_read_data($_POST["path"],NULL,true);
}
$page = array(
"description" => "EXIFviewerはローカルのJPEGファイルをアップロードすることなくサイズ、位置情報、撮影情報等のEXIFデータを確認できるWEBサービスです。",
"keywords" => "PHP,EXIF,WEBサービス,JPEG,画像,位置情報",
"title" => "EXIFviewer JPEGファイルのEXIF情報をチェックするWEBサービス",
"blob" => $blob,
"exif" => $exif
);
$template = $twig->loadTemplate("index.html.twig");
$template->display($page);

続いて呼ばれる側のテンプレート2ファイル(実際名指しされるのはindex.html.twigの方だけだけれど)。

 
<!-- base.html.twig -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="css/base.css" type="text/css">
{% if description is not empty %}
<meta name="description" content="{{ description }}">
{% else %}
<meta name="description" content="AkisiのWEB制作日記の提供するウェブサービス">
{% endif %}
{% if keywords is not empty %}
<meta name="keywords" content="{{ keywords }}">
{% else %}
<meta name="keywords" content="ウェブサービス,WEB">
{% endif %}
.
.
<title>{{ title }}</title>
{% block head %}
{% endblock head %}
.
.
</head>
<body>
<div id="main">
	<nav><div id="back"><a href="http://akisi.tabiyaku.net/" title="AkisiのWEB制作日記に戻る">AkisiのWEB制作日記に戻る</a></div></nav>
<div id="upperad">
<script>
 
//adsenseコード
 
</script>
</div>
{% block main %}
{% endblock main %}
<div id="lowerad">
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
<div class="loweradbox">
<script>
 
//adsenseコード
 
</script>
</div>
</div>
</div>
</body>
</html>
 
<!-- index.html.twig -->
{% extends "base.html.twig" %}
{% block head %}
<link rel="stylesheet" href="css/index.css" type="text/css">
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
<script type="text/javascript">
$(function(){
 
$("#filesel").change(function(){
var reader = new FileReader();
reader.readAsDataURL(document.getElementById("filesel").files[0]);
reader.onload = function(e){
$("#path").val(e.target.result);
}
if(window.createObjectURL){
var ourl = window.createObjectURL(document.getElementById("filesel").files[0]);
} else if(window.URL) {
var ourl = window.URL.createObjectURL(document.getElementById("filesel").files[0]);
} else {
var ourl = window.webkitURL.createObjectURL(document.getElementById("filesel").files[0]);
}
$("#viewer").empty().html('<img src="' + ourl + '">');
$("#blob").val(ourl);
});//end of fileselchange
 
//Googlemap関連コード(省略)
 
});//end of jQueryready
</script>
{% endblock head %}
{% block main %}
<div id="instruction">
<h1>セットした画像のEXIFデータを表示</h1>
</div>
<div id="controlls">
<form>
<p><input type="file" id="filesel" type="image/jpeg"></p>
</form>
<form action="index.php" method="post">
<div id = "viewer">
{% if blob is not empty %}
<img src="{{ blob }}">
{% endif %}
</div>
<input type="hidden" name="blob" id ="blob">
<input type="hidden" name="path" id ="path">
<p>
<span><input type="submit" value="表示" class="btn btn-primary"></span>
</p>
</form>
</div>
<div id="tablearea">
{% if exif is not empty %}
<h2>EXIF情報</h2>
<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th></th></tr></thead><tbody>
{% for key, section in exif %}
{% for name, val in section %}
{% if val is iterable %}
{% for valname, vval in val %}
<tr><td>{{ key }} {{ name }} {{ valname }}</td><td id="{{ name }}{{ valname }}">{{ vval }}</td></tr>
{% endfor %}
{% else %}
<tr><td>{{ key }} {{ name }}</td><td id="{{ name }}">{{ val }}</td></tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody></table>
{% else %}
画像のEXIFデータが見つかりませんでした。
{% endif %}
</div>
<div id="googlemapdivarea">
<div id="googlemapdiv"></div>
</div>
{% endblock main %}

テンプレートのcssは、共通部分のためのbase.cssと、index.cssに分ける。手入れ前のソースでは色付きハイライトされているPHP部分がなくなり、その代わりにtwigのfor inで配列を回している。テンプレート関係で新しく登場したのは、for inで回す際にキー値にもアクセスする場合の、{% for key, value in array %}という書き方。それから、配列に要素があるかどうか調べるのには{% if array is iterable %}のように書く、といったことくらいだろうか。
テンプレートの使い分け判断に使う値が変数の中にあるようであれば(あるいは変数自身の存在で判断できるのであれば)変数のみをそのままテンプレートに渡し、そうでないようならロジック側で切り替え判断のためのフラグを値で用意し、変数とフラグを合わせてテンプレートに渡す、このようにしてロジックとビューの分離をはかるわけである。

あとは、テンプレート中のJavaScriptに波括弧が含まれる場合{% raw %}{% endraw %}で囲むのを忘れずに。

TwigをLinuxの公開サーバに上げる段になってハマり易いこと

Twigで個人的にハマったこと。MAMPなどのローカル環境で動作したプログラムを公開サーバにアップロードしてみると、以下のようなエラーが出て動かないということがあった。

Class ‘Twig_Loader_Filesystem’ not found

原因は、ざっくり言ってしまうとLinuxが大文字小文字の扱いにうるさいからだった。たとえばAutoloader.phpへのパスが”twig/Twig/Autoloader.php”だった場合、”Twig/Twig/Autoloader.php”というように指定してしまうと、OSXやWindowsなどの環境では理解してくれるものの、Linuxサーバ上では理解をしてくれない。
また、Autoloader.phpの上位のディレクトリ名も、”Twig”でないといけない。思う所あって”twig”というように小文字のディレクトリにしてアップロードしていたから、”twig/Autoloader.php”という正しいパスを指定してもエラーが出てしまっていることに当惑した。”Twig”ディレクトリは勝手にリネームしてはいけない。それが今回の失敗の教訓。


Conceptual:EXIFviewerに撮影地の地図表示機能を追加

予告通り、ローカル領域にあるjpegファイルのEXIF情報を表示するWEBサービス、EXIFviewerの機能を追加してみた。今回追加した機能は、jpegファイルのEXIFの中に撮影地情報が格納されていた場合、Google Mapsで表示するというもの。EXIFファイルの中のGPS情報をテーブルに表示するまでは前のヴァージョンでも実現していたため、今回の説明はGoogle Maps APIと連携する際の注意点が中心となる。

連携の前段階、既存プログラムの手直し

前のヴァージョンのEXIFviewerの作成手順では、表の成形部分で特にid指定などもしないセルを吐き出していた。今回、緯度経度情報をGoogle Maps APIに渡すには、JavaScript(jQuery)を使ったクライアントサイドでの値取得が必要となる。そのため、値の入ったセルにidで一意の名前をつけるようにする。また、前回手を付けられなかった値のサニタイジングもついでに行う。

if(!empty($_POST["path"])){
if($exif = exif_read_data($_POST["path"],NULL,true)){
echo "<h2>EXIF情報</h2>";
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . ' ' .  htmlentities($valname,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name . $valname,ENT_QUOTES,"UTF-8") . '">' . "$vval</td></tr>";
	}
	} else {
	echo "<tr><td>" . htmlentities($key,ENT_QUOTES,"UTF-8") . ' ' . htmlentities($name,ENT_QUOTES,"UTF-8") . "</td><td id=" . '"' . htmlentities($name,ENT_QUOTES,"UTF-8") . '">' . htmlentities($val,ENT_QUOTES,"UTF-8") . "</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像をセットして下さい。";
}

これがPHP部分。画像のEXIFにGPSの情報が不足なく入っていた場合に初めて処理を行うようにするので、このPHP部分で情報の不足をチェックし、地図表示の実行フラグとして非表示のセルにでも入れれば、JavaScript側の記述がスマートに済む筈。今回それをしなかったのは、不特定多数が利用する可能性があるため、出来るだけサーバ側の負担を軽く、クライアントサイドで処理を担保してもらうように設計したため。

Google Maps APIの使用方法

Google Maps APIを使いたい場合、htmlのヘッダ部で読み込む事になる。

<head>
.
.
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
</head>

こういった具合。クエリストリングのsensor=falseという部分は、利用者の位置情報を求めるかどうかということで、falseに設定しておけば鬱陶しい確認ダイアログが出ないだろう。
今回は、id=”googlemapdiv”というdiv要素の中にマップを表示する場合で説明する。jQueryのreadyの中に、以下のように記述した。

$(function(){
.
.
var option = {
zoom: 17,//地図の縮尺初期設定(0〜19)
center: new google.maps.LatLng(imagelat,imagelong),
//地図の中心の座標 LatLng(緯度,経度)
//とりあえず今回はimagelat,imagelngというユーザ定義変数を使った
//もちろんこれより前に宣言されてないといけない
mapTypeId: google.maps.MapTypeId.ROADMAP,//地図の種別初期設定
zoomControl: true,//ズームGUIの表示非表示
streetViewControl: true,//ストリートビュー表示可能にするか
scaleControl: true,//ズームスライダーGUIの表示非表示
mapTypeControl: true//地図の種別切り替えGUIの表示非表示
};
var map = new google.maps.Map($("#googlemapdiv")[0],option);
var marker = new google.maps.Marker({
				position : option.center,
				map : map
			});
});

コントロールの指定については必須ではない。google.maps.Mapメソッドに引数として地図の設置場所(DOMオブジェクトで指定)と、各種オプションを代入する。

Google Maps API用に緯度経度を10進法に変換する

さて、お膳立ても出来たので、単純にPHP側で成形した表のGPS情報の部分を取得し、Google Maps APIに渡せば良い、、とはいかない。
iPhone等で記録されるEXIFのGPSデータは、60進法で記録されているが、Google Maps APIが受け付けるのは10進法の数字なのだ。そこで、画像から取得した緯度経度を60→10進法に変換する処理を用意しないといけない。
60→10進法変換の方法だが、たとえば60進法で12.3という数字があった場合、10の位の1は10進法の60*1を表し、1の位の2は1*2、小数点第一位の3は60分の1*3を表している…といった数学の知識を、思い出してほしい。iPhoneなどが採用している60進法の表示方式では、”北緯36度46.5分0秒,東経137度54.52分0秒”といったように書くが、これは成形された表ではこのように表示されている。

EXIFでの緯度経度

EXIFでの緯度経度

GPSLatitudeRefやGPSLongitudeRefが東西南北(EWSN)を表している事は分かり易い。GPSLatitude 0が表すのは、緯度の”度”の部分。”36/1″という数字は、分母が1で分子が36ということ、つまり分数だ。同様に、GPSLatitude 1は100分の4650、つまり46.5分を表すといったように、面倒臭い書き方をしている。しかもこの値が60進法なのだ。
変換手順は、分数の文字列をなんとか少数に直して、それから10進法への変換を行うという形になる。
そして最後に10進法に直した後の値に、西経、南緯の場合マイナスをつけるという手順も必要だ。

 
//緯度を求める
var imagelat = parseFloat($("#GPSLatitude0").text().split("/")[0])/parseFloat($("#GPSLatitude0").text().split("/")[1]) + parseFloat($("#GPSLatitude1").text().split("/")[0])/(parseFloat($("#GPSLatitude1").text().split("/")[1])*60) + parseFloat($("#GPSLatitude2").text().split("/")[0])/(parseFloat($("#GPSLatitude2").text().split("/")[1])*3600);
if($("#GPSLatitudeRef").text() == "S"){
imagelat = "-" + imagelat;
}
 
//経度を求める
var imagelong = parseFloat($("#GPSLongitude0").text().split("/")[0])/parseFloat($("#GPSLongitude0").text().split("/")[1]) + parseFloat($("#GPSLongitude1").text().split("/")[0])/(parseFloat($("#GPSLongitude1").text().split("/")[1])*60) + parseFloat($("#GPSLongitude2").text().split("/")[0])/(parseFloat($("#GPSLongitude2").text().split("/")[1])*3600);
if($("#GPSLongitudeRef").text() == "W"){
imagelong = "-" + imagelong;
}

このようにして出てきた結果を、google.maps.LatLngメソッドの引数として与えてやれば良い。

ということで、EXIFViewerでの地図表示が可能になった。労力のわりに、いまや出来て当たり前の機能のように見えるからどうにも報われないのです。


BootstrapとGoogle Maps API同時使用時の表示崩れを修正する

Twitter社の提供するCSSフレームワーク、Bootstrap。これを使うと、面倒臭いWEBのGUIデザインを、要素へのクラス指定一発で解決することが出来る。以前紹介を行ったとおりCDNでの利用も可能なので、WEBサイトを作成する前にとりあえず(jQueryと一緒に)呪文のように読み込んでおくと、ページ制作中痒いところにも咄嗟に手が届き易い。

もう一つ、企業サイトであれば”会社概要”ページのアクセスマップなどで使われるであろう、Google Maps API。こちらも頻繁に利用されるものに違いないのだが、BootstrapとGoogle Maps APIを同時に利用して地図の表示を行うと、以下のスクリーンショットのように表示崩れが発生する。

Google Maps表示不具合

Google Maps表示不具合

まず由々しきこととして、左端の縮尺スライダーが表示されない。このような状態だと、スクリーンショットを貼っておいた方がまだ良かったのではないかという疑問も湧くだろう。また、右上の地図と衛星写真の切り替えメニューで、カーソルをホバーした時のドロップダウンメニューが少し見苦しい。具体的には、チェックボックスとラベルの間に改行が入ってしまうのである。

Google Maps表示不具合の原因

これらの不具合の原因は、Bootstrap側で読み込むCSSで、img属性やlabel属性など比較的影響の大きいところでスタイル指定を行っているからである。具体的には、

img{
max-width:100%;
width:auto\9;
height:auto;
vertical-align:middle;
border:0;
-ms-interpolation-mode:bicubic;
}
 
label{
display:block;
margin-bottom:5px;
}

といったような記載のある箇所である。問題となっているのは、img属性のmax-width:100%指定と、label属性のdisplay:inline-block指定である。

特に対応せずにGoogle Mapsが表示できているケース

img属性の指定によってマップのスライダーが表示されない不具合については、Bootstrap側としても対応策をとっている(どのヴァージョンからかは、生憎把握していないが)。

#map_canvas img,.google-maps img{
max-width:none;
}

このようなCSSを追加して、Google Mapsのデフォルトで包括要素として使われるdivのid=”map_canvas”以下のimg属性に特別にスタイル指定している。そのため、素直にマップを表示する領域のidを#map_canvasにしている場合、不具合は起こらないはずだ。問題は、別のid名やクラス名をしている場合。Bootstrap側の付け焼き刃的対策では問題が解決しない。

解決策

解決策は、Bootstrapの付け焼き刃解決策と同じように、包括要素以下のimg属性・label属性にスタイルを指定してやれば良い。たとえば包括要素がid=”googlemapdiv”だった場合、スタイルシートに以下のように記述する。

#googlemapdiv img{
max-width : none;
}
 
#googlemapdiv label{ 
width : auto;
display : inline; 
}

また、マップのコントロール部分の上位要素であるclass=”gm-style”以下の各属性に指定する形でも良いだろう。

.gm-style img{
max-width : none;
}
 
.gm-style label{ 
width : auto;
display : inline; 
}

これで正常に表示されるはず。

GoogleMaps正常表示

GoogleMaps正常表示

BootstrapやGoogle Mapsなどの複数WEBサービスを組み合わせて利用する場合、どこかは絶対ぶつかるものだと最初から割り切っていた方が良いのだろう。ちなみに今回の問題はEXIFviewerに機能を追加する過程で確認したもの。そろそろ新ヴァージョンを公開できるはず。


Conceptual:シンプルなPHP製EXIF表示サービスEXIFviewerを公開

わけあって写真の位置情報なんかを弄るプログラムが必要になって、EXIFについて調べ直している。

調べ直しているというのは、以前に一度PHPにおけるEXIFの扱いなどについて調べていて、DamniPhoneEXIFという誰も得しないWEBサービスに応用したことがあるからだ。DamniPhoneEXIFはPHP標準のEXIFクラスを使わずに、PELというライブラリを使っている。PELを使わないとEXIFデータの書き込みが出来ないためであるが、初期のiPhone3Gで起こる位置情報の経度の東西が逆転するバグを解決するためだけのサービスとして作ったので、そこから色々な機能を建て増していくには不適当だった。それなら単純に標準のクラスでEXIF表示するサービスから作り直そう、ということでEXIFviewerを作った。

キモは、ユーザのローカルファイルを一旦HTML5のDataURLで受けているため、サーバに画像のコピーを作らずに画面リロード後の確認用画像の表示が出来ているというところ。これはOnlyCSViewでもやっていたことなので、詳しいやり方についてはOnlyCSViewの作成エントリを参照してほしい。

ところでEXIF形式って何?

EXIF形式というのは、JPEGやTIFFなどの画像ファイルに画像についての情報を付与してまとめたデータ形式だ。デジカメメーカーが主体となって、写真の撮影状況(撮影日、しぼり、感度、位置情報etc)のデータを画像に埋め込めるようにしたもの。正確に言うと、埋め込むわけではなく新たなファイル形式の大括りを作って、そこに画像の情報と当の画像ファイルをパッケージ化する。そのようにして出来たファイルの拡張子は.jpgや.tiffそのままなので、一見するとEXIFの情報が入ったファイルとの区別がつかない。かくして、デジカメユーザは知らず知らずに、EXIF形式のお世話になっている場合が多い。
ではどのように情報を格納するかというと、TIFF形式以来のファイル先頭へのタグ付けにより、情報を格納している。まずTIFFという名前が、(Tagged Image File Format)の略であり、この形式が登場したことにより、画像ファイルに画像の情報を入れられるようになった。JPEGファイルは同様のタグ付け形式のJFIFというフォーマットをほぼ標準としており、このJFIFをもっと大げさにしたものがEXIFというわけだ。

EXIF形式の構造 ざっくばらんに

つまり、EXIFはTIFFの落とし子だ。ファイルのバイナリデータの一番最初には、TIFFヘッダと呼ばれる8バイトのデータがある。この8バイトのデータのうち4バイトは、次のまとまりへの参照(ファイルの頭からの距離)を格納している。このように、ひとまとまりの情報の中に、他のまとまりの場所情報をセットで含んでいるのが、TIFF形式の特徴だ。
EXIFにどんなまとまりがあるかというと、IFD(Image File Directory)という名前のまとまりがあり、そのまとまりの中にさらに複数の情報がぎっしり詰まっている。
IFDの中にはEXIF IFDというものや、GPS IFDというものがあり、EXIF情報をいただきたい場合この場所にアクセスして情報を見る必要があるわけです。ややこしいね。

PHPのEXIFクラスでのアクセス

先程紹介したPelでは、イメージファイルをコンストラクタに与えて、ゲッターメソッドで一つ一つのまとまりの鍵を開けてアクセスしなければならない。DamniPhoneEXIFでGPSの東西を変えるだけでも、こんなに入れ子のプログラムができてしまった。

<?php
require_once('PelJpeg.php');
if(!empty($_POST["path"])){
$pj = new PelJpeg($_POST["path"]);
if($pjexif = $pj->getExif()){
if($pjtiff = $pjexif->getTiff()){
if($pjifd = $pjtiff->getIfd()){
if($pjgps = $pjifd->getSubIfd(PelIfd::GPS)){
if($gpsarray = $pjgps->getEntries()){
if(isset($gpsarray[3]) && isset($gpsarray[4])){
if(($gpsarray[4]->getText()) == "W"){
$gpsarray[3]->setText("E");
echo "converted west to east.<br>";
}
else if(($gpsarray[3]->getText()) == "E"){
$gpsarray[3]->setValue("W");
echo "converted east to west.<br>";
}
}
}
}
}
}
}
}
?>

PHPのexif_read_data関数では、引数にまず欲しいデータより上位のまとまりを指定して、ごっそりまとまりを取得した後には、foreachや添字でアクセスできる。

exif_read_data(ファイル名,
セクション名(省略化、複数の場合カンマ区切り),
配列で取得するかフラグ(省略可、デフォルトfalse,
サムネイル取るかフラグ(省略化、デフォルトfalse)

とりあえず今回のヴァージョンでは全部表に吐き出してしまっている。

<?php
if(!empty($_POST["path"])){
if($exif = exif_read_data($_POST["path"],NULL,true)){
echo "<table class='exiftable table table-striped'><thead><tr><th>キー名</th><th>値</th></tr></thead><tbody>";
foreach ($exif as $key => $section) {
    foreach ($section as $name => $val) {
	if(is_array($val)){
	foreach ($val as $valname => $vval) {
	echo "<tr><td>$key $name $valname</td><td>$vval</td></tr>";
	}
	} else {
	echo "<tr><td>$key $name</td><td>$val</td></tr>";
	}
    }
}
echo "</tbody></table>";
} else {
echo "画像のEXIFデータが見つかりませんでした。";
}
} else {
echo "画像をセットして下さい。";
}
?>

php.netの説明によると、exif_read_data関数の第一引数にはURL形式は与えられないとのこと。けれども試してみたら、普通に外部サイトの画像URLを与えても表示できた。そこで、$_POST[“path”]にはDataURLを与えることにする。画像のセットで起動して、jQueryでフィールドにエンコードしたURLを入れておくだけ。

外部サイトの画像URLが参照できるというのは面白いので、サービスとして機能を増やせる余地がありそうだ。浮気のアリバイ調査に使えますね。