This PowerShell script organizes and renames all photos in a selected folder using EXIF data. It will also create thumbnails of the images into a thumbs folder.
Simply copy the script into a PowerShell ISE script window and execute! You will be prompted for a Source and Target folder.
Example:
Source:
- unsorted\DSC_0236.jpg
- unsorted\DSC_0258.jpg
- unsorted\IMG_0258.jpg
Target:
- sorted\2014\05_May\2014-05-01_19-08-26_258.jpg
- sorted\2014\05_May\thumbs\2014-05-01_19-08-26_258_thumb_400x400.jpg
# ==============================================================================================
# 
# Microsoft PowerShell Source File 
# 
# This script will organize photo and video files by renaming the file based on the date the
# file was created and moving them into folders based on the year and month. It will also append
# an index number to the end of the file name just to avoid name collisions. The script will
# look in the SourceRootPath (recursing through all subdirectories) for any files matching
# the extensions in FileTypesToOrganize. It will rename the files and move them to folders under
# DestinationRootPath, e.g. DestinationRootPath\2011\02_February\2011-02-09_21-41-47_680.jpg
# 
# JPG files contain EXIF data which has a DateTaken value. Other media files have a MediaCreated
# date. 
#
# The code for extracting the EXIF DateTaken is based on a script by Kim Oppalfens:
# http://blogcastrepository.com/blogs/kim_oppalfenss_systems_management_ideas/archive/2007/12/02/organize-your-digital-photos-into-folders-using-powershell-and-exif-data.aspx
# ============================================================================================== 
# Modified by Brock Hensley brock@brockhensley.com
# - Prompts for many options (source/destination folders, label, thumbnail sizes (multiple at once even))
# - Resize thumbnail rotates orientation appropriately
# - Named by incremental number instead of random number
# - Ability to apply time fix for multiple cameras with out of sync clocks
# ============================================================================================== 
[Reflection.Assembly]::LoadFile("C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Drawing.dll") 
function Select-Folder($message='Select folder', $path = 0) { 
    $object = New-Object -comObject Shell.Application  
    $folder = $object.BrowseForFolder(0, $message, 0, $path) 
    if ($folder -ne $null) { 
        return $folder.self.Path 
    } 
} 
# Defaults
$FileTypesToOrganize = @("*.jpg","*.jpeg")
# Prompts to override defaults
$Label = ""
$prompt = Read-Host "What to name files (default is date taken timestamp + index#: YYYY-MM-DD_HH-MM-SS_x#)"
if ($prompt -ne $null -and $prompt -ne "") {$Label = $prompt}
$Index = 10000
$prompt = Read-Host "Index starting number (default: $Index)"
if ($prompt -ne $null -and $prompt -ne "") { $Index = [Convert]::ToInt32($prompt) }
$SourceRootPath = "C:\UnsortedPhotos"
$prompt = Select-Folder "Select source folder (default: $SourceRootPath )"
if ($prompt -ne $null -and $prompt -ne "") {$SourceRootPath = $prompt}
$DestinationRootPath = "C:\SortedPhotos"
$prompt = Select-Folder "Select destination folder (default: $DestinationRootPath )"
if ($prompt -ne $null -and $prompt -ne "") {$DestinationRootPath = $prompt}
$CopyRenameOrig = $true
$prompt = Read-Host "Copy and Rename originals by date? ([y]/n)"
if ($prompt -ne $null -and $prompt -ne "" -and $prompt.ToLower() -eq "n") {
    $CopyRenameOrig = $false
}
$CreateThumb = $true
$prompt = Read-Host "Create thumbnails? ([y]/n)"
if ($prompt -ne $null -and $prompt -ne "" -and $prompt.ToLower() -eq "n") {
    $CreateThumb = $false
}
if ($CreateThumb -eq $true)
{
    $ThumbSizes = "128, 256, 512, 1024"
    Write-Host "Enter thumbnail sizes to create in comma seperated list. Default:" $ThumbSizes
    Write-Host "or enter N to skip creating thumbnails"
    $prompt = Read-Host "Thumbnail sizes"
    if ($prompt -ne $null -and $prompt -ne "") {
        if ($prompt.ToLower() -eq "n")
        {
            $CreateThumb = $false
        }
        else {
            $ThumbSizes = $prompt
        }
    }
}
# Functions
function GetMediaCreatedDate($File) {
 $Shell = New-Object -ComObject Shell.Application
 $Folder = $Shell.Namespace($File.DirectoryName)
 $CreatedDate = $Folder.GetDetailsOf($Folder.Parsename($File.Name), 191).Replace([char]8206, ' ').Replace([char]8207, ' ')
 if (($CreatedDate -as [DateTime]) -ne $null) {
  return [DateTime]::Parse($CreatedDate)
 } else {
  return $null
 }
}
function ConvertAsciiArrayToString($CharArray) {
 $ReturnVal = ""
 foreach ($Char in $CharArray) {
  $ReturnVal += [char]$Char
 }
 return $ReturnVal
}
function GetExifDateTaken($File) {
 $FileDetail = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $File.Fullname
    if ($FileDetail -ne $null)
    {
        $DateTimePropertyItem = $FileDetail.GetPropertyItem(36867)
        $FileDetail.Dispose()
    }
    if ($DateTimePropertyItem -eq $null)
    {
        return $null
    }
 $Year = ConvertAsciiArrayToString $DateTimePropertyItem.value[0..3]
 $Month = ConvertAsciiArrayToString $DateTimePropertyItem.value[5..6]
 $Day = ConvertAsciiArrayToString $DateTimePropertyItem.value[8..9]
 $Hour = ConvertAsciiArrayToString $DateTimePropertyItem.value[11..12]
 $Minute = ConvertAsciiArrayToString $DateTimePropertyItem.value[14..15]
 $Second = ConvertAsciiArrayToString $DateTimePropertyItem.value[17..18] 
    # CameraA and CameraB taking pictures of same event
    # ...CameraB timestamps don't match CameraA due to DST etc.
    # Set $timefix to $true to make adustments here
    # otherwise by default this should be set to $false
    $timefix = $false
    if ($timefix -eq $true)
    {
        $Hour = ([Convert]::ToInt32($Hour) + 1).ToString()
        $Minute = ([Convert]::ToInt32($Minute) - 18).ToString()
    }
 $DateString = [String]::Format("{0}-{1}-{2} {3}:{4}:{5}", $Year, $Month, $Day, $Hour, $Minute, $Second).Trim()
 if (($DateString -as [DateTime]) -ne $null) {
  return [DateTime]::Parse($DateString)
 } else {
  return $null
 }
}
function GetCreationDate($File) {
    if ($File.Extension.ToLower() -eq ".jpg" -or $File.Extension.ToLower() -eq ".jpeg")
    {
        $CreationDate = GetExifDateTaken($File)
    }
    else
    {
        $CreationDate = GetMediaCreatedDate($File)
    }
 return $CreationDate
}
function BuildDesinationPath($Path, $Date) {
 return [String]::Format("{0}\{1}\{2}_{3}", $Path, $Date.Year, $Date.ToString("MM"), $Date.ToString("MMMM"))
}
function BuildThumbDesinationPath($Path, $Date, $ThumbSizeLabel) {
 return [String]::Format("{0}\{1}\{2}_{3}\thumbs\{4}", $Path, $Date.Year, $Date.ToString("MM"), $Date.ToString("MMMM"), $ThumbSizeLabel)
}
function BuildNewFilePath($Path, $Date, $Extension, $Index, $Label) {
    if ($Label -eq "")
    {$Label = $Date.ToString("yyyy-MM-dd_HH-mm-ss")}
    return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Label, $Index, $Extension.ToLower())
    if ($Label -ne "")
    {
        return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Label, $Index, $Extension.ToLower())
    }
    return [String]::Format("{0}\{1}_x{2}{3}", $Path, $Date.ToString("yyyy-MM-dd_HH-mm-ss"), $Index, $Extension.ToLower())
}
function BuildThumbFilePath($Path, $Date, $Extension, $Size, $Index, $Label) {
    if ($Label -eq "")
    {$Label = $Date.ToString("yyyy-MM-dd_HH-mm-ss")}
    return [String]::Format("{0}\thumbs\{5}\{1}_x{2}_{3}{4}", $Path, $Label, $Index, $Size, $Extension.ToLower(), $Size)
}
function ScaleImage($image, $maxWidth, $maxHeight)
{
    try { $prop = $image.GetPropertyItem(274) } catch {}
    if ($prop -ne $null) {
        $orientation = [int]$prop.Value[0];
    }
    else
    {
        $orientation = 1
    }
    switch ($orientation)
    {
        1 {
            # No rotation required.
            break;
            }
        2 {
            $image.RotateFlip("RotateNoneFlipX");
            break;
            }
        3 {
            $image.RotateFlip("Rotate180FlipNone");
            break;
            }
        4 {
            $image.RotateFlip("Rotate180FlipX");
            break;
            }
        5 {
            $image.RotateFlip("Rotate90FlipX");
            break;
            }
        6 {
            $image.RotateFlip("Rotate90FlipNone");
            break;
            }
        7 {
            $image.RotateFlip("Rotate270FlipX");
            break;
            }
        8 {
            $image.RotateFlip("Rotate270FlipNone");
            break;
            }
        }
    [Double]$ratioX = $maxWidth / $image.Width;
    [Double]$ratioY = $maxHeight / $image.Height;
    $ratio = [System.Math]::Min($ratioX, $ratioY);
    [int]$newWidth = $image.Width * $ratio;
    [int]$newHeight = $image.Height * $ratio;
    # Now calculate the X,Y position of the upper-left corner (one of these will always be zero)
    [int]$posX = [Convert]::ToInt32(($maxWidth - ($image.Height * $ratio)) / 2);
    [int]$posY = [Convert]::ToInt32(($maxHeight - ($image.Width * $ratio)) / 2);
    Write-Host -NoNewline " "$newWidth"x"$newHeight
    $newImage = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $newWidth,$newHeight
    $g=[System.Drawing.Graphics]::FromImage($newImage);
    $g.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic # use high quality resize algorythm
    $g.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
    $g.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
    $g.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
    $g.Clear([System.Drawing.Color]::White)
    $g.DrawImage($image, 0, 0, $newWidth, $newHeight)
    return $newImage;
}
function CreateDirectory($Path){
 if (!(Test-Path $Path)) {
  New-Item $Path -Type Directory | Out-Null
 }
}
function ConfirmContinueProcessing() {
 $Response = Read-Host "Continue? (Y/N)"
 if ($Response.Substring(0,1).ToUpper() -ne "Y") { 
  break 
 }
}
# Begin
$StartTime = Get-Date
Write-Host "*Begin" $StartTime
$Files = Get-ChildItem $SourceRootPath -Recurse -Include $FileTypesToOrganize
$Count = 0
$Existing = 0
$ThumbCount = 0
$ExistingThumb = 0
$Unknown = 0
$LastCreationDate = $null
foreach ($File in $Files) {
 $CreationDate = GetCreationDate($File)
    if ($CreationDate -eq $null -or ($CreationDate -as [DateTime]) -eq $null) {
        $Unknown++
  Write-Host " Unable to determine creation date of file. " $File.FullName
        if ($LastCreationDate -eq $null)
        {
            $CreationDate = [DateTime]::Now
        }
        else
        {
            $CreationDate = $LastCreationDate
        }
        Write-Host " (borrowing creation date: " $CreationDate " )"
        #ConfirmContinueProcessing
    }
    $LastCreationDate = $CreationDate
 $DestinationPath = BuildDesinationPath $DestinationRootPath $CreationDate
 CreateDirectory $DestinationPath
    $NewFilePath = BuildNewFilePath $DestinationPath $CreationDate $File.Extension $Index $Label
    Write-Host ""
    Write-Host ""
 Write-Host "[Orig] " $File.FullName
    Write-Host "--New: " $NewFilePath
    if ($CopyRenameOrig -eq $true)
    {
     if (Test-Path $NewFilePath) {
            $Existing++
      Write-Host " -- **** (skipping) Unable to make file. File already exists: $NewFilePath ****"
      #ConfirmContinueProcessing
     } else {
            Copy-Item $File.FullName $NewFilePath
            $Count++
     }
    }
    if ($CreateThumb -eq $true)
    {
        Write-Host " -- Creating thumbnails: " -NoNewline
        $ThumbSizes.Split(",") | ForEach {
            $ThumbSize = $_.Trim();
            $ThumbSizeLabel = $ThumbSize + "x" + $ThumbSize
            $DestinationThumbPath = BuildThumbDesinationPath $DestinationRootPath $CreationDate $ThumbSizeLabel
            CreateDirectory $DestinationThumbPath
            $ThumbFilePath = BuildThumbFilePath $DestinationPath $CreationDate $File.Extension $ThumbSizeLabel $Index $Label
            if (Test-Path $ThumbFilePath) {
                $ExistingThumb++;
                Write-Host " -- **** (skipping thumb: $ThumbFilePath) ****"
            } else {
                $full = [System.Drawing.Image]::FromFile($File.FullName);
                $newImage = ScaleImage $full $ThumbSize $ThumbSize
                #Encoder parameter for image quality 
                $myEncoder = [System.Drawing.Imaging.Encoder]::Quality
                $encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1) 
                $encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter($myEncoder, 100)
                # get codec
                $myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders()|where {$_.MimeType -eq 'image/jpeg'}
    
                if ($newImage -ne $null){
                    $newImage.Save($ThumbFilePath, $myImageCodecInfo, $($encoderParams));
                }
                $full.Dispose();
                if ($newImage -ne $null){$newImage.Dispose();}
            }
            $ThumbCount++
        }
    }
    $Index++
}
$TimeSpan = New-TimeSpan $StartTime (Get-Date)
Write-Host ""
Write-Host "**** Done! Total:" $Count " Existing (skipped):" $Existing " Unknown dates:" $Unknown " Duration:" $TimeSpan.Minutes"min " $TimeSpan.Seconds "sec"