A while ago I started experimenting with writing GUI applications in PowerShell. This script that updates the thumbnailPhoto attribute of a user was my first attempt, and since it works quite well I thought that I might share it with you.
The thumbnailPhoto attribute is used more and more, and it is now supported by Outlook, SharePoint and Lync. It is also supported by Office 365, and when using DirSync the data is synced to Windows Azure Active Directory.
This script uses Windows Presentation Foundation (WPF) which is part of .Net 3.0. The window form is easily built with Microsoft Visual Studio Express 2013 for Windows Desktop and can be saved as a XAML document, which we can import in our PowerShell script.
Data are saved and retrieved from Active Directory using ADSI, which means no additional PowerShell modules must be installed. The only requirements are PowerShell 2.0 or newer with .Net Framework 3.0. If running PowerShell version 2.0 you also need the -STA parameter to powershell.exe.
The application itself is very simple, just type the username in the search box, and browse for a photo. When clicking Apply the image data is saved to Active Directory. By default all users has permission to change their own thumbnailPhoto. To change other users, permissions needs to be delegated.
There is a limit in picture size, maximum size is 100 kB, but since Office 365 cannot handle that large image I have set the limit in the script to 10 kB. Also recommended resolution is 96×96 pixels.

Here’s the full script, please note that you need a valid path to the .xaml document.
Set-ThumbnailPhotoGUI-mainform.xaml
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Thumbnail photo changer app" Height="350" Width="525" ResizeMode="NoResize">
<Grid x:Name="list1" Margin="0,0,0,2">
<Label Content="Username:" HorizontalAlignment="Left" Width="105" Margin="20,23,0,250" />
<TextBox x:Name="txtSearch" Margin="138,27,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" Width="122"/>
<Button x:Name="btnSearch" Content="Search" HorizontalAlignment="Left" VerticalAlignment="Top" Width="50" Margin="271,27,0,0"/>
<Label Content="Surname:" HorizontalAlignment="Left" Height="30" Margin="20,92,0,0" VerticalAlignment="Top" Width="92"/>
<Label x:Name="lblSn" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,92,0,0"/>
<Label Content="UserPrincipalName:" HorizontalAlignment="Left" Height="30" Margin="20,123,0,0" VerticalAlignment="Top" Width="115"/>
<Label x:Name="lblUPN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="346" Margin="134,123,0,0"/>
<Label Content="Givenname:" HorizontalAlignment="Left" Height="30" Margin="20,61,0,0" VerticalAlignment="Top" Width="92"/>
<Label x:Name="lblGivenName" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="315" Margin="134,61,0,0"/>
<Image x:Name="imgThumb" HorizontalAlignment="Left" Height="96" Margin="29,203,0,0" VerticalAlignment="Top" Width="96"/>
<Button Name="btnImg" Content="Change photo..." HorizontalAlignment="Left" Height="28" Margin="149,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
<Label Content="DistinguishedName:" HorizontalAlignment="Left" Height="30" Margin="20,156,0,0" VerticalAlignment="Top" Width="126"/>
<Label x:Name="lblDN" Content="" Height="30" VerticalAlignment="Top" HorizontalAlignment="Left" Width="373" Margin="134,156,0,0"/>
<Button x:Name="btnApply" Content="Apply" HorizontalAlignment="Left" Height="28" Margin="278,267,0,0" VerticalAlignment="Top" Width="111" IsEnabled="False"/>
</Grid>
</Window>
Set-ThumbnailPhotoGUI.ps1
if ([threading.thread]::CurrentThread.ApartmentState.ToString() -eq 'MTA') {
Write-Error "This script requires PowerShell to run in Single-Threaded Apartment. Please run powershell.exe with parameter -STA"
Exit
}
Add-Type -AssemblyName PresentationFramework
[xml]$XAML = Get-Content "Set-ThumbnailPhotoGUI-mainform.xaml"
$XAML.Window.RemoveAttribute("x:Class")
$global:userinfo = @{
dn = $null
photo = [byte[]]$null
}
function Invoke-FileBrowser {
param([string]$Title,[string]$Directory,[string]$Filter="jpeg (*.jpg)|*.jpg")
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog
$FileBrowser.InitialDirectory = $Directory
$FileBrowser.Filter = $Filter
$FileBrowser.Title = $Title
$Show = $FileBrowser.ShowDialog()
If ($Show -eq "OK") { Return $FileBrowser.FileName }
}
function Get-LDAPUsers($sam) {
$activeDomain = New-Object DirectoryServices.DirectoryEntry
$domain = $activeDomain.distinguishedName
$searcher = [System.DirectoryServices.DirectorySearcher]"[ADSI]LDAP://$domain"
$searcher.filter = "(&(samaccountname=$sam)(objectClass=user)(objectClass=person))"
$result = $searcher.findall()
if ($result.count -gt 1) {
Write-Warning "More than one user found. Please refine your search."
} elseif ($result.count -eq 1) {
$result = $searcher.findone().getDirectoryEntry()
[pscustomobject]@{
UserPrincipalName = $result.userprincipalname[0]
DistinguishedName = $result.distinguishedname[0]
samAccountName = $result.samaccountname[0]
Givenname = $result.givenname[0]
Surname = $result.sn[0]
ThumbnailPhoto = $result.thumbnailphoto
}
}
}
function Update-WPFForm($sam) {
$selecteduser = Get-LDAPUsers $sam
if ($selecteduser) {
$global:userinfo.dn = $selecteduser.DistinguishedName
$global:userinfo.photo = [byte[]]$($selecteduser.thumbnailphoto)
$lblGivenName.Content = $selecteduser.GivenName
$lblSN.Content = $selecteduser.SurName
$lblUPN.Content = $selecteduser.UserPrincipalName
$lblDN.Content = $global:userinfo.dn
$txtSearch.Text = $selecteduser.SamAccountName
$imgThumb.Source = $global:userinfo.photo
$btnImg.IsEnabled = $true
}
}
#region GUI
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$window=[Windows.Markup.XamlReader]::Load($reader)
$btnSearch = $window.FindName("btnSearch")
$btnImg = $window.FindName("btnImg")
$btnApply = $window.FindName("btnApply")
$txtSearch = $window.FindName("txtSearch")
$lblGivenName = $window.FindName("lblGivenName")
$lblSn = $window.FindName("lblSn")
$lblUPN = $window.FindName("lblUPN")
$lblDN = $window.FindName("lblDN")
$imgThumb = $window.FindName("imgThumb")
#endregion GUI
#region Events
#region btnSearchClick
$btnSearch_click = $btnSearch.add_Click
$btnSearch_click.Invoke({
Update-WPFForm($txtSearch.Text)
})
#endregion btnSearchClick
#region btnImgClick
$btnImg_click = $btnImg.add_Click
$btnImg_click.Invoke({
$file = Invoke-FileBrowser -Title "Choose a thumbnail photo" -Filter "Jpeg pictures (*.jpg)|*.jpg"
if ($file) {
if ((Get-Item $file).length -gt 10kB) {
Write-Warning "Picture too large. Maximum size is 10 kB."
} else {
$global:userinfo.photo = [byte[]](Get-Content $file -encoding byte)
$imgThumb.Source = $global:userinfo.photo
$btnApply.IsEnabled = $true
}
}
})
#endregion btnImgClick
#region btnApplyClick
$btnApply_click = $btnApply.add_Click
$btnApply_click.Invoke({
$user = New-Object DirectoryServices.DirectoryEntry "LDAP://$($global:userinfo.dn)"
$user.put("thumbnailPhoto", $global:userinfo.photo)
$user.setinfo()
$btnApply.IsEnabled = $false
Write-Verbose "User $($global:userinfo.dn) updated."
})
#endregion btnApplyClick
#region textChange
$txtSearch_Change = $txtSearch.add_KeyDown
$txtSearch_Change.Invoke({
if ($_.Key -ieq 'Return') {
Update-WPFForm($txtSearch.Text)
}
})
#endregion textChange
#endregion Events
$window.ShowDialog() | Out-Null
This is just an example how a useful script with GUI can be written with quite simple means. Now let’s get the camera and take some photos!
/ Andreas