VB.NETで複数画像を元にアイコンファイル(.ico)を作成する

**＜概要＞**
VB.NETで、複数画像（Bitmap配列）を元にPNG形式のアイコンファイル(.ico)を作成するサンプルプログラムです。アイコンファイルのフォーマットの説明はあれども、簡単に実装できそうなサンプルはなかったので作成してみました（これまた需要は少なそうですが…💧）。フルカラーで複数サイズ（最大256x256）の画像を埋め込みできます。

なお、（画像さえ準備できれば）画像ファイルを読み込んでBitmap配列に落とし込む部分は簡単に実現できるかと思いますので、その部分はサンプルには含んでいません。

※フルカラーでなくて良いならVisualStudioのアイコンエディタを使って作成することもできます。

※単一の画像ファイルから小さいアイコンを作成するのであれば、Iconクラスだけで簡単に実装できるようです。（その場合はこちら↓のサイトが参考になると思います）

https://dobon.net/vb/dotnet/graphics/saveimage.html


アイコンファイルを作成するに当たり、ファイルフォーマットに関しては次のサイトを参考にさせて頂きました。

https://www.setsuki.com/hsp/ext/ico.htm

こちらのサイトに記載されているフォーマットはビットマップ形式がベースとなっていましたが、ビットマップですと透過色をどのように設定するのか等々、勉強することが非常に多くとても大変そうでしたので、当記事ではＰＮＧ形式のアイコンとしました。

ＰＮＧ形式の場合は
・ファイルヘッダー（６バイト）
・アイコンヘッダー（１６バイト×アイコンの数）
・アイコンデータ　（ＰＮＧヘッダ含め、全アイコン分を一括出力）
という構成になります。

---

**＜サンプルソース＞**

<details open><summary>Bitmap配列 → Icoの変換関数</summary>

~~~
    ''' <summary>
    ''' Bitmapリストの内容をPNG形式のIconファイルとして保存
    ''' </summary>
    Private Sub BitmapListToIcon(ByRef listBitmap As List(Of Bitmap), ByVal saveFileName As String)
        Const C_FILE_HEAD_SIZE = 6
        Const C_ICON_HEAD_SIZE = 16

        Dim wFileHead(C_FILE_HEAD_SIZE - 1) As Byte 'ファイルヘッダ（６バイト）
        'icoReserved 予約（常に０）
        wFileHead(0) = &H0
        wFileHead(1) = &H0
        'icoResourceType リソースタイプ（1-アイコン、2-カーソル）
        wFileHead(2) = &H1   '上位ビットと下位ビットは逆転している、、、
        wFileHead(3) = &H0
        'icoResourceCount 作成するアイコンファイルに含めるアイコンの数（＝ビットマップ配列の数）
        Dim countBytes As Byte() = BitConverter.GetBytes(CShort(listBitmap.Count))
        wFileHead(4) = countBytes(0)
        wFileHead(5) = countBytes(1)

        'ファイルヘッダをファイルに出力（新規 または 上書き）
        My.Computer.FileSystem.WriteAllBytes(saveFileName, wFileHead, False)

        '--
        '-- アイコンヘッダの作成
        '--
        '各アイコンデータのファイル内での位置を初期セット（ファイルヘッダー(６バイト)＋アイコンヘッダー(１６バイト×アイコン数)）
        Dim startAddr As Integer = C_FILE_HEAD_SIZE + (listBitmap.Count * C_ICON_HEAD_SIZE)
        'アイコンデータを溜め込んで置く領域の宣言
        Dim listData As New List(Of System.IO.MemoryStream)
        '
        For Each wBitmap As Bitmap In listBitmap
            'ビットマップをPNG形式に変換してメモリストリームに保存
            Dim m As New System.IO.MemoryStream
            wBitmap.Save(m, System.Drawing.Imaging.ImageFormat.Png)
            
            'メモリの内容は全ヘッダー出力後にファイルに書き込むため、保存しておく
            listData.Add(m)
            
            '--
            '-- アイコンヘッダの設定
            '--
            Dim wIconHead(C_ICON_HEAD_SIZE - 1) As Byte
            '幅
            wIconHead(0) = IntToByte(wBitmap.Width)
            '高さ
            wIconHead(1) = IntToByte(wBitmap.Height)
            'カラー数（※２５６色以上は０をセット。フルカラーのはずなのでここでは固定でゼロをセット）
            wIconHead(2) = &H0
            '予約
            wIconHead(3) = &H0
            '予約（ホットスポットｘ座標）※設定内容不明。参考にしたアイコンがこの値だった、、、
            wIconHead(4) = &H1
            wIconHead(5) = &H0
            '予約（ホットスポットｙ座標）※設定内容不明。参考にしたアイコンがこの値だった、、、
            wIconHead(6) = &H20
            wIconHead(7) = &H0
            'アイコンデータのサイズ（要素８～１１の４バイトにセット）
            IntSetToByteArr(m.Length, wIconHead, 8)
            'アイコンデータのファイル内での開始位置（要素１２～１５の４バイトにセット）
            IntSetToByteArr(startAddr, wIconHead, 12)
            '
            'アイコンヘッダをファイルに出力（追加で書き込む）
            My.Computer.FileSystem.WriteAllBytes(saveFileName, wIconHead, True)

            '次画像用にファイル内での開始位置をずらす
            startAddr += m.Length

        Next

        'アイコンデータをファイルに出力
        For Each m As System.IO.MemoryStream In listData
            'アイコンデータをファイルに出力（追加で書き込む）
            My.Computer.FileSystem.WriteAllBytes(saveFileName, m.ToArray, True)
        Next

        'メモリストリームの解放
        For i As Integer = listData.Count - 1 To 0 Step -1
            listData(i).Dispose()
            listData.RemoveAt(i)
        Next

    End Sub

    ''' <summary>
    ''' Integerをバイトに変換して返却
    ''' </summary>
    Private Function IntToByte(ByVal value As UInteger) As Byte
        If value >= 256 Then
            Return &H0   '１バイトなので256以降はゼロで保存
        Else
            Return Convert.ToByte(value)
        End If
    End Function
    ''' <summary>
    ''' Integer値を４バイトのバイナリ値に変換し、引数「バイト配列」の特定位置にセットする
    ''' </summary>
    Private Sub IntSetToByteArr(ByVal value As Integer, ByVal byteArr As Byte(), ByVal startIdx As Integer)
        Dim wBytes() As Byte = BitConverter.GetBytes(value)
        wBytes.CopyTo(byteArr, startIdx)
    End Sub
~~~

</details>

**＜実行イメージ＞**
<details><summary>(開く/閉じる)</summary>

（アイコンの元となる画像）
![アイコンの元画像.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4065880/d8e8fa5e-5e27-4e58-b12a-9ff9a38cb52e.jpeg)

（作成されたアイコンをVisualStudioのアイコンエディタで開いたところ）
![作成アイコンをエディタで表示.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4065880/049ef468-3a6b-4414-bda2-95a69306ad8b.jpeg)

</details>

---

**＜補足＞**

・プログラムを見て頂ければわかるように、基本的にはフォーマット通りに、ファイルヘッダーやアイコンヘッダー、アイコンデータをファイルの特定の位置に出力しているだけです。

データに関してもBitmapクラスの標準機能を利用して画像をPNG形式に変換しているだけですので、特に難しいことはしていません。

－－

・アイコンヘッダーにはファイル内におけるデータの開始位置（ヘッダーに対応するデータの開始位置）やデータサイズを埋め込む必要があったため、BITMAPを先にPNG形式へと変換し溜め込んでから、ヘッダーを出力し、ヘッダー出力が全て終わった後に、溜め込んだものを全てファイル出力する構造となっています。

－－

・注意点としては、数値型変数はメモリ上は上位ビットと下位ビットが逆転しているところでしょうか。
例えば１０進数「２５６」は１６進数では「０１００」ですが、これを２バイトの変数に保存するとメモリ上は最初のアドレスに「００」が、次のアドレスに「０１」が保存されメモリを上から順番に見ると「０００１」であるかのように見えます。

サンプル中でリソースタイプの値が１なのに、２バイト目に「１」、３バイト目に「３」が入っていて、設定値があたかも「０１００」＝２５６であるかのように見えるかもしれませんが正しい値が設定されています。

BitConverter.GetBytesで必要なバイト配列に変換すれば特に意識する必要は無いとは思いますが、手動でバイト単位に値を設定する場合はご注意ください。

－－


以上です!