写真に設定されているジオタグや撮影条件などのExifデータをAndroidで扱うために,Metadata ExtractorとSanselanAndroidを使う例です

Androidから写真のExifデータを扱う

写真に設定されているジオタグや撮影条件などのExifデータを扱うためには, Androidの標準で ExifInterfaceクラス があります.

しかしこれにはいくつか不満があります.

  1. 扱える属性が少ない.API Level 11になればかなり揃いますが,それ以前だとかなり物足りないです
  2. 書き込みをすると,対応していない属性の値が破壊されてしまう(ように見える)
  3. Android 2.0以上でないと使えない

それで他の方法がないか調べたところ,Androidで使えそうなライブラリが見つかりました.

これらの使い方について,簡単に書きます.

-使用したバージョン
Metadata Extractormetadata-extractor-2.3.1-src.jar
SanselanAndroid2012/11/12時点のトランクソースをsvnで取得したもの

Androidのバージョンは,2.3.4です.

実装例

Exifを読み出す

Metadata Extractorの例
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.lang.Rational;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifDirectory;
import com.drew.metadata.exif.GpsDirectory;

public static void extractExifLatLonDrew(String imagePath) {
	try {
		Metadata metadata = JpegMetadataReader.readMetadata(new File(imagePath));
		Directory directory = metadata.getDirectory(GpsDirectory.class);
		if (directory instanceof GpsDirectory) {
			final GpsDirectory gps = (GpsDirectory) directory;

			Rational[] lat = gps.getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
			float latitude = lat[0].floatValue() + lat[1].floatValue() / 60 + lat[2].floatValue() / 3600;

			Rational[] lng = gps.getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
			float longitude = lng[0].floatValue() + lng[1].floatValue() / 60 + lng[2].floatValue() / 3600;

			if ("S".equals(gps.getString(GpsDirectory.TAG_GPS_LATITUDE_REF))) {
				// 緯度
				latitude = -latitude;
			}
			if ("W".equals(gps.getString(GpsDirectory.TAG_GPS_LONGITUDE_REF))) {
				// 経度
				longitude = -longitude;
			}
		}
		Directory exifDirectory = metadata.getDirectory(ExifDirectory.class);
		int tag;
		tag = ExifDirectory.TAG_ISO_EQUIVALENT;
		if (exifDirectory.containsTag(tag)) {
			// ISO感度
			int v = exifDirectory.getInt(tag);
		}
		tag = ExifDirectory.TAG_EXPOSURE_TIME;
		if (exifDirectory.containsTag(tag)) {
			// 露出時間(= シャッタースピードの逆数)
			double v = exifDirectory.getDouble(tag);
		}
		tag = ExifDirectory.TAG_FNUMBER;
		if (exifDirectory.containsTag(tag)) {
			// 絞りのF値
			double v = exifDirectory.getDouble(tag);
		}
		tag = ExifDirectory.TAG_FOCAL_LENGTH;
		if (exifDirectory.containsTag(tag)) {
			// 焦点距離
			double v = exifDirectory.getDouble(tag);
		}
	} catch (Exception e) {
		//
	}
}
SanselanAndroidの例
import org.apache.sanselan.Sanselan;
import org.apache.sanselan.common.IImageMetadata;
import org.apache.sanselan.formats.jpeg.JpegImageMetadata;
import org.apache.sanselan.formats.tiff.TiffField;
import org.apache.sanselan.formats.tiff.TiffImageMetadata;
import org.apache.sanselan.formats.tiff.constants.ExifTagConstants;

public static void extractExifLatLonSans(String imagePath) {
	try {
		IImageMetadata metadata = Sanselan.getMetadata(new File(imagePath));
		if (metadata instanceof JpegImageMetadata) {
			JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
			TiffImageMetadata exifMetadata = jpegMetadata.getExif();
			if (exifMetadata != null) {
				TiffImageMetadata.GPSInfo gpsInfo = exifMetadata.getGPS();
				if (gpsInfo != null) {
					// 緯度
					double latitude = gpsInfo.getLatitudeAsDegreesNorth();
					// 経度
					double longitude = gpsInfo.getLongitudeAsDegreesEast();
				}
			}
			TiffField field;
			field = jpegMetadata.findEXIFValue(ExifTagConstants.EXIF_TAG_ISO);
			if (field != null) {
				// ISO感度
				int v = field.getIntValue();
			}
			field = jpegMetadata.findEXIFValue(ExifTagConstants.EXIF_TAG_EXPOSURE_TIME);
			if (field != null) {
				// 露出時間(= シャッタースピードの逆数)
				double v = field.getDoubleValue();
			}
			field = jpegMetadata.findEXIFValue(ExifTagConstants.EXIF_TAG_FNUMBER);
			if (field != null) {
				// 絞りのF値
				double v = field.getDoubleValue();
			}
			field = jpegMetadata.findEXIFValue(ExifTagConstants.EXIF_TAG_FOCAL_LENGTH);
			if (field != null) {
				// 焦点距離
				double v = field.getDoubleValue();
			}
		}
	} catch (Exception e) {
		//
	}
}
なお,後述の書き込みでSanselanAndroidを使うとすると読み込みもSanselanAndroidに統一すれば良さそうな気がしますが, SanselanAndroidだと,ExifInterfaceで書き込んでしまったことによってデータが失われたExifファイルを読み込もうとしたときに
java.io.IOException: Not a Valid TIFF File
という例外を出して読めませんでした.

それに対してMetadata Extractorの場合は,消えている値が多いものの読むことはできたため,使う意味があると思います.

Exifを更新する

SanselanAndroid
import org.apache.sanselan.Sanselan;
import org.apache.sanselan.formats.jpeg.JpegImageMetadata;
import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter;
import org.apache.sanselan.formats.tiff.TiffImageMetadata;
import org.apache.sanselan.formats.tiff.write.TiffOutputSet;

public static void saveLatLong(String imagePath, float lat, float lon, String newFilePath) {
	File originalFile = new File(imagePath);
	try {
		JpegImageMetadata jpegMetadata = (JpegImageMetadata) Sanselan.getMetadata(originalFile);
		TiffImageMetadata exif;
		if (jpegMetadata != null) {
			exif = jpegMetadata.getExif();
		} else {
			exif = null;
		}
		TiffOutputSet outputSet = null;
		if (exif != null) {
			outputSet = exif.getOutputSet();
		}
		if (outputSet == null) {
			outputSet = new TiffOutputSet();
		}

		outputSet.setGPSInDegrees(lon, lat);

		File newFile = new File(newFilePath);

		OutputStream os = new FileOutputStream(newFile);
		os = new BufferedOutputStream(os);
		try {
			new ExifRewriter().updateExifMetadataLossless(originalFile, os, outputSet);
		} finally {
			os.close();
		}
	} catch (Exception ex) {
		//
	}
}
既存のExifから読み出したTiffOutputSetに更新後の値を上書きして,ExifRewriter#updateExifMetadataLossless()を実行します.

元のファイル(originalFile)と別のファイルnewFileを作成していますので,置換するならoriginalFileを削除してnewFileをリネームなどすれば良いと思います.

Exifを新規作成する

SanselanAndroid
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.Sanselan;
import org.apache.sanselan.common.BinaryConstants;
import org.apache.sanselan.formats.jpeg.exifRewrite.ExifRewriter;
import org.apache.sanselan.formats.tiff.constants.ExifTagConstants;
import org.apache.sanselan.formats.tiff.constants.TagInfo;
import org.apache.sanselan.formats.tiff.write.TiffOutputDirectory;
import org.apache.sanselan.formats.tiff.write.TiffOutputField;
import org.apache.sanselan.formats.tiff.write.TiffOutputSet;

public static void saveLatLong(String imagePath, float lat, float lon, Date originalDate, String newFilePath) {
	File originalFile = new File(imagePath);
	try {
		TiffOutputSet outputSet = new TiffOutputSet(BinaryConstants.BYTE_ORDER_BIG_ENDIAN);
		TiffOutputDirectory exifDirectory = outputSet.getOrCreateExifDirectory();

		addTag(exifDirectory, ExifTagConstants.EXIF_TAG_DATE_TIME_ORIGINAL,
			new SimpleDateFormat("yyyy:MM:dd HH:mm:ss").format(originalDate);

		outputSet.setGPSInDegrees(lon, lat);

		File newFile = new File(newFilePath);

		OutputStream os = new FileOutputStream(newFile);
		os = new BufferedOutputStream(os);
		try {
			new ExifRewriter().updateExifMetadataLossless(originalFile, os, outputSet);
		} finally {
			os.close();
		}
	} catch (Exception ex) {
		//
	}
}
private static void addTag(TiffOutputDirectory outDir, TagInfo tagInfo, Number v) throws ImageWriteException {
	if (v != null) {
		outDir.add(TiffOutputField.create(tagInfo, BinaryConstants.BYTE_ORDER_BIG_ENDIAN, v));
	}
}
private static void addTag(TiffOutputDirectory outDir, TagInfo tagInfo, String v) throws ImageWriteException {
	if (v != null) {
		outDir.add(TiffOutputField.create(tagInfo, BinaryConstants.BYTE_ORDER_BIG_ENDIAN, v));
	}
}
更新との違いは,TiffOutputSetを既存から読み出すのではなく,新規に作成するところです.

指定しているエンディアン(BYTE_ORDER_BIG_ENDIAN)は全体で揃えるように注意してください.

privateメソッドのaddTag()は,null値のスキップとエンディアン指定を組み込んだ省力化メソッドです.

補足

ここで紹介したバージョンのSanselanAndroidには,Canonのデジカメで撮影したExifのメーカーノート部が壊れるという問題があります.

以下のパッチを入れれば,対応できます.

参考

kamolandをフォローしましょう


© 2021 KMIソフトウェア