free-automations / windows / rename-photos.md Public
rename-photos.md
DeviceWindowsCostFreeSetup3 minutes

Rename photos by date

Turns IMG_4821.jpg into 2026-09-24_14-05-33.jpg, using the date the camera recorded. Photos from different phones then sort in the order they were taken. It shows you the new names first and never overwrites a file.

1. Save the script

Save this as rename-photos.ps1 in your Scripts folder (for example C:\Users\Thandi\Scripts).

rename-photos.ps170 lines · 2.6 KB
<#
Rename photos and videos in a folder to the date they were taken, for example
2026-09-24_14-05-33.jpg, so they sort in order. JPG files use the date stored by
the camera; other files use the date they were last changed. It only shows what
it would do unless you add -Apply, and it never overwrites a file.

  powershell -ExecutionPolicy Bypass -File .\rename-photos.ps1 -Folder "C:\Users\You\Pictures\Holiday"
  powershell -ExecutionPolicy Bypass -File .\rename-photos.ps1 -Folder "C:\Users\You\Pictures\Holiday" -Apply

Free from Voypelo Solutions, voypelo.co.za. MIT licence.
#>
param(
    [Parameter(Mandatory = $true)][string]$Folder,
    [switch]$Apply
)

Add-Type -AssemblyName System.Drawing
$extensions = @('.jpg', '.jpeg', '.png', '.heic', '.mp4', '.mov')

function Get-TakenDate($file) {
    if ($file.Extension -match '^\.(jpe?g)$') {
        try {
            $img = [System.Drawing.Image]::FromFile($file.FullName)
            try {
                # 36867 is the EXIF "date taken" tag written by cameras and phones.
                if ($img.PropertyIdList -contains 36867) {
                    $raw = [System.Text.Encoding]::ASCII.GetString($img.GetPropertyItem(36867).Value).Trim([char]0)
                    return [datetime]::ParseExact($raw, 'yyyy:MM:dd HH:mm:ss', $null)
                }
            } finally {
                $img.Dispose()
            }
        } catch {
            # Unreadable or unusual file: fall back to the file date below.
        }
    }
    return $file.LastWriteTime
}

if (-not (Test-Path -LiteralPath $Folder -PathType Container)) {
    Write-Error "Expected a folder at $Folder, found nothing there"
    exit 1
}
$count = 0
Get-ChildItem -LiteralPath $Folder -File |
    Where-Object { $extensions -contains $_.Extension.ToLower() } |
    ForEach-Object {
        $stamp = (Get-TakenDate $_).ToString('yyyy-MM-dd_HH-mm-ss')
        $ext = $_.Extension.ToLower()
        if ($_.BaseName -eq $stamp -or $_.BaseName -like "${stamp}_*") { return }  # already named
        $newName = $stamp + $ext
        $n = 2
        while (Test-Path -LiteralPath (Join-Path $Folder $newName)) {
            $newName = '{0}_{1}{2}' -f $stamp, $n, $ext
            $n++
        }
        if ($Apply) {
            Rename-Item -LiteralPath $_.FullName -NewName $newName
            Write-Output ('renamed      {0}  ->  {1}' -f $_.Name, $newName)
        } else {
            Write-Output ('would rename {0}  ->  {1}' -f $_.Name, $newName)
        }
        $count++
    }

if ($Apply) {
    Write-Output ('{0} file(s) renamed.' -f $count)
} else {
    Write-Output ('{0} file(s) would be renamed. Run again with -Apply to rename them.' -f $count)
}

2. Preview, then rename

In Command Prompt, point it at a folder of photos. First it only shows the new names:

powershell -ExecutionPolicy Bypass -File "%USERPROFILE%\Scripts\rename-photos.ps1" -Folder "%USERPROFILE%\Pictures\Holiday"

If they look right, add -Apply:

powershell -ExecutionPolicy Bypass -File "%USERPROFILE%\Scripts\rename-photos.ps1" -Folder "%USERPROFILE%\Pictures\Holiday" -Apply

Running it again on the same folder changes nothing, so it is safe to repeat.

Why it helps

Photos from a trip, a wedding or a site visit from three different phones finally line up in order.