EXIF抽出スクリプトのサンプル

JPEGファイルからExifを取り出すPerlスクリプトです。ソースを短くするため、シンプルな機能に絞っており、あまり最適化もしていません。ひな形としてお使いください。

最初の%target_tagで定義したExifタグのみを取り出すので、必要なデータだけを抽出して利用できます。実際の出力はサブルーチンread_dir_entryで行っているので、ここのprint関数の部分を書き換えると、XHTMLのテーブルなど、好みのフォーマットで出力できます。

コメントフィールドは、とりあえず内容にかかわらずそのまま出力しています。必要に応じて、RDFであることをチェックしたり、XMLを適当な形に変換するなど書き換えてください。

#!/usr/local/bin/perl

#グローバルで使う変数
($buf, $long_tmpl, $short_tmpl) = ();

#抽出するExifタグを定義する連想配列(ハッシュ)。'tagID' => 'label'の形式
%taget_tag = (
'270' => 'imageDescription',
'271' => 'make',
'272' => 'model',
'306' => 'dateTime',
'315' => 'artist',
'33432' => 'copyright',
'33434' => 'exposureTime',
'33437' => 'fNumber',
'37386' => 'focalLength',
'37396' => 'subjectArea',
'37500' => 'makerNote',
'37510' => 'userComment',
'41986' => 'exposureMode',
'41987' => 'whiteBalance',
'41988' => 'digitalZoomRatio',
'41989' => 'focalLengthIn35mmFilm',
'41990' => 'sceneCaptureType',
'41991' => 'gainControl',
'41992' => 'contrast',
'41993' => 'saturation',
'41994' => 'sharpness',
);

my($fld_marker, $tiff_size, $magic);

#入力ストリームは適宜書き換え
open(IN, $ARGV[0]) || die "Can't open $ARGV[0]: $!" if $ARGV[0];
binmode(IN);

read(IN, $_, 2);
return unless(unpack("H4", $_) eq 'ffd8'); #is JPEG

# TIFFタグを解析していくメインループ
while(1){
	read(IN, $_, 2);
	$fld_marker = unpack("H4", $_);
	read(IN, $_, 2);
	$tiff_size = unpack("n", $_);
	if($fld_marker eq 'ffe1'){
		read(IN, $_, 6);
		$magic = unpack("A6", $_);
		if($magic eq "Exif"){
			read(IN, $buf, $tiff_size - 8); # as global buffer variable
			read_tiff();
		}else{
			read(IN, $_, $tiff_size - 8); #dummy
		}
	}elsif($fld_marker eq 'fffe'){
		read(IN, $_, $tiff_size - 2); #dummy
		read_comment($_);
	}elsif($fld_marker eq 'ffda' or $fld_marker eq ''){
		#終了条件は、とりあえずSOSもしくはタグが見つからない(ファイル終了)
		last;
	}else{
		read(IN, $_, $tiff_size - 2); #dummy
	}
}
close IN;


###
### Exifを解析するサブルーチン
###

sub read_tiff{
	my($byte_order, $offset, $iifd);
	$byte_order = unpack("A2", substr($buf, 0, 2));
	if($byte_order eq "II"){
		($long_tmpl, $short_tmpl) = ("V", "v"); #インテル順
	}elsif($byte_order eq "MM"){
		($long_tmpl, $short_tmpl) = ("N", "n"); #モトローラ順
	}else{
		die "Incorrect byte order:$byte_order :$!";
	}

	$offset = unpack($long_tmpl, substr($buf, 4, 4));
	1 while($offset = read_ifd($offset, 0));
}

# IFDの解析
sub read_ifd{
	my($offset, $parent_tag_id) = @_;
	my($num_dir_entry) = unpack($short_tmpl, substr($buf, $offset, 2));
	my($next_offset) = unpack($long_tmpl, substr($buf, $offset + 2 + $num_dir_entry * 12, 4));
	$offset += 2;
	while($num_dir_entry){
		read_dir_entry(substr($buf, $offset, 12), --$num_dir_entry, $parent_tag_id);
		$offset += 12;
	}

	$next_offset;
}

# ディレクトリエントリの解析
sub read_dir_entry{
	my($dir_entry, $num_dir_entry, $parent_tag_id) = @_;
	my($tag, $type, $num_data, $dir_offset) = unpack("$short_tmpl$short_tmpl$long_tmpl$long_tmpl", $dir_entry);
	my $data_value = '';
	my($dir_size) = (1, 1, 2, 4, 8)[$type-1] || 1;

	if($tag==34665 || $tag==34853 || $tag==40965){
		1 while $dir_offset = read_ifd($dir_offset, 0, $nest + 1,$tag);
	}elsif($taget_tag{$tag}){
		#userCommentは、文字列と想定して出力
		$type = 2 if($taget_tag{$tag} eq 'userComment'); 
		if($num_data * $dir_size > 4){
			foreach $i (0 .. $num_data - 1){
				$data_value .= read_data($type, substr($buf, $dir_offset + $i * $dir_size, $dir_size));
			}
		}else{
			foreach $i (0 .. $num_data - 1){
				$data_value .= read_data($type, substr($dir_entry, 8 + $i * $dir_size, $dir_size));
			}
		}
		
		#この出力部分を変更すれば、好みのフォーマットに
		print "$taget_tag{$tag}:$data_value\n";
	}
}

# データタイプに応じたデコード
sub read_data{
	my ($type, $data) = @_;
	if ($type==1){
		return unpack("C", $data);
	}elsif($type==2){
		return unpack("A", $data);
	}elsif($type==3){
		return unpack($short_tmpl, $data);
	}elsif($type==4){
		return unpack($long_tmpl, $data);
	}elsif($type==5){
		return join("/", unpack("$long_tmpl$long_tmpl", $data));
	}else{
		return unpack("H*", $data); #分からない形式はとりあえず16進で
	}
}

# コメントフィールド
sub read_comment{
	my $comment_data = shift;
	#とりあえずどんなコメントでもそのまま出力
	print $comment_data;
}

### End of sample script