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

      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

ソースコード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
; #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
 - , , , , , , ,

S