Hey, Scripting Guy! Event 5 *Solutions* from Expert Commentators (Beginner and Advanced; the 400-meter race)

Hey, Scripting Guy! Event 5 *Solutions* from Expert Commentators (Beginner and Advanced; the 400-meter race)

  • Comments 2
  • Likes

  

(Note: These solutions were written for Event 5.)  


Beginner Event 5: The 400-meter race

The 400-meter race is a common track event. You will therefore be asked to perform the very common task of reading from the registry and writing to the registry.  

Guest commentator: Mark Schill

Image of guest commentator Mark Schill

Mark Schill is a Systems Engineer for an international services company that specializes in systems automation. He is an advisory committee member for PowerShellCommunity.org. and is also a moderator for the Official Scripting Guys Forum. Mark also maintains the String Theory blog.


VBScript solution

The first thing I decided to do was to create a function that takes the number of downloads as an input parameter. I prefer to use functions in my scripts whenever possible because it allows me to reuse my code easily in other scripts. The next step was to actually determine the correct registry key to modify. Because I had no idea which key it was, I had to look it up on the Internet.

I then used the RegWrite method of the WScript.Shell object to write to the registry key. To use the RegWrite method, you must first create an instance of the WshShell object. This is seen here.

Set objShell = WScript.CreateObject("WScript.Shell")

The RegWrite method takes three parameters. The first is the registry key, the second is the value, and the last parameter is the type of registry key it is. It is seen here:

DownloadKey = "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPer1_0Server"
objShell.RegWrite DownloadKey, intNumberofDownloads, "REG_DWORD"

I could also have used Windows Management Instrumentation (WMI) to perform the writing, but I thought this was simpler. And because we have to work with HKEY_Current_User, we cannot edit the remote registry. I also did not have to worry about whether the key exists because the RegWrite method will create the value if it does not exist.

BeginnerEvent5Solution.vbs

SetDownloadJobsNumber 10
Function SetDownloadJobsNumber( intNumberofDownloads)
  Set objShell = WScript.CreateObject("WScript.Shell")
  objShell.RegWrite "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPer1_0Server", intNumberofDownloads, "REG_DWORD"
  Downloads = objShell.RegRead("HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPer1_0Server")
          WScript.Echo "Current Downloads: " & Downloads
End Function

To provide verification to the user, I used the RegRead method from the WshShell object to read the value to a variable in order to display it to the screen:

Image of verification displayed


Guest commentator: Tobias Weltner 

Image of guest commentator Tobias Weltner

Dr. Tobias Weltner is Germany’s first Microsoft MVP for Windows PowerShell. He has written two Windows PowerShell books for Microsoft Germany, and trains and coaches throughout Europe. He loves to share his knowledge on his International Web site and is also a Windows PowerShell.com community director. As a developer, he created a number of scripting editors and development environments to make scripting easier, such as Systemscripter (VBScript) and the new PowerShellPlus, which is a Windows PowerShell rapid development interactive console, editor, and debugger. PowerShellPlus was selected as a finalist for "Best of Show" at TechEd 2009 in Los Angeles. You can contact Tobias at tobias@powershell.com.


Windows PowerShell solution

By default, Internet Explorer allows only two simultaneous downloads. Given today's bandwidth, this limitation is a bit outdated, so let us use Windows PowerShell to lift it.

First, you must know where exactly the setting is stored inside the registry. As it turns out, the Internet Explorer download limitation is stored in two DWORD values called MaxConnectionsPer1_0Server and MaxConnectionsPerServer. These values are located here: HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings.

Next, we must to read and write registry values. Windows PowerShell makes it really easy to create keys because it supports registry "drives" like HKCU: and HKLM:, and you can use file system commands to create and delete keys. Reading and writing registry values inside a key is not that easy though. This is why I decided to first create two generic new functions called Get-RegistryValue and Set-RegistryValue. They can read and write any registry value. Let us check out Get-RegistryValue function first.

Get-RegistryValue uses Get-ItemProperty to retrieve the value. There are two things to note. First, I do not use any virtual registry drive but instead specify the registry provider ("Registry::"). This way, I can read just any registry key and do not have to know specific virtual drive names. Second, Get-ItemProperty returns not just the value but also a bunch of additional properties, wrapped as an object. Because I just want to know the value, I enclose Get-ItemProperty in parentheses and access only the value that I find in a property with the same name as the valuename. Isn't it cool how you can access object properties through variables?

The Set-RegistryValue function accepts four parameters: the registry key you want to write the value to, the value name, its value, and optionally the data type. The fourth parameter defaults to "String", but when we later set the Internet Explorer settings, we need a numeric DWORD value and can simply specify "DWORD" to get just that kind of entry. Allowable types are String, ExpandString, Binary, Dword, MultiString and QWord.

Now that I can read and write to the registry, I create the functions Get-MaxIEDownloads and Set-MaxIEDownloads. Internally, they use the previous generic functions to read and write the Internet Explorer settings with the correct location. Because the Internet Explorer limit is stored in two different locations, I use the .NET math function Min() to return the lower of both in case they are different. Note also that I added a trap statement so in case something unexpected happens, the script exits gracefully with a meaningful message. Note also the use of the exit statement. With it, you exit the function without an additional error message.

At this time, you could already set a new Internet Explorer limit by using these functions:

Set-MaxIEDownloads 30

Get-MaxIEDownloads

Finally, I added yet another function called Enter-MaxIEDownloads, which prompts for a new value. (Optionally, you can uncomment additional lines to get a nice InputBox(),  dialog which I "borrowed" from the VB.NET assemblies.) Enter-MaxIEDownloads also uses a switch statement to validate the user input in case a user enters anything invalid. The complete BeginnerEvent5Solution.ps1 script is seen here:

BeginnerEvent5Solution.ps1


function Get-RegistryValue($key, $valuename) {
          (Get-ItemProperty "Registry::$key").$valuename
}

function Set-RegistryValue($key, $valuename, $value, $type = 'String') {
          Set-ItemProperty "Registry::$key" $valuename $value -type $type
}

function Get-MaxIEDownloads {
          trap {
                   "Unable to read registry value: $($_.Exception.Message)"
                   exit 1
          }

          $key = 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
          $valuename1 = 'MaxConnectionsPer1_0Server'
          $valuename2 = ' MaxConnectionsPerServer'

          # read current max number of downloads
          $value1 = Get-RegistryValue $key $valuename1
          $value2 = Get-RegistryValue $key $valuename1
          # should be the same, to make sure find lower value of both
          [Math]::Min($value1, $value2)
}

function Set-MaxIEDownloads($value) {
          trap {
                   "Unable to write registry value: $($_.Exception.Message)"
                   exit 1
          }

          $key = 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
          $valuename1 = 'MaxConnectionsPer1_0Server'
          $valuename2 = ' MaxConnectionsPerServer'

          Set-RegistryValue $key $valuename1 $value 'DWORD'
          Set-RegistryValue $key $valuename2 $value 'DWORD'
}

function Enter-MaxIEDownloads {
          trap {
                   "Your input was invalid. Use a number greater than 0"
                   exit 1
                   }
         
          $oldvalue = Get-MaxIEDownloads

          [Int]$newvalue = Read-Host "Enter new number of IE downloads (current value: $oldvalue)"
         
          # ask for new number using InputBox()
          #[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.VisualBasic") | Out-Null
          #[Int]$newvalue = [Microsoft.VisualBasic.Interaction]::InputBox("Please enter number of maximum #simultaneous downloads (currently $oldvalue):", "Change Downloads", $oldvalue)

          # check input and write to registry:
          switch ($newvalue)  {
                   0         { "You cancelled" }
                   {$_ -gt 0}{
                                Set-MaxIEDownloads $newvalue
                               "New simultenous IE downloads: $newvalue"
                              }
                   default   { "Invalid input: $newvalue" }
          }
}

Enter-MaxIEDownloads

When you run the BeginnerEvent5Solution.ps1 script, you see this confirmation message:

Image of the confirmation message displayed

 

Advanced Event 5: The 400-meter race

The 400-meter race is a common track event. For this common event, you will be asked to identify pictures that were taken with a common camera. 

Guest commentator: Gary Siepser

Image of guest commentator Gary Siepser

Gary Siepser is a senior premier field engineer for Microsoft. He is a Microsoft Certified Master in Exchange 2007 and holds several other Microsoft certifications. He spends most of his time these days mixing it up between teaching Windows Powershell and helping customers with their Exchange Servers.


VBScript solution

To write my script solution for this challenge, I first spent time researching exactly how to programmatically read the metadata. I struck out quite a bit at first. Lately, I have been using Windows Powershell exclusively, so I worked in it until I could read JPG metadata before even starting with VBScript. I found a .NET way to get the metadata out, but that will not work easily in a VBScript. Then, I tried to find a way to actually access the .NET class I was using in VBScript. That looked tough, so I just kept searching for a COM way to read metadata. I cannot believe I missed it in early searches, but right in the Windows 2000 Scripting Guide, there it was: my COM solution called Shell.Application.

I wrote a series of small test scripts to work out the details of getting just the pieces of data needed for the challenge. This was when I discovered that the chart in the above page was not right during my testing. This would haunt me later when I took the script to my Windows XP box to test it.

After I had a basic model script working (no folder scanning, just a single file metadata check), I began to focus on expanding the script to walk a folder structure. I had done this kind of thing in the past by using a recursive function. To keep the script simple, I used the scripting.filesystemobject object to loop down through any folders beneath the root folder starting point.

As I wrote the script, I wanted to break code up into functions. When I first started writing the script, I did not imagine that nearly all the code would end in functions. But that is what happened.  

So my coding experience was much like a scavenger hunt, and it was challenging at every step. After playing around with Windows Vista, odd characters started showing up in the Date Taken field. At first I could not even tell what they were. I eventually wrote a small function to go through a string character by character and then dumped out certain ones depending on their ASCII value. This really worked like a champ.

Later, I ran into another speed bump when I tested the script on Windows XP. I noticed that I was not getting back any metadata at all. This was when I went back with my earlier test scripts and realized that the index numbers were different. At least they now matched the chart on the Web page where I found the original code snippet. In the end, I had to write the operating system version, review the script, and put some behavior into the script to use different index numbers for the different operating system versions. In the AdvancedEvent5Solution.vbs script, I displayed Camera Make for Windows XP. With additional testing, we could probably get the Make on Windows XP, but I did not have the time. At the end of the day, it is not the best, most complete, or prettiest code I have ever written, but it completes the task.  

The complete AdvancedEvent5Solution.vbs script is seen here.

AdvancedEvent5Solution.vbs

'Written by: Gary Siepser, Microsoft Premier Field Engineer

'References:
'         http://www.microsoft.com/technet/scriptcenter/guide/sas_fil_lunl.mspx?mfr=true
'         http://msdn.microsoft.com/en-us/library/bb773938(VS.85).aspx
'         http://www.microsoft.com/technet/scriptcenter/funzone/games/tips08/gtip1207.mspx


'*******************************************************
'**  User should edit the following line to be the
'**   root location where we will look at JPGs
'*******************************************************

strRoot = "C:\TestFolder"

'*******************************************************


'We need to determine the operating system version early on because there are differences in the index numbers for the
'metadata tags between the OS'...at least my Windows Vista and Windows XP boxes.  We'll set vars for the index nums
'differently as you see below

CurrentOSVer = OSVer
Select Case CurrentOSVer
Case "6"
          DATE_TAKEN = 12
          CAMERA_MAKE = 32
          CAMERA_MODEL = 30
Case "5"
          DATE_TAKEN = 25
          CAMERA_MODEL = 24
End Select


'We'll write all the output to a var rather than do a bunch of echos all over the script.  This
'makes it much easier if you want to edit the script and say so something different than just the
'echos.  Only one line need be edited

strMainCodeOutput = ""


'This calls the FolderDiver function, which begins the process of walking through the folder tree

Call FolderDiver(strRoot)


'So now thet script has returned, the line below is what makes the script echo the results. This
'would be the line to change if you wanted to say output the results to a file or whatever

wscript.echo strMainCodeOutput



'So here is our FolderDiver function.  This needs to be a function as we use it recursivly to walk down
'the tree of folders.  I used the filesystem objects because it’s easy.  Later we will use folder objects
'that come out of shell.application, but I just found it easier to use FSO here for the tree dive

Sub FolderDiver(objRoot)

          Set objFso = CreateObject("Scripting.FilesystemObject")  
          Set objRoot = objFSO.GetFolder(cstr(objRoot))


          'Here we adding to the maincodeoutput building it up.  I set up the action part of the script as a
          'function so I could make the calls look nice and graceful here

          strMainCodeOutput = strMainCodeOutput & ListPhotoDetailsforFilesinFolder(objroot.path)

         
          'Here is the recursion part, the function calls itself for any subfolder. This gives us a nice tree of function
          'calls and it works like a champ

          If objRoot.SubFolders.Count > 0 Then
                   For Each objSubfolder In objRoot.SubFolders
                             FolderDiver objSubfolder
                   Next
          End If

End Sub



'This is out main action function.  This is the part that actually solves the task at hand and looks up the metadata

Function ListPhotoDetailsforFilesinFolder(objroot)

          strOutput = ""    

          Set objShell = CreateObject("Shell.Application")
          Set objFolder = objShell.Namespace(objroot)


          'We are fed a folder when this function is called, so here we just attack all the files in the passed folder

          For Each strFileName in objFolder.Items

         
                   'Lets just look at JPEG images. Now technically there is metadata like this in lots of files, but if
                   'you have lots of other files in the same folder as the pics, the results just aren’t as pretty. So for
                   'now I will lock it down to just JPGs...it’s easy to change.

                   If strFileName.type = "JPEG Image" Then


                             'So here we call the function that actually pulls the metadata, and we pass it the index
                             'number of the metadata we need. Remember this varies apparently by operating system...which I find odd BTW

                             strdatetaken = cstr(objFolder.GetDetailsOf(strFileName, DATE_TAKEN))
                             strmodel = cstr(objFolder.GetDetailsOf(strFileName, CAMERA_MODEL))


                             'In my testing I couldn't seem to get the make on Windows XP, so due to time constraints to write
                             'this script, I just decided to leave it off.  Windows XP can see this though in Explorer at least

                             If CurrentOSVer > 5 Then
                                      strmake = cstr(objFolder.GetDetailsOf(strFileName, CAMERA_MAKE))
                             Else
                                      strmake  = "Metadata not Present in File"
                             End If


                             'So now I want to make the output look more readable if we can’t get the metadata out of
                             'the file.  So I check all three values to see if they are empty, and if so just replace the
                             'var with something a little nicer.  With Date Taken, I needed to clean out some odd chars
                             'that seem to show up on my Windows Vista machine (its question marks show up, but they don’t
                             'render properly in Windows Powershell, so I just wrote a function to cut them out of the string

                             If strDatetaken = "" Then
                                      strDatetaken = "Metadata not Present in File"
                             Else
                                      strDateTaken = CleanNonDisplayableCharacters(strDateTaken)
                             End If

                             If strmake = "" Then strmake="Metadata not Present in File"
                             If strmodel = "" Then strmodel="Metadata not Present in File"


                             'We have already done all the work. Now let’s assemble the info into a string to output
                             'as the function which will ultimately make its way into the master output for the whole
                             ' list

                             strOutput = strOutput & vbCRLF
                             strOutput = strOutput & strFileName & vbCRLF
                             strOutput = strOutput & vbTab & "Full Path: " & strFileName.path & vbCRLF
                             strOutput = strOutput & vbTab & "Date Taken: "  & strDateTaken & vbCRLF
                            

                             'Let’s not even add make for Windows XP

                             If CurrentOSVer > 5 Then strOutput = strOutput & vbTab & "Camera Make: " & strMake & vbCRLF
                             strOutput = strOutput & vbTab & "Camera Model: "  & strModel & vbCRLF
                            
         
                             'Good boys and girls clean up their vars...I don’t get them all, but these are the important ones

                             strdateTaken = ""
                             strmake = ""
                             strmodel = ""

                   End If

          Next     


          'Let’s now take out temp output var and set the output for the function.  I like doing this with functions so
          'I just use them in an expression cleanly

          ListPhotoDetailsforFilesinFolder = strOutput

End Function



'This function cleans out weird chars that come back from Date Taken (or anything for that matter).  It cleans ASCII values
'above 127 and gets rid of the ? which seemed to come out on my Windows Vista box a lot.  I beat my head on the desk a bit on how to
'write this one, until it just dawned on me: just loop through the len and take a little substring of each char, do an ASC function,
'and throw away the ones we don’t want

Function CleanNonDisplayableCharacters(strInput)

          strTemp = ""

          For i = 1 to len(strInput)
                   strChar = Mid(strInput,i,1)
                   If Asc(strChar) < 126 and not Asc(strChar) = 63 Then
                             strTemp = strTemp & strChar
                   End If

          Next

          CleanNonDisplayableCharacters = strTemp

End Function


'This is a little function to test the operating system version.  I had trouble finding a really good way to get this.  I really didn’t want
'to resort to WMI, but it seems to work well in my testing

Function OSVer
          Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
          Set colOperatingSystems = objWMIService.ExecQuery("SELECT * FROM Win32_OperatingSystem")
          For Each objOperatingSystem in colOperatingSystems

                   'I shave the result down to just the major version number by just getting the first char

                   OSVer = Left(cstr(objOperatingSystem.Version),1)
          Next
End Function



Guest commentator: James Brundage

James Brundage is a software developer engineer in testing for Microsoft. He is a frequent poster on the Windows PowerShell team blog, and he maintains the Media and Microcode blog on MSDN.


Windows PowerShell solution

The 400-meter race of the 2009 Summer Scripting Games is making a Windows PowerShell script that will take all of the photos in a directory and move them into a subdirectory named for the camera type. Because the operation to move the photos is something that should work about the same for 1 image as it does for 1,000 images, it is a problem that can be solved elegantly with the object pipeline in Windows PowerShell. It is a good thing that this is  a pipelinable problem as well, because I have 30 GB of photos and would hate to have to wait an hour just to find out that my script did not work.

The pipeline of actions is something like this: Get an item in a directory, try to turn it into an image, get the metadata from the image, and group based off of that metadata. Your pipeline written in Windows PowerShell should match the following example:

Get-ChildItem  | Get-Image | Get-ImageProperty | Group-Object Camera

However, envisioning the problem as a pipeline is only part of the solution. The hardest part is implementing it and this part makes use of a few really magical Windows PowerShell 2.0 tricks.

A few months before the Scripting Games started, I had the first step of this pipeline. I wrote a post on our Windows PowerShell team blog that introduced a Get-Image function, which will take the results of Get-ChildItem (a.k.a. dir) and try to load up each item as an image. Even though I wrote Get-Image a while ago, it demonstrates the first nifty trick. Get-Image has a string parameter, $file, which has an alias of FullName and can take its value from pipeline by a property name (snippet below). This little chunk of code will take the full name property of a file on disk and use it as the parameter for $file, thus allowing dir | Get-Image to work. This trick is great when you want to make functions that interact well with the output of Get-ChildItem on the file system only:

#requires -version 2.0

function Get-Image {

    param(   

  [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]

    [Alias('FullName')]

    [string]$file)

   

}

As luck would have it, getting the metadata from these images was pretty easy because each image object had a collection of extended properties in a collection attached to the object. Sadly, using this property bag directly would make the rest of the pipeline somewhat funny to use, because each of the items is in COM. My pipeline would become something like:

Get-ChildItem |

Get-Image |

Group-Object { $_.Properties.Item(“EquipMake”) + $_.Properties.Item(“EquipModel”) }

This is considerably more annoying to type, so I figured out a new trick to turn the properties into a real property bag. Windows PowerShell 2.0 introduces modules. You can define a module in a binary, by using a script file, or dynamically by using a script block. You can also import any module as a custom object, so variables that were exposed from that module become properties on an object.  Finally, you can pass an argument into a module. This trick allows me to take anything that was not quite a property bag (like a hashtable or a complex COM object) and turn it into a property bag.

This makes my Get-ImageProperty function pretty easily. I simply take $image from the pipeline and create a new module from it. I am also doing some special casing to help turn the date information that COM gives me into a .NET datetime. Here is the entire process block of my Get-ImageProperty function:

New-Module -ArgumentList $image {

            param($image)

            $FullName = $image.FullName

            $Image = $image                       

            $Variables = @("FullName", "Image")

            foreach ($property in $image.Properties) {

                $Variables += $property.Name

                if ($property.Value -like "????:??:??*") {

                    $chunks = $property.Value.ToString().Split(" ")

                    $dt = $chunks[0].Replace(":", "/") + ' ' + $chunks[1] -as [DateTime]

                    if ($dt) {

                        New-Variable -Name $property.Name -Value $dt                       

                        continue

                    }

                }

                New-Variable -Name $property.Name -Value $property.Value

            }

            Export-ModuleMember -Variable $variables

        } -AsCustomObject

 

You can use the New-Module approach to make property bags in situations where before you might have used Add-Member or Select-Object to make a property bag. There are a couple of distinct advantages over each approach. It works faster than b for anything more than a few properties, because you do not use as long of a pipeline. It works less deterministically than Select-Object, because I do not have to construct a special hard-coded hashtable to translate the properties.

To make a final solution, I rolled up the whole pipeline into one big function called Move-ImageByCriteria. Because no part of the solution that I have built up until now is specific to grouping by camera, this means that the approach I have built is flexible enough to work with other arbitrary image metadata. (So, if next year’s games have the same event but need to group on date taken, I’m already set.) Move-ImageByCriteria makes use of two cool tricks; one from 1.0 and one from 2.0. It takes similar parameters to Get-ChildItem (-Path, -Include, -Exclude, -Filter, -Recurse). The first thing that the function does is create a hashtable of the parameters that it is going to use to call Get-ChildItem. It then uses a Windows PowerShell 2.0 feature called Splatting to use this hashtable as the arguments for Get-ChildItem. The first chunk of the function looks like this:

        $GetChildItemParameters = @{

            Path = $Path

            Filter = $Filter

            Include = $Include

            Exclude = $Exclude

            Recurse = $Recurse

        }

       

        Get-ChildItem @GetChildItemParameters |

            Get-Image |

            Get-ImageProperty |

The second nifty trick happens after Get-ImageProperty, which pipes into Copy-Item. Copy-Item has a few parameters that take ValueFromPipelineByPropertyName, and any parameter that takes ValueFromPipelineByPropertyName can be represented with a script block that translates the parameters. The source path is easy; it is the FullName property that is in my property bag after Get-ImageProperty. Here’s a quick snippet below:

Copy-Item -Path { $_.FullName } -Destination {

            } -ErrorAction SilentlyContinue

The destination is a little more complex. In the destination field, link all of the properties you are grouping by together and then check if it is a valid destination. If the destination is not valid, simply return nothing from this script block, which causes that particular Copy-Item to fail. Because I don’t want to be troubled by seeing red text when I run my script, I use –ErrorAction SilentlyContinue to prevent the display of errors. And because I want to see the progress of my long-running sort, I display a progress bar whenever you copy items around.

Because I gave Windows PowerShell well-formed objects to work with at every step of the line, I leveraged Windows  PowerShell’s object pipeline to solve many image-related problems, instead of getting stuck solving a single problem in a hard-coded way. At Microsoft, we use this maxim to describe this kind of problem solving approach: “Underpromise, overdeliver”. I hope this solution shows how easy the right language (Windows PowerShell) and the right frame of mind (the object pipeline) make "overdelivering" a piece of cake.

The complete AdvancedEvent5Solution.ps1 script is shown here.

AdvancedEvent5Solution.ps1

#requires -version 2.0
function Get-Image {
    <#
        .Synopsis
            Returns an image object for a file
        .Description
            Uses the Windows Image Acquisition COM object to get image data
        .Example
            Get-ChildItem $env:UserProfile\Pictures -Recurse | Get-Image        
        .Parameter file
            The file to get an image from
    #>
    param(   
    [Parameter(ValueFromPipelineByPropertyName=$true,Mandatory=$true)]
    [Alias('FullName')]
    [string]$file)
   
    process {
        $realItem = Get-Item $file -ErrorAction SilentlyContinue    
        if (-not $realItem) { return }
        $image  = New-Object -ComObject Wia.ImageFile       
        try {       
            $image.LoadFile($realItem.FullName)
            $image |
                Add-Member NoteProperty FullName $realItem.FullName -PassThru |
                Add-Member ScriptMethod Resize {
                    param($width, $height, [switch]$DoNotPreserveAspectRatio)                   
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-ScaleFilter @psBoundParameters -passThru -image $image
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru |
                Add-Member ScriptMethod Crop {
                    param([Double]$left, [Double]$top, [Double]$right, [Double]$bottom)
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-CropFilter @psBoundParameters -passThru -image $image
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru |
                Add-Member ScriptMethod FlipVertical {
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-RotateFlipFilter -flipVertical -passThru
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru |
                Add-Member ScriptMethod FlipHorizontal {
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-RotateFlipFilter -flipHorizontal -passThru
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru |
                Add-Member ScriptMethod RotateClockwise {
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-RotateFlipFilter -angle 90 -passThru
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru |
                Add-Member ScriptMethod RotateCounterClockwise {
                    $image = New-Object -ComObject Wia.ImageFile
                    $image.LoadFile($this.FullName)
                    $filter = Add-RotateFlipFilter -angle 270 -passThru
                    $image = $image | Set-ImageFilter -filter $filter -passThru
                    Remove-Item $this.Fullname
                    $image.SaveFile($this.FullName)                   
                } -PassThru
               
        } catch {
            Write-Verbose $_
        }
    }   
}

function Get-ImageProperty {
    <#
    .Synopsis
        Retrieves Extended Properties of an image
    .Description
        Retrieves the Extended Properties of an image and returns them as a property bag.
        The full name and real image are inserted as additional properties.
        Because the underlying API giving the properties is COM, this function also
        converts COM's Date Time format into .NET's DateTime format.
    .Example
        Get-ChildItem $env:UserProfile\Pictures | Get-Image | Get-ImageProperty
    #>
    param(
    [Parameter(ValueFromPipeline=$true)]   
    $Image
    )
   
    process {
        New-Module -ArgumentList $image {
            param($image)
            $FullName = $image.FullName
            $Image = $image                       
            $Variables = @("FullName", "Image")
            foreach ($property in $image.Properties) {
                $Variables += $property.Name
                if ($property.Value -like "????:??:??*") {
                    $chunks = $property.Value.ToString().Split(" ")
                    $dt = $chunks[0].Replace(":", "/") + ' ' + $chunks[1] -as [DateTime]
                    if ($dt) {
                        New-Variable -Name $property.Name -Value $dt                       
                        continue
                    }
                }
                New-Variable -Name $property.Name -Value $property.Value
            }
            Export-ModuleMember -Variable $variables
        } -AsCustomObject
    }
}
function Move-ImageByCriteria
{
    <#
    .Synopsis
        Moves images into folders on disk
    .Description
        Moves images into folders on disk using one or more properties or script blocks
    .Example
        # Moves all the current user's photos into a directory by Month and Year.  Copies images, but does not remove them after the work is done.
        Move-ImageByCriteria $env:UserProfile\Pictures -ScriptBlock {$_.DateTime.Month}, {$_.DateTime.Year } -Recurse  -Copy
    .Example
        # Groups all the current users's photos into a directory by Camera Make and Model
        Move-ImageByCriteria -Path $env:UserProfile\Pictures -Property "EquipMake","EquipModel"
    #>
    [CmdletBinding(DefaultParameterSetName='Property')]
    param(
    [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
    [String[]]
    $Path,
   
    [Parameter(ParameterSetName='Property',
        Mandatory=$true,
        Position=1)]
    [String[]]
    $Property,


    [Parameter(ParameterSetName='ScriptBlock',
        Mandatory=$true,
        Position=1)]
    [ScriptBlock[]]
    $ScriptBlock,

    [String]
    $Filter,

    [String[]]
    $Include,

    [String[]]
    $Exclude,

    [Switch]
    $Recurse,
   
    [switch]
    $Copy,

    [Switch]
    $HideProgress)
   
    process {
        $GetChildItemParameters = @{
            Path = $Path
            Filter = $Filter
            Include = $Include
            Exclude = $Exclude
            Recurse = $Recurse
        }
       
        Get-ChildItem @GetChildItemParameters |
            Get-Image |
            Get-ImageProperty |
            Copy-Item -Path { $_.FullName } -Destination {
                $newPath = ""
                $item  = $_
                if ($psCmdlet.ParameterSetName -eq "Property") {
                    foreach ($p in $property) {
                        $newPath = $newPath + ' ' + $_.$p
                    }                                       
                } else {
                    if ($psCmdlet.ParameterSetName -eq "ScriptBlock") {
                        foreach ($s in $scriptBlock) {
                            $newPath = $newPath + ' ' + (& $s)
                        }                   
                    }
                }
                if (-not $newPath.Trim()) {
                    if (-not $HideProgress) {
                        $script:percent += 5
                        if ($script:percent -gt 100) { $script:percent = 0 }
                        Write-Progress $_.FullName "Skipping – no MetaData" -PercentComplete $percent
                    }
                    return
                }
                $newPath = $newPath.Trim()
                $destPath = Join-Path $path $newPath
                if (-not (Test-Path $destPath)) {
                    $null = New-Item $destPath -Force -Type Directory
                }
                $leaf = Split-Path $_.FullName -Leaf
                $fullPath = Join-Path $destPath $leaf
                if (-not $HideProgress) {
                    $script:percent += 5
                    if ($script:percent -gt 100) { $script:percent = 0 }
                    Write-Progress $_.FullName $fullPath -PercentComplete $percent
                }
                if (-not $script:CopiedFiles) {
                    $script:CopiedFiles = @{}                   
                }
                $script:CopiedFiles.($_.FullName) = $fullPath
                $fullPath
            } -ErrorAction SilentlyContinue
        if (-not $copy) {
            foreach ($key in $script:CopiedFiles.Keys) {
                Remove-Item $key -ErrorAction SilentlyContinue
            }
        }           
        Remove-Variable CopiedFiles -ErrorAction SilentlyContinue
    }
}


Thank you, Mark, Tobias, Gary, and James for today's commentaries. Once again, we have been treated to some awesome scripting techniques, powerful scripting methodologies, and some sweet scripting tricks. It has been a privilege to see the solutions offered by our expert commentators, as well as to shuffle through the submissions that are posted to the PoshCode code gallery. If you are a spectator or if you are a participant, you can review the submissions. Each one is different, and each one is cool. Also, feel free to vote on the solution you like the best. (You can do this even if you are not participating.) However, if you are not participating, we have one question for you: "Why not?" It is not too late to fire up the old script editor and hammer away.

Join us on Monday as we hear from more experts in the field on Event 6. If you need assistance over the weekend as you are working on a solution for Event 6, you can post questions to the Scripting Games Forum. For all the latest Scripting Games news, follow us on Twitter. Have a great scripting weekend, and we will see you on Monday.

 

Ed Wilson and Craig Liebendorfer, Scripting Guys

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • All the Scripting Games links in one location! Let the learning begin. (We will update this page every

  • This one involves setting some registry settings to increase the number of concurrent downloads.&#160;