写真&動画のファイル名を自動で分かりやすく変更するフリーソフト公開
2018/04/07
Warning: Undefined variable $nlink in /home/crossx/itjo.jp/public_html/wp/wp-content/themes/stinger7_child_test/functions.php on line 451
写真や動画を整理する方法については前回の投稿で紹介しました↓

しかし、数万枚の写真を整理しようとしたとき、ファイル名を手作業で変更していくのはあまりにも無謀! ということで自動化ツールを作りました。
ファイル名変更ツール
こんな感じで自動的に分かりやすいファイル名を生成、「変更」ボタンを押すと一括で変更してくれます。
ファイルに含まれる情報だけでファイル名を生成しています。方法は以下の通りです。
| 日付 | ファイル内部に埋め込まれた撮影日時を取得。 ファイルの更新日時と比較して適宜調整して、ファイル名の日時を決定。 |
|---|---|
| 機種名 | ファイル内部に埋め込まれた撮影機種名を取得。 情報が無いときは機種名は無しにする。 |
| 動画の長さ | FFmpegのツールで自動取得。 |
これを使えば数万枚だろうがドラッグアンドドロップして「変更」を押すだけで適切なファイル名にしてくれます。
ファイル名生成ルール
| ファイル名 | |
|---|---|
| ルール | [画像] YYYY-MM-DD HH.mm.ss [機種名, 元のファイル名].jpg/png/gif/bmp [動画] YYYY-MM-DD HH.mm.ss 00m00s [機種名, 元のファイル名].mp4/avi/mov/mts |
| 例 | 2016-05-01 15.08.38 [iPhone 6, IMG_6824].jpg 2016-06-18 09.15.05 3m24s [CX670, 00026].mts |
Exifについて
2000年頃移行に発売されたデジタルカメラや携帯電話はほぼすべてExif(JPEGファイルに機種などの撮影データを埋め込む機能)に対応しているので、JPEGファイルであればほぼ問題なく変換できます。
PNGファイルやGIFファイルはExif情報が無いので、変更日時を頼りに日時を決めています。ファイルの変更日時は携帯電話からPC、PCからPCへとコピー/移動しても基本的に書き換わることはないので、ある程度信頼できる日時情報として利用することができます。
ファイル名決定時にExifや動画埋め込み情報ではなく変更日時を使った場合には、それを明示するために日時の後ろに「(M)」の表記が付くようになっています。
[例]2016-05-08 18.10.37(M) [iPhone 6s, IMG_6701].png
詳細な日時決定アルゴリズム
| 条件 | 日付 | (M)表記 |
|---|---|---|
| 撮影日時が無い | 更新日時 | あり |
| 撮影日時が更新日時より8~10時間遅れている (GMTになっている) |
撮影日時 (+9時間補正) |
なし |
| 撮影日時が更新日時と前後1時間以上ずれている | 更新日時 | あり |
※ 撮影日時:ExifまたはFFprobeで得られる撮影日時
その他付加機能
- iPhoneなどのHDR機能で撮影した写真は、HDR版ファイルの機種名に「(HDR On)」が、非HDR版ファイルの機種名に「(HDR Off)」が付加されます。
- パノラマ機能で撮影した写真は、機種名に「(Panorama)」が付加されます。
- 拡張子は小文字に変更されます。「.jpeg」は「.jpg」に変更されます。
ダウンロード
※ 動画の長さを取得するために、.exeと同じフォルダにffprobe.exeを入れてください。
公式に取りに行くのが面倒な方はこちら↓
ソースコード
; #SingleInstance, Force
#NoTrayIcon
#NoEnv
SetWorkingDir, %A_ScriptDir%
FFPROBE_PATH := A_ScriptDir "\ffprobe.exe"
Gui, 1:Default
Gui, +Resize
Gui, Font, S11
Gui, Add, Text, X20 Y10, 変更したいファイルをドラッグアンドドロップしてください。
Gui, Font, S9
Gui, Add, ListView, X10 Y+10 W860 H400 Grid VvLVFiles, 場所|変更前|変更後
LV_ModifyCol(1, 150)
LV_ModifyCol(2, 200)
LV_ModifyCol(3, 450)
Gui, Add, Button, X680 Y+10 W130 H35 VvButtonChangeFileName GrButtonChangeFileName, 変更
Gui, Show, X300 Y800
return
GuiSize:
AutoXYWH("wh", "vLVFiles")
AutoXYWH("xy", "vButtonChangeFileName")
return
GuiClose:
ExitApp
; 実際に名前を変更
rButtonChangeFileName:
nefFileNum := 0
Loop, % gaoAllChangeFileList.MaxIndex()
{
fileDir := gaoAllChangeFileList[A_Index].Dir
fileNameBefore := gaoAllChangeFileList[A_Index].FileNameBefore
fileNameAfter := gaoAllChangeFileList[A_Index].FileNameAfter
FileMove, %fileDir%\%fileNameBefore%, %fileDir%\%fileNameAfter%
}
MsgBox, 0x40, PhotoRename, % gaoAllChangeFileList.MaxIndex() + nefFileNum "個のファイル名を変更しました。"
return
GuiDropFiles:
Gui, Submit, NoHide
GuiControl, Disable, vButtonChangeFileName
aArg := []
Loop, Parse, A_GuiEvent, `n
{
aArg.Insert(A_LoopField)
}
; 各引数に対してループ
gaoAllChangeFileList := []
LV_Delete()
Loop, % aArg.MaxIndex()
{
filePath := aArg[A_Index]
arg := FileExist(filePath)
; フォルダの場合は中身を変更
if arg contains D
{
Loop, Files, %filePath%, D
filePath := A_LoopFileLongPath
aoFileList := ChangeFileName(filePath "\*")
AddObject(gaoAllChangeFileList, aoFileList)
}
; ファイルの場合はそれのみを変更
else
{
Loop, Files, %filePath%, F
filePath := A_LoopFileLongPath
aoFileList := ChangeFileName(filePath)
AddObject(gaoAllChangeFileList, aoFileList)
}
}
GuiControl, Enable, vButtonChangeFileName
return
AddObject(paoAllChangeFileList, paoChangeFileList)
{
if(!paoAllChangeFileList.MaxIndex())
objIndex := 1
else
objIndex := paoAllChangeFileList.MaxIndex() + 1
Loop, % paoChangeFileList.MaxIndex()
{
paoAllChangeFileList[objIndex] := paoChangeFileList[A_Index].Clone()
objIndex++
}
}
; ファイル名の変更
ChangeFileName(pFilePath)
{
global ORIGINAL_DIR, BY_MACHINE_DIR
aoChangeFileList := Object()
recurs := ""
if(RegExMatch(pFilePath, "\\\*$"))
recurs := "R"
Loop, Files, %pFilePath%, F%recurs% ; ファイル対象
{
modelName := ""
filmingDate := ""
duration := ""
SplitPath, A_LoopFileLongPath, fileName, fileDir, fileExt, fileNameNoExt
; 拡張子チェック
If fileExt not in jpg,jpeg,png,gif,avi,mov,mp4,wmv,mts,3g2,3gp,amc
continue
; 変更済み形式になっているか確認
oldName := fileNameNoExt
if(RegExMatch(fileNameNoExt, "^\d{4}-\d\d-\d\d.*(\[|,\s)([^,]+)\]$", reg))
{
oldName := reg2
}
newName := MakeNewName(A_LoopFileLongPath, A_LoopFileTimeModified, oldName)
if(fileName == newName)
continue
aoChangeFileList.Insert({Dir:fileDir, FileNameBefore:fileName, FileNameAfter:newName})
LV_Add("", fileDir, fileName, newName)
}
return aoChangeFileList
}
NumGetOrder(pAddr, pOffset, pLength, pBigEndian=true)
{
global
ret := 0
Loop, %pLength%
{
ret *= 256
if(pBigEndian)
ret += *(pAddr+pOffset+A_Index-1)
else
ret += *(pAddr+pOffset+pLength-A_Index)
}
return ret
}
; 新しいファイル名を生成
MakeNewName(pFilePath, pModifiedDate, pOldName)
{
global FFPROBE_PATH
SplitPath, pFilePath, fileName, fileDir, fileExt, fileNameNoExt
; Exif
durationSec := 0
customRendered := 0
if fileExt in jpg,jpeg
{
oExifData := ReadExif(pFilePath)
; print(oExifData)
if(oExifData != "")
{
exifModelName := Trim(oExifData.Model)
if(RegExMatch(oExifData.DateTimeOriginal, "(?P<Year>\d{4}):(?P<Month>\d\d):(?P<Day>\d\d)\s+(?P<Hour>\d\d):(?P<Min>\d\d):(?P<Sec>\d\d)", reg))
filmingDate := regYear regMonth regDay regHour regMin regSec
else if(RegExMatch(oExifData.DateTime, "(?P<Year>\d{4}):(?P<Month>\d\d):(?P<Day>\d\d)\s+(?P<Hour>\d\d):(?P<Min>\d\d):(?P<Sec>\d\d)", reg))
filmingDate := regYear regMonth regDay regHour regMin regSec
}
if(oExifData.customRendered != "")
customRendered := oExifData.customRendered
}
; ffprobe
else
{
ffprobeReturn := StdoutToVar("""" FFPROBE_PATH """ """ pFilePath """", "","","", "UTF-8")
; print(ffprobeReturn)
; print(gModelName, pFilePath, pModifiedDate, pOldName)
; モデル名
if(RegExMatch(ffprobeReturn, "model.*?:(.*)\r", reg))
exifModelName := Trim(reg1)
; 撮影日時
if(RegExMatch(ffprobeReturn, "date.*?:.*?(?P<Year>\d{4})-(?P<Month>\d\d)-(?P<Day>\d\d)(\s+|T)(?P<Hour>\d\d):(?P<Min>\d\d):(?P<Sec>\d\d)", reg))
filmingDate := regYear regMonth regDay regHour regMin regSec
else if(RegExMatch(ffprobeReturn, "creation_time.*?:.*?(?P<Year>\d{4})-(?P<Month>\d\d)-(?P<Day>\d\d)\s+(?P<Hour>\d\d):(?P<Min>\d\d):(?P<Sec>\d\d)", reg))
filmingDate := regYear regMonth regDay regHour regMin regSec
else if(RegExMatch(ffprobeReturn, "creation_time.*?:.*?(?P<Year>\d{4})-(?P<Month>\d\d)-(?P<Day>\d\d)/?\s+(?P<Hour>\d\d):(?P<Min>\d\d)", reg))
filmingDate := regYear regMonth regDay regHour regMin "00"
; 動画の長さ
if(RegExMatch(ffprobeReturn, "Duration:.*?(\d\d):(\d\d):(\d\d)\.(\d\d)", reg))
{
durationSec := reg1 * 3600 + reg2 * 60 + reg3
hour := reg1 + 0
min := reg2 + 0
sec := reg3 + 0
if(hour==0 && min==0)
duration := sec "s"
else if(hour==0)
duration := min "m" NumberPadding(sec,2) "s"
else
duration := hour "h" NumberPadding(min,2) "m" NumberPadding(sec,2) "s"
}
}
if(RegExMatch(exifModelName, "^ERROR:"))
exifModelName := ""
; 日付処理
timeStamp := pModifiedDate
modified := ""
StringLeft, filmingDateMonth, filmingDate, 6
StringLeft, modifiedDateMonth, pModifiedDate, 6
if(filmingDate != "") ; Exif/ffprobeに撮影時刻がある
{
; print(pModifiedDate, filmingDate)
timeTemp := pModifiedDate
timeTemp += -durationSec, Seconds
timeTemp -= filmingDate, Seconds
; およそ9時間遅れならGMTとの差を修正
if(8 * 3600 < timeTemp && timeTemp < 10 * 3600)
{
filmingDate += 9 * 3600, Seconds
timeTemp -= 9 * 3600
}
if(timeTemp < -3600 || 3600 < timeTemp) ; 更新日時と差が大きい
modified := "(M)" ; 更新日時
else ; 更新時刻と撮影時刻がほぼ同じ
timeStamp := filmingDate ; ほぼ同じなら撮影日時
}
else ; Exif/ffprobeに撮影時刻がない
modified := "(M)" ; 撮影時刻の記録がない
FormatTime, timeFormat, %timeStamp%, yyyy-MM-dd HH.mm.ss
rendered := customRendered == 3 ? " (HDR On)"
: customRendered == 4 ? " (HDR Off)"
: customRendered == 6 ? " (Panorama)"
: ""
newNameNoExt := timeFormat (modified=="" ? "" : modified) " "
newNameNoExt .= (duration!="" ? duration " " : "")
newNameNoExt .= "["
newNameNoExt .= (exifModelName=="" ? "" : exifModelName rendered ", ")
newNameNoExt .= pOldName "]"
StringLower, fileExt, fileExt
if(fileExt == "jpeg")
fileExt := "jpg"
return newNameNoExt "." fileExt
}
ReadExif(pFilePath)
{
oExifData := Object()
fileObject := FileOpen(pFilePath, "r")
SetFormat, Integer, H
Loop
{
; print(A_Index)
if( fileObject.RawRead(buff, 2) != 2 )
{
return "" ; 2バイト読めなかった
}
if( NumGet(buff,"UChar") == "0xFF" ) ; セグメントの先頭
{
; 0xFFE1:APP1
if(NumGet(buff,1,"UChar") == "0xE1")
break ; 下へ降りる
; 0xFFD8:中身の無いセグメント(SOI)
else if(NumGet(buff,1,"UChar") == "0xD8")
continue
; その他:
else
{
if( fileObject.RawRead(buff,2) == 2 )
; データ部をスキップ
fileObject.Position += NumGetOrder(&buff,0,2,true) - 2
}
}
else ; セグメントの先頭が不適切
return ""
}
fileObject.RawRead(buff,8)
size := NumGetOrder(&buff,0,2,true)
name := StrGet(&buff+2,4,"CP0")
if(name != "Exif")
return ""
; Exif領域の読み込み
VarSetCapacity(exifBuff, size+1)
fileObject.RawRead(exifBuff,size-8)
; バイトオーダー
orderStr := StrGet(&exifBuff,2,"CP0")
bigEndian := (orderStr == "MM")
; IFDオフセット
tiff := NumGet(buff,10,"UShort")
OffsetIFD0 := NumGetOrder(&exifBuff, 4, 4, bigEndian)
; IFDを読み取る
oExifData := ReadIFD(&exifBuff, OffsetIFD0, bigEndian)
SetFormat, Integer, D
; print(oExifData)
return oExifData
}
ReadIFD(pExifBuffAddr, pOffset, pBigEndian)
{
oValue := Object()
tagNum := NumGetOrder(pExifBuffAddr, pOffset, 2, pBigEndian)
aTagTypeToBytes := [1, 1, 2, 4, 8, 1, 4, 8]
aTagTypeToName := ["Byte", "ASCII", "Short", "Long", "Rational", "Undefined", "SLong", "SRational"]
Loop, %tagNum%
{
offset := pOffset + 2 + 12*(A_Index-1)
tagNumber := NumGetOrder(pExifBuffAddr, offset, 2, pBigEndian)
tagType := NumGetOrder(pExifBuffAddr, offset+2, 2, pBigEndian)
tagCount := NumGetOrder(pExifBuffAddr, offset+4, 4, pBigEndian)
tagValueOffset := NumGetOrder(pExifBuffAddr, offset+8, 4, pBigEndian)
tagValueBytes := aTagTypeToBytes[tagType] * tagCount
tagTypeName := aTagTypeToName[tagType]
if(tagType == 2) ; ASCII
{
tagValueBytes := tagCount
if(tagValueBytes <= 4)
value := StrGet(pExifBuffAddr+offset+8, 4, "CP0")
else
value := StrGet(pExifBuffAddr+tagValueOffset, tagValueBytes, "CP0")
}
else if(tagType == 3 && tagValueBytes <= 4)
{
if(tagValueBytes <= 2)
value := NumGetOrder(pExifBuffAddr, offset+8, 2, pBigEndian)
else
value := NumGetOrder(pExifBuffAddr, offset+8, 4, pBigEndian)
}
if(tagNumber == 0x010F)
oValue.Make := value
else if(tagNumber == 0x010E)
oValue.ImageDescription := value
else if(tagNumber == 0x0110)
oValue.Model := value
else if(tagNumber == 0x0132)
oValue.DateTime := value
; ExifIFD
else if(tagNumber == 0x8769)
{
oInsideValue := ReadIFD(pExifBuffAddr, tagValueOffset, pBigEndian)
For, key, value in oInsideValue
oValue[key] := value
}
else if(tagNumber == 0x9003)
oValue.DateTimeOriginal := value
else if(tagNumber == 0x9004)
oValue.CreateDate := value
else if(tagNumber == 0x9291)
oValue.SubSecTimeOriginal := value
else if(tagNumber == 0x9292)
oValue.SubSecTimeDigitized := value
else if(tagNumber == 0xA401)
oValue.CustomRendered := value
else
{
}
}
return oValue
}


