In part one of this series I showed the finished version of photo-tagging script I’ve been using. I based my work (which is available for download) on James Brundage’s PSImageTools module for PowerShell which is part of the PowerPack included with the Windows 7 Resource kit (and downloadable independently). In this post I want to show the building blocks that were in the original library provide and the ones I added.
Producing a modified image using this module usually means working to the following pattern:

  • Read an image
  • Create a set of filters
  • Apply the filters to the image
  • Save the modified image

If you are wondering what a filter is, that will become clear in a moment. James B’s original module had these commands.

Get-Image Loads an image from a file
Add-CropFilter Creates a filter to crop the image to a given size
Add-OverlayFilter Creates a filter to an overlay such as a watermark or copyright notice
Add-RotateFlipFilter Creates a filter to rotate the image in multiples of 90 degrees or to mirror it vertically or horizontally
Add-ScaleFilter Creates a filter to resize the image
Set-ImageFilter Applies a set of filters to one or more images
Get-ImageProperty Gets Items of EXIF data from an image
ConvertTo-Bitmap Loads a file, applies a conversion filter to it, and saves it as a BMP
ConvertTo-Jpeg Loads a file, applies a conversion filter to it, and saves it as a JPG
Copy-ImageIntoOrganizedFolder Organizes pictures into folders based on EXIF data

You can see there are 4 kinds of filter with their own commands in the list and each one makes some modification to the image: cropping, scaling, rotating, or adding an overlay. Inside the two ConvertTo commands, a 5th kind of filter, conversion, is used and I added a function to create filters to do that. I made some changes to the existing functions to give better flexibility with how they can be called, and added some further functions, mostly to work with EXIF data embedded in the image file. The full list of functions I added is as follows:

Save-Image Not strictly required but it is a logical command to have at the end of a pipe line, instead of calling a method of the image object
New-ImageFilter Not strictly required either but it makes the syntax of adding filters more logical
New-Overlay Takes text and font information and creates a bitmap with the text in that font
Add-ConversionFilter Creates a conversion filter for JPG, GIF, TIF, BMP or PNG format (as used in ConvertTo-Jpeg / Bitmap without applying it to an image or saving it)
Add-ExifFilter Adds a filter to set EXIF data
Copy-Image Copies one or more images, renaming, rotating and setting title keyword tags in the process.
Get-EXIF Returns an object representing the EXIF data of the image
Get-EXIFItem Returns a single item of EXIF data using its EXIF ID (the common IDs are defined as constants
Get-PentaxMakerNoteProperty Decodes information from the Maker-Note Exif field, I have only implemented this for Pentax data
Get-PentaxExif Similar to Get-Exif but with Maker-Note fields for Pentax

The image below was resized and labelled using these commands.  The first step is to create an image to act as an overlay:  I’m going to a copyright notice in Red red text, in 32 point Arial 

PS> $Overlay = New-overlay -text "© James O'Neill 2008" -size 32 -TypeFace "Arial"  `
                           -color "red" -filename "$Pwd\overLay.jpg" 

I’m using a Click for the 800 pixel high versionpicture I took in 2008: and I could have used a more complex command to build the text from the date taken field in the EXIF data.  Next I’m going to create a chain of filters to:

  • Resize my image to be 800 pixels high (the aspect ratio is preserved by default),
  • Add my overlay
  • Set the EXIF fields for the keyword-tags, title and Copyright information
  • Save the image as a JPEG with a 70/100 quality rating

Despite the multi-line formatting here, this is a single PowerShell command:  $filter = new-Filter | add | add | add...

PS> $filter = new-Imagefilter |  
     Add-ScaleFilter      -passThru -height 800 -width 65535  |
     Add-OverlayFilter    -passThru –top    750 –left  0     –image    $Overlay |
     Add-ExifFilter       -passThru -ExifID $ExifIDKeywords  -typeName "vectorofbyte" -string "Ocean" |
     Add-ExifFilter       -passThru -ExifID $ExifIDTitle     -typeName "vectorofbyte" -string "StingRay"  |
     Add-ExifFilter       -passThru -ExifID $ExifidCopyright
-typeName "String" -value "© James O'Neill 2008" |
     Add-ConversionFilter -passThru –typeName jpg -quality 70

Given a set of filters, a script can get an image,  apply the filters to it and save it. Originally these 3 steps needed 3 commands to be piped together like this
PS> Get-Image   C:\Users\Jamesone\Pictures\IMG_3333.JPG  |
      Set-ImageFilter -filter $filter |
         Save-image -fileName {$_.FullName -replace ".jpg$","-small.jpg"}

I streamlined this first by changing James B’s  Set-ImageFilter so that if it is given something other than an image object, it hands it to Get-Image.  In other words Get-Image X | Set-Image is reduced to Set-Image X (and I made sure X could be path, including one with wild cards or one or more file objects) . After processing I added a -savepath parameter so that set-image –SavePath P is the same as Set-Image | Save-Image P . P can be a path, or script block which becomes a path, or empty to over-write the image. Get an image,  apply the filters to it and save it becomes a singe command.
PS> Set-ImageFilter –Image ".\IMG_3333.JPG" -filter $filter `
                    –SaveName {$_.FullName -replace ".jpg$","-small.jpg"}

The workflow for my photos typically begins with copying files from a memory card, replacing the start of the filename - like the “IMG_” in the example above - with text like “DIVE” (I try to keep the sequential numbers the camera stamps on the pictures as a basis for a unique ID). Next, I rotate any which were shot in portrait format so they display correctly and finally I add descriptive information to the EXIF data: keyword tags like “Ocean” and titles like “Stingray”. So it made sense to create a copy-image function which would handle all of that in one command. The only part of this which hasn’t already appeared is rotation. The Orientation EXIF field contains 8 to show the image has been rotated 90 degrees, 6 indicates 270 degrees of rotation, and 1 to show the image is correctly rotated, so it is a question of read the data, and depending on what we find add filters to rotate and reset the orientation data.

$orient = Get-ExifItem -image $image -ExifID $ExifIDOrientation   
if ($orient -eq 8) {Add-RotateFlipFilter -filter $filter -angle 270
                    Add-exifFilter       -filter $filter -ExifID $ExifIDOrientation`
                                         -value  1       -typeid $ExifUnsignedInteger }   

There is similar code to deal with rotation in the opposite direction, and rotation is just another filter like adding the EXIF data for keywords or title, all job of Copy-Image does is to build a chain of filters to add Title and Keyword tags and rotate the image, determine the full path the new copy should be saved to and invoke Set-ImageFilter. To make it more flexible,  gave Copy-Image the ability to add filters to an existing filter chain: in the part one you could see Copy-GPSImage  which finds the GPS data to apply to a picture and produces a series of filters from it: these filters are passed on to Copy-Image which does the rest. 

The last aspect of Copy-Image to look at is renaming:  -Replace has become one of my favourite PowerShell operators. It takes a regular expression and a block of text, and replaces all instances of expression found in a string with the text. Regular expressions can be complex but “IMG” is perfectly valid so if I have a lot of pictures to name as “OX-” for “Oxford”  I can call the function with a replace parameter of "IMG","OX-" . Inside Copy-Image, the parameter $replace is used with the -replace operator (using PowerShell’s ability  to treat “img”,”ox” as one parameter in two parts).   $savePath is worked out as follows:

if ($replace)   {$SavePath= join-path -Path $Destination `
                     -ChildPath ((Split-Path $image.FullName -Leaf) -Replace $replace)}
else            {$SavePath= join-path -Path $Destination `
                     -ChildPath  (Split-Path $image.FullName -Leaf)  }

As mentioned above I went to some trouble to make sure the functions can accept image objects or names of image files or file objects – because at different times, different ones will suit me. So all of the following are valid ways to copy multiple files from my memory card to the current directory ($pwd), renaming, rotating and applying the keyword tag “oxfordshire”

PS[1]> Copy-Image E:\DCIM\100PENTX\img4422*.jpg -Destination $pwd `
           -Rotate -keywords "oxfordshire" -replace "IMG","OX-"
PS[2]> dir  E:\DCIM\100PENTX\img4422*.jpg | Copy-Image -Destination $pwd `
            -Rotate -keywords "oxfordshire" -replace "IMG","OX-"
PS[3]> get-image  E:\DCIM\100PENTX\img4422*.jpg | Copy-Image -Destination $pwd `
            -Rotate -keywords "oxfordshire"   -replace "IMG","OX-"
PS[4]> $i = get-image  E:\DCIM\100PENTX\img4422*.jpg; Copy-Image $i -Destination $pwd `
            -Rotate -keywords "oxfordshire" -replace "IMG","OX-"
PS[5]> dir  E:\DCIM\100PENTX\img4422*.jpg | get-image |  Copy-Image -Destination $pwd `
            -Rotate -keywords "oxfordshire"  -replace "IMG","OX-"

Of course if I have the GPS data from taking the logger with me on a walk I can use Copy-GPSImage to geotag the files as they are copied, and in the next part I’ll look at how the GPS data is processed.