Featured Post

Organize and rename photos by EXIF data with PowerShell

This PowerShell script organizes and renames all photos in a selected folder using EXIF data. It will also create thumbnails of the images i...

Thursday, May 22, 2014

Organize and rename photos by EXIF data with PowerShell

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"

5 comments:

  1. Very helpful script, just what I was seeking. Thanks so much for sharing.

    ReplyDelete
  2. Great Script. But for my .mts and .mp4 Files it just did not find out the CreateDate. The Function GetMediaCreatedDate($File) doesn't work for those Filetypes. With ExifTool I could see, that there is a CreateDate Tag in the File. Any Suggestions how the Function could be adapted for those types of Files?

    ReplyDelete
    Replies
    1. Interesting, I had not tried mp3 files, only images... If I get some free time I will revisit this with this in mind.

      Delete
  3. That’s an informative post I spend my great timing on reading this post.

    ReplyDelete