Exifを他の画像にコピー(Android)

# 概要
AndroidでExifを扱うには`ExifInterface`を利用するのが一般的。
読み込み・書き込みの作例も多くみられるが、他の画像にコピーする例が見当たらなかった。
ので、書き残します。

# 想定される用途

* 写真画像から位置情報等を追記したい、あるいは消したい
* 写真画像を画像処理して、その結果にはExifを残したい

# 選択肢
掲題の問題を実現するために次の選択肢を思いついた:

1. すべて1からゴリゴリ書く
2. ExifInterfaceを利用してタグをすべて読み取り書き写す
3. ExifInterfaceのソースに手を加えてコピー処理を実装する
4. ExifInterfaceにリフレクションで手を突っ込んでコピー処理を実装する

一番手軽な4.を採用した。

# ビルド環境
* __ExifInterface Support Library 26.1.0__
* AndroidStudio 3.1.4
* buildToolsVersion "27.0.3"
* compileSdkVersion 26
* minSdkVersion 14
* targetSdkVersion 26

ExifInterfaceにはネイティブ版とサポートライブラリ版があるが、ソースを見る限りでは今回の内容はどちらにも適用できると思われる(未確認)。

# ソース

```java
public class ExifUtils {
	//リフレクション処理用ヘルパ
	//=========================================================================
	private static Field getField(Object o, String f) {
		Field result=null;
		try {
			result=o.getClass().getDeclaredField(f);
			result.setAccessible(true);
		} catch (NoSuchFieldException e) {
		}
		return result;
	}
	
	private static Object getFieldObject(Object o,Field f) {
		Object result=null;
		try {
			result=f.get(o);
		} catch (IllegalAccessException e) {
		}
		return result;
	}
	
	private static HashMap[] getAttributeMapArray(ExifInterface exif) {
		//HashMap<String,ExifInterface.ExifAttribute>[] mAttributes をリフレクションで取得する
		
		Field f=getField(exif,"mAttributes");
		if(f==null) return null;
		
		Object o=getFieldObject(exif,f);
		if(o==null) return null;
		
		if(!o.getClass().isArray()) return null;
		return (HashMap[]) o;
	}
	
	private static ByteOrder getByteOrder(ExifInterface exif) {
		Field f=getField(exif,"mExifByteOrder");
		if(f==null) return null;
		
		Object o=getFieldObject(exif,f);
		if(o==null || !(o instanceof ByteOrder)) return null;
		return (ByteOrder)o;
	}
	
	private static void setByteOrder(ExifInterface exif,ByteOrder bo) {
		Field f=getField(exif,"mExifByteOrder");
		if(f==null) return;
		
		try {
			f.set(exif,bo);
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
	}
	
	//Exif操作
	//=========================================================================
	
	//Exifをコピーする
	//-------------------------------------------------------------------------
	public static void copyExif(ExifInterface from,ExifInterface to) {
		if(from==null || to==null) return;
		
		//画像サイズ情報を取得
		int tw= to.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH,0);
		int tl= to.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH,0);
		
		//ExifタグのHashMapを取得
		HashMap[] fMap=getAttributeMapArray(from);
		HashMap[] tMap=getAttributeMapArray(to);
		if(fMap==null || tMap==null) return;
		
		ByteOrder bo=getByteOrder(from);
		setByteOrder(to,bo);
		
		int i,c;
		for(i=0,c=Math.min(fMap.length,tMap.length);i<c;i++) {
			tMap[i].clear();
			tMap[i].putAll(fMap[i]);
		}
		
		//画像サイズ情報を書き戻す
		to.setAttribute(ExifInterface.TAG_IMAGE_WIDTH,tw==0?null:Integer.toString(tw));
		to.setAttribute(ExifInterface.TAG_IMAGE_LENGTH,tl==0?null:Integer.toString(tl));
		
		//サムネイル情報を消去する
		to.setAttribute(ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH,null);
		to.setAttribute(ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH,null);
		to.setAttribute(ExifInterface.TAG_ORF_THUMBNAIL_IMAGE,null);
	}
	
	//タグの一括削除
	//-------------------------------------------------------------------------
	public static void removeTags(ExifInterface exif,String[] tags) {
		if(exif!=null) {
			for(String tag:tags) exif.setAttribute(tag,null);
		}
	}
	
	//位置情報を削除
	//-------------------------------------------------------------------------
	public static void removeLocationTags(ExifInterface exif) {
		if(exif!=null) {
			//GPS関連セクションを全消去
			HashMap[] map=getAttributeMapArray(exif);
			if(map!=null && map.length>=3) map[2].clear();
		}
	}
	
	//時間情報を削除
	//-------------------------------------------------------------------------
	private static final String sTimeTags[]={
			ExifInterface.TAG_DATETIME,
			ExifInterface.TAG_SUBSEC_TIME,
			ExifInterface.TAG_GPS_DATESTAMP,
			ExifInterface.TAG_GPS_TIMESTAMP,
			
			//編集関連時間情報
			ExifInterface.TAG_DATETIME_ORIGINAL,
			ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
			ExifInterface.TAG_DATETIME_DIGITIZED,
			ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
	};
	
	public static void removeTimeTags(ExifInterface exif) {
		removeTags(exif,sTimeTags);
	}
	
	//画像ファイルにExifのコピーを書き込む
	//-------------------------------------------------------------------------
	public static boolean saveExif(File file, ExifInterface srcExif) {
		return saveExif(file,srcExif,false,false);
	}
	
	public static boolean saveExif(File file, ExifInterface srcExif,boolean removeLocation, boolean removeTime) {
		if(srcExif==null) return false;
		
		try {
			ExifInterface exif=new ExifInterface(file.getAbsolutePath());
			copyExif(srcExif,exif);
			if(removeLocation) removeLocationTags(exif);
			if(removeTime) removeTimeTags(exif);
			exif.saveAttributes();
		} catch (IOException e) {
			return false;
		}
		
		return true;
	}
}
```

## 使い方

* 元ファイルからExifを取得する
* Exifを書き込みたいファイルのFileオブジェクトを生成
* `saveExif(file,srcExif)` でExifがコピーされて書き込まれる
    * `saveExif(file,srcExif,removeLocation,removeTime)` で位置情報、時間情報の削除を指示できる
* サムネイルはコピーされない
* JPEG以外のファイルに対して書き込みを行っても何も起きない

# うんちく
使えればよいという人は以降は読む必要はありません。

## Bitmap.compress の処理の流れにExif記入を追加できないか
できない。
compressはOutputStreamを引数に受けて書き込みを行うので割り込めそうにも思えるが、その余地はない。
一度ファイルに保存してからあらためてExifを書き込む必要がある。

## バイトオーダー
Exifデータにはバイトオーダー(バイトの並び順)の差異が存在する。
手元にある画像では、GalaxyNoteで撮影した画像はリトルエンディアン、moto G4 plusで撮影した画像はビッグエンディアンだった。

Exifデータを丸ごとコピーする際にはバイトオーダーも指定しなおさなければならない。
ExifInterfaceで実装する場合並び順の転置作業は必要なく、コピー後にバイトオーダーを指定しなおすだけでよい。
詳細はソース参照のこと。ソースではバイトオーダー指定処理は実装済みなのでそのまま利用できる。

## タグの削除
ExifInterfaceにおけるタグ値の種類にはString,int,doubleが存在するが、いずれの種類のタグも `setAttribute(tag,null)` で削除される。

## 位置情報、時間情報の削除
正攻法であれば `setAttribute(tag,null)` を用いて関連タグを片っ端から削除する必要がある。

例えば `exif.setDateTime(0)` のようにすれば削除できるのではないかと思ったりもするが、実際には値が0のタグが書き込まれて削除はされない。
この例なら撮影日時が1970年1月1日 0:00になる。

上のソースでは、GPS情報に関してはリフレクションを使って関連セクションをごっそり消している。
時間情報については地道に消している。