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"
Very helpful script, just what I was seeking. Thanks so much for sharing.
ReplyDeleteGlad it helped!
DeleteGreat 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?
ReplyDeleteInteresting, I had not tried mp3 files, only images... If I get some free time I will revisit this with this in mind.
DeleteThat’s an informative post I spend my great timing on reading this post.
ReplyDelete