ImageMagickで変換したBMP画像を、Go言語で読み込んだら「bmp: unsupported BMP image」と言われた


# 環境
* go version go1.10.2 windows/amd64
* Windows10 
* Windows Paint
* ImageMagick 7.0.4-10 Q16 x64 2017-02-18


# 背景

画像フォーマットをgo言語で確認したいです。

```go:ReadImage.go
package main

import (
	"fmt"
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"os"

	_ "golang.org/x/image/bmp"
)

func main() {
	imageFile := os.Args[1]
	f, _ := os.Open(imageFile)
	defer f.Close()
    //画像フォーマットを取得
	_, format, err := image.DecodeConfig(f)
	if err != nil {
		fmt.Println(err)
		return
	}
	//png, jpg, bmp, etc
	fmt.Println(format)
}
```

```shell-session
$ go build
$ ReadImage.exe rena.png
png
```

`rena.png`は以下の画像です。

<img width="200" alt="rena.png" src="https://qiita-image-store.s3.amazonaws.com/0/37903/fc113be8-872d-defa-e509-6c8274fbef66.png">


# 問題
BMPファイルを読み込んだら、「bmp: unsupported BMP image」というエラーが発生しました。

* 読み込んだBMPファイル（`rena-convert.bmp`）：ImageMagickのconvertコマンドで変換
* 変換元のBMPファイル（`rena.bmp`）：Windows Paintで保存

```shell-session
$ # OriginalのBMPファイル
$ ReadImage.exe rena.bmp
bmp

$ # ImageMagickのconvertコマンドで変換（後でdiffを確認するため、何も変換しない）
$ convert rena.bmp rena-convert.bmp

$ # 変換したBMPファイル
$ ReadImage.exe rena-convert.bmp
bmp: unsupported BMP image
```


# 原因調査

## identifyコマンドで、画像情報を確認
Imagemagickの`identify -verbose`で、画像の情報を確認します。


```shell-session
$ # OriginalのBMPファイルのidentify
$ identify --verbose rena.bmp > rena.bmp.identify

$ # 変換したBMPファイルのidentify
$ identify --verbose rena-convert.bmp > rena-convert.bmp.identify

# diffを出力
$ diff -u  rena.bmp.identify rena-convert.bmp.identify > diff.txt
```

![image](https://qiita-image-store.s3.amazonaws.com/0/37903/ee97fe26-07c8-44f2-dac7-bad106076422.png)


```diff:diff.txt
--- rena.bmp.identify	2018-05-24 11:14:45.086983300 +0900
+++ rena-convert.bmp.identify	2018-05-24 11:30:34.944869600 +0900
@@ -1,5 +1,5 @@
-Image: rena.bmp
-  Format: BMP3 (Microsoft Windows bitmap image (V3))
+Image: rena-convert.bmp
+  Format: BMP (Microsoft Windows bitmap image)
   Class: DirectClass
   Geometry: 400x300+0+0
   Resolution: 56.69x56.69
@@ -49,7 +49,6 @@
       skewness: 0.725172
       entropy: 0.917511
   Rendering intent: Perceptual
-  Gamma: 0.454545
   Chromaticity:
     red primary: (0.64,0.33)
     green primary: (0.3,0.6)
@@ -68,15 +67,15 @@
   Compression: None
   Orientation: Undefined
   Properties:
-    date:create: 2018-05-24T11:12:30+09:00
-    date:modify: 2018-05-24T11:12:30+09:00
+    date:create: 2018-05-24T11:30:11+09:00
+    date:modify: 2018-05-24T11:30:11+09:00
     signature: eb75c46523cfb88d54ff8ca96b10ef9ea8fba88bd52f849f76bf70818977ac48
   Artifacts:
     verbose: true
   Tainted: False
   Filesize: 360KB
   Number pixels: 120K
-  Pixels per second: 59.98MB
+  Pixels per second: 120.1MB
   User time: 0.000u
-  Elapsed time: 0:01.002
+  Elapsed time: 0:01.000
   Version: ImageMagick 7.0.4-10 Q16 x64 2017-02-18 http://www.imagemagick.org

```

### 分かったこと
* 変換前のBMP（rena.bmp）
    - format: BMP3 (Microsoft Windows bitmap image (V3))
    - Gamma あり
* 変換後のBMP（rena-convert.bmp）
    - format: BMP (Microsoft Windows bitmap image)
    - Gamma なし


```plain:rena.bmp.identify
Image: rena.bmp
  Format: BMP3 (Microsoft Windows bitmap image (V3))
  Class: DirectClass
  Geometry: 400x300+0+0
  Resolution: 56.69x56.69
  Print size: 7.05592x5.29194
  Units: PixelsPerCentimeter
  Type: TrueColor
  Endianess: Undefined
  Colorspace: sRGB
  Depth: 8-bit
  Channel depth:
    Red: 8-bit
    Green: 8-bit
    Blue: 8-bit
  Channel statistics:
    Pixels: 120000
    Red:
      min: 0 (0)
      max: 255 (1)
      mean: 131.226 (0.514612)
      standard deviation: 57.9187 (0.227132)
      kurtosis: -1.03751
      skewness: -0.166315
      entropy: 0.956321
    Green:
      min: 0 (0)
      max: 228 (0.894118)
      mean: 79.6108 (0.312199)
      standard deviation: 41.0085 (0.160818)
      kurtosis: -0.19308
      skewness: 0.604492
      entropy: 0.919659
    Blue:
      min: 0 (0)
      max: 243 (0.952941)
      mean: 64.5745 (0.253233)
      standard deviation: 33.3705 (0.130865)
      kurtosis: 1.05109
      skewness: 1.09448
      entropy: 0.876554
  Image statistics:
    Overall:
      min: 0 (0)
      max: 255 (1)
      mean: 91.8038 (0.360015)
      standard deviation: 53.5228 (0.209893)
      kurtosis: -0.391929
      skewness: 0.725172
      entropy: 0.917511
  Rendering intent: Perceptual
  Gamma: 0.454545
  Chromaticity:
    red primary: (0.64,0.33)
    green primary: (0.3,0.6)
    blue primary: (0.15,0.06)
    white point: (0.3127,0.329)
  Matte color: grey74
  Background color: white
  Border color: srgb(223,223,223)
  Transparent color: none
  Interlace: None
  Intensity: Undefined
  Compose: Over
  Page geometry: 400x300+0+0
  Dispose: Undefined
  Iterations: 0
  Compression: None
  Orientation: Undefined
  Properties:
    date:create: 2018-05-24T11:12:30+09:00
    date:modify: 2018-05-24T11:12:30+09:00
    signature: eb75c46523cfb88d54ff8ca96b10ef9ea8fba88bd52f849f76bf70818977ac48
  Artifacts:
    verbose: true
  Tainted: False
  Filesize: 360KB
  Number pixels: 120K
  Pixels per second: 59.98MB
  User time: 0.000u
  Elapsed time: 0:01.002
  Version: ImageMagick 7.0.4-10 Q16 x64 2017-02-18 http://www.imagemagick.org
```


## BMPのバージョン
BMP（のヘッダ）には、いくつかの種類があります。

![image](https://qiita-image-store.s3.amazonaws.com/0/37903/8d295f7e-24c2-35c8-d5d0-41fababcab4b.png)

https://qiita.com/yoya/items/3d588687a30175601885 引用

「INFO」がBMP v3に対応します。
（v4の一つ前という点でv3らしい）
ヘッダのサイズ（dibHeaderSize）は、バージョンの判定に使われます。

* Windows BMP v2
    - Info header size: 12
    - Info header name: BITMAPCOREHEADER
* Windows BMP v3
    - Info header size: 40
    - Info header name: BITMAPINFOHEADER
* Windows BMP v4
    - Info header size: 108
    - Info header name: BITMAPV4HEADER
* Windows BMP v5
    - Info header size: 124
    - Info header name: BITMAPV5HEADER


http://fileformats.archiveteam.org/wiki/BMP#Well-known_versions 参考

ちなみに、`BITMAPINFOHEADER`（BMP v3)が標準のようです。

>With OS/2 no longer supported after Windows 2000, for now the common Windows format is the BITMAPINFOHEADER header. 

https://en.wikipedia.org/wiki/BMP_file_format 引用

## ImageMagickの対応フォーマット
ImageMagickに対応しているフォーマットは、BMP, BMP2, BMP3の3種類です。

```shell-session
$ identify -list format
   Format  Module    Mode  Description
------------------------------------------
...
     BMP* BMP       rw-   Microsoft Windows bitmap image
     BMP2* BMP       -w-   Microsoft Windows bitmap image (V2)
     BMP3* BMP       -w-   Microsoft Windows bitmap image (V3)
...
```
  

>By default the BMP format is version 4. Use BMP3 and BMP2 to write versions 3 and 2 respectively.

https://www.imagemagick.org/script/formats.php 引用

* デフォルトはv4？（後述の疑問点参照）

## go言語のBMP読み込み処理

https://github.com/golang/image/blob/master/bmp/reader.go

```go:golang.org/x/image/bmp/reader.go

// ErrUnsupported means that the input BMP image uses a valid but unsupported
// feature.
var ErrUnsupported = errors.New("bmp: unsupported BMP image")

//...

func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown bool, err error) {
	// We only support those BMP images that are a BITMAPFILEHEADER
	// immediately followed by a BITMAPINFOHEADER.
	const (
		fileHeaderLen = 14
		infoHeaderLen = 40
	)
	var b [1024]byte
	if _, err := io.ReadFull(r, b[:fileHeaderLen+infoHeaderLen]); err != nil {
		return image.Config{}, 0, false, err
	}
	if string(b[:2]) != "BM" {
		return image.Config{}, 0, false, errors.New("bmp: invalid format")
	}
	offset := readUint32(b[10:14])
    //【yuji38kwmt追記】ヘッダサイズを取得して、判定
	if readUint32(b[14:18]) != infoHeaderLen {
		return image.Config{}, 0, false, ErrUnsupported
	}
	width := int(int32(readUint32(b[18:22])))
	height := int(int32(readUint32(b[22:26])))
    //.....

}

```

* ヘッダサイズ（`readUint32(b[14:18])`）が`40`でなければ、`ErrUnsupported`
    - ⇒BMP v3しか対応していない
* 変換前(`rena.bmp`）のヘッダサイズ：`40` ⇒ BMP v3
* 変換後(`rena-convert.bmp`）のヘッダサイズ：`124`⇒ BMP v5？

### 【補足】ファイルサイズの比較

```shell-session
$ #ファイルサイズをbyte単位で表示
$ du -ba rena.bmp rena-convert.bmp
360054  rena.bmp
360138  rena-convert.bmp
```

* 変換したファイルの方が、84byte大きい。`84 = 124 -40`
    - ⇒ 変換前と後で、ヘッダサイズのみ増加


# 解決

`-define`オプションで、BMPのフォーマットを指定します。

```shell-session
$ convert -define bmp:format=bmp3 rena.bmp rena-bmp3.bmp

```

>valid values are bmp2, bmp3, and bmp4. This option can be useful when the method of prepending "BMP2:" to the output filename is inconvenient or is not available, such as when using the mogrify utility.

https://www.imagemagick.org/script/command-line-options.php#define 引用


### 補足
* `-format`オプションでは、BMP3を指定できない
* 出力先のファイル名の先頭に、`BMP3:`を指定できる

```shell-session
# NG: BMP3に変換されない
$ convert -format bmp3 rena.bmp rena-convert.bmp

# OK: BMP3に変換できる
$ convert  rena.bmp BMP3:rena-convert.bmp
```






# まとめ
* Windows Bitmapファイル（のヘッダ）には、いくつかのバージョンがある
* ImageMagickでBMPに変換すると、デフォルトBMP v4(v5?)
* go言語の標準ライブラリは、BMP v3のみ対応している


# 参考にしたサイト
[【バイナリファイル入門】Bitmapファイルを手書きで作って遊んでみる](https://qiita.com/chooyan_eng/items/151e67684e5ef8d1a695)
[Microsoft Windows Bitmap File Format Summary](http://www.fileformat.info/format/bmp/egff.htm)
[Command-line Tools: Identify @ ImageMagick](https://www.imagemagick.org/script/identify.php)
[Command-line Tools: Convert @ ImageMagick](https://www.imagemagick.org/script/convert.php)
[画像フォーマットをバイナリ解析するツール](http://app.awm.jp/image.js/format.html)

# 補足
## なぜフォーマットが複数あるのか？

* BMPはポータブルのフォーマットではない
* OSのAPIが変わると、BMPフォーマットも変わる

>The structure of BMP format files is closely tied to the API of both Windows and OS/2. In this regard, BMP was never meant to be a portable format or used for bitmap data interchange between different operating systems. As each of these operating system APIs has changed, the BMP format has changed along with it.



> The reason that there are different headers is that Microsoft extended the DIB format several times. The new extended headers can be used with some GDI functions instead of the older ones, providing more functionality. 

 
http://www.fileformat.info/format/bmp/egff.htm


## go言語はモノクロBMPもサポートしていない

ソースコードには、以下のコメントが記載されています。

>We only support 1 plane, 8 or 24 bits per pixel and no compression. 

モノクロBMPはbpp(bits per pixel)が"1"なので、サポート対象外です。

```go:reader.go
	// We only support 1 plane, 8 or 24 bits per pixel and no compression.
	planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34])
	if planes != 1 || compression != 0 {
		return image.Config{}, 0, false, ErrUnsupported
	}
	switch bpp {
	case 8:
        //...
	case 24:
        //...
	case 32:
        //...
        }


```

## リトルエンディアン
全ての整数値は、リトルエンディアンで格納されます。

>All of the integer values are stored in little-endian format (i.e. least-significant byte first).

https://en.wikipedia.org/wiki/BMP_file_format 引用

したがって、go言語のコードでは、以下のようなリトルエンディアンに対応させた関数が定義されています。

```go:golang.org/x/image/bmp/reader.go
func readUint16(b []byte) uint16 {
	return uint16(b[0]) | uint16(b[1])<<8
}

func readUint32(b []byte) uint32 {
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}
```

# 分からなかったこと

@yoya さんが、解説してくれました。
https://qiita.com/yoya/items/3d588687a30175601885#bmpv4-gamma-%E3%81%AE%E6%B3%A8%E6%84%8F-%E8%BF%BD%E8%A8%98

## BMP v4？v5？
* ドキュメントには、「デフォルトBMP v4」と記載されている
* ヘッダサイズを確認すると、「124」でBMP v5

## gamma値が消えた理由
* BMP v3には、ガンマ値を格納する場所がない。
* BMP v4には、ガンマ値を格納する場所がある。
* ImageMagickの`identify`コマンドで表示されたガンマ値は、どのように取得した？
* ヘッダを拡張した際に、ゼロ埋めしたからガンマ値が消えた？


# 「第4回名古屋若手Webエンジニア交流会」でLTしました。
https://wakateweb-nagoya.connpass.com/event/85250/