写真&動画のファイル名を自動で分かりやすく変更するフリーソフト公開
2018/04/07
写真や動画を整理する方法については前回の投稿で紹介しました↓
しかし、数万枚の写真を整理しようとしたとき、ファイル名を手作業で変更していくのはあまりにも無謀! ということで自動化ツールを作りました。
ファイル名変更ツール
こんな感じで自動的に分かりやすいファイル名を生成、「変更」ボタンを押すと一括で変更してくれます。
ファイルに含まれる情報だけでファイル名を生成しています。方法は以下の通りです。
日付 | ファイル内部に埋め込まれた撮影日時を取得。 ファイルの更新日時と比較して適宜調整して、ファイル名の日時を決定。 |
---|---|
機種名 | ファイル内部に埋め込まれた撮影機種名を取得。 情報が無いときは機種名は無しにする。 |
動画の長さ | 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 }