PerlでのMastdon toot

マストドンでの自動トゥート作った。

Perlは、https通信ができるように、必要な対応はしておいてください。

【参考】

engineer-milione.com

<<インスタンス>>は、利用しているインスタンスのURLを入れてください。

<<アクセストークン>>は、マストドンの「アカウント設定」「開発」でアプリを作成したら、設定されるので、それを入れてください。

なお、アプリには、アクセス権「write:statuses」を付与しておいてください。

ちなみに、インスタンスsocial.vivaldi.netは、LWP::UserAgentのUserAgentを拒否しているので、他のUserAgentを設定しました。

#/usr/bin/perl

use utf8;
use strict;
#use warnings;
use Encode qw(encode decode);
use LWP::UserAgent;
use HTTP::Request;

use constant {
	##Mastdon
	REQUEST_URI_M	=> 'https://<<インスタンス>>/api/v1/statuses',
	ACCESS_TOKEN_M	=> 'Bearer <<アクセストークン>>',
};

sub tootMessage{
	my $message	= shift;
	my $log		= '';
	#文字数チェックと改行除く。
	my $c_text = $message;
	$c_text =~ s/\r/ /sig;
	$c_text =~ s/\n/ /sig;
	my $length = length($c_text);
	if($length > 500){
		return("Message length[$length] too much at tootMessage.[$c_text]\n");
	}
	### リクエストパラメータの準備
	my %param = (status => _encode_url($message), visibility => 'public');#visibiltyは適宜変更すること
	my $res;
	eval{
		### リクエストを生成する
		my $content = &_get_content(%param);
		print $content."\n";
		print REQUEST_URI_M."\n";
		print ACCESS_TOKEN_M."\n";
		my $req = HTTP::Request->new ('POST', REQUEST_URI_M, [Authorization => ACCESS_TOKEN_M, 'Content-Type' => 'application/x-www-form-urlencoded'], $content)
		or die 'Failed to initialize HTTP::Request';
		### リクエストを投げる
		my $ua = LWP::UserAgent->new	or die 'Failed to initialize LWP::UserAgent';
		$ua->agent("");#UserAgentで拒否しているインスタンスがあるので、拒否しないUserAgentに設定する
		$res = $ua->request ($req)		or die 'Failed to request';
		my $sl = $res->status_line;
		$log .= "mastdon status_line is $sl.";
	};
	if($@){	chomp($@);$log .= $@;}
	else{	$log .= "toot $c_text.";}
	return ($log);
}

### POST するデータ文字列を生成する
sub _get_content {
	my (%param) = @_;
	join '&', map {
		sprintf '%s=%s', $_, $param{$_};
		} keys %param;
}
### 文字列を URL エンコードする
sub _encode_url {
	my $str = encode('UTF-8',shift);
	$str =~ s!([^a-zA-Z0-9_.~-])!sprintf '%%%02X',ord($1)!ge;
	return ($str);
}

MapBox Markerにclickイベント

GoogleMapでは、Markerにclickイベントを定義するときは、

const marker = new google.maps.Marker({
	position: marker_loc,
	map: gmap,
	title: '赤ピン'
});
//マーカークリックでclickEventを実行
google.maps.event.addListener(marker, 'click', clickEvent);

でできるけど、MapBoxでは、

const marker = new mapboxgl.Marker({
	color: '#ff0000'
	})
	.setLngLat(marker_loc)
	.addTo(gmap);
//マーカークリックでclickEventを実行
marker.on('click', clickEvent);

では、動かん!!!

MapBoxのDocumentをよく読むと、MarkerのEventsは、「drag」「dragstart」「dragend」の3つしかないことがわかった。

Markerのソースコードを確認したところ、「click」は、MarkerのPopup機能に使われているため、それ以外では使えなくなっていることがわかった。(以下のリンクの399行目からの_onMapClick)

そこで、Markerクラスをオーバーライドして、_onMapClickを書き換えることとした。

class customMarker extends mapboxgl.Marker{
    _onMapClick(e) {
        const targetElement = e.originalEvent.target;
        const element = this._element;

        if (targetElement === element || element.contains((targetElement))) {
            clickEvent();
        }
    }
}

const marker = new customMarker({
	color: '#ff0000'
	})
	.setLngLat(marker_loc)
	.addTo(gmap);

で、MapBoxのMarkerに、clickイベントを実行できるようにできた。

Swift CSV作成

Swiftで、Arrayから、CSV形式のStringを生成するコードを作成しました。

使い方は、headerに行の項目名などをCSV形式で指定し、arrayにCSVにしたいデータを指定して、コールすると返り値がCSV形式のStringとなります。

ただし、Swiftは内部的に文字コードUnicodeなので、出力するときは、dataUsingEncoding(NSShiftJISStringEncoding)のメソッドで、S-JISに変換してください。

struct MakeCSV {
    static func MakeCSV(header:String, array:AnyObject, footer:String)->String{
        var csv:String = ""
        let maxindex1st = array.count-1
        for(var i=0;i<=maxindex1st;i++){
            let row = array[i]
            var rowcsv :String = ""
            let maxindex2nd = row.count-1
            for(var j=0;j<=maxindex2nd;j++){
                if j != 0 {rowcsv = rowcsv + ","}//2列目以降は、先頭にコンマをつける
                var str:String = ""
                if(row[j] is String || row[j] is NSString){ //文字列の場合
                    //改行コードをCR+LFに
                    str = (row[j] as! String).stringByReplacingOccurrencesOfString("\r\n", withString:"\n", options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
                    str = str.stringByReplacingOccurrencesOfString("[\r\n]", withString:"\r\n", options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
                    //ダブルコーテーションをエスケープ
                    str = str.stringByReplacingOccurrencesOfString("\"", withString:"\"\"", options: NSStringCompareOptions.RegularExpressionSearch, range: nil)
                }else if(row[j] is NSDate){ //日付の場合
                    let dateFormatter = NSDateFormatter()
                    dateFormatter.locale = NSLocale(localeIdentifier: "ja_JP")
                    dateFormatter.timeStyle = .MediumStyle
                    dateFormatter.dateStyle = .MediumStyle
                    str = dateFormatter.stringFromDate(row[j] as! NSDate) as String
                }else if(row[j] is NSNumber){  //NSNumberの場合
                    str = (row[j] as! NSNumber).stringValue
                }
                if str != "" { rowcsv = rowcsv + "\"" + str + "\"" }
            }
            csv = csv + rowcsv + "\r\n"
        }
        return header + "\r\n" + csv + footer
    }
}

iOSはenableHighAccuracy:trueはお勧めしない

iOS端末で、navigator.geolocation.getCurrentPositionのオプションで、enableHighAccuracyをtrueにすると、環境によっては、なかなかcallbackされないことがありました。enableHighAccuracy:trueはお勧めしません。

//ユーザーの現在の位置情報を取得(位置情報の精度を高くする)
navigator.geolocation.getCurrentPosition(
successCallback, errorCallback, {enableHighAccuracy:true;}
);

Active Perl 5.8にCrypt::SSLeayインストールするときの注意事項メモ

WindowsからCrypt::SSLeayのインストール・詳細版 - 基本は根性ナシな日記にActive PerlへのCrypt::SSLeayのインストール方法が記載されていますが、これだけでは、うまくいきません。

IISからCGIでCrypt::SSLeayを利用する場合は、ssleay32.dll/libleay32.dllのアクセス権限を、IISからアクセスできるようにする必要があります。

  1. コマンドプロンプトから、「ppm install http://theoryx5.uwinnipeg.ca/ppms/Crypt-SSLeay.ppdを実行する。
  2. ssleay32.dllとlibleay32.dllをFetchが始まるので、PATHが通っているディレクトリにこれらのファイルを保存する。
  3. ssleay32.dllとlibleay32.dllのアクセス権限に<EVERYONE>もしくは<IUSER_マシン名>から、実行できるようにプロパティを変更する。

TwitterAPI1.1 https対応の自動ツイートコード

ツイッターAPI1.1が、突然httpsにのみしか応答しなくなったので、Perlでのツイート用コードを修正しました。REQUEST_URIを変えただけです。

ただし、LWP::UserAgentモジュールがhttpsに対応するためには、Crypt::SSLeayか、Net::SSLeayモジュールが必要です。(Active Perl5.8へのCrypt::SSLeayのインストールは結構手間取りました。このことは、後日、記事に書きます。)

http://www.magicvox.net/archive/2010/12311401/のソースを一部改変しています。

use uft8;
use strict; use warnings; use Encode qw(encode decode); use MIME::Base64; use LWP::UserAgent; use Digest::HMAC_SHA1; use HTTP::Request; use constant { ##Twitter REQUEST_METHOD => 'POST', REQUEST_URI => 'https://api.twitter.com/1.1/statuses/update.json', CONSUMER_KEY => 'コンシューマーキー', CONSUMER_SECRET => 'コンシューマーシークレット', ACCESS_TOKEN => 'アクセストークン', ACCESS_SECRET => 'アクセスシークレット', }; ########################################### ## 以下のソースコードは ## http://www.magicvox.net/archive/2010/12311401/ ## 掲載のものを一部修正したもの。 ## MITライセンス sub tweetMessage{ my $message = shift; my $lat = shift; my $lon = shift; my $log = ''; #文字数チェックと改行除く。 my $c_text = $message; $c_text =~ s/\r//sig; $c_text =~ s/\n//sig; my $length = length($c_text); if($length > 200){ return("Message length[$length] too much at tweetMessage.[$c_text]\n"); } ### リクエストパラメータの準備 my %oauth = ( oauth_consumer_key => CONSUMER_KEY, oauth_token => ACCESS_TOKEN, oauth_signature_method => 'HMAC-SHA1', oauth_timestamp => time, oauth_nonce => 'SD'.sprintf("%09d",int(rand(100000000))), oauth_version => '1.0'); my %param = (status => _encode_url($c_text)); if($lat && $lon){$param{lat} = $lat; $param{long} = $lon;} $oauth{oauth_signature} = get_signature( REQUEST_METHOD, REQUEST_URI, %oauth, %param); my $res; eval{ ### リクエストを生成する my $content = get_content(%param); my $req = HTTP::Request->new (REQUEST_METHOD, REQUEST_URI, ['Authorization' => get_oauth_header(%oauth), 'Content-Type' => 'application/x-www-form-urlencoded'], $content) or die 'Failed to initialize HTTP::Request'; ### リクエストを投げる if(not(F_DEBUG)){ my $ua = LWP::UserAgent->new or die 'Failed to initialize LWP::UserAgent'; $res = $ua->request ($req) or die 'Failed to request'; my $sl = $res->status_line; $log .= &addLog("twitter status_line is $sl."); } }; if($@){ $log .= &addLog("$@");} else{ $log .= &addLog("tweet『$c_text』with lat:$lat, lng:$lon.");} return ($log); } ### 送信するパラメータから oauth_signature を求める sub get_signature { my ($method, $uri, %params) = @_; my $param = join '&', map { join '=', $_, $params{$_}; } sort keys %params; my $base_string = join '&', $method, _encode_url ($uri), _encode_url ($param); my $key = join '&', CONSUMER_SECRET, ACCESS_SECRET; _encode_base64 (Digest::HMAC_SHA1::hmac_sha1 ($base_string, $key)); } ### OAuth HTTP ヘッダ文字列の生成する sub get_oauth_header { my (%param) = @_; 'OAuth '. join ', ', map { sprintf '%s="%s"', $_, _encode_url ($param{$_}); } keys %param; } ### POST するデータ文字列を生成する sub get_content { my (%param) = @_; join '&', map { sprintf '%s=%s', $_, $param{$_}; } keys %param; } ### 文字列を URL エンコードする sub _encode_url { my $str = encode('UTF-8',shift); $str =~ s!([^a-zA-Z0-9_.~-])!sprintf '%%%02X',ord($1)!ge; return ($str); } ### データを MIME::Base64 エンコードする sub _encode_base64 { my $str = shift; $str = MIME::Base64::encode ($str); # エンコードされた文字列の末尾に改行コードがくっついてくるっぽい $str =~ s/^s+|s+$//g; return ($str); ########################################### ## 以下のソースコードは ## http://www.magicvox.net/archive/2010/12311401/ ## 掲載のものを一部修正したもの。 ## MITライセンス sub tweetMessage{ my $message = shift; my $lat = shift; my $lon = shift; my $log = ''; #文字数チェックと改行除く。 my $c_text = $message; $c_text =~ s/\r//sig; $c_text =~ s/\n//sig; my $length = length($c_text); if($length > 200){ return("Message length[$length] too much at tweetMessage.[$c_text]\n"); } ### リクエストパラメータの準備 my %oauth = ( oauth_consumer_key => CONSUMER_KEY, oauth_token => ACCESS_TOKEN, oauth_signature_method => 'HMAC-SHA1', oauth_timestamp => time, oauth_nonce => 'SD'.sprintf("%09d",int(rand(100000000))), oauth_version => '1.0'); my %param = (status => _encode_url($c_text)); if($lat && $lon){$param{lat} = $lat; $param{long} = $lon;} $oauth{oauth_signature} = get_signature( REQUEST_METHOD, REQUEST_URI, %oauth, %param); my $res; eval{ ### リクエストを生成する my $content = get_content(%param); my $req = HTTP::Request->new (REQUEST_METHOD, REQUEST_URI, ['Authorization' => get_oauth_header(%oauth), 'Content-Type' => 'application/x-www-form-urlencoded'], $content) or die 'Failed to initialize HTTP::Request'; ### リクエストを投げる if(not(F_DEBUG)){ my $ua = LWP::UserAgent->new or die 'Failed to initialize LWP::UserAgent'; $res = $ua->request ($req) or die 'Failed to request'; my $sl = $res->status_line; $log .= &addLog("twitter status_line is $sl."); } }; if($@){ $log .= &addLog("$@");} else{ $log .= &addLog("tweet『$c_text』with lat:$lat, lng:$lon.");} return ($log); } ### 送信するパラメータから oauth_signature を求める sub get_signature { my ($method, $uri, %params) = @_; my $param = join '&', map { join '=', $_, $params{$_}; } sort keys %params; my $base_string = join '&', $method, _encode_url ($uri), _encode_url ($param); my $key = join '&', CONSUMER_SECRET, ACCESS_SECRET; _encode_base64 (Digest::HMAC_SHA1::hmac_sha1 ($base_string, $key)); } ### OAuth HTTP ヘッダ文字列の生成する sub get_oauth_header { my (%param) = @_; 'OAuth '. join ', ', map { sprintf '%s="%s"', $_, _encode_url ($param{$_}); } keys %param; } ### POST するデータ文字列を生成する sub get_content { my (%param) = @_; join '&', map { sprintf '%s=%s', $_, $param{$_}; } keys %param; } ### 文字列を URL エンコードする sub _encode_url { my $str = encode('UTF-8',shift); $str =~ s!([^a-zA-Z0-9_.~-])!sprintf '%%%02X',ord($1)!ge; return ($str); } ### データを MIME::Base64 エンコードする sub _encode_base64 { my $str = shift; $str = MIME::Base64::encode ($str); # エンコードされた文字列の末尾に改行コードがくっついてくるっぽい $str =~ s/^s+|s+$//g; return ($str);
}
 

EXIFのタグをJavascriptで読む

MOON GIFTに記載されているexif.jsでは、リトルエンディアンの場合、バグがありましたので、修正しました。


//MOON GIFT(http://www.moongift.jp/2012/05/20120502-2/)を一部改変
Exif = {};
(function () {
  var MARKER_SOI = 0xffd8;
  var MARKER_APP1 = 0xffe1;
  var FMT_SIZE = [
    1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8
  ];

  function BinaryReader(arr, endian) {
    this.arr = new Uint8Array(arr);
    this.endian = endian || 0;
    this.cur = 0;
    this._stack = ;
  }

  BinaryReader.LITTLE_ENDIAN = 0;
  BinaryReader.BIG_ENDIAN = 1;
  BinaryReader.SEEK_ABSOLUTELY = 0;
  BinaryReader.SEEK_RELATIVELY = 1;

  BinaryReader._makeReadMethod = function (size) {
    return function (endian) {
      var target;

      typeof endian === "undefined" && (endian = this.endian);
      target = this.arr.subarray(this.cur, this.cur + size);

      this.cur += size;

      return BinaryReader.pack(target, endian);
    };
  };

  BinaryReader.pack = function (arr, endian) {
    var f = endian === BinaryReader.LITTLE_ENDIAN
      ? Array.prototype.reduceRight // Little Endian
      : Array.prototype.reduce; // Big Endian

    return f.call(arr, function (a, b) {
      return (a << 8) + b;
    }, 0);
  };

  BinaryReader.pack_signed = function (arr, endian) {
    var n = BinaryReader.pack(arr, endian);

    if (n & (1 << arr.length * 8 - 1)) {
      n = n - (Math.pow(2, arr.length * 8));
    }

    return n;
  }

  BinaryReader.packstr = function (arr) {
      var res = "";
      for (var i=0; i<arr.length; i++)
        res += String.fromCharCode(arr[i]);
      return res;
      //return Array.prototype.map.call(arr, String.fromCharCode).join("");
  };

  BinaryReader.prototype = {
    constructor : BinaryReader,

    setEndian : function (endian) {
      this.endian = endian;
    },

    byte : BinaryReader._makeReadMethod(2),
    word : BinaryReader._makeReadMethod(4),
    dword : BinaryReader._makeReadMethod(8),

    skip : function (size) {
      this.cur += size;
      return this;
    },

    str : function (size) {
      var arr = this.arr.subarray(this.cur, this.cur + size);

      this.cur += size;
      return BinaryReader.packstr(arr);
    },

    getarr : function (size) {
      var ret = this.arr.subarray(this.cur, this.cur + size);

      this.cur += size;
      return ret;
    },

    seek : function (offset, type) {
      if (type === BinaryReader.SEEK_RELATIVELY) {
        this.cur += offset;
      } else {
        //default
        this.cur = offset;
      }
      return this;
    },

    push : function () {
      this._stack.push(this.cur);
      return this;
    },

    pop : function () {
      this.cur = this._stack.pop();
      return this;
    }
  };

  Exif.loadFromArrayBuffer = function (arr) {
    var soi, app1 = {}, ifd0, subifd, gpsifd, base, endian;

    var reader = new BinaryReader(arr, BinaryReader.BIG_ENDIAN);

    //check magic byte
    soi = reader.byte();
    if (soi !== MARKER_SOI) {
      throw "Not JPG file format";
    }

    //check if file format is Exif
    while(1){
		app1.marker = reader.byte();
		app1.size = reader.byte();
		if (app1.marker == MARKER_APP1) {
		  break;
		}else if(app1.marker == 0xfffe || !app1.marker){
		  throw "Not Exif file format";
		}
		reader.skip(app1.size-2);
	}

    app1.exifHeader = reader.str(4);
    base = reader.skip(2).cur;
    app1.endian = reader.byte();

    if (app1.endian === 0x4949) {
      reader.setEndian(BinaryReader.LITTLE_ENDIAN);
      endian = BinaryReader.LITTLE_ENDIAN;
    } else {
      endian = BinaryReader.BIG_ENDIAN;
    }

    app1.ifd0Offset = reader.skip(2).word();

    //load IFD0
    reader.seek(base + app1.ifd0Offset);
    ifd0 = Exif.nameIfd(loadIfd(endian), Exif.IFD0_TABLE);

    reader.seek(base + ifd0.exifIFDPointer);
    subifd = Exif.nameIfd(loadIfd(endian), Exif.SUBIFD_TABLE);

    if ("gpsInfoIFDPointer" in ifd0) {
      reader.seek(base + ifd0.gpsInfoIFDPointer);
      gpsifd = Exif.nameIfd(loadIfd(endian), Exif.GPSIFD_TABLE);
    }

    return {
      ifd0 : ifd0,
      subifd : subifd,
      gpsifd : gpsifd
    };

    function loadIfd(endian) {
      var ifd = {}, entries = , i, j, size, bytesLen, data;

      ifd.entryLength = reader.byte();
      ifd.entries = entries;

      for (i = 0; i < ifd.entryLength; i++) {
        entries[i] = {};
        entries[i].tag = reader.byte();
        entries[i].fmt = reader.byte();
        entries[i].len = reader.word();
        size = FMT_SIZE[entries[i].fmt - 1];
        bytesLen = size * entries[i].len;

        if (bytesLen <= 4) {
          data = reader.getarr(4);
        } else {
          data = reader.word();
          reader.push();
          reader.seek(base + data);
          data = reader.getarr(bytesLen);
          reader.pop();
        }
        
        switch (entries[i].fmt) {
          case 1: //unsigned byte
          case 3: //unsigned short
          case 4: //unsigned long
            entries[i].data = ;
            for (j = 0; j < entries[i].len; j++) {
              entries[i].data[j] = BinaryReader.pack(data.subarray(j * size, (j + 1) * size), endian);
            }
            entries[i].len === 1 && (entries[i].data = entries[i].data[0]);
            break;

          case 6: //signed byte
          case 8: //signed short
          case 9: //signed long
            entries[i].data = ;
            for (j = 0; j < entries[i].len; j++) {
              entries[i].data[j] = BinaryReader.pack_signed(data.subarray(j * size, (j + 1) * size), endian);
            }
            entries[i].len === 1 && (entries[i].data = entries[i].data[0]);
            break;

          case 2: //ascii string
            entries[i].data = BinaryReader.packstr(data);
            break;

          case 5: //unsigned raditional
            entries[i].data = ;
            for (j = 0; j < entries[i].len; j++) {
              entries[i].data[j] =
                BinaryReader.pack(data.subarray(j * 8 , j * 8 + 4), endian) /
                BinaryReader.pack(data.subarray(j * 8 + 4, j * 8 + 8), endian)
              ;
            }
            break;

          case 10: //signed raditional
            entries[i].data = ;
            for (j = 0; j < entries[i].len; j++) {
              entries[i].data[j] =
                BinaryReader.pack(data.subarray(j * 8 , j * 8 + 4), endian) /
                BinaryReader.pack(data.subarray(j * 8 + 4, j * 8 + 8), endian)
              ;
            }
            break;

          default:
            entries[i].data = data;
        }
      }
      ifd.next = reader.word();

      return ifd;
    }
  };

  Exif.nameIfd = function (ifd, table) {
    var nifd = {}, entries = ifd.entries, name;

    for (var i = 0; i < ifd.entryLength; i++) {
      name = table[entries[i].tag];
      nifd[name || entries[i].tag] = ifd.entries[i].data;
    }

    return nifd;
  };

  Exif.IFD0_TABLE = {
    256 : "mageWidth",
    257 : "imageLength",
    258 : "bitsPerSample",
    259 : "compression",
    262 : "photometricInterpretation",
    274 : "orientation",
    277 : "samplesPerPixel",
    284 : "planarConfiguration",
    530 : "yCbCrSubSampling",
    531 : "yCbCrPositioning",
    282 : "xResolution",
    283 : "yResolution",
    296 : "resolutionUnit",
    273 : "stripOffsets",
    278 : "rowsPerStrip",
    279 : "stripByteCounts",
    513 : "jPEGInterchangeFormat",
    514 : "jPEGInterchangeFormatLength",
    301 : "transferFunction",
    318 : "whitePoint",
    319 : "primaryChromaticities",
    529 : "yCbCrCoefficients",
    532 : "referenceBlackWhite",
    306 : "dateTime",
    270 : "imageDescription",
    271 : "make",
    272 : "model",
    305 : "software",
    315 : "artist",
    3432 : "copyright",
    34665 : "exifIFDPointer",
    34853 : "gpsInfoIFDPointer"
  };

  Exif.SUBIFD_TABLE = {
    36864 : "exifVersion",
    40960 : "flashPixVersion",
    40961 : "colorSpace",
    37121 : "componentsConfiguration",
    37122 : "compressedBitsPerPixel",
    40962 : "pixelXDimension",
    40963 : "pixelYDimension",
    37500 : "makerNote",
    37510 : "userComment",
    40964 : "relatedSoundFile",
    36867 : "dateTimeOriginal",
    36868 : "dateTimeDigitized",
    37520 : "subSecTime",
    37521 : "subSecTimeOriginal",
    37522 : "subSecTimeDigitized",
    33434 : "exposureTime",
    33437 : "fNumber",
    34850 : "exposureProgram",
    34852 : "spectralSensitivity",
    34855 : "iSOSpeedRatings",
    34856 : "oECF",
    37377 : "shutterSpeedValue",
    37378 : "apertureValue",
    37379 : "brightnessValue",
    37380 : "exposureBiasValue",
    37381 : "maxApertureValue",
    37382 : "subjectDistance",
    37383 : "meteringMode",
    37384 : "lightSource",
    37385 : "flash",
    37386 : "focalLength",
    41483 : "flashEnergy",
    41484 : "spatialFrequencyResponse",
    41486 : "focalPlaneXResolution",
    41487 : "focalPlaneYResolution",
    41488 : "focalPlaneResolutionUnit",
    41492 : "subjectLocation",
    41493 : "exposureIndex",
    41495 : "sensingMethod",
    41728 : "fileSource",
    41729 : "sceneType",
    41730 : "cFAPattern",
    40965 : "interoperabilityIFDPointer"
  };

  Exif.GPSIFD_TABLE = {
    0 : "versionID",
    1 : "latitudeRef",
    2 : "latitude",
    3 : "longitudeRef",
    4 : "longitude",
    5 : "altitudeRef",
    6 : "altitude",
    7 : "timeStamp",
    8 : "satellites",
    9 : "status",
    10 : "measureMode",
    11 : "dOP",
    12 : "speedRef",
    13 : "speed",
    14 : "trackRef",
    15 : "track",
    16 : "imgDirectionRef",
    17 : "imgDirection",
    18 : "mapDatum",
    19 : "destLatitudeRef",
    20 : "destLatitude",
    21 : "destLongitudeRef",
    22 : "destLongitude",
    23 : "bearingRef",
    24 : "bearing",
    25 : "destDistanceRef",
    26 : "destDistance"
  };

})();