Backgrounds

Copper Contributor

Has anybody worked out how to add custom backgrounds programmatically?  We will need to push our corp imagery to make it available for all staff.  Previously this was done by GPO putting images into %APPDATA%\Microsoft\Teams\Backgrounds.  The new location seems to be %LOCALAPPDATA%\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads - however manual copy of images to this location, even if you include the image and thumb from the old location don't work.    When using the GUI to add images, they are put into the uploads folder with a randomised name and auto generated thumb.   I also tried copying these to a different users machine same location but it doesn't recognise them, so they must be indexed somehow elsewhere?

29 Replies

@aaronm443 Interested to follow along and see how this thread goes. I'm hoping it's purely the preview nature of the new Teams, and nothing as nefarious as MS taking away functionality found in the old client in preparation to re-sell to us new functionality in "premium" Teams, which does have a centralised backgrounds deployment feature I seem to recall.

I hope that is not restricted to the Premium SKU. That would be a kick in the teeth to take functionality away only to sell it back for an additional cost.
Agree. I don't mind Premium licencing for a GUI central management feature (that we won't use), that's fine.... but don't stop us manually deploying backdrops into the right places if we wish. :\ I spent quite a lot of time with ProcMon the other day trying to work out if it updates a file or the registry with details of manually installed backgrounds but couldn't figure out what it does.

@aaronm443 Any update on this? I think it was rotten enough to take away the ability to push custom backgrounds through the Teams admin console, but at least with "old" Teams the IT department could work around this by using other file distribution methods. I just upgraded to New Teams last week and I'm having the same experience as you did a month ago, with randomized file names in a hard-to-find location path.

I haven't spent any more time on this as yet. After testing, the new Teams is so far from feature complete/parity with the old Team we can't consider it for enterprise deployment in the near future. Our testers have mostly reverted to old teams, which seems to still be getting feature updates (via the Public and Developer preview channels) that the new teams is not (eg the greenscreen background is only in old Teams, and was just released to preview)! It's an odd situation.
Any update or solution on this?
We also have been deploying custom backgrounds via win32 app in Intune. So far we've only enabled the new Teams version for IT dept to test out but would definitely be an issue for us if there is no way to deploy these without having to buy the premium license..

@aaronm443 
Just worked on this. Yes, custom Backrgounds can be deployed here:
%localappdata%\Local\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads

 

Make sure you follow the notation of Backgrounds you add manually. Seems the new Teams client renames the files to "a GUID.jpeg". If you follow that notation, your backgrounds appear in the new Teams.

 

I copied the filename of a BG I added manually and incremented a number. Seems to work so far. I can definitely see MSFT pay-walling this though.

@yamautomate thank you for looking into this.   I can't replicate your success with just <guid>.jpeg, however I found that manually adding/creating a <guid>_thumb.jpeg with a maximum width of 280px seems to have worked!

@aaronm443 
Yes you also need to have a Thumbnail for each background picture. That Thumbnail needs to match the GUID of the background picture:

  • 0b19f8d7-66d7-8856-ba5a-aaa04a3d309d.jpeg
  • 0b19f8d7-66d7-8856-ba5a-aaa04a3d309d_thumb.jpeg

In my approach, I use an Azure Blob to store the pictures with their Thumbail (I add them manually to "classic" Teams beforehand, grab them from the "Uploads" Folder and upload them to the Blob).
Then my PowerShell Script (distributed via Intune) downloads all Files from that Blob. If NewTeamsIsPresent on the System where the Script runs, it downloads the same file as for "classic" Teams but creates a GUID from the Filename. 

As I was quite happy that you found the needed Path for the "New" Teams Client, I'll happily share my working Script with you. May it be helpful for some1 out there:


yamautomate/Set-TeamsCustomBackgrounds: Sets custom Backgrounds for Teams (old and new!) (github.com...

 

@aaronm443 

 

I've created a powershell script to convert a directory of image files to png, create both an original and thumbnail image with the correct guid and filename. I was then able to see the files in the new teams. 

brzayas_0-1693425178826.png

 

 

# Install the necessary .NET namespace

Add-Type -AssemblyName System.Drawing

# Import the necessary .NET namespace
Add-Type -AssemblyName System.Drawing

# Dummy function to satisfy the GetThumbnailImage method
function dummyCallback { return $false }

# Define the folder containing the image files
$sourceFolder = "C:\Path\To\Source\Images"

# Define the folder to save the converted images
$destinationFolder = "C:\Path\To\Destination"

# Loop through each image file in the source folder
Get-ChildItem -Path $sourceFolder -File | ForEach-Object {

    # Generate a GUID for the new image name
    $guid = [guid]::NewGuid().ToString()

    # Create a .NET Bitmap object from the image file
    $originalImage = [System.Drawing.Image]::FromFile($_.FullName)

    # Save the image as a PNG with a GUID-based name
    $originalImage.Save("$destinationFolder\$guid.png", [System.Drawing.Imaging.ImageFormat]::Png)

    # Create a thumbnail image
    $thumbWidth = 278
    $thumbHeight = 159
    $thumbnailImage = $originalImage.GetThumbnailImage($thumbWidth, $thumbHeight, [System.Drawing.Image+GetThumbnailImageAbort]$dummyCallback, [System.IntPtr]::Zero)

    # Save the thumbnail image as a PNG with a GUID-based name and "_thumb" suffix
    $thumbnailImage.Save("$destinationFolder\$guid`_thumb.png", [System.Drawing.Imaging.ImageFormat]::Png)

    # Dispose of the image objects to free resources
    $originalImage.Dispose()
    $thumbnailImage.Dispose()
}



 

Thanks for sharing the script! Was looking into a solution for deploying for either and your script seems to be a nice solution.
I am curious if you ever ran into any weird errors trying to download from the blob storage? I've got the permissions all set but keep getting error "409 (conflict)" when its trying to download during the script. Rather odd and not very helpful error.
I love this, well done and thanks.

I'm trying to get my head around how I could store images centrally and get clients to check on a scheduled bases for newly added images... for now this script is great as a run once, but the random GUIDs mean it can't be run multiple times. I guess the only way around that is some sort of local log file that the script updates/checks against which has the original image name and the GUID that was produced, and if either doesn't exist then carry on, otherwise skip.

Anyone else any better ideas?

@Steve Prentice So I'm not hosting these images in an Azure blob. What I implemented in my environment was deploying this as an Intune Win32 App and using PowerShell scripts to Install, Uninstall and Detect the correct files are placed in both the Old Teams Background directory and the New Teams. Since our org are still on a mix of both app variants. As far as wanting to update images, what you can do is when their are new images you can just create a new Intune App with the new photos whenever you have new images to upload. The GUID is a unique identifier so different images won't have the same GUID.

 

If anyone would like to go that route, below is what I setup. 

 

  • I have a directory with the install/uninstall/detection Scripts and a folder called bg that contains all the background images, you can place as many as you want. 
    • brzayas_2-1693941229554.png

       

    • Using the IntuneWinAppUtil.exe I package all the files for creating the app on Intune
    • When creating the app I used these Install/Uninstall commands
      • Install: 
        %SystemRoot%\sysnative\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -command .\install.ps1
      • Uninstall: 
        %SystemRoot%\sysnative\WindowsPowerShell\v1.0\powershell.exe -executionpolicy bypass -command .\uninstall.ps1
      • brzayas_1-1693941088457.png

         

Below are the scripts for this app:

install.ps1

 

 

 

$PackageName = "Teams-Backgrounds"
$Version = "1"

$Path_4Log = "$ENV:LOCALAPPDATA\_MEM"
Start-Transcript -Path "$Path_4Log\Log\$PackageName-install.log" -Force
$ErrorActionPreference = "Stop"

try{
    # Local folder
    $TeamsBG_Folder = "$env:APPDATA\Microsoft\Teams\Backgrounds\Uploads"
    $TeamsBG_Folder_New = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads"
    # Ensure the folder is present
    New-Item -ItemType directory -Path $TeamsBG_Folder -Force
    
    # Copy backgrounds
New-Item -ItemType directory -Path $TeamsBG_Folder_New -Force
Copy-Item -path ".\bg\*" -Destination $TeamsBG_Folder_New -Recurse -Force
    Copy-Item -path '.\bg\*' -Destination $TeamsBG_Folder -Recurse -Force

    # Validation File
    New-Item -Path "$Path_4Log\Validation\$PackageName" -ItemType "file" -Value $Version -Force
}catch{
    Write-Host "_____________________________________________________________________"
    Write-Host "ERROR"
    Write-Host "$_"
    Write-Host "_____________________________________________________________________"
}

Stop-Transcript

 

 

 

 

uninstall.ps1

 

 

 

$PackageName = "Teams-Backgrounds"

$Path_4Log = "$ENV:LOCALAPPDATA\_MEM"
Start-Transcript -Path "$Path_4Log\Log\$PackageName-uninstall.log" -Force

$TeamsBG_Folder = "$env:APPDATA\Microsoft\Teams\Backgrounds\Uploads"
$TeamsBG_Folder_New = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads"
$TeamsBG_Files = Get-ChildItem -Path '.\bg' -Name

# Delete distributed backgrounds

Get-ChildItem $TeamsBG_Folder_New | Where{$_.Name -in $TeamsBG_Files} | Remove-Item
Get-ChildItem $TeamsBG_Folder | Where{$_.Name -in $TeamsBG_Files} | Remove-Item

# Delete detection file
Remove-Item -Path "$Path_4Log\Validation\$PackageName" -Force

Stop-Transcript

 

 

 

 

check.ps1 

 

 

 

$PackageName = "Teams-Backgrounds"
$Version = "1"

$Path_4Log = "$ENV:LOCALAPPDATA\_MEM"
$ProgramVersion_current = Get-Content -Path "$Path_4Log\Validation\$PackageName"

if($ProgramVersion_current -eq $Version){
    Write-Host "Found it!"
}

 

 

 

 

I hope this helps anyone looking for an alternative way to deploy teams backgrounds.

Like everything in IT, many solutions are available. Here's mine in case it's of any help for someone else.

 

What it does:

Download images from Azure Blob directly to New Teams Upload folder

 

How it does:

checks for New Teams to be installed and for Blob URL to be reacheable

Creates "Upload" folder if doesn't exist and bittransfer all files contained in a Blob folder into New Teams user folder using Intune Win32 app deploy

 

How it checks:

Detection is a dynamic script which check for each file in Azure Blob folder to exist in New Teams folder. If one or more files are missing, it will trigger a new deploy therefore Azure Blob folder can have pictures removed or added and it will dynamically reflects on clients devices

 

It's a Win32 app that needs to be packed with intunewinapputil

 

Main Script:

# hide windows terminal console
# https://stackoverflow.com/questions/74968665/hide-the-windows-terminal-console-window-with-powershell
function Hide-ConsoleWindow() {
    $ShowWindowAsyncCode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
    $ShowWindowAsync = Add-Type -MemberDefinition $ShowWindowAsyncCode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
  
    $hwnd = (Get-Process -PID $pid).MainWindowHandle
    if ($hwnd -ne [System.IntPtr]::Zero) {
      # When you got HWND of the console window:
      # (It would appear that Windows Console Host is the default terminal application)
      $ShowWindowAsync::ShowWindowAsync($hwnd, 0)
    } else {
      # When you failed to get HWND of the console window:
      # (It would appear that Windows Terminal is the default terminal application)
  
      # Mark the current console window with a unique string.
      $UniqueWindowTitle = New-Guid
      $Host.UI.RawUI.WindowTitle = $UniqueWindowTitle
      $StringBuilder = New-Object System.Text.StringBuilder 1024
  
      # Search the process that has the window title generated above.
      $TerminalProcess = (Get-Process | Where-Object { $_.MainWindowTitle -eq $UniqueWindowTitle })
      # Get the window handle of the terminal process.
      # Note that GetConsoleWindow() in Win32 API returns the HWND of
      # powershell.exe itself rather than the terminal process.
      # When you call ShowWindowAsync(HWND, 0) with the HWND from GetConsoleWindow(),
      # the Windows Terminal window will be just minimized rather than hidden.
      $hwnd = $TerminalProcess.MainWindowHandle
      if ($hwnd -ne [System.IntPtr]::Zero) {
        $ShowWindowAsync::ShowWindowAsync($hwnd, 0)
      } else {
        Write-Host "Failed to hide the console window."
      }
    }
  }

Hide-ConsoleWindow
## Set variables
$name = "NewTeams_backgrounds"
$ver = "1.0"
$type = "APP"
$logname = "$type-$name.log"
$logPath = "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\"
$LogFile = Join-Path $logPath $logname

# Checking permissions
Write-Output "checking permissions on $logPath"
$acl = Get-Acl -Path $logPath
$aclcheck = $acl.Access | Where-Object { $_.IsInherited -eq $false -and $_.FileSystemRights -match 'Modify'}
if ($null -eq $aclcheck) {
  try {
    # Changing permissions to Logs folder
Write-Output "adding Modify permissions on builtin\users group"
$rights = 'Modify' 
$inheritance = 'ContainerInherit, ObjectInherit' 
$propagation = 'None' 
$type = 'Allow'
$SID = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-545")
$ACE = New-Object System.Security.AccessControl.FileSystemAccessRule($SID, $rights, $inheritance, $propagation, $type)
$acl = Get-Acl -Path $logPath
$acl.SetAccessRule($ACE)
$acl | Set-Acl -Path $logPath
Write-Output "permissions modified on $logPath"
}
catch {
  Write-Output "Error setting up permissions for $logPath. The error is below:"
  Write-Output $_
}
  }
Write-Output "permissions are fine"

# Function to write log entries
function Write-Log {
  param(
      [string]$Message
  )

  $Timestamp = Get-Date -Format "dd-MM-yyyy HH:mm:ss"
  $LogEntry = "$Timestamp - $Message"
  $LogEntry | Out-File -FilePath $LogFile -Append
}

# Main script
try {
Write-Log "Installing $name version $ver"

Write-Log "checking New Teams folders"

$NewTeams_background = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds"
$NewTeams_Uploads = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads"
if (!(Test-Path -path $NewTeams_background)) { 
    Write-Log "creating folder $NewTeams_background"
    New-Item $NewTeams_background -itemtype directory -force};
if (!(Test-Path -path $NewTeams_Uploads)) { 
    Write-Log "creating folder $NewTeams_Uploads"
    New-Item $NewTeams_Uploads -itemtype directory -force};

$URL = "YOUR_BLOB_URL_WITH_SAS"
$uri = "https://yourcompanyblobname.blob.core.windows.net/yourcompanyfolder/NewTeams_Background"
$body = Invoke-RestMethod -uri $URL
$xml = [xml]$body.Substring($body.IndexOf('<'))
$files = $xml.ChildNodes.Blobs.Blob.Name | where-Object {$_ -like "NewTeams_Background/*"}
$files = $files -replace "NewTeams_Background/",""
Write-Log "following files are on Azure Blob: $files"
$files | ForEach-Object {
$downloadpath = $NewTeams_Uploads + "/" + $_
if (!(Test-Path -Path $downloadpath)){
$URL = "$($uri)/$_"
Write-Log "Downloading $_"
#Invoke-WebRequest -Uri $URL -OutFile "$NewTeams_Uploads\$_" -TimeoutSec 30 -UseBasicParsing:$true -ErrorAction SilentlyContinue
Start-BitsTransfer -Source $URL -Destination "$NewTeams_Uploads\$_" -Priority Normal -ErrorAction SilentlyContinue
}
}

Write-Log "Installation of $name completed"
}
catch {
Write-Log "Error running $name install. The error is below:"
Write-Log $_
}

you'd need to change $uri and $URL accordingly

Pictures need to be uploaded into $uri location

It has a function to hide the powershell and terminal window since it's installed under User environment

It saves log into the default Intune log folder and since it runs as user, it checks for log folder correct permissions

 

Detection script:

$NewTeams_Uploads = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe\LocalCache\Microsoft\MSTeams\Backgrounds\Uploads"
$URL = "YOUR_COMPANY_BLOB_URL_WITH_SAS"
$missing = 0
$body = Invoke-RestMethod -uri $URL
$xml = [xml]$body.Substring($body.IndexOf('<'))
$files = $xml.ChildNodes.Blobs.Blob.Name | where-Object {$_ -like "NewTeams_Background/*"}
$files = $files -replace "NewTeams_Background/",""
$files | ForEach-Object {
$download_path = $NewTeams_Uploads + "/" + $_
if (!(Test-Path -Path $download_path)){
$missing++
}
}
if ($missing -eq "0"){
    Write-Host "New Teams backgrounds pictures installed"
}

 

Then you have simple requirements script, one for Blob URL to be reacheable:

$url = "YOUR_COMPANY_BLOB_URL_WITH_SAS"
$client = New-Object System.Net.WebClient
$client.DownloadString($url) | Out-Null
Write-Host "The URL is reachable"

 

and one for New Teams folder to exist (to be checked with logged on credentials):

# runs with logged on creds
$NewTeams_path = "$env:LOCALAPPDATA\Packages\MSTeams_8wekyb3d8bbwe"
if (Test-Path $NewTeams_path)
{
Write-Host "New Teams installed"
}

 

Enjoy

I had an existing script that downloaded images with standard names like "hps_background.jpg"

 

To get around the GUID requirement, I created an SHA1 hash of the image post-download (which downloads to the legacy app location), and then create a GUID based on the first 32-characters of that hash.

 

That way the hash is always unique and can be run regularly.

Thanks @hps_steve that's a great idea, totally solves the random GUID issue. :)

For anyone else wanting to do the same, something like the following PowerShell would help:

 

$FilePath = "C:\Path\To\Saved\Picture.png"

# Calculate the SHA-1 hash of the downloaded file
$hasher = [System.Security.Cryptography.SHA1]::Create()
$fileStream = [System.IO.File]::OpenRead($FilePath)
$hashBytes = $hasher.ComputeHash($fileStream)
$fileStream.Close()
$hasher.Dispose()

# Convert the original hash to a hexadecimal string and make it lowercase
$originalSha1Hash = [BitConverter]::ToString($hashBytes).ToLower() -replace '-', ''

# Create a shorter hash (e.g., first 32 characters) for the GUID
$shortSha1Hash = $originalSha1Hash.Substring(0, 32)

# Create a GUID from the short hash
$guidFromHash = $shortSha1Hash.Substring(0, 8) + '-' + $shortSha1Hash.Substring(8, 4) + '-' + $shortSha1Hash.Substring(12, 4) + '-' + $shortSha1Hash.Substring(16, 4) + '-' + $shortSha1Hash.Substring(20)

# Display the SHA-1 hash and GUID
Write-Host "SHA-1 Hash: $originalSha1Hash"
Write-Host "GUID from Hash: $guidFromHash"

 

@Steve Prentice You could use "Get-FileHash" to get the SHA1 hash.

$hash = Get-FileHash -Path $FilePath -Algorithm SHA1
$originalSha1Hash = $hash.Hash.toLower()
Even better, thank you! :)