Tuesday, August 9, 2016

Renaming Photos Based on Date Taken

I have digital photos coming from multiple sources: my DSLR camera, my phone, my wife's phone, etc. Each comes with its own naming scheme. What I really want is a way to organize the photos based on when they are taken--especially if my wife and I were taking photos on the same day, so we can see the photos in the same order as an event happened.
Here's an example of some photos from various sources. Note the variation of file names. Also note the highlighted file that has no date taken set. Notice what happens later when I try to rename that file. What you would need to do is manually set the date taken on that file to avoid the issue, but I'm trying to illustrate the point.
So, I set out to write a script that would rename all photos based on the date taken.
It turns out that it's not hard to do via PowerShell. If you have .NET 4.0 you can use the System.Drawing assembly to inspect each photo to see its date taken property.
Here's how to call my script and its results. First, you can specify all the parameters if you like.
Notice the warning about the one image with no date taken property. The resulting files are:
 
You can also call the script and take the defaults. Naturally, the directory is required. If you don't provide, the script will prompt as it's a required parameter.
The images would then be named:

Here's the code that I came up with. The comments should explain everything that is happening and where you can/should modify.


#code written by Crazy-S Farm guy
#http://powershelltidbits.blogspot.com
#use at your own risk

Param
(
    [Parameter(Mandatory=$true)]
    [string] $Directory,
    [Parameter()]
    [string[]] $Extensions,
    [Parameter()]
    [string] $FileNameStart = "IMG"
)


#these are the valid file extensions to be considered as images
$validPhotoExtensions = @(".jpg",".gif",".jpeg",".png")

#use this assembly to look for date taken on photos
#you may need to change if your DLL is located somewhere else
[reflection.assembly]::LoadFile("C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Drawing.dll") | Out-Null

#here's the format of the files after renaming
#it starts with the $FileNameStart (optional parameter), which will be IMG unless you specify otherwise
#here's an example:
#IMG-2016-04-19-200013794.jpg
#note that the last 3 digits are milliseconds and not part of this date format variable, read below for explanation
$imageDateRenameFormat = "yyyy-MM-dd-HHmmss"


#if you don't specify a set of file exensions to look for, it will default to all image type extensions
if (($Extensions -eq $null) -or ($Extensions.Count -eq 0))
{
    $Extensions = @(".jpg",".gif",".jpeg",".png")
}

#get all the files in the specified directory
$files = Get-ChildItem -Path $Directory
foreach ($file in $files)
{
    #make sure the file extension is part of what you asked to process
    if ($Extensions -contains ($file.Extension))
        {
            #figure out what mod date
            [datetime]$modDate = [datetime]$file.LastWriteTime
            Write-Verbose "file $($file.Name) mod date: $modDate"
            try  
            {
                #if not a photo type, will keep using modification date (lastwritetime)
                if ($validPhotoExtensions -contains ($file.Extension))
                {
                    Write-Verbose -Message "file is image type, trying to determing date taken"
                    $imgData = New-Object System.Drawing.Bitmap($file.FullName)
                    if ($imgData -ne $null)
                    {
                        [byte[]]$ImgBytes = $null
                        # Gets 'Date Taken' in bytes
                        try
                        {
                            $ImgBytes = $ImgData.GetPropertyItem(36867).Value
                        }
                        catch
                        {
                            #that property doesn't exist.
                            Write-Warning -Message ("file: " + $file.Name + " has no date taken property set")
                            #therefore, will continue to use last modified date
                        }

                        if ($ImgBytes -ne $null)
                        {
                            # Gets the date and time from bytes 
                            [string]$dateString = [System.Text.Encoding]::ASCII.GetString($ImgBytes)
                            Write-Verbose -Message "date string for image taken date: $dateString"
                            # Formats the date to the desired format
                            $modDate = [datetime]::ParseExact($dateString,"yyyy:MM:dd HH:mm:ss`0",$Null)
                        }
                    }
                }
                else
                {
                    Write-Verbose -Message "file is not image type. will use modified date"
                }
            }
            catch  
            {
                Write-Warning -Message ("ERROR reading file " + $file.Name + " -- " + $Error[0] ) 
            }
            finally
            {
                if ($imgData -ne $null)
                {
                    $ImgData.Dispose() 
                }
            }


            [string]$dateTaken = $modDate.ToString($imageDateRenameFormat)
            #okay... there's the dumb part... the cameras dont give me any milliseconds. and... occasionally I have pictures sometimes
            #taken at the same second. so... here's a trick. instead of having to analyze and add _1 or _2 to files,
            #let's just take the milliseconds from the creation time and stick them here. there is rarely a collision there
            $createDate = $file.CreationTime
            #here's where we are adding the 3 millisecond digits to the new file name.
            $dateTaken += ($createDate.ToString("fff"))
            #build the final new file name. start with the leading characters (IMG unless you change)
            #next the dateTaken formatted as above + 3 millisecond value from the creation date
            #lastly the same extension as the original file 
            $newFileName = ($FileNameStart + "-" + $dateTaken + $file.Extension)
            #now, make sure that the file doesn't already exist in the directory
            #you could easily make this write the files into a different directory and change the move to a copy
            #however, you should still ensure that the file doesn't exist there, in case you happen to have two images
            #that are going to collide
            if (Test-Path -Path ($Directory + "\" + $newFileName))
            {
                Write-Warning -Message ("Unable to rename " + $file.Name + " File $newFileName already exists in directory!")
            }
            else
            {
                Move-Item -Path ($Directory + "\" + $file) -Destination ($Directory + "\" + $newFileName)
            }
        }
        else
        {
            Write-Warning -Message ("File " + $file.Name + " not valid type")
        }
}