概要
表題の通り
Visual Studio 2019のVisual Basicにて、
指定した画像中に任意のExif情報を埋め込む。コードのサンプルでございます。
バイナリを直接強引に叩いて
特段、NuGetなどで他のプラグインや
別途外部dllを必要としないようにしています。
今回は、必要最低限な??
・カメラ名
・撮影日時
・写真タイトル
・気温・気圧
・GPS情報
に絞って埋め込んでいます。
このため、必須のExifタグの欠落や、省略している所、私の勘違いが多分に含まれていますので、予めご了承ください。
また、以下のコートはながったるいあり合わせのものですので、ご注意ください。
※手抜きの為に、JFIFの内容も共存しています。
※特にGPS周りは、だいぶ力業でやっています。
※コード内のIDなどは決め打ちしていますので、このコードに関して、もし引用される場合は、TageSPは何ら動作不良や損害のの責任を持ちませんが、それでも良いなら、ご自身が必要なExif情報の内容に書き換えた上で使用していただけると幸いでございます。
参考ページ
この記事のコードの作成に当たり
〇けんしのページ - Exifファイルフォーマット -
〇Exif explanation
〇JPEGのExif情報をとっかかりにバイナリデータと戯れる | 株式会社ビヨンド
〇EXIF Tags
〇Exif固有のIFD - otouNow 日々雑々
以上のサイト様の情報を参考にさせていただきました。
この場にて御礼申し上げます。
本題
標題にある機能のための
Visual Basic 2019での
Windows10開発環境でのコードは、以下の通りでございます。
Button1_Clickのイベント内部が主幹の部分でございます。
Imports System.Threading.Tasks
Public Class Form1
'デスクトップのパスを格納
ReadOnly desktop_path As String = System.Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)
'出力jpegバイナリを入れるところ
Dim ary_jpeg_file As List(Of Byte)
'各ヘッダーなど固定値を入れるところ
ReadOnly JPEG_SOI As Byte() = New Byte(19) {&HFF, &HD8, &HFF, &HE0, &H0, &H10, &H4A, &H46, &H49, &H46, &H0, &H1, &H1, &H1, &H0, &H60, &H0, &H60, &H0, &H0}
ReadOnly APP1_Marker As Byte() = New Byte(1) {&HFF, &HE1}
ReadOnly Exif_Header As Byte() =
New Byte(5) {&H45, &H78, &H69, &H66, &H0, &H0}
ReadOnly Little_Endian As Byte() = New Byte(1) {&H49, &H49}
ReadOnly TIFF_Header As Byte() = New Byte(5) {&H2A, &H0, &H8, &H0, &H0, &H0}
ReadOnly No_next_IFD As Byte() = New Byte(3) {0, 0, 0, 0}
'動的に作るコントロール
Dim txtBox(10) As TextBox
Dim lblBox(6) As Label
Dim cmbBox(1) As ComboBox
Dim Button1 As Button
Private Sub Button1_Click(sender As Object, e As EventArgs)
Dim Out_FileName As String '最終的に出力する画像フルパス
Dim temp_FileName As String '一旦出力するExifがない画像のフルパス(あとで消す)
'ファイルを開く
Using ofd As New OpenFileDialog()
ofd.Filter = "画像ファイル(*.jpg;*.jpeg;*.png;*.gif;*.bmp)|*.jpg;*.jpeg;*.png;*.gif;*.bmp"
ofd.Title = "Exifを入れ込む画像ファイルを選択してください"
'ダイアログを表示する
If ofd.ShowDialog() = DialogResult.OK Then
'選択したファイルパスの取得
Out_FileName = ofd.FileName
'出力ファイルパスの取得
Out_FileName = IO.Path.Combine(desktop_path, IO.Path.GetFileNameWithoutExtension(Out_FileName) & "_EXIF_TEST.jpg")
'仮出力した画像パスの取得
temp_FileName = IO.Path.Combine(desktop_path, IO.Path.GetFileNameWithoutExtension(Out_FileName) & "_EXIF_TEST.tmp")
'一旦EXIFが無い画像を出力する
Using task2 As Task = Task.Run(
Sub()
'今回は一旦ストリームを介する方法にて画像ファイルを取り扱っている
Using fs As New System.IO.FileStream(
ofd.FileName,
System.IO.FileMode.Open,
System.IO.FileAccess.Read)
Dim img As System.Drawing.Image = System.Drawing.Image.FromStream(fs)
fs.Close()
Using bmp = New Bitmap(img)
img.Dispose()
'元画像をロードする
'96(&H60)DPIで固定する[JPEG_SOIの所にそう書いているから]
bmp.SetResolution(96, 96)
'jpeg形式で一旦保存する
bmp.Save(temp_FileName, Imaging.ImageFormat.Jpeg)
End Using
End Using
Application.DoEvents()
Threading.Thread.Sleep(50)
Application.DoEvents()
End Sub
)
'処理が終わるまで待機させる
'⇒たまに画像保存しないうちに先に進んでしまう事があるため
task2.Wait()
End Using
Else
'キャンセル時
Exit Sub
End If
End Using
'出力バイナリの初期化
ary_jpeg_file = New List(Of Byte)
ary_jpeg_file.Clear()
'APP1_Dataの開始位置を格納
Dim APP1_Data_Start_Pos As Integer = 0
'APP1_Dataのサイズを格納
Dim APP1_Data_Size As Int16 = 0
'TIFF Headerの開始位置を格納
Dim TIFF_Data_Start_Pos As Int16 = 0
'★★★↓ここから★★メモリ上で疑似書き込みを行う★★★(オフセット位置取得などのため)
ary_jpeg_file.AddRange(JPEG_SOI)
ary_jpeg_file.AddRange(APP1_Marker)
'APP1の開始位置を記憶
APP1_Data_Start_Pos = ary_jpeg_file.Count
'APP1の領域を0バイトとして仮登録
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt16(0)))
'各ヘッダの書き込み
ary_jpeg_file.AddRange(Exif_Header)
'TIFFの開始位置を記憶
TIFF_Data_Start_Pos = ary_jpeg_file.Count
ary_jpeg_file.AddRange(Little_Endian)
ary_jpeg_file.AddRange(TIFF_Header)
'IDの個数を仮登録
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt16(0)))
'各コンテンツの情報を格納
Dim contents_list_id As New List(Of UInt16)
Dim contents_list_type As New List(Of UInt16)
Dim contents_list_binary As New List(Of Byte())
Dim contents_list_offset As New List(Of UInt32)
'カメラの名称ID
contents_list_id.Add(Convert.ToUInt16(&H110))
'カメラの名称TYPE(ASCII)
contents_list_type.Add(Convert.ToUInt16(&H2))
'カメラの名前のバイナリ
contents_list_binary.Add(System.Text.Encoding.ASCII.GetBytes(txtBox(0).Text))
'作成日時のID
contents_list_id.Add(Convert.ToUInt16(&H9003))
'作成日時の名称TYPE(ASCII)
contents_list_type.Add(Convert.ToUInt16(&H2))
'作成日時の名前のバイナリ
contents_list_binary.Add(System.Text.Encoding.ASCII.GetBytes(txtBox(1).Text.Replace("/", ":")))
'写真のタイトルID
contents_list_id.Add(Convert.ToUInt16(&H10E))
'写真のタイトルTYPE(ASCII)
contents_list_type.Add(Convert.ToUInt16(&H2))
'写真のタイトルのバイナリ
contents_list_binary.Add(System.Text.Encoding.ASCII.GetBytes(txtBox(2).Text))
'気温の値のバイナリ取得
Dim temp_dec As Decimal = 10
Decimal.TryParse(txtBox(3).Text, temp_dec)
Dim bs3 As New List(Of Byte)
bs3.AddRange(BitConverter.GetBytes(Convert.ToInt32(temp_dec * 10)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToInt32(10)))
'気温ID
contents_list_id.Add(Convert.ToUInt16(&H9400))
'気温TYPE(ASCII)
contents_list_type.Add(Convert.ToUInt16(&HA))
'気温のバイナリ
contents_list_binary.Add(bs3.ToArray())
bs3.Clear()
'気圧の値のバイナリ取得
temp_dec = 1013.25
Decimal.TryParse(txtBox(4).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec * 100)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(100)))
'気圧ID
contents_list_id.Add(Convert.ToUInt16(&H9402))
'気圧TYPE(ASCII)
contents_list_type.Add(Convert.ToUInt16(&H5))
'気圧のバイナリ
contents_list_binary.Add(bs3.ToArray())
bs3.Clear()
'▽▽▽▽GPS関係▽▽▽▽▽▽▽▽▽▽▽
'https://otounow.jimdofree.com/exif%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88/exif%E5%9B%BA%E6%9C%89%E3%81%AEifd/
'GPSへのポインタID
contents_list_id.Add(Convert.ToUInt16(&H8825))
'GPSへのポインタTYPE
contents_list_type.Add(Convert.ToUInt16(&H4))
'GPSのバイナリ作成
Dim gps_tags_count As Int16 = 0
Dim gps_tags_offsetN As Int32 = 0
Dim gps_offsetN_index As Int32 = 0
Dim gps_tags_offsetE As Int32 = 0
Dim gps_offsetE_index As Int32 = 0
bs3.Clear()
'GPS IFD
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H4)))
'北緯or南緯
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H1)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H2))) 'ASCII
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H2))) 'データ長さ
bs3.AddRange(System.Text.Encoding.ASCII.GetBytes(cmbBox(0).SelectedItem)) 'NorS
bs3.Add(0)
bs3.Add(0)
bs3.Add(0)
gps_tags_count += 1
'緯度
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H2)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H5)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H3))) 'データ長さ
gps_offsetN_index = bs3.Count '格納開始インデックスを記録しておく
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H0))) 'データへのオフセット
gps_tags_count += 1
'東経or西経
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H3)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H2))) 'ASCII
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H2))) 'データ長さ
bs3.AddRange(System.Text.Encoding.ASCII.GetBytes(cmbBox(1).SelectedItem)) 'EorW
bs3.Add(0)
bs3.Add(0)
bs3.Add(0)
gps_tags_count += 1
'経度
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H4)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt16(&H5)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H3))) 'データ長さ
gps_offsetE_index = bs3.Count '格納開始インデックスを記録しておく
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H0))) 'データへのオフセット
gps_tags_count += 1
'緯度
gps_tags_offsetN = bs3.Count 'ここまでのバイト数を記録しておく
temp_dec = 34
Decimal.TryParse(txtBox(5).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H1)))
temp_dec = 36
Decimal.TryParse(txtBox(6).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H1)))
temp_dec = 21.5496
Decimal.TryParse(txtBox(7).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec * 10000)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(10000)))
'経度
gps_tags_offsetE = bs3.Count 'ここまでのバイト数を記録しておく
temp_dec = 135
Decimal.TryParse(txtBox(8).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H1)))
temp_dec = 42
Decimal.TryParse(txtBox(9).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(&H1)))
temp_dec = 2.4186
Decimal.TryParse(txtBox(10).Text, temp_dec)
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(temp_dec * 10000)))
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(10000)))
'GPSのバイナリ
contents_list_binary.Add(bs3.ToArray())
bs3.Clear()
'△△△△GPS関係△△△△△△△△△△△△△△△△
'各IDとバイナリなどの仮入力
For i As Integer = 0 To contents_list_id.Count - 1
'ID
ary_jpeg_file.AddRange(BitConverter.GetBytes(contents_list_id(i)))
'TYPE
ary_jpeg_file.AddRange(BitConverter.GetBytes(contents_list_type(i)))
'バイナリ長
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt32(contents_list_binary(i).Length)))
'TIFF_Data_Start_Posからのオフセット位置
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt32(0)))
Next
'以下ASCII領域
ary_jpeg_file.AddRange(No_next_IFD)
'各オフセット位置の取得
For i As Integer = 0 To contents_list_id.Count - 1
Dim offset As UInt32 = ary_jpeg_file.Count - TIFF_Data_Start_Pos
If contents_list_id(i) = &H8825 Then
'GPSの場合
'さらに緯度・経度のデータへのオフセットを格納
gps_tags_offsetN += offset
gps_tags_offsetE += offset
'オフセット位置の訂正
bs3.Clear()
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(gps_tags_offsetN)))
contents_list_binary(i)(gps_offsetN_index) = bs3(0)
contents_list_binary(i)(gps_offsetN_index + 1) = bs3(1)
contents_list_binary(i)(gps_offsetN_index + 2) = bs3(2)
contents_list_binary(i)(gps_offsetN_index + 3) = bs3(3)
bs3.Clear()
bs3.AddRange(BitConverter.GetBytes(Convert.ToUInt32(gps_tags_offsetE)))
contents_list_binary(i)(gps_offsetE_index) = bs3(0)
contents_list_binary(i)(gps_offsetE_index + 1) = bs3(1)
contents_list_binary(i)(gps_offsetE_index + 2) = bs3(2)
contents_list_binary(i)(gps_offsetE_index + 3) = bs3(3)
bs3.Clear()
End If
contents_list_offset.Add(offset)
ary_jpeg_file.AddRange(contents_list_binary(i))
ary_jpeg_file.Add(0) '1バイト開ける
Next
'APP1のサイズを取得
APP1_Data_Size = ary_jpeg_file.Count - APP1_Data_Start_Pos
'初期化
ary_jpeg_file.Clear()
'★★★↓ここから★★実際にファイルを作成して書き込んで行く
Using fs = New System.IO.FileStream(Out_FileName,
System.IO.FileMode.Create,
System.IO.FileAccess.Write)
ary_jpeg_file.AddRange(JPEG_SOI)
ary_jpeg_file.AddRange(APP1_Marker)
'APP1の領域サイズ
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt16(APP1_Data_Size)).Reverse)
ary_jpeg_file.AddRange(Exif_Header)
ary_jpeg_file.AddRange(Little_Endian)
ary_jpeg_file.AddRange(TIFF_Header)
'IDの個数を登録
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt16(contents_list_id.Count)))
'各IDとバイナリなどの仮入力
For i As Integer = 0 To contents_list_id.Count - 1
'ID
ary_jpeg_file.AddRange(BitConverter.GetBytes(contents_list_id(i)))
'TYPE
ary_jpeg_file.AddRange(BitConverter.GetBytes(contents_list_type(i)))
'バイナリ長
Select Case contents_list_type(i)
Case 2 'ASCIIの場合は文字列長
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt32(contents_list_binary(i).Length)))
Case Else '数値の場合は1で固定
ary_jpeg_file.AddRange(BitConverter.GetBytes(Convert.ToUInt32(1)))
End Select
'TIFF_Data_Start_Posからのオフセット位置
ary_jpeg_file.AddRange(BitConverter.GetBytes(contents_list_offset(i)))
Next
'以下ASCII領域
ary_jpeg_file.AddRange(No_next_IFD)
'各バイナリの入力
For i As Integer = 0 To contents_list_id.Count - 1
ary_jpeg_file.AddRange(contents_list_binary(i))
ary_jpeg_file.Add(0) '1バイト開ける
Next
'一旦、バイト型配列の内容をすべて上書き
fs.Write(ary_jpeg_file.ToArray(), 0, ary_jpeg_file.Count)
fs.Flush()
ary_jpeg_file.Clear()
Using fs2 As New System.IO.FileStream(
temp_FileName,
System.IO.FileMode.Open,
System.IO.FileAccess.Read)
Dim pos As Integer = 2
Dim bs(1) As Byte
Dim BUFSIZE As Integer = 1023
Do
fs2.Seek(pos, IO.SeekOrigin.Begin)
fs2.Read(bs, 0, bs.Count)
If bs(0) = &HFF AndAlso bs(1) = &HDB AndAlso pos > 2 Then
'本体データの書き込み開始位置の捜索
Exit Do
End If
pos += 1
Loop
ReDim bs(BUFSIZE)
'画像の本体ファイルの残りをすべて読み込む→書き込む
While True
'ファイルの一部を読み込む
fs2.Seek(pos, IO.SeekOrigin.Begin)
Dim readSize As Integer = fs2.Read(bs, 0, BUFSIZE)
fs.Write(bs, 0, readSize)
fs.Flush()
'ファイルをすべて読み込んだときは終了する
If readSize < BUFSIZE Then
Exit While
End If
pos += BUFSIZE
End While
End Using
End Using
'初期化
contents_list_id.Clear()
contents_list_type.Clear()
contents_list_binary.Clear()
contents_list_offset.Clear()
'一時ファイルを消す
Try
IO.File.Delete(temp_FileName)
Catch ex As Exception
End Try
'終了のお知らせ
MessageBox.Show("END")
'出来た画像を開きます
Process.Start(Out_FileName)
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.SuspendLayout()
'動的にコントロール(ボタン)を配置する
For i As Integer = 0 To txtBox.Count - 1
txtBox(i) = New TextBox
Me.Controls.Add(txtBox(i))
txtBox(i).Left = 110
txtBox(i).Top = i * 30 + 10
txtBox(i).Width = 120
Next
For i As Integer = 0 To lblBox.Count - 1
lblBox(i) = New Label
Me.Controls.Add(lblBox(i))
lblBox(i).Left = 30
lblBox(i).Top = i * 30 + 10
lblBox(i).AutoSize = True
Next
For i As Integer = 0 To cmbBox.Count - 1
cmbBox(i) = New ComboBox
Me.Controls.Add(cmbBox(i))
cmbBox(i).Width = 120
Next
'クリックした際にButton1_Click()イベントが起きるボタンを生成
Button1 = New Button
Me.Controls.Add(Button1)
AddHandler Button1.Click, AddressOf Button1_Click '動的にイベントを関連付ける
'各コントロールに初期値の入力
txtBox(0).Text = "camera_name"
lblBox(0).Text = "カメラ名"
txtBox(1).Text = DateTime.Now.AddDays(-3).ToString()
lblBox(1).Text = "撮影日時"
txtBox(2).Text = "syashinn_title"
lblBox(2).Text = "写真タイトル"
txtBox(3).Text = "21.2"
lblBox(3).Text = "気温"
txtBox(4).Text = "1013.25"
lblBox(4).Text = "気圧"
lblBox(5).Text = "緯度"
txtBox(5).Text = "1013.25"
cmbBox(0).Items.Clear()
cmbBox(0).Items.Add("N")
cmbBox(0).Items.Add("S")
cmbBox(0).SelectedIndex = 0
cmbBox(0).Location = New Point(lblBox(5).Left + 80, lblBox(5).Top)
For i As Integer = 5 To 7
txtBox(i).Location = New Point(cmbBox(0).Left + (130 * (i - 4)), cmbBox(0).Top)
Next
txtBox(5).Text = "34"
txtBox(6).Text = "36"
txtBox(7).Text = "21.5496"
lblBox(6).Text = "経度"
cmbBox(1).Items.Clear()
cmbBox(1).Items.Add("E")
cmbBox(1).Items.Add("W")
cmbBox(1).SelectedIndex = 0
cmbBox(1).Location = New Point(lblBox(6).Left + 80, lblBox(6).Top)
For i As Integer = 8 To 10
txtBox(i).Location = New Point(cmbBox(1).Left + (130 * (i - 7)), cmbBox(1).Top)
Next
txtBox(8).Text = "135"
txtBox(9).Text = "42"
txtBox(10).Text = "2.4186"
Button1.Location = New Point(cmbBox(1).Left, cmbBox(1).Top + 30)
Button1.Width = 360
Button1.Text = "画像を選択して、上記のExif情報を付与する"
Me.ResumeLayout()
End Sub
End Class
なお、埋め込んだExif情報がある程度正常なのかを確認するために
〇online metadata and exif viewer
のサイト様など。
オンラインでExifを確認するサイトにて、確認を行っております。
↓その写真のExifをonline metadata and exif viewer様にて確認した画像
サンプル
上記のコードを
新規作成した新しいVisualBasic2019のForm1のコードに丸っと書き写して
実行すると、以下のような画面が表示されますので。
画面に必要事項を入力してから、「画像を選択して~」のボタンをクリックください。
コードは最低限の内容となっていまので、異常な値を入力した場合は、
エラーが発生したり、そのまま処理されても不都合が生じますので、気を付けてください。
私としては、写真を加工して出力する際に
元々扱った写真に入っていたExifの情報を、加工後に出力した画像にも、引き継げられるように出来たらいいなと考えて作成いたしました。
以上
ありがとうございました