Exifの回転情報をピクセル情報に反映する

## リポジトリ

**[minodisk/go-fix-orientation](https://github.com/minodisk/go-fix-orientation)**
使い方は[README](https://github.com/minodisk/go-fix-orientation/blob/master/README.md#go-fix-orientation-)を参照。

## Exifの回転情報とは何なのか

まずは、実際に何が起こるの見て欲しい。大抵のブラウザ[^browsers]は`<img>`でロードした画像の**Exifのorientationを描画に反映しない**ので、orientationタグの入っているjpegをそのまま貼ってみる[^samples]。orientationタグに2〜8という値が入っているjpegが回転や反転しているのが分かるだろうか。これらはorientationタグを解釈するビューワで見ると全て正しい方向の`F`に見える。

[^browsers]: OSXのSafari9/Chrome47/Firefox42とUbuntu14のChrome47/Firefox42とWin7のIE11/Chrome47/Firefox42で確認した。
[^samples]: ここで貼っている画像は全て上記リポジトリのテストに使っているのと同一の画像。

|orientationタグ|画像|
|--:|:-:|
|なし|![f.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/25b43a74-f210-0889-dc87-a11e5970a9c8.jpeg)|
|1|![f-orientation-1.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/2bdf7968-bdd7-86ae-9d60-734d76fe295f.jpeg)|
|2|![f-orientation-2.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/dd74eaef-f5ab-2d8d-edb9-582acdc50a23.jpeg)|
|3|![f-orientation-3.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/c12882b0-4d91-d51a-1745-21538d5c2e4d.jpeg)|
|4|![f-orientation-4.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/6b195b30-0281-48c6-a437-f76f7272a53a.jpeg)|
|5|![f-orientation-5.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/4101a6aa-ce67-6463-1bf7-58f6b78d52d9.jpeg)|
|6|![f-orientation-6.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/2316ad68-118f-ee7b-3d18-16fdec5067e6.jpeg)|
|7|![f-orientation-7.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/21c39854-e8b5-6ce2-3b74-9f4dfa49de9f.jpeg)|
|8|![f-orientation-8.jpg](https://qiita-image-store.s3.amazonaws.com/0/6752/cdcc7ace-9bdc-aa04-9ea8-4317533003d8.jpeg)|

### カメラとExif

一部のカメラは、ピクセル情報とは別に回転情報をExifとして保存する。画像素子が受け取った情報をそのままピクセル情報として記録し、現在のカメラの回転している方向を1,3,6,8といった値でExifのorientationタグに保存する。スマホのインカメラのように反転する情報も仕様上は2,4,5,7として記録できるということだろう。[^inside_cam]

[^inside_cam]: 実際のスマホのインカメラでは、写真を撮る間は画面に反転した映像を出力し、保存される画像は反転していないものをよく見る。撮影者はスマホの画面を鏡を見ている感覚で見ているので、鏡と同じように反転して表示するのが違和感が少ないということだろう。

## 目的

このような画像をブラウザで正しく表示したい場合はもちろんだが、このような画像に対してリサイズ・クロップ・その他の画像処理する時には前処理としてExifの回転情報をピクセル情報に反映する必要がある。Exifのorientationタグから回転すべき方向を判定し、必要に応じてピクセル情報を回転する。

## 実装

### 使うパッケージ

- [rwcarlsen/goexif](https://github.com/rwcarlsen/goexif): Exifデコーダ
- [graphics-go](https://code.google.com/p/graphics-go/): 画像処理パッケージ

### 解説

短いのでさらりと読めると思う。何点かあるポイントをつまんで説明する。

#### アフィン変換のインスタンス化

graphicsパッケージにはアフィン変換の実装があるので、変換シーケンスをインスタンス化できる。この実装のおかげでorientationタグの値と変換シーケンスをマッピングでき、画像処理のロジックに余計な分岐を入れずに済む。

```go
var affines map[int]graphics.Affine = map[int]graphics.Affine{
	1: graphics.I,
	2: graphics.I.Scale(-1, 1),
	3: graphics.I.Scale(-1, -1),
	4: graphics.I.Scale(1, -1),
	5: graphics.I.Rotate(toRadian(90)).Scale(-1, 1),
	6: graphics.I.Rotate(toRadian(90)),
	7: graphics.I.Rotate(toRadian(-90)).Scale(-1, 1),
	8: graphics.I.Rotate(toRadian(-90)),
}
```

#### 画像処理

1. 画像をデコード
2. orientationタグを取得
3. 回転情報を変換
4. 返す

```go
func Process(r io.Reader) (d image.Image, err error) {
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return
	}
	s, _, err := image.Decode(bytes.NewReader(b))
	if err != nil {
		return
	}
	o, err := ReadOrientation(bytes.NewReader(b))
	if err != nil {
		// 回転情報の取得に失敗するケース
		return s, nil
	}
	d = ApplyOrientation(s, o)
	return
}
```

##### 回転情報の取得に失敗するケース

- Exif情報が存在しないフォーマット (PNGやGIFなど)
- Exif情報が存在し得るフォーマットだが、存在しない
- Exif情報が存在するがorientationタグが存在しない
- Exif情報が存在するがorientationタグがintとして認識できない

などのケースがあるが、何れにしても回転する必要がない（もしくはしたくてもできない）ので元画像をそのまま返す。

#### 回転情報の取得

1. Exifをデコード
2. orientationタグを取得
3. intにする
4. 返す

```go
func ReadOrientation(r io.Reader) (o int, err error) {
	e, err := exif.Decode(r)
	if err != nil {
		return
	}
	tag, err := e.Get(exif.Orientation)
	if err != nil {
		return
	}
	o, err = tag.Int(0)
	if err != nil {
		return
	}
	return
}
```

#### 回転情報の反映

1. 出力画像の矩形を求める
2. 出力画像を作る
3. 元画像を回転したデータを出力画像に反映する
4. 出力画像を返す

```go
func ApplyOrientation(s image.Image, o int) (d draw.Image) {
	bounds := s.Bounds()
	if o >= 5 && o <= 8 {
		bounds = rotateRect(bounds)
	}
	d = image.NewRGBA64(bounds)
	affine := affines[o]
	affine.TransformCenter(d, s, interp.Bilinear)
	return
}

func rotateRect(r image.Rectangle) image.Rectangle {
	s := r.Size()
	return image.Rectangle{r.Min, image.Point{s.Y, s.X}}
}
```

##### orientationが5〜8の時

出力画像が元画像の縦横を入れ替えたサイズになるので、矩形の幅と高さを反転する。

## 参考

- [デジタルスチルカメラ用 画像ファイルフォーマット規格 Exif2.3](http://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf): P.28〜31にorientationタグについての詳細な仕様が書かれている。