リポジトリ
minodisk/go-fix-orientation
使い方はREADMEを参照。
Exifの回転情報とは何なのか
まずは、実際に何が起こるの見て欲しい。大抵のブラウザ1は<img>
でロードした画像のExifのorientationを描画に反映しないので、orientationタグの入っているjpegをそのまま貼ってみる2。orientationタグに2〜8という値が入っているjpegが回転や反転しているのが分かるだろうか。これらはorientationタグを解釈するビューワで見ると全て正しい方向のF
に見える。
orientationタグ | 画像 |
---|---|
なし | |
1 | |
2 | |
3 | |
4 | |
5 | |
6 | |
7 | |
8 |
カメラとExif
一部のカメラは、ピクセル情報とは別に回転情報をExifとして保存する。画像素子が受け取った情報をそのままピクセル情報として記録し、現在のカメラの回転している方向を1,3,6,8といった値でExifのorientationタグに保存する。スマホのインカメラのように反転する情報も仕様上は2,4,5,7として記録できるということだろう。3
目的
このような画像をブラウザで正しく表示したい場合はもちろんだが、このような画像に対してリサイズ・クロップ・その他の画像処理する時には前処理としてExifの回転情報をピクセル情報に反映する必要がある。Exifのorientationタグから回転すべき方向を判定し、必要に応じてピクセル情報を回転する。
実装
使うパッケージ
- rwcarlsen/goexif: Exifデコーダ
- graphics-go: 画像処理パッケージ
解説
短いのでさらりと読めると思う。何点かあるポイントをつまんで説明する。
アフィン変換のインスタンス化
graphicsパッケージにはアフィン変換の実装があるので、変換シーケンスをインスタンス化できる。この実装のおかげでorientationタグの値と変換シーケンスをマッピングでき、画像処理のロジックに余計な分岐を入れずに済む。
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)),
}
画像処理
- 画像をデコード
- orientationタグを取得
- 回転情報を変換
- 返す
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として認識できない
などのケースがあるが、何れにしても回転する必要がない(もしくはしたくてもできない)ので元画像をそのまま返す。
回転情報の取得
- Exifをデコード
- orientationタグを取得
- intにする
- 返す
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
}
回転情報の反映
- 出力画像の矩形を求める
- 出力画像を作る
- 元画像を回転したデータを出力画像に反映する
- 出力画像を返す
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: P.28〜31にorientationタグについての詳細な仕様が書かれている。