PHPでアップロードされたJpegファイルのExif情報にアクセスする方法・サンプルです。
PHPでサーバにアップロードされた写真からGPS情報を自動削除(上書き)するために、Jpeg、Exifの仕様を調べてサンプルプログラムを作成しました。
まずはデータ構造です。
Jpegファイルは0xFFD8で始まって、0xFFD9で終わるようです。
セグメント→IFD→フィールドという構造になっています。
セグメントのリストの中からExifのセグメントを見つけ、0thIFDのフィールドからGPS IFDへのポインタを探します。
GPS IFDへのポインタを見つけたらあとはそのフィールドを読み書きするだけです。
PHPでExif情報へのアクセスはImageMagickを使うこともできるようですが、なるべく外部ライブラリを使いたくないのでバイナリデータを直接編集します。
参照するだけであれば標準の拡張モジュールでも可能なようです。
まずは拡張モジュールで参照してみます。php.iniでexif拡張モジュールと依存するmbstring拡張モジュールを有効にします。
extension=mbstring
extension=exif
あとはワンライナーで表示できます。
php -r "var_dump(exif_read_data('IMG_0126.JPG'));"
~
["GPSLatitudeRef"]=>
string(1) "N"
["GPSLatitude"]=>
array(3) {
[0]=>
string(4) "43/1"
[1]=>
string(3) "3/1"
[2]=>
string(8) "1061/100"
}
["GPSLongitudeRef"]=>
string(1) "E"
["GPSLongitude"]=>
array(3) {
[0]=>
string(5) "141/1"
[1]=>
string(4) "29/1"
[2]=>
string(8) "4786/100"
}
~
string(1) "N"
["GPSLatitude"]=>
array(3) {
[0]=>
string(4) "43/1"
[1]=>
string(3) "3/1"
[2]=>
string(8) "1061/100"
}
["GPSLongitudeRef"]=>
string(1) "E"
["GPSLongitude"]=>
array(3) {
[0]=>
string(5) "141/1"
[1]=>
string(4) "29/1"
[2]=>
string(8) "4786/100"
}
~
では本題のサンプルプログラムです。
Jpegファイルを参照モードで開いて、バイナリデータからGPS情報を見つけて表示します。
<?php
$filename = 'IMG_0126.JPG';
$h = fopen($filename, 'r');
$h = fopen($filename, 'r');
// Pointer Of TIFF Header
$pot = 0;
$pot = 0;
// Offset To GPS IFD
$otg = -1;
$otg = -1;
// GPS Data
$data = [];
$data = [];
// Start Of Image(SOI)
// 0xFFD8
fread($h, 2);
$pot += 2;
// 0xFFD8
fread($h, 2);
$pot += 2;
// Segments
// mark(2) | len(2) | data(len-2) * X
while (true) {
$mark = fread($h, 2);
$pot += 2;
if ('ffda' == bin2hex($mark)) {
break;
}
$len = unpack('n', fread($h, 2))[1] - 2;
$pot += 2;
// mark(2) | len(2) | data(len-2) * X
while (true) {
$mark = fread($h, 2);
$pot += 2;
if ('ffda' == bin2hex($mark)) {
break;
}
$len = unpack('n', fread($h, 2))[1] - 2;
$pot += 2;
if ('ffe1' == bin2hex($mark)) {
echo("Exif found.\n");
exif($h);
break;
} else {
fread($h, $len);
$pot += $len;
}
}
echo('==END==');
exit;
echo("Exif found.\n");
exif($h);
break;
} else {
fread($h, $len);
$pot += $len;
}
}
echo('==END==');
exit;
// Exif segment
// 0xFFE1
function exif($h) {
// Exif ID
$exifId = trim(unpack('a*', fread($h, 6))[1]);
$GLOBALS['pot'] += 6; // Fixed the Pointer Of TIFF Header
if ('Exif' != $exifId) {
echo('"Exif" ID Error. Read='.$exifId."\n");
return;
}
// 0xFFE1
function exif($h) {
// Exif ID
$exifId = trim(unpack('a*', fread($h, 6))[1]);
$GLOBALS['pot'] += 6; // Fixed the Pointer Of TIFF Header
if ('Exif' != $exifId) {
echo('"Exif" ID Error. Read='.$exifId."\n");
return;
}
// TIFF Header
$tiff = fread($h, 8);
$bigEndian = strpos(bin2hex($tiff), '4d4d') === 0;
echo(($bigEndian ? 'Big Endian' : 'Little Endian').".\n");
$tiff = fread($h, 8);
$bigEndian = strpos(bin2hex($tiff), '4d4d') === 0;
echo(($bigEndian ? 'Big Endian' : 'Little Endian').".\n");
// 0th IFD
ifd($h, $bigEndian);
ifd($h, $bigEndian);
// Found Offset To GPS IFD.
if (0 <= $GLOBALS['otg']) {
// Point to GPS IFD
rewind($h);
fread($h, $GLOBALS['pot'] + $GLOBALS['otg']);
if (0 <= $GLOBALS['otg']) {
// Point to GPS IFD
rewind($h);
fread($h, $GLOBALS['pot'] + $GLOBALS['otg']);
// GPS IFD
ifd($h, $bigEndian);
ifd($h, $bigEndian);
$latitudeRef = $bigEndian ? '0001' : '0100';
$latitude = $bigEndian ? '0002' : '0200';
$longtitudeRef = $bigEndian ? '0003' : '0300';
$longtitude = $bigEndian ? '0004' : '0400';
$latitude = $bigEndian ? '0002' : '0200';
$longtitudeRef = $bigEndian ? '0003' : '0300';
$longtitude = $bigEndian ? '0004' : '0400';
for ($i = 0; $i < count($GLOBALS['data']); $i++) {
$f = $GLOBALS['data'][$i];
switch (substr($f, 0, 4)) {
case $latitudeRef:
echo('Latitude ref: '.unpack('a*', hex2bin(substr($f, 16, 4)))[1]."\n");
break;
case $latitude:
$offset = unpack($bigEndian ? 'N' : 'V', hex2bin(substr($f, 16, 8)))[1];
$data = ref($h, $GLOBALS['pot'] + $offset, $bigEndian);
echo('Latitude: '.$data."\n");
break;
case $longtitudeRef:
echo('Longtitude ref: '.unpack('a*', hex2bin(substr($f, 16, 4)))[1]."\n");
break;
case $longtitude:
$offset = unpack($bigEndian ? 'N' : 'V', hex2bin(substr($f, 16, 8)))[1];
$data = ref($h, $GLOBALS['pot'] + $offset, $bigEndian);
echo('Longtitude: '.$data."\n");
break;
}
}
}
}
$f = $GLOBALS['data'][$i];
switch (substr($f, 0, 4)) {
case $latitudeRef:
echo('Latitude ref: '.unpack('a*', hex2bin(substr($f, 16, 4)))[1]."\n");
break;
case $latitude:
$offset = unpack($bigEndian ? 'N' : 'V', hex2bin(substr($f, 16, 8)))[1];
$data = ref($h, $GLOBALS['pot'] + $offset, $bigEndian);
echo('Latitude: '.$data."\n");
break;
case $longtitudeRef:
echo('Longtitude ref: '.unpack('a*', hex2bin(substr($f, 16, 4)))[1]."\n");
break;
case $longtitude:
$offset = unpack($bigEndian ? 'N' : 'V', hex2bin(substr($f, 16, 8)))[1];
$data = ref($h, $GLOBALS['pot'] + $offset, $bigEndian);
echo('Longtitude: '.$data."\n");
break;
}
}
}
}
// IFD
// count(2) | field(12) * X | offset(4)
function ifd($h, $bigEndian) {
// Count
$count = unpack($bigEndian ? 'n' : 'v', fread($h, 2))[1];
// count(2) | field(12) * X | offset(4)
function ifd($h, $bigEndian) {
// Count
$count = unpack($bigEndian ? 'n' : 'v', fread($h, 2))[1];
// Fields
for ($i = 0; $i < $count; $i++) {
if ($GLOBALS['otg'] < 0) {
field($h, $bigEndian);
} else {
$GLOBALS['data'][] = bin2hex(fread($h, 12));
}
}
for ($i = 0; $i < $count; $i++) {
if ($GLOBALS['otg'] < 0) {
field($h, $bigEndian);
} else {
$GLOBALS['data'][] = bin2hex(fread($h, 12));
}
}
// Offset To Next IFD
fread($h, 4);
}
fread($h, 4);
}
// Field
// mark(2) | type(2) | count(4) | offset(4)
function field($h, $bigEndian) {
// Mark
$mark = fread($h, 2);
// mark(2) | type(2) | count(4) | offset(4)
function field($h, $bigEndian) {
// Mark
$mark = fread($h, 2);
// Type
fread($h, 2);
fread($h, 2);
// Count
fread($h, 4);
fread($h, 4);
// Offset
$offset = unpack($bigEndian ? 'N' : 'V', fread($h, 4))[1];
if (($bigEndian ? '8825' : '2588') == bin2hex($mark)) {
echo("Offset To GPS IFD Found.\n");
$GLOBALS['otg'] = $offset;
}
}
$offset = unpack($bigEndian ? 'N' : 'V', fread($h, 4))[1];
if (($bigEndian ? '8825' : '2588') == bin2hex($mark)) {
echo("Offset To GPS IFD Found.\n");
$GLOBALS['otg'] = $offset;
}
}
function ref($h, $pointer, $bigEndian) {
rewind($h);
fread($h, $pointer);
rewind($h);
fread($h, $pointer);
$data = unpack($bigEndian ? 'N6' : 'V6', fread($h, 8 * 3));
$v = $data[1] / $data[2] + $data[3] / $data[4] / 60 + $data[5] / $data[6] / 3600;
$v = $data[1] / $data[2] + $data[3] / $data[4] / 60 + $data[5] / $data[6] / 3600;
return $v;
}
}
出力結果は以下のようになります。
D:\server\work\exif>php exif.php
Exif found.
Big Endian.
Offset To GPS IFD Found.
Latitude ref: N
Latitude: 43.052947222222
Longtitude ref: E
Longtitude: 141.49662777778
==END==
Exif found.
Big Endian.
Offset To GPS IFD Found.
Latitude ref: N
Latitude: 43.052947222222
Longtitude ref: E
Longtitude: 141.49662777778
==END==
GPS情報を削除すると色々なオフセットに影響出てしまいますが、東京駅のジオコードに上書きする等は簡単にできそうです。
バイナリデータの操作としては、データ構造の把握と、オーダーバイト(ビッグエンディアン/リトルエンディアン)を忘れないことがポイントです。