jump to navigation

Fun with Powershell, Part 5 – Organizing pictures Saturday, 05/12/2012

Posted by Percy in Fun with Powershell, Technology.
trackback

After the birth of my son recently, there were (as you can imagine) a lot of pictures taken.  Luckily, my father-in-law loves to take pictures with his iPhone.  So, in a week or so he’s taken almost 100 photos – which is awesome.  Sarah and I have been so busy with just taking care of him, it’s been nice to have an extra pair of hands to take pictures.  The issue has been on how to get the pictures from his iPhone or iPad (where he has his own iTunes account) onto our machines (where we have our own iTunes account).  So, the syncing with iCloud just wasn’t going to work.  I had his iPad, but most of the pictures were from his iPhone.  So, the pictures weren’t physically on the iPad.  I could have saved them from the photo stream, but I didn’t want to add any more files to his iPad than he already had.  I tried a few ways of getting the files off (without paying for an app), and I finally settled on just e-mailing them to myself.  Since gmail has a attachment limit, I could only send 15-20 pictures at a time.  That resulted in almost 10 separate e-mails, which was fine.  Gmail also has a feature that allows you to download all the attachments from an e-mail into a single zip file.  So, now, I have about 10 zip files with 15-20 pictures a piece that I want to organize.  Further, each file has a similar set of file names (image.jpeg, image_2.jpeg, image_3.jpeg, etc.).

Now, I have my own way of organizing my pictures.  I have a folder of all my pictures with subfolders named “[Date in yyyy-mm-dd format] [Name of event]”.  Then, each picture within the directory is named the same as the directory but with a number suffix – “[Date in yyyy-mm-dd format] [Name of event] ###.[Extension]”.  You can see that here:

So, what I need is something that will do the following:

  1. Look through the list of zip files
  2. For each zip file, unzip the contents into a temporary folder
  3. For each image, find the date that the image was taken via the metadata
  4. If a directory of the format “[date] [name]” doesn’t exist, create it
  5. Copy the current image into the directory of the format “[date] [name] ###.[ext]” making sure to increment the ### correctly

I could do this all manually, but with ~10 zip files and ~100 images, that could take some time.  What do I do, I start writing a PowerShell script!  🙂

So, my first step is to throw all the zip files into a directory, and then just do a Get-ChildItem on them to iterate over each one individually.

Get-ChildItem {Path} -Filter *.zip |
% {
}

Ok, now I need to get a temp folder, and then unzip all of the contents of the current zip file into that temp folder. I found this article about how to unzip files using a powershell script. So, now, my script looks like this:

Get-ChildItem {Path} -Filter *.zip |
% {
	$tempFolder = Join-Path -Path $([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName()))
	if(!(Test-Path $tempFolder))
	{
		New-Item $tempFolder -Type Directory | Out-Null
	}
	
	$shellApp = New-Object -Com shell.application 
	$zipFile = $shellApp.namespace($_.FullName)
	$destination = $shellApp.namespace($tempFolder) 
	$destination.Copyhere($zipFile.items())
}

Ok, now I can loop through each file that I’ve unzipped. I just need some code that will pull the date taken from the image file itself. I found this article which links to this folder which contains a script called “ExifDateTime.ps1” that does what I need. So, adding that to my script, I now have this:

Get-ChildItem {Path} -Filter *.zip |
% {
	$tempFolder = Join-Path -Path $([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName()))
	if(!(Test-Path $tempFolder))
	{
		New-Item $tempFolder -Type Directory | Out-Null
	}
	
	$shellApp = New-Object -Com shell.application 
	$zipFile = $shellApp.namespace($_.FullName)
	$destination = $shellApp.namespace($tempFolder) 
	$destination.Copyhere($zipFile.items())

	Get-ChildItem $tempFolder | 
	% {
		$fileStream = New-Object System.IO.FileStream($_.FullName,
		                                            [System.IO.FileMode]::Open,
		                                            [System.IO.FileAccess]::Read,
		                                            [System.IO.FileShare]::Read,
		                                            1024,     # Buffer size
		                                            [System.IO.FileOptions]::SequentialScan
		                                           )
		$img = [System.Drawing.Imaging.Metafile]::FromStream($fileStream)
		$exifDT = $img.GetPropertyItem('36867') # Date taken
		$exifDtString = [System.Text.Encoding]::ASCII.GetString($ExifDT.Value)
		[datetime]::ParseExact($exifDtString,"yyyy:MM:dd HH:mm:ss`0",$Null)
}

Now, I throw in a little image manipulation via System.Drawing.Bitmap (rotate the image by 90 degrees, since most have been taking on a iPhone and they are vertical rather than horizontal), and then I copy the file to my custom output directory (creating the directory if it doesn’t exist). Finally, I delete the temporary folder. All in all, around 70 lines for doing some pretty hefty file organization is not a bad deal. Here’s the final output:

$root = [Root Directory]
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
Get-ChildItem $root -Filter *.zip |
% {
	$tempFolder = Join-Path -Path $([System.IO.Path]::GetTempPath()) -ChildPath ([System.IO.Path]::GetFileNameWithoutExtension([System.IO.Path]::GetRandomFileName()))
	if(!(Test-Path $tempFolder))
	{
		Write-Host "Creating $tempPath"
		New-Item $tempFolder -Type Directory | Out-Null
	}
	
	Write-Host "Unzipping $($_.FullName) to $tempFolder"
	$shellApp = New-Object -Com shell.application 
	$zipFile = $shellApp.namespace($_.FullName)
	$destination = $shellApp.namespace($tempFolder) 
	$destination.Copyhere($zipFile.items())
	
	Get-ChildItem $tempFolder | 
	% {
		Write-Host "Getting date taken for $($_.FullName)"
		$fileStream = New-Object System.IO.FileStream($_.FullName,
		                                            [System.IO.FileMode]::Open,
		                                            [System.IO.FileAccess]::Read,
		                                            [System.IO.FileShare]::Read,
		                                            1024,     # Buffer size
		                                            [System.IO.FileOptions]::SequentialScan
		                                           )
		$img = [System.Drawing.Imaging.Metafile]::FromStream($fileStream)
		try
		{
			$exifDT = $img.GetPropertyItem('36867') # Date taken
			$exifDtString = [System.Text.Encoding]::ASCII.GetString($exifDT.Value)
			$dateTaken = [datetime]::ParseExact($exifDtString,"yyyy:MM:dd HH:mm:ss`0",$Null)
		}
		catch {}
		Write-Host "Date taken - $dateTaken"
		$prefix = [string]::Format("{0:yyyy-MM-dd} [Custom Format]", $dateTaken)
		$destinationPath = "$($root)\Output\$prefix"
		
		if(!(Test-Path $destinationPath))
		{
			Write-Host "Creating $destinationPath"
			New-Item $destinationPath -Type Directory | Out-Null
		}
		
		$fileWritten = $false
		$index = 1
		while(!$fileWritten)
		{
			$destinationFile = [string]::Format("{0}\{1} {2:000}{3}", $destinationPath, $prefix, $index, $_.Extension)
			if(!(Test-Path $destinationFile))
			{
				Write-Host "Copying to $destinationFile"
				$i = New-Object System.Drawing.Bitmap($_.FullName)
				$i.RotateFlip([System.Drawing.RotateFlipType]::Rotate90FlipNone)
				$i.Save($destinationFile)
				
				$fileWritten = $true
			}
			$index++
		}
		
		$fileStream.Close()
	}
	
	"Removing $tempFolder"
	Remove-Item $tempFolder -Force -Recurse
}

Enjoy!

Advertisements

Comments»

1. Nicholas P. Basham - Thursday, 05/23/2013

Thanks for the post! This really helped me get started with what I needed to do.

Just a warning to those that use this, I had some errors when it came across the movie files (keep in mind this says ‘organizing pictures’), so you might want to start with taking those out first.

I organize my photos just about the same way, but I wanted to take away the file number and replace it with the exact time so I could merge photos taken from multiple devices (two iPhones) and have the pictures in the order the events happened (the way Androids do it – or at least the Droid that I have does it that way – “yyyy-mm-dd hh.mm.ss”). I made some changes to read files from a folder and move them to another folder – it’s posted below for more enjoyment. Basically the same thing, just a few tweaks.

I haven’t come across photos taken at exactly the same time, perhaps but hopefully the yellow warning message will take care of that (when ran from powershell window).

$root = (get-location).path
[Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”)
Get-ChildItem To_Modify_Files_Names |
% {
Write-Host “Getting date taken for $($_.FullName)”
$fileStream = New-Object System.IO.FileStream($_.FullName,
[System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read,
[System.IO.FileShare]::Read,
1024, # Buffer size
[System.IO.FileOptions]::SequentialScan
)
$img = [System.Drawing.Imaging.Metafile]::FromStream($fileStream)
try
{
$exifDT = $img.GetPropertyItem(‘36867’) # Date taken
$exifDtString = [System.Text.Encoding]::ASCII.GetString($exifDT.Value)
$dateTaken = [datetime]::ParseExact($exifDtString,”yyyy:MM:dd HH:mm:ss`0″,$Null)
}
catch {}
Write-Host “Date taken – $dateTaken”
$prefix = [string]::Format(“{0:yyyy-MM-dd} [Custom Format]”, $dateTaken)
$prefix2 = [string]::Format(“{0:yyyy-MM-dd HH.mm.ss}”, $dateTaken)
$destinationPath = “$($root)\File-Name-Fixed”

if(!(Test-Path $destinationPath))
{
Write-Host “Creating $destinationPath”
New-Item $destinationPath -Type Directory | Out-Null
}

$destinationFile = [string]::Format(“{0}\{1}{2}”, $destinationPath, $prefix2, $_.Extension)
if(!(Test-Path $destinationFile))
{
Write-Host “Copying to $destinationFile”
$i = New-Object System.Drawing.Bitmap($_.FullName)

$i.Save($destinationFile)
} else {
write-warning “$destinationFile already exists! This picture might have been taken at same time as another picture!”
}

$fileStream.Close()
}

Percy - Friday, 05/24/2013

All the .zip files I processed contained only images. However, you could use the -Filter parameter on the $tempFolder to only return images – Get-ChildItems $tempFolder -Filter .jpg. That would only return jpg files.

Thanks for reading, and I’m glad it helped.


Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: