📸

PowerShellで写真整理

2023/06/24に公開

概要

Windows PowerShellで指定フォルダ配下の写真を日付別フォルダに移動する。

使い方

  1. シェルを起動
  2. フォルダ選択ダイアログで写真整理対象(移動元)の親フォルダを指定
  3. フォルダ選択ダイアログで整理後(移動先)の親フォルダを指定
  4. 確認ダイアログで「実行」を押す
    ※処理完了後、処理結果がテキストエディタで表示される

制限事項

  • 拡張子はjpgとJPGのみ対応
  • 同一ファイル名が重複した場合、ファイル名末尾に「(番号)」と追加して移動
  • ファイル名末尾が「(9)」まで存在する場合は移動させない
  • 移動元フォルダは削除しない

仕様

  • EXIF情報の撮影日でフォルダを振り分ける
  • EXIF情報が無い場合はファイルの作成日時を撮影日として扱う
  • 年、年月、年月日のフォルダ階層を生成
    ※/yyyy/yyyy_MM/yyyy_MM_dd
  • Shellスクリプトと同一フォルダに処理結果のログを残す

フローチャート

大まかな処理の流れはこんな感じ。

処理フロー概要

plantuml
@startuml

start

if (整理元親フォルダ選択) then (未選択)
    stop
else (選択済)
endif

if (整理先親フォルダ選択) then (未選択)
    stop
else (選択済)
endif

if (確認ダイアログ) then (キャンセル)
    stop
else (OK)
endif

:整理元親フォルダ配下の画像のパスをリスト化;

repeat

if (EXIF情報から撮影日時が取得できる) then (yes)
    :撮影日時が整理先;
else
    :ファイル作成日時が整理先;
endif

if (整理先フォルダが存在しない) then (yes)
    :整理先フォルダを作成;
endif

if (すでに同名のファイルが整理先に存在する) then (yes)
    :ファイル名の末尾に連番を付けて移動;
else
    :ファイルを移動;
endif

repeat while (リストに残りがある)

stop

@enduml

ソース

ソースはこんな感じ。

Add-Type -AssemblyName System.Windows.Forms

# 移動元選択
$f = New-Object System.Windows.Forms.Form
$f.topMost = $true
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
    Description = "移動元フォルダ選択"
    ShowNewFolderButton = $false
}
if ($dialog.ShowDialog($f) -ne [System.Windows.Forms.DialogResult]::OK) {
  Read-Host '移動元未選択'
  exit
}
$path = $dialog.SelectedPath

# 移動先選択
$outDialog = New-Object System.Windows.Forms.FolderBrowserDialog -Property @{
    Description = "移動先フォルダ選択"
}
if ($outDialog.ShowDialog() -ne [System.Windows.Forms.DialogResult]::OK) {
  Read-Host '移動先未選択'
  exit
}
$outPath = $outDialog.SelectedPath

# 確認
$confirmResult = [System.Windows.Forms.MessageBox]::Show(
    "元:" + $path + "`r`n" + "先:" + $outPath,
    "実行確認",
    "YesNo",
    "info",
    "button2"
)
if ($confirmResult -ne "Yes") {
    Read-Host ""キャンセル""
    exit
}

# ログ
$logFilePath = "photpMove_$((Get-Date).ToString(""yyyyMMddhhmmss"")).log"
$logFilePath = (Join-Path $PSScriptRoot $logFilePath)
echo "移動元:$path" > $logFilePath
echo "移動先:$outPath" >> $logFilePath

# 移動処理
Add-Type -AssemblyName System.Drawing
$4files = Get-ChildItem -Path $path -Include *.jpg,*.JPG -Recurse
foreach($f in $4files) {
    $fileFullName = $f.FullName
    $fileTime = $f.LastWriteTime
    try {
        # EXIFから撮影日時取得
        $img = New-Object Drawing.Bitmap($fileFullName)
        $byteAry = ($img.PropertyItems | Where-Object{$_.Id -eq 36867}).Value
        $byteAry[4] = 47
        $byteAry[7] = 47
        $fileTime = [datetime][System.Text.Encoding]::ASCII.GetString($byteAry)
    } catch {
        echo "EXIF無:$fileFullName">> $logFilePath
    } finally {
        $img.Dispose()
        $img = $null
    }

    # ディレクトリ作成
    $yearPath = (Join-Path $outPath  $fileTime.toString("yyyy"))
    $monthPath = (Join-Path $yearPath  $fileTime.toString("yyyy_MM"))
    $datePath = (Join-Path $monthPath  $fileTime.toString("yyyy_MM_dd"))
    if (!(Test-Path $datePath -PathType Container)) {
        # ディレクトリが無ければ作成
        New-Item $datePath -ItemType Directory
    }

    # ファイル移動
    $targetPath = Join-Path $datePath $f.Name;
    if ($fileFullName -eq $datePath) {
        # 移動元と移動先が同じ場合は移動無し
        echo "移動不要:$fileFullName" >> $logFilePath
        continue
    }
    if (Test-Path $targetPath -PathType Leaf) {
        # 別名で移動
        $isMoved = $false
        for ($i = 0; $i -lt 9; $i++) {
            $n = [System.IO.Path]::GetFileNameWithoutExtension($targetPath)
            $t = (Join-Path $datePath "$n-$i.jpg")
            if (!(Test-Path $t -PathType Leaf)) {
                Move-Item $f.FullName $t
                $isMoved = $true
                echo "移動:$f -> $t" >> $logFilePath
                break
            }
        }
        if (!$isMoved) {
            # 別名を付けれない場合は移動無し
            echo "移動不可:$fileFullName" >> $logFilePath
        }
    } else {
        # ファイル移動
        Move-Item $fileFullName $datePath
        echo "移動:$fileFullName -> $datePath" >> $logFilePath
    }
}

# ログファイルを表示して終了
Start-Process -FilePath $logFilePath
Read-Host "終了"

蛇足

フローチャートに無い処理がある。
整理先フォルダの判定がフローチャートと違う。
などの突っ込みどころがある。

実装していると、設計より簡単な組み方って出てこない?
ログ出力みたいに、あった方が良い処理というのもある。
すべて設計書にフィードバックするべきか?って話になる。
そもそも、実装並みに細かい設計書作るなら、そのまま実装した方が早い…
いまなら自動生成などいろいろあるっぽいし。

設計書は大まかなイメージ、実装はある程度は担当の自由というのが理想。
そもそも設計だけ作って実装丸投げっていう上下関係がよろしくないのかも。
などと申しております同志…

なぜ作ったか

ハードディスク内にある昔の写真を整理しようと思った。
手作業なんて人間のやるべきことではない。
スマートフォンで撮影してクラウド保存の時代でも(?)安物のコンデジを持ち歩いているので、今後も使い道が有るかもしれない。

参考にしたサイト

いろいろ見て回ったのですべてを記載しないが、一か所だけ紹介。

EXIF情報取得

https://qiita.com/Kosen-amai/items/52ec7e4e2f15f6a09bc3

Discussion