In this post, I will show how you can sync your Windows account pictures using Active Directory and group policies.

Prerequisites

Let’s get a few things out of the way first:

  • I don’t know if and how this applies to an Azure AD setup, I’m talking about a locally hosted Active Directory server
  • The account pictures need to be in JPEG format, 96x96px and not larger than 100kb in size
  • This apparently doesn’t work on Windows 11 (yet)

Please keep the above points in mind.

Create the group policy

The following steps are necessary to create the required group policy and add the necessary permissions:

  1. Connect to your domain controller and open the group policy manager, in there create a new group policy (use a descriptive name)
  2. Open/edit the policy and create the required registry key:
    1. Navigate to Computer Configuration/Policies/Windows Settings/Security Settings/Registry using the tree view
    2. Right click into the registry list and create a new key with the path MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users
    3. In the next window grant Full Access to the <DOMAIN>\Users group so that each user can set their account picture
    4. In the “Add Object” window, select Replace existing permission on all subkeys with inheritable permissions
  3. Next, navigate to Computer Configuration/Administrative Templates/System/Group Policy and set Configure user Group Policy Loopback Processing mode to Merge
  4. You can now close the group policy for now (or leave it open, it’s a multi window os y’know)

Create the updater script

The following PowerShell script should be saved on some accessible path on your domain controller, in my case its located at \\<DOMAIN>\sysvol\<DOMAIN>\scripts\SetAccountPictureFromAD.ps1:

 Function ResizeImage {
    Param (
        [Parameter(Mandatory = $True, HelpMessage = "The image as bytes")]
        [ValidateNotNull()]
        $imageSource,
        
        [Parameter(Mandatory = $true, HelpMessage = "The canvas size can be between 16px and 1000px")]
        [ValidateRange(16, 1000)]
        $canvasSize,
        
        [Parameter(Mandatory = $true, HelpMessage = "The image quality can be between 1 and 100")]
        [ValidateRange(1, 100)]
        $ImgQuality = 100
    )
    
    [void][System.Reflection.Assembly]::LoadWithPartialName("System.Drawing")
    $imageBytes = [byte[]]$imageSource
    $ms = New-Object IO.MemoryStream($imageBytes, 0, $imageBytes.Length)
    $ms.Write($imageBytes, 0, $imageBytes.Length);
    $bmp = [System.Drawing.Image]::FromStream($ms, $true)
    
    # Image size after conversion
    $canvasWidth = $canvasSize
    $canvasHeight = $canvasSize
    
    # Set picture 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, $ImgQuality)
    
    # Get image type
    $myImageCodecInfo = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
    
    # Get aspect ratio
    $ratioX = $canvasWidth / $bmp.Width;
    $ratioY = $canvasHeight / $bmp.Height;
    $ratio = $ratioY
    if ($ratioX -le $ratioY) {
        $ratio = $ratioX
    }
    
    # Create an empty picture
    $newWidth = [int] ($bmp.Width * $ratio)
    $newHeight = [int] ($bmp.Height * $ratio)
    $bmpResized = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
    $graph = [System.Drawing.Graphics]::FromImage($bmpResized)
    $graph.Clear([System.Drawing.Color]::White)
    $graph.DrawImage($bmp, 0, 0 , $newWidth, $newHeight)
    
    # Create an empty stream
    $ms = New-Object IO.MemoryStream
    $bmpResized.Save($ms, $myImageCodecInfo, $($encoderParams))
    
    # cleanup
    $bmpResized.Dispose()
    $bmp.Dispose()
    return $ms.ToArray()
}

$ADUserInfo = ([ADSISearcher]"(&(objectCategory=User)(SAMAccountName=$env:username))").FindOne().Properties
$ADUserInfo_sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value

If ($ADUserInfo.thumbnailphoto) {
    $img_sizes = @(32, 40, 48, 96, 192, 200, 240, 448)
    $img_base = "C:\Users\Public\AccountPictures"
    $reg_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AccountPicture\Users\$ADUserInfo_sid"

    If ((Test-Path -Path $reg_key) -eq $false) { New-Item -Path $reg_key } { write-verbose "Registry key exists: [$reg_key]" }

    Try {
        ForEach ($size in $img_sizes) {
            $dir = $img_base + "\" + $ADUserInfo_sid
            If ((Test-Path -Path $dir) -eq $false) { $(New-Item -ItemType directory -Path $dir).Attributes = "Hidden" }
            $file_name = "Image$($size).jpg"
            $path = $dir + "\" + $file_name
            Write-Verbose " Create file: [$file_name]"
            
            try {
                ResizeImage -imageSource $($ADUserInfo.thumbnailphoto) -canvasSize $size -ImgQuality 100 | Set-Content -Path $path -Encoding Byte -Force -ErrorAction Stop
                Write-Verbose " File saved: [$file_name]"
            }
            catch {
                If (Test-Path -Path $path) {
                    Write-Warning "File exists: [$path]"
                }
                else {
                    Write-Warning "File doesn't exist: [$path]"
                }
            }
            
            $name = "Image$size"
            try {
                $null = New-ItemProperty -Path $reg_key -Name $name -Value $path -Force -ErrorAction Stop
            }
            catch {
                Write-Warning "Registry key edit error [$reg_key] [$name]"
            }
        }
    }
    Catch {
        Write-Error "Check the permissions to files or registry!"
    }
} 

Make sure that the permissions of the script allow authenticated users to execute it!

Adding the script to the group policy

The following steps are necessary to add the script to the group policy:

  1. Open up the group policy again, or return to the editor window
  2. Navigate to User Configuration/Policies/Windows Settings/Scripts (Logon/Logoff) and double click Logoff
  3. In the new window, change to the PowerShell Scripts tab and add the path to the script
    • In my case it’s located at \\<DOMAIN>\sysvol\<DOMAIN>\scripts\SetAccountPictureFromAD.ps1
    • If you add the script to the wrong tab, the clients will “freeze”/take ages when logging out!
  4. Close the policy editor

Activating the group policy

To activate the group policy and start syncing pictures, you should drag/link the policy to an organisational unit that contains your client devices (not users).

Adding pictures to Active Directory

There are multiple ways to add the profile pictures to the Active Directory user accounts:

  • You could open the properties of the account and add an image to the thumbnailphoto property using the UI (never tried that)
  • You could use PowerShell:
    1. Put all the images in the correct size and format into some folder
    2. Open PowerShell
    3. Use the following command to load the image into a variable:
      • $ADphoto = [byte[]](Get-Content "<path to file>" -Encoding byte)
    4. Use the following command to put the variable contents into the AD account:
      • Set-ADUser "<username>" -Replace @{thumbnailPhoto=$ADphoto}
    5. You can repeat step 3 followed by 4 for all the users, the variable will be overwritten each time

Testing

To quickly test if everything works, you can login to any connected client that is in the organisational unit where the group policy applies. Run gpupdate in the PowerShell and then log out and back in again. The profile picture should be synced correctly.

Troubleshooting

  • If the logoff takes a really long time, make sure that you added the script to the PowerShell Scripts Tab, not the Scripts tab
  • If nothing happens, double check the permissions of the script on disk or the registry key in the group policy
  • Make sure that Windows is activated, as a non activated windows will not show account pictures