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

単機能のwebアプリケーション、技術デモなどの作成録

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が参照できるというのは面白いので、サービスとして機能を増やせる余地がありそうだ。浮気のアリバイ調査に使えますね。


Conceptual:OnlyCSViewに、ただ何となくBootstrapを適用

ユーザが指定したCSVファイルを表形式でAjax表示するだけのWEBサービス、OnlyCSView。作成して以来、大して弄ることも無く放置していたのだが、Twitter Bootstrapの雰囲気を掴むために、インターフェースだけ変更しようと思い、手入れをした。

そもよ、Twitter Bootstrapって何?

Twitter BootstrapというのはCSSフレームワークの一つで、かのTwitterが提供する無償フレームワーク。これを使えば、簡単にカラムレイアウトが実現できたり、レスポンシブデザインに対応できたり、Twitterのサービスで使っているようなGUI部品が要素へのクラス指定だけで実現できたり…とそこそこ楽しめる。CSSコーダなど、WEBデザイン・レイアウト方面の方々に知名度はある技術だと思うけど、生粋のWEBプログラマでは、Bootstrapの存在を知らないという場合も多い。

どういう部品が使えるのか、どういう機能が付けられるかについては、こちらのデモページが大変参考になるだろう。
ちなみに、OnlyCSViewでは「表示」ボタンと、テーブルの視認性を上げる奇数行ハイライトで明示的に使っている。それぞれ、class=”btn btn-primary”という指定と、class=”table table-striped”という指定をしているだけ。なお、文字コード選択のセレクトメニューの見た目も変化しているが、こちらはclass指定をせず勝手に変更されたもの。こういったおせっかいもあるので、それが気にならない人間がBootstrapに手を出せばよい。

BootstrapのCDNはよ

こういった仕事で使うにはちょっと…というおもちゃは、すぐに影響なく外せるよう、CDNで利用したい。Bootstrapにも勿論CDNがあるので、魔法の呪文を書いておこう。

<!--Bootstrapの本体(ヴァージョンは適宜)-->
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/js/bootstrap.min.js"></script>

jQueryが必須になる。ちなみに、レスポンシブ対応させるには、もう一つ二つファイルを読み込まないといけない。そちらはCDNが無いので、ダウンロードしてくるしかないけど。
ちなみに、このCDNを提供しているNetDNAという会社は、本来はCDNを商売にしている会社なので、急にサービスの停止などされる可能性はある。自己責任で。

OnlyCSViewの機能増えてないけど?

本当は見た目の更新だけでなく、ファイルのドラッグ&ドロップに対応させようとして、仕組みは書いていたのだけど。ただ、一般的なファイルダイアログを使ったアップロードと、ドラッグ&ドロップによるアップロードで同じGUIを使うというところに無理があったため、どうしようかと思っている。賑やかしで置いているだけで、利用者もいないだろうし別の開発を優先する予定。


Conceptual:Ajax対応CSV表示プログラムOnlyCSViewを計画する

以前、PHPの便利なfgetcsv関数を使ったCSV表示プログラムを書きました。このプログラムはソースコードを読んで判るように、フォームで受け取ったパスのファイルを一時ファイルとしてコピーし、関数に与え処理した結果をページの再読み込み時に表示という手順を踏んでいます。もちろんPHPはサーバサイドで呼ばれるので、結果の表示に最低一回の画面遷移を必要とするnon-Ajaxなプログラムです。今回はこのプログラムを新しくAjax対応させて、OnlyCSViewという名前のWEBサービスにする計画を立てました。

完成までには色々と仕様的な壁にぶち当たりました。その過程をここに順を追って記しておくことで、これからPHPプログラムをAjax対応させようとする方、ならびに自分自身が同じ轍と格闘しないようにしようと思います。

Ajax対応のキモ 〜jQuery.post命令でPHPerにも直感的に

まずは今回のajax対応に関しては、jQueryを使うということに決めました。理由としてライブラリを使わない素のJavaScriptでの開発は、非同期処理実装のためブラウザ毎に異なった命令を使わなければならないということがあり、面倒臭くスマートでないからです。面倒臭いことは時にプログラミングの醍醐味でもあったりしますが、できればメイン部分の処理か、あるいは誰もやらないような事の方に労力をかけたいと思いますよね。
Ajax部分はPHPプログラムで慣れたPOSTを使いたかったので、jQuery.post命令を使います。

jQuery.post(引数1,引数2,引数3,引数4)
引数1 呼び出すurl 今回の例ではcsvtojson.phpという処理部分のファイル
引数2 呼び出し先に与えるデータ。複数の場合はラベルをつける {"label1" : data1,"label2" : data2}
PHP側では$_POST["label1"]のように取り出せる。省略可
引数3 エラーも含め値が帰ってきたら行う処理。省略可
引数4 データの種類。省略可

csvtojson.phpには何を書けば良いでしょうか。PHPのみで作ったCSV表示プログラムの中から、ユーザが指定したCSVファイルが何かを知る機能、と、配列に格納したデータを表に整形する機能、を取り除いて、この2つの機能はJavaScript側で実装するようにします。そうすることで、csvtojson.phpは入力がファイルパス(文字列)、出力が配列という単機能のプログラムにまとまります。
PHPとJavaScriptで配列の受け渡しをするには、JSONというフォーマットを使います。上に書いたjQuery.postの説明の引数2のところで既に出してしまいましたが、何の事はなくJavaScriptのオブジェクトの書式と同じです。ただしラベルは必ず文字列でなければなりません。PHPの配列をJSONにエンコード/デコードする際には、配列をjson_encode関数もしくはjson_decode関数に与えてやるだけで済みます。最後にエンコード済みのJSONフォーマットをechoして、csvtojson.phpの仕事は終わりです。

jQuery.postの方では、引数4に”json”と指定してやる事で、帰ってきたデータをJSONと解釈してくれます。その他にとりうるオプションとして、”xml”,”html”,”script”,”jsonp”,”text”があります。もしCSVの整形表示部までcsvtojson.phpに担当させる設計にするならば、整形後のhtmlを”html”で受けるといった具合です。引数3はfunction(){…と結果が帰ってきた際の処理を匿名関数でそのまま書いてしまえば良く、さらにこの関数の引数として宣言された変数で、帰ってきたデータを受けられます。
実際にjQuery.postでcsvtojson.phpを呼んだ部分はこのようになりました。

jQuery.post("csvtojson.php", {"filepath" : filepath,"charset" : charset},function(data,status){
//処理。変数dataはcsvtojson.phpが返してきた値(JSON)。
//data[i][j]のようにアクセスして値を取り出せる。省略可。
//変数statusには成功、失敗などのステータスが入る。省略可。
},"json");

Ajax対応落とし穴 〜JavaScriptはファイルのローカルパスを知ることができない

メイン処理部分は後回しにして、csvtojson.phpに与えるローカルファイルパス取得の処理を考えます。JavaScriptではWEBページ内のフォームに入力された値を、フォームのname属性で特定して操作する事ができます。したがって、ユーザーに<input type=”file”>でローカルファイルをセットさせて、その値を取り出す。理論的にはそれで大丈夫なはずです。

//HTML部分
<input type="file" name="csvfile" accept="application/excel" />
.
.
.
//JavaScriptでセットされたパスを取得
var filepath = document.controlls.csvfile.value;

ところが、実際にはこの書き方ではパスを取得できません。試しに上のコードに続いてalert(filepath);というコードを実行すると、結果は以下のようになります。

fakepath

fakepathという単語で、実際のファイルパスがマスキングされています。良く言われる、JavaScriptではセキュリティ対策のためローカルファイルにアクセスできないという表現ですが、ファイルへのアクセスにエラーが出るという実装ではなく、そもそもローカルファイルパスを知る事ができないという仕様によるもののようです。したがって、PHPに受け渡してしまえば処理できるというものでもありません。

Ajax対応のキモ 〜HTML5のFile APIを使ってみる

そこで、ローカルファイルアクセスの問題を克服したとこちらも良く言われる、HTML5を使います。HTML4以前で作られたページでHTML5の機能を有効にするには、DOCTYPE宣言をこれまでの複雑なものから、<!DOCTYPE html>というシンプルなものに変えるだけです。詳しい説明はまた別のエントリで行いますね。
HTML5は2014年の正式勧告を目指して仕様策定中という段階であり、API導入の状況は各ブラウザによってまちまちです。今回使用するFile APIは、現時点でFirefoxとChromeが対応、Safariは部分的対応といった状況です。今回のプログラムがFirefoxおよびChromeのみ対応となってしまうのは、このSafari未対応部分の機能を使うからです。
File API対応ブラウザであれば、<input type=”file”>でユーザが選択したファイルに、name属性でなくid属性でアクセスできます。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//JavaScriptでセットされたファイルを変数に代入
var userfile = document.getElementById("csvfile").files[0];
//ファイル名取得
var name = userfile.name;
//サイズ取得
var size = userfile.size;
//タイプ取得
var type = userfile.type;
//urn取得
var urn = userfile.urn;

File APIは複数ファイルの選択にも対応しているため、セットされたファイルはインデクスで指定して取り出します。一つしかセットされていない場合はfiles[0]で指定します。
このurnというプロパティが、いかにもファイルパスだろうという安易な予想をしたのですが、実際ローカルファイルをセットして値を取得してみると空でした。大抵のFile APIの解説でも省略されているのですが、URNというのは、URLと同じくICANNによって唯一性が保証されたファイル名のことで、パスのようにユーザが主体的に値を設定せずとも存在しているものではないようです。いずれは活用するようになるかもしれない名前空間といったところでしょうか。

Ajax対応落とし穴 〜objectURLはセッションを越えられない

File APIにはローカルファイルのパスを知る2通りの方法が用意されています。どちらの方法も発想の逆転で、指定されたローカルファイルに新しくパスを与えるということでセキュリティ上の問題をクリアしています。方法の1つ、objectURLはファイルへの参照を示し、2つ目のDataURLはurl文字列にファイルの内容をそのまま展開してしまいます。もちろんobjectURLの方が圧倒的にバイト数が少ないので、Ajax通信で使うフォーマットとしてはこちらを採用したくなります。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//objectURLを作成
 
//Firefoxの場合
var filepath = window.URL.createObjectURL(document.getElementById("csvfile").files[0]);
//Chromeの場合
var filepath = window.webkitURL.createObjectURL(document.getElementById("csvfile").files[0]);
 
//jQuery.postに与える
jQuery.post("csvtojson.php", {"filepath" : filepath},function(){
//処理
},"json");

しかしながら、こうして作成したObjectURLはセッションを越えられないため、csvtojson.phpはこれを無効なパスと解釈します。残念ではあるのですが、ObjectURLにはまた別のところで活躍してもらうとしましょう。

Ajax対応のキモ 〜filereaderでData URLを作成しパス渡しを実現

Data URLというのは、先程も述べましたがファイル内容をそのまま展開してしまったURLのことで、このURLの解釈自体は大抵のブラウザが対応しています。具体的な採用例としては、Google画像検索で表示される画像がData URLです。File APIのfilereaderではアクセスしたローカルファイルをいくつかのフォーマットに展開することができるのですが、その内の一つのフォーマットがData URLなのです。filereaderの使い方を見てみましょう。

//HTML部分
<input type="file" id="csvfile" accept="application/excel" />
.
.
.
//filereaderインスタンスを作成し、メソッドでファイル読み込み
var reader = new FileReader();
//バイナリ文字列へ
reader.readAsBinaryString(document.getElementById("csvfile").files[0]);
//テキストへ(第二引数は文字コード)
reader.readAsText(document.getElementById("csvfile").files[0],"UTF-8");
//Data URLへ
reader.readAsDataURL(document.getElementById("csvfile").files[0]);
 
//読み込み終了時の処理(非同期なので他のステータスにもメソッドあり)
reader.onload = function(e){
//変数filepathに結果を代入
var filepath = e.target.result;
};

filereaderを使う場合には、FirefoxとChromeの分岐処理は必要ありません。作成されたData URLを、jQuery.postでcsvtojson.phpに送ると正常に処理されました。トラフィックには優しくない方法ですが、とりあえず既存PHPプログラムのAjax対応への糸口が見えました。

$.post("csvtojson.php", {"filepath" : filepath},function(data, textStatus){
var $table = $('<table id="ct"></table>');
for(var i in data){
var $onerow = $("<tr></tr>");
for(var j in data[i]){
$("<td></td>").text(data[i][j]).appendTo($onerow);
}
$onerow.appendTo($table);
}
$("#tablearea").append($table);
},"json");

csvtojson.phpが返してきたJSONをテーブルに整形しています。こちらもjQueryを使って幾分楽をしていますね。

ということで、とりあえず最低限のAjax対応が果たせました。デモはこちらから。
段々肉付けをして便利なサービスにしていきたいと思います。


新ページおよび新カテゴリ”Conceptual”を追加

このサイトに新しく”Conceptual”という名前のページを作成し、完成させたプログラムおよびデモンストレーションへのリンクを張ることにしました。ページタイトルの下のメニューバーからリンクを辿ってみてください。あるいはここから。
同時に同名のブログ記事カテゴリを新しく設け、サブカテゴリからプログラム毎の作成過程などを追えるようにしました。キメラ系人間のtroublesomeな開発実録をお楽しみください。