2011年01月22日

exif_read_data() の使えないPHPでExifを読み出す

exif_read_data() の使えないPHPでExifを読み出す

PHPの関数には、Exif情報を読み出すexif拡張モジュールがあって、exif_read_data()関数を使うと読み出せるのだが、exifサポートを有効化するには、PHPを --enable-exif でコンパイルしなければならない。なので、PHPをコンパイルできないレンタルサーバ等で、exifサポートが有効になっていない環境では使えない。

Exifパーサをイチから作るのはタイヘンだが、GPLライセンスでThe PHP JPEG Metadata Toolkitというのがあったのでありがたく拝借。これを使えばexif_read_data()がなくてもExifが読み出せる。

ソースのzipをダウンロードして、使うサーバで以下のファイルを配置する。
EXIF.php
EXIF_Makernote.php
EXIF_Tags.php
IPTC.php
JFIF.php
JPEG.php
Photoshop_IRB.php
PictureInfo.php
PIM.php
pjmt_utils.php
Unicode.php
XML.php
XMP.php
Makernotes/
agfa.php
canon.php
casio.php
epson.php
fujifilm.php
konica_minolta.php
kyocera.php
nikon.php
olympus.php
panasonic.php
Pentax.php
ricoh.php
sony.php
Makernotes/以下は、EXIF_Makernote.php の中で動的に読み込まれているので、不要なものは配置しなくて良いだろう。また、この読込ルーチンで、opendir() に失敗するとreaddir()がNULLを返してループすることがあるので、ちょっと変更。
EXIF_Makernote.php:
// Open the directory
$dir_hnd = @opendir ( $dir );

// Cycle through each of the files in the Makernotes directory

while ( $dir_hnd && ( $file = readdir( $dir_hnd ) ) !== false )
{
:

:
}
// close the directory
if( $dir_hnd ) closedir( $dir_hnd );

そして、Exifを読み込む側で、
include_once 'EXIF.php';

// $filename = 読み込むjpeg等のファイルのパス
$exif = get_EXIF_JPEG( $filename );
実行結果
  [0]=>
array(10) {
["Tags Name"]=>
string(4) "TIFF"
["Tiff Offset"]=>
int(12)
[271]=>
array(9) {
["Tag Number"]=>
int(271)
["Tag Name"]=>
string(19) "Make (Manufacturer)"
["Tag Description"]=>
string(0) ""
["Data Type"]=>
int(2)
:

という具合で、Exifが取れる。構造は、タグ番号 => array() で、タグ番号の意味は "Tags Name" を見れば分かるが、この対応付けは EXIF_Tags.php に書かれているので、この番号をキーに配列を探せば良い。たとえばGPS位置情報タグは、$exif[0][34853] あたり。このままでは使いにくいので、exif_read_data()のように平たい配列に変換するため、
function Interpret_EXIF_to_Array($exif, &$ret_array, $tagname_num = true)
{
if( !is_array($exif) || count( $exif ) <= 0 ) return false;
$i = 0;
while( array_key_exists($i, $exif) )
{
interpret_IFD_array($exif[$i], $ret_array, $tagname_num);
$i++;
}
return true;
}
function interpret_IFD_array($ifd, &$ret_array, $tagname_num)
{
if( !is_array($ifd) ) return false;

foreach( $ifd as $tagid => $tag )
{
if ( !is_numeric($tagid) || !$tag['Decoded'] ) continue;
$tagname = $tagname_num ? $tagid : $tag['Tag Name'];

switch($tag['Type']){
case "SubIFD":
$ret_array[ $tagname ] = array();
foreach ( $tag['Data'] as $subIFD )
{
interpret_IFD_array( $subIFD, $ret_array[ $tagname ], $tagname_num );
}
break;
case "Maker Note":
case "IPTC":
case "XMP":
case "IRB":
break;
case "Numeric":
default:
$ret_array[ $tagname ] = $tag['Text Value'];
break;
}
}
return true;
}
とりあえずざっくりと簡単にこんな関数を用意。$tagname_num にtrueを渡すと配列キーがタグの番号で、falseを渡すと文字列で返す。実行結果
// $filename = 読み込むjpeg等のファイルのパス
// $ret に結果が入る
Interpret_EXIF_to_Array( get_EXIF_JPEG( $filename ), $ret);
これで、
array(10) {
[271]=>
string(3) "hogehogoe"
[272]=>
string(5) "ahoaho"
[282]=>
string(39) "72/1 (72) pixels per 'Resolution Unit' "
[283]=>
string(39) "72/1 (72) pixels per 'Resolution Unit' "
[296]=>
string(6) "Inches"
[531]=>

大体、exif_read_data()と似たデータが取れるようになった。


タグ:Linux PHP Exif
posted by usoinfo at 09:44 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月18日

PHPで漢数字を数値に変換する

漢数字の表記はどういうものか。ルールが明らかになれば、変換できる。ああ、面倒だ。。。

・漢数字で使われる文字は、数字そのものを表す「〇一二三四五六七八九」と、桁数を表す「十百千万億…」
数字そのものを表す文字を数漢字、桁数を表す文字を桁漢字とする。
数漢字だけで表記されているものは、アラビア数字と同じなので簡単である。右が下位桁で、左に向かって上位桁。これは単純に置換すればいい。「二〇一一年」は、置換して「2011年」となる。
問題は桁漢字の混ざる場合である。

・桁漢字は、上位桁漢字の入れ子になり得る「十百千」と、一般的には入れ子にならない「万」以上で異なる
「一〇〇〇億円」「五〇〇億円」以外に、「一千億円」「五百億円」はあり得る、ということの計算機的表現をどうするかということ。
上位桁漢字の入れ子になり得る「十百千」を下桁字、「万」より上の文字を上桁字とする。
上桁字は単純にその先行部分の倍率を表す。「六万六」とあれば、" 6 x 10^4 + 6 " となるので、文字列を上桁字で区切って、上桁字の先行部分を数値化した上で倍率を掛ける。桁漢字の続かない部分は倍率が1である。
下桁字は、上桁字の先行部分に現れる可能性がある。「一千円」の「千」は倍率10^3だが、「一千億円」の「千」は、後続の「億」に絡んで10^3の10^12倍なので、倍率10^15を掛けることになる。

・下桁字は、単体でその1つ上の桁の「1」を表す可能性がある。
アラビア数字では、12は " 1 x 10^1 + 2 " で、位の値が1であっても1は省略されない。漢数字では、「十二」で、下桁字は先行部分があれば桁漢字として、なければ数漢字として機能する。「二十二」は "2/10/2" の3文字を " 2 x 10^1 + 2 " で、十 = 10^1 と単純に置き換えればいいが、「十二」は "10/2" の2文字で " 1 x 10^1 + 2 " となるから、"1" を補う必要がある。

と、いうことで、変換ロジックの作戦を次のように立案。

1. 入力された文字列を上桁字で分割する
2. 分割された各部分の上桁字を除いた部分を、下桁字を考慮して数値に変換する
  2.1. 左から入力を読み、数漢字であればそのまま数字として左詰に先行部分を入力する
  2.2. 下桁字が来たら、先行部分があれば倍率を乗じて加算、先行部分がなければ10/100/1000を加算する
  2.3. 終端が来たら、倍率1として先行部分を加算し、和を返す
3. 各部分の上桁字に応じて結果に倍率を乗じ、和を返す

作戦案に従ってコードに書き起こした結果。
define(SOURCE_CHARSET,	'EUC-JP');	// ソースコードの文字コード

function knum2arabic_10000($kanji, $incharset = 'EUC-JP')
{
// $kanji: 10000未満の漢数字表記文字列 $incharset: 入力文字コード
$kannum = array( // 数漢字-数値マップ
'0' => 0, '〇' => 0,
'1' => 1, '一' => 1,
'2' => 2, '二' => 2,
'3' => 3, '三' => 3,
'4' => 4, '四' => 4,
'5' => 5, '五' => 5,
'6' => 6, '六' => 6,
'7' => 7, '七' => 7,
'8' => 8, '八' => 8,
'9' => 9, '九' => 9,
);
$decinum = array( // 下桁字-倍率マップ
'十' => pow(10,1),
'百' => pow(10,2),
'千' => pow(10,3),
);
if( $incharset != SOURCE_CHARSET )
$kanji = mb_convert_encoding($kanji, SOURCE_CHARSET, $incharset);

$ival = 0;
$lnum = false;
for($pos=0;$pos<mb_strlen($kanji,SOURCE_CHARSET);$pos++){
$ch = mb_substr($kanji, $pos, 1,SOURCE_CHARSET);
if( isset($kannum[$ch]) ){
$lnum .= $kannum[$ch];
continue;
}
if( is_numeric($ch) ){
$lnum .= $ch;
continue;
}
if( isset($decinum[$ch]) ){
$ival += $lnum ? intval($lnum) * $decinum[$ch] : $decinum[$ch];
$lnum = false;
}
}
if( $lnum ) $ival += intval($lnum);
return $ival;
}

function knum2arabic($kanji, $incharset = 'EUC-JP')
{
// $kanji: 漢数字表記文字列 $incharset: 入力文字コード
$deci = array( // 上桁字-倍率マップ
'万' => pow(10,4),
'億' => pow(10,8),
'兆' => pow(10,12),
// 対応桁数を増やすときはここに追加
);
if( $incharset != SOURCE_CHARSET )
$kanji = mb_convert_encoding($kanji, SOURCE_CHARSET, $incharset);

$oldencoding = mb_regex_encoding();
mb_regex_encoding(SOURCE_CHARSET);

if( !mb_ereg_search_init($kanji) ){
mb_regex_encoding($oldencoding);
return -1;
}

foreach($deci as $i => $v){
$pat_sep .= $i;
}
$pattern = '[十百千一二三四五六七八九〇0-90-9]+['.$pat_sep.']'; // 数漢字として扱う文字

$i = 0;
while( ($match = mb_ereg_search_pos($pattern)) ) {
$part[$i] = substr($kanji, $match[0], $match[1]);
$kanji = str_replace($part[$i],'',$kanji);
$i++;
mb_ereg_search_init($kanji);
}
$part[$i] = $kanji;

$ival = 0;
for($i=0;$i<count($part);$i++){
$ipart = knum2arabic_10000($part[$i], SOURCE_CHARSET);
$imult = $deci[ mb_substr($part[$i], mb_strlen($part[$i], SOURCE_CHARSET)-1, 1, SOURCE_CHARSET) ];
$ival += $ipart * ($imult != 0 ? $imult : 1);
}

mb_regex_encoding($oldencoding);
return $ival;
}

実行すると

$k = '二〇〇八';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

$k = '六千九百三十三万二千三百十四';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

$k = '2億33万2310';
$r = knum2arabic($k, 'EUC-JP');
echo $k.' = '.$r."\n";

結果
二〇〇八 = 2008
六千九百三十三万二千三百十四 = 69332314
2億33万2310 = 200332310


ただ、この関数、変な表記のチェックはしないので、たとえば「二〇〇十」とか「十百万」とかは正しく変換できない。まあ、前者は200x10 = 2000 で正しいとも言えるし、後者は百万を取っ払った上で変換して結果に 10^6 を掛けるべきだろう。

※数漢字とか桁漢字とかの用語は表記の簡便のため適当に今思いついたもので、本当はなんて言うかはわかりません。
posted by usoinfo at 11:18 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月17日

PHPで空のディレクトリを下層まで再帰的に消す

日別にディレクトリを掘ってログファイルをしまっておいたりすると、ログを消した時にディレクトリが空になるので、あるディレクトリから下にある空のディレクトリを全部消したい時がけっこうある。平たく言えば find . -type d -empty -exec rm -rf {} \; をPHPでやりたい。PHPの関数に用意されていれば便利だと思うのだが、まだないので自作することになる。
function remove_empty_dir_recursive($dir)
{
if( !is_dir($dir) ) return false;
if( !($dh = opendir($dir)) ) return false;
while( ($file = readdir($dh)) !== false) {
if( strpos($file,".") === 0 ) continue;
if( is_dir($dir."/".$file) &&
remove_empty_dir_recursive($dir."/".$file) ) continue;
closedir($dh);
return false;
}
closedir($dh);
rmdir($dir);
return true;
}

remove_empty_dir_recursive( 消したいトップディレクトリ ); で、その配下で空のディレクトリをrmdirする。いっちょあり。

ついでに、ファイルがあっても全部消しちゃうときはこう。
function remove_everything_dir_recursive($dir)
{
if( !is_dir($dir) ) return false;
if( !($dh = opendir($dir)) ) return false;
while( ($file = readdir($dh)) !== false) {
if( strpos($file,".") === 0 ) continue;
if( is_dir($dir."/".$file) ){
remove_everything_dir_recursive($dir."/".$file);
continue;
}
unlink($dir."/".$file);
}
closedir($dh);
rmdir($dir);
return;
}
いっちょあり。
posted by usoinfo at 14:28 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月14日

MySQLの=演算子は( '数値じゃない文字列' = 0 )でTRUEを返す

まあ、やってみればその通りなんだけども。
mysql> SELECT 'ABC' = 0;
+-----------+
| 'ABC' = 0 |
+-----------+
| 1 |
+-----------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT '111' = 0;
+-----------+
| '111' = 0 |
+-----------+
| 0 |
+-----------+
1 row in set (0.00 sec)


MySQLでは文字列と数値の自動変換が行われる。MySQLのマニュアルの「式評価でのタイプ変換」をざっと見たら、文字列と数値の比較は "他のすべてのケースでは、引数は浮動少数点 ( 実 ) 数として比較されます。" に該当するので、'ABC' は数値に暗黙的に変換される。で、'ABC'は、数値に解釈できなかったから 0 になる。よって 0 = 0 は 1 だ。

合ってるけど、なんかうっかりすると危ないな。
$sqlstr = "DELETE FROM hogehoge WHERE colid = ".$colid
なんていうコードを書いて$colid=0と入っていて、うっかり$colidをクオートし忘れると、テーブル全部消えちゃう。
この文字列の数値への暗黙の型変換で解釈不能の時は、Warning 1292 が出るようなので、これをWarningじゃなくてエラーにする設定方法はないものだろうか。システム変数にそれらしきものは見当たらない。
タグ:Linux MySQL PHP
posted by usoinfo at 08:37 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月13日

Javascriptの小数点演算の変なゴミをなんとかする

そもそもJavascriptは鬼門で、出来ることなら使わずに済ませたいのだが、昨今そうも言っておられない。ので仕方なくJavascriptを使うこともある。

で、Javascript の小数点演算でゴミがつく(演算誤差が出る)のは、概ね既知の問題だとは思うのだが、これ、PHPみたいに何とかスパッと言語の方で丸めて解決してくれないものだろうか。変数に型のある言語なら有効桁数を気にして書くが、Javascriptは勝手に型変換なのに演算誤差は出てくるので、使いにくい。

どうでもいいが、勝手に型変換とかってに桃天使!はちょっと聞き間違いそうにならないこともない。
var	f = 1;
var i;
for(i=0;i<10;i++){
f += 0.2;
document.write( 'f='+f+'<br />' );
}
実行
f=1.2
f=1.4
f=1.5999999999999998
f=1.7999999999999998
f=1.9999999999999997
f=2.1999999999999997
f=2.4
f=2.6
f=2.8000000000000002
f=3.0000000000000004
まあ、こんなことになるわな。ブラウザによって結果は微妙に違うけど。
しょうがないので、一端整数にしてから演算して元に戻す四則演算の関数を作る。

function getMultipler(f)
{
var i = f.toString(10).indexOf('.');
return i == - 1 ? 0 : f.toString(10).length - i - 1;
}

function f_add(f1,f2)
{
var m = getMultipler(f1) > getMultipler(f2) ? getMultipler(f1) : getMultipler(f2);
return (f1 * Math.pow(10,m) + f2 * Math.pow(10,m)) / Math.pow(10,m);
}

function f_sub(f1,f2)
{
var m = getMultipler(f1) > getMultipler(f2) ? getMultipler(f1) : getMultipler(f2);
return (f1 * Math.pow(10,m) - f2 * Math.pow(10,m)) / Math.pow(10,m);
}

function f_mul(f1,f2)
{
var m = getMultipler(f1) > getMultipler(f2) ? getMultipler(f1) : getMultipler(f2);
return (f1 * Math.pow(10,m)) * (f2 * Math.pow(10,m)) / Math.pow(10,m*2);
}

function f_div(f1,f2)
{
var m = getMultipler(f1) > getMultipler(f2) ? getMultipler(f1) : getMultipler(f2);
return (f1 * Math.pow(10,m)) / (f2 * Math.pow(10,m));
}
実行
f	= 1;
for(i=0;i<10;i++){
f = f_add(f, 0.2);
document.write( 'f='+f+'<br />' );
}
f=1.2
f=1.4
f=1.6
f=1.8
f=2
f=2.2
f=2.4
f=2.6
f=2.8
f=3
しかしこれだと、桁数が増えたときに微妙に対応できないな。。。
そもそも、Javascriptサンには、演算子のオーバーロードくらいさせてくれと言いたい。
タグ:javascript
posted by usoinfo at 15:46 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月09日

GPSトレースマップの線の色を速度によって変えてみる

GPSトレースマップ出力ツールにいくつか変更を加えて、速度によって線の描画色の濃淡を変える機能を追加してみた。

T4d29399b273353b6-256.png

隣接する2ポイント間の距離をヒュベニの式で算出して、タイムスタンプでその時の速度を計算。速い程濃く、白に近づく程遅い。この地図は昨日静岡まで往復してきたログをプロットしたもの。
タグ:GPS OpenStreetMap
posted by usoinfo at 13:55 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2011年01月03日

GPSのログから直接軌跡を描画する

OSMの緯度経度トレースマップ出力ツールを、GPX形式のログファイルを直接アップロードすると、自動で地図に軌跡を描画するようにしてみた。
時系列の並び替え・日本じゃないポイントの除外・間引き処理を実行して、それなりに全体が収まるズームレベルにペタッと線を引く。

gpxtrace_sample.jpg

こんな感じに。だいたい3時間分を500ポイントに間引いているが、割ときれいに線が引ける。

隣接するポイント間での移動速度も計算すれば出せるはずなので、速度によって線の色を変えた、スピードマップみたいなものを描いたら面白いかもしれないな。
タグ:GPS OpenStreetMap
posted by usoinfo at 13:17 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2010年12月28日

tar で展開する先のディレクトリを指定

tar はカレントディレクトリに展開しようとするので、ssh でコマンドラインを渡して実行させる時に展開先を指定したかったのだが。
COMMON OPTIONS
-C, --directory DIR
change to directory DIR

これか!と思ってうっかり付けてもエラーが出た。EXAMPLESもよく見よう。
-C は尻につけるんだ。

/home/usost/testdl.tar のtarファイルを /home/usost/temp に展開する時は
% tar xvf /home/usost/testdl.tar -C /home/usost/temp

こうだった。

複数のサーバ群に同じパッチを突っ込みたかったのである。
for SERVER in serverA serverB serverX serverY
do
scp testdl.tar ${SERVER}:/home/usost
ssh ${SERVER} "tar xvf /home/usost/testdl.tar -C /tmp/ahoaho"
ssh ${SERVER} "/tmp/ahoaho/なんかパッチ当てるコマンドとか"
done
posted by usoinfo at 06:35 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2010年12月26日

MS VirtualPC でゲストのCentOSの時計が狂う

こんなのあったんだ。Virtual Machine Additions for Linux
しらなんだ。

ダウンロードしてインストールしても何の説明もないし不親切だな。インストールするとProgram Files\Microsoft Virtual Server\Virtual Machine Additions とかいうディレクトリが出来てて、そこに Readme もある。

端的に言うと、出てきたisoをゲストのCentOSでマウントして中に入ってるrpmを突っ込み、kernel-develがなければyum installで突っ込み、/lib/modules/vmadd/module で make して make vmadd-install-module して depmod して サービスvmadd/vmadd-heartbeat/vmadd-timesync/vmadd-shutdownあたりを起動するようにすればいい(SELinuxがenforcingの人はそれも)。

そして、grub.conf のカーネルの起動パラメータに clock=pit を追加。これでだいたい正しい時計になった。
どうやらホストOSの時計をハートビートで合わせに行ってるようだ。ntpd が全然同期しないくらいの激しい狂いっぷりだったので困っていた。やれやれだ。
posted by usoinfo at 15:00 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする

2010年12月25日

wgetで取ってきたMacのファイル名を一括で修正する

MacOSのファイル名は UTF-8 なんだが、Normalization Form D(NFD) というやつである。こいつはちょいと曲者で、LinuxやらWindowsやらの UTF-8 は Normalization Form C(NFC) なので、Macのファイル名をそのまま読もうとすると、「ケ゛ルハルト・ハ゛ルクホルン」みたいになってしまう。困る。
それで、WebサーバがMacであったとする。そのサーバから wget でまとめてファイルを取ってくると、wget は urlencode したものをファイル名にして保存するので、結局ファイル名は、urlencoded な UTF8-NFDということになる。つまり、何が何だかわからんファイル名がズラズラ並んで困る。

いちいち手で変換するのは気が遠くなるので、一括で変換してしまいたいが、ここからがいろいろと面倒で、
  1. CentOS5にバンドルされてる iconv/nkf は UTF8-NFD に対応していない
  2. ファイル名コード一括変換ができる convmv はNFD-NFC変換に対応しているが urldecode できない

ということで、どちらも帯に短したすきに長し。(convmvは標準では入ってないかもしれないが、Baseリポジトリにあるので、yum install convmvで一発)

どうすればいいかと思うと、結局、まずurlencodeされた名前を元の名前にmvして(UTF8-NFDのファイル名になる)、更に convmv で UTF8-NFC にしてやれば、めでたく解決するので、これを指定ディレクトリ以下一括でなんとかするスクリプトを書いた。


#!/bin/sh
# wgetname_nfd2nfc.sh

conv_recursive()
{
for fname in $1/*
do
newname=`echo ${fname}|nkf --url-input -w`
if [ "${fname}" != "${newname}" ]; then
mv "${fname}" "${newname}"
fname=${newname}
fi
if [ -d "${fname}" ] ; then
conv_recursive ${fname}
fi
done
return 1
}

if [ "$1" == "" ]; then
echo "Usage: $0 startdir"
exit
fi

conv_recursive $1
convmv -r -f utf8 --nfd -t utf8 --nfc --notest $1/*

第一引数に指定されたディレクトリ(末尾の/は不要)から下のディレクトリのファイル名をまとめて変換する。
これで

% wget --recursive http://ahoaho/hogehoge/
% wgetname_nfd2nfc.sh .

などとやれば、文字化けファイル名を一括変換。
posted by usoinfo at 16:17 | Comment(0) | 開発 | このブログの読者になる | 更新情報をチェックする
×

この広告は180日以上新しい記事の投稿がないブログに表示されております。