写真&動画のファイル名を自動で分かりやすく変更するフリーソフト公開

      2018/04/07

top

写真や動画を整理する方法については前回の投稿で紹介しました↓

写真&動画のファイル名を自動で分かりやすく変更するフリーソフト公開
写真や動画を整理する方法については前回の投稿で紹介しました↓ しかし、数万枚の写真を整理しようとしたとき、ファイル名を手作業で変更していくのはあまりにも無謀!... 続きを読む

しかし、数万枚の写真を整理しようとしたとき、ファイル名を手作業で変更していくのはあまりにも無謀! ということで自動化ツールを作りました。

SPONSORED LINK

ファイル名変更ツール

ChangeName

こんな感じで自動的に分かりやすいファイル名を生成、「変更」ボタンを押すと一括で変更してくれます。

ファイルに含まれる情報だけでファイル名を生成しています。方法は以下の通りです。

日付 ファイル内部に埋め込まれた撮影日時を取得。
ファイルの更新日時と比較して適宜調整して、ファイル名の日時を決定。
機種名 ファイル内部に埋め込まれた撮影機種名を取得。
情報が無いときは機種名は無しにする。
動画の長さ 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」に変更されます。
SPONSORED LINK

ダウンロード

名前変更 ver.1.0 (2016-06-26)

※ 動画の長さを取得するために、.exeと同じフォルダにffprobe.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
}

itjo レスポンシブ 本文下

 - PC
 - , , , , , , ,