ITブログ

PHPでJpegファイルのExif情報にアクセス

PHPでアップロードされたJpegファイルのExif情報にアクセスする方法・サンプルです。

PHPでサーバにアップロードされた写真からGPS情報を自動削除(上書き)するために、Jpeg、Exifの仕様を調べてサンプルプログラムを作成しました。
まずはデータ構造です。
it6.png
 
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"
  }
 
では本題のサンプルプログラムです。
Jpegファイルを参照モードで開いて、バイナリデータからGPS情報を見つけて表示します。
<?php
$filename = 'IMG_0126.JPG';
$h = fopen($filename, 'r');
 
// Pointer Of TIFF Header
$pot = 0;
 
// Offset To GPS IFD
$otg = -1;
 
// GPS Data
$data = [];
 
// Start Of Image(SOI)
// 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;
 
  if ('ffe1' == bin2hex($mark)) {
    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;
  }
 
  // TIFF Header
  $tiff = fread($h, 8);
  $bigEndian = strpos(bin2hex($tiff), '4d4d') === 0;
  echo(($bigEndian ? 'Big Endian' : 'Little Endian').".\n");
 
  // 0th IFD
  ifd($h, $bigEndian);
 
  // Found Offset To GPS IFD.
  if (0 <= $GLOBALS['otg']) {
    // Point to GPS IFD
    rewind($h);
    fread($h, $GLOBALS['pot'] + $GLOBALS['otg']);
 
    // GPS IFD
    ifd($h, $bigEndian);
 
    $latitudeRef   = $bigEndian ? '0001' : '0100';
    $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;
      }
    }
  }
}
 
// IFD
// 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));
    }
  }
 
  // Offset To Next IFD
  fread($h, 4);
}
 
// Field
// mark(2) | type(2) | count(4) | offset(4)
function field($h, $bigEndian) {
  // Mark
  $mark = fread($h, 2);
 
  // Type
  fread($h, 2);
 
  // Count
  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;
  }
}
 
function ref($h, $pointer, $bigEndian) {
  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;
  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==
 
GPS情報を削除すると色々なオフセットに影響出てしまいますが、東京駅のジオコードに上書きする等は簡単にできそうです。
バイナリデータの操作としては、データ構造の把握と、オーダーバイト(ビッグエンディアン/リトルエンディアン)を忘れないことがポイントです。