Deploy and Apply Desktop Wallpaper & Lockscreen using Intune

As ever, with technology there’s many ways to achieve “the same outcome”. So, this isn’t the first post on the internet about this, and it won’t be the last, but in this post, I show how to deploy branding material to devices which can then be referenced for use as Desktop Wallpaper and Lockscreen or Logon Screen images within Windows.

I’ve written a PowerShell script which, when bundled into a Win32App, will copy branding material to the local device and then make necessary registry changes to apply the customisation, setting the Desktop Wallpaper and Lockscreen images using the PersonalizationCSP entries within HKLM.

Due to the nature of the way this is deployed, you can continue to maintain Desktop Wallpaper and Lockscreen images natively using the CSP configuration options within Intune! This isn’t a replacement for the native CSP configuration, it’s more of an additional method with benefits, with the most important benefit being that this is also a delivery mechanism for getting local branding material to devices for use.

Theres an assumption herein, that Windows Spotlight and the Slideshow screensaver are disabled via pre-existing Intune policies. This is a pre-requisite.

Create yourself a working directory, and within this directory, create three directories: Wallpaper, Lockscreen and Theme. Within this working directory, we’ll save out our PowerShell scripts. Within each subfolder, we’ll add our branding material, for example:

  • ..\Desktop Branding\
  • ..\Desktop Branding\Wallpaper\
  • ..\Desktop Branding\Wallpaper\wallpaper.jpg
  • ..\Desktop Branding\Lockscreen\
  • ..\Desktop Branding\Lockscreen\lockscreen.jpg
  • ..\Desktop Branding\Theme\
  • ..\Desktop Branding\Theme\YourTheme.theme

I’ve put together a helper to create the shown layout.

# Create DesktopBranding landing zone
$parentPath = "C:\Temp\DesktopBranding"
$subFolders = @("Wallpaper", "Lockscreen", "Theme")

# Check if the parent directory exists
if (-not (Test-Path -Path $parentPath)) {
    # Create the parent directory
    New-Item -Path $parentPath -ItemType Directory | Out-Null
    Write-Host "Created parent directory: $parentPath"

    # Create child directories
    foreach ($folder in $subFolders) {
        New-Item -Path (Join-Path -Path $parentPath -ChildPath $folder) -ItemType Directory | Out-Null
        Write-Host "Created child directory: $folder"
    }
}
else {
    Write-Host "Directory already exists: $parentPath"
}

From there, we need to create the scripts that perform the heavy lifting.

Set-DesktopBranding.ps1

<#
.SYNOPSIS
  Configure Desktop Wallpaper and Lockscreen image for all users with logging.

.DESCRIPTION
  Copies branding material down to the local device and iterates through every user profile on a Windows system
  setting the Desktop Wallpaper and Lockscreen image, and logs all actions to the Intune log directory.

.EXAMPLE 
  .\Set-DesktopBranding.ps1 -Location "Vini" -WallpaperImg "DesktopWallpaper.jpg" -LockscreenImg "LockImg.png" -Style "Stretch"

  !! IMPORTANT!! If using Intune, the Install Command is recommended to be as follows, to ensure that the WOW6432NODE is not referenced;
  %SystemRoot%\SysNative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File .\Set-DesktopBranding.ps1 -Location "Vini"

.PARAMETER LOCATION
  This variable sets the landing zone under $env:ProgramData, this would be your company name or "EUC" or similar.

.PARAMETER WALLPAPERIMG
  This is the filename of the Desktop Wallpaper image that is saved into .\Wallpaper\

.PARAMETER LOCKSCREENIMG
  This is the filename of the Lockscreen Image image that is saved into .\Lockscreen\

.PARAMETER STYLE
  This is the Wallpaper style. Fill is default if not referenced.

.PARAMETER TILE
  If -Tile is referenced during the runtime, the Wallpaper will be configured to Tile. Probably not required in this day and age.

.LOG LOCATION
  $env:ProgramData\Microsoft\IntuneManagementExtension\Logs\App-Install-DesktopBranding.log
  Logging to this location allows the use of Collect Diagnostics within Intune to gather the log file.

.NOTES
  A Theme file is also copied to the relevant directory on the device if included. The use and assignment of Theme is not automated with this script.
  Creation of a Theme file needs to occur outside of this process.

.AUTHOR
  James Vincent
  October 2025
#>

[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)]
    [string]$Location,
    [string]$WallpaperImg = "wallpaper.jpg",
    [string]$LockscreenImg = "lockscreen.jpg",
    [ValidateSet('Fill','Fit','Stretch','Center','Span')]
    [string]$Style = "Fill",
    [switch]$Tile
)

# === GLOBAL VARIABLES ===
$script:FullLocation   = Join-Path -Path $env:ProgramData -ChildPath "$($Location)"
$script:WallpaperPath  = Join-Path $FullLocation "Wallpaper\$WallpaperImg"
$script:LockscreenPath = Join-Path $FullLocation "Lockscreen\$LockscreenImg"
$script:StyleVals      = $null
$script:LogFile        = "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\App-Install-DesktopBranding.log"

# Ensure log directory exists
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) {
    New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}

# ==================== LOGGING ====================
function Write-Log {
    param(
        [Parameter(Mandatory)]
        [string]$Message,
        [ValidateSet("INFO", "WARN", "ERROR")]
        [string]$Level = "INFO"
    )
    $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $logLine = "[$timestamp] [$Level] $Message"

    # Write to console and file
    switch ($Level) {
        "INFO"  { Write-Host $logLine -ForegroundColor Gray }
        "WARN"  { Write-Warning $Message }
        "ERROR" { Write-Error $Message }
    }

    try {
        Add-Content -Path $script:LogFile -Value $logLine
    } catch {
        Write-Host "Failed to write to log file: $($_.Exception.Message)"
    }
}

# ==================== FUNCTIONS ====================

function Assert-Admin {
    $current = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($current)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-Log "Script must be run as Administrator." "ERROR"
        throw "This script must be run from an elevated PowerShell session."
    }
    Write-Log "Confirmed running with Administrator privileges."
}

function Get-StyleValues {
    param([string]$Style,[switch]$TileSwitch)
    $map = @{
        'Center'  = @{ WallpaperStyle = 0;  TileWallpaper = 0 }
        'Stretch' = @{ WallpaperStyle = 2;  TileWallpaper = 0 }
        'Fit'     = @{ WallpaperStyle = 6;  TileWallpaper = 0 }
        'Fill'    = @{ WallpaperStyle = 10; TileWallpaper = 0 }
        'Span'    = @{ WallpaperStyle = 22; TileWallpaper = 0 }
    }
    $vals = $map[$Style].Clone()
    if ($TileSwitch) { $vals.TileWallpaper = 1 }
    Write-Log "Style set to '$Style' (WallpaperStyle=$($vals.WallpaperStyle), Tile=$($vals.TileWallpaper))."
    return $vals
}

function Copy-BrandingMaterial {
    Write-Log "Ensuring branding materials exist at $FullLocation..."
    if (-Not (Test-Path $FullLocation)) {
        Write-Log "Specified location not found: $FullLocation. Creating it..." "WARN"
        New-Item -ItemType Directory -Path $FullLocation -Force | Out-Null
    }

    $dirs = @("$FullLocation\Wallpaper", "$FullLocation\Lockscreen", "$FullLocation\Theme")
    foreach ($d in $dirs) {
        if (-not (Test-Path $d)) {
            New-Item -ItemType Directory -Path $d -Force | Out-Null
            Write-Log "Created directory: $d"
        }
    }

    foreach ($ext in 'jpeg','jpg','png','bmp','gif') {
        xcopy ".\Wallpaper\*.$ext" "$FullLocation\Wallpaper\" /Y /I | Out-Null
        xcopy ".\Lockscreen\*.$ext" "$FullLocation\Lockscreen\" /Y /I | Out-Null
    }
    xcopy ".\Theme\*.theme" "$FullLocation\Theme\" /Y /I | Out-Null

    Write-Log "Branding materials copied successfully."
}

function Set-DesktopBackground {
    Write-Log "Configuring Wallpaper"

    try {
        $policyKey = "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PersonalizationCSP"
        if (-not (Test-Path $policyKey)) {
            New-Item -Path "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name "PersonalizationCSP" -Force | Out-Null
            Write-Host "$policyKey not found, Registry location created."
        }
        if (Test-Path $policyKey) {
            New-ItemProperty -Path $policyKey -Name 'DesktopImagePath' -Value $WallpaperPath -PropertyType String -Force | Out-Null
            Write-Log "DesktopImagePath written to $policyKey."
            New-ItemProperty -Path $policyKey -Name 'DesktopImageUrl' -Value $WallpaperPath -PropertyType String -Force | Out-Null
            Write-Log "DesktopImageUrl written to $policyKey."
            New-ItemProperty -Path $policyKey -Name 'DesktopImageStatus' -Value 1 -PropertyType DWord -Force | Out-Null
            Write-Log "DesktopImageStatus written to $policyKey."
        } else {
            Write-Host "$policyKey not found, Registry location created."
            Write-Log "$policyKey not found: $($_.Exception.Message)" "ERROR"
        }
        Write-Log "Wallpaper has been applied"
    } catch {
        Write-Log "Error configuring Wallpaper: $($_.Exception.Message)" "ERROR"
    }
}

function Set-Lockscreen {
    Write-Log "Configuring Lockscreen for the device."

    try {
        $policyKey1 = "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\PersonalizationCSP"
        if (-not (Test-Path $policyKey1)) {
            New-Item -Path "Registry::HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion" -Name "PersonalizationCSP" -Force | Out-Null
            Write-Host "$policyKey1 not found, Registry location created."
        }
        if (Test-Path $policyKey1) {
            New-ItemProperty -Path $policyKey1 -Name 'LockScreenImagePath' -Value $LockscreenPath -PropertyType String -Force | Out-Null
            Write-Log "LockScreenImagePath written to $policyKey1."
            New-ItemProperty -Path $policyKey1 -Name 'LockScreenImageUrl' -Value $LockscreenPath -PropertyType String -Force | Out-Null
            Write-Log "LockScreenImageUrl written to $policyKey1.."
            New-ItemProperty -Path $policyKey1 -Name 'LockScreenImageStatus' -Value 1 -PropertyType DWord -Force | Out-Null
            Write-Log "LockScreenImageStatus written to $policyKey1."
        } else {
            Write-Host "$policyKey1 not found, Registry location created."
            Write-Log "$policyKey1 not found: $($_.Exception.Message)" "ERROR"
        }
        Write-Log "Lockscreen has been applied"
    } catch {
        Write-Log "Error configuring Lockscreen: $($_.Exception.Message)" "ERROR"
    }
}

# ==================== MAIN ====================
try {
    Write-Log "=== Script started ==="
    Assert-Admin
    Copy-BrandingMaterial
    $script:StyleVals = Get-StyleValues -Style $Style -TileSwitch:$Tile
    Write-Log "Applying Wallpaper and Lockscreen settings..."
    Set-DesktopBackground
    Set-Lockscreen
    RUNDLL32.EXE user32.dll, UpdatePerUserSystemParameters ,1 ,True
    Write-Log "Completed applying settings. They will take effect at next logon."
    Write-Log "=== Script completed successfully ==="
}
catch {
    Write-Log "Fatal error: $($_.Exception.Message)" "ERROR"
    exit 1
}

Uninstall-DesktopBranding.ps1

<#
.SYNOPSIS
  Removes custom Desktop Wallpaper and Lockscreen images, and tidies the registry entries for all users, with logging.

.DESCRIPTION
  Reverses the effects of the "Set-DesktopBranding.ps1" script.
  - Deletes copied Wallpaper and Lockscreen files from ProgramData.
  - Removes related registry policy entries.
  - Resets Wallpaper to Windows default.
  - Logs all actions and errors to a file.

.PARAMETER LOCATION
  The same location folder name used in the installation script (under ProgramData).

.EXAMPLE
  .\Uninstall-DesktopBranding.ps1 -Location "Vini"

.AUTHOR
  James Vincent
  October 2025  
#>

[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory = $true)]
    [string]$Location
)

# Combine $env:ProgramData with the provided $Location
$FullLocation = Join-Path -Path $env:ProgramData -ChildPath $Location

# Define LogFile location
$script:LogFile = "$env:ProgramData\Microsoft\IntuneManagementExtension\Logs\App-Uninstall-DesktopBranding.log"

# Ensure log directory exists
$logDir = Split-Path $LogFile
if (-not (Test-Path $logDir)) {
    New-Item -Path $logDir -ItemType Directory -Force | Out-Null
}

# ==================== LOGGING ====================
function Write-Log {
    param(
        [Parameter(Mandatory)]
        [string]$Message,
        [ValidateSet("INFO", "WARN", "ERROR")]
        [string]$Level = "INFO"
    )
    $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $logLine = "[$timestamp] [$Level] $Message"

    # Write to console and file
    switch ($Level) {
        "INFO"  { Write-Host $logLine -ForegroundColor Gray }
        "WARN"  { Write-Warning $Message }
        "ERROR" { Write-Error $Message }
    }

    try {
        Add-Content -Path $script:LogFile -Value $logLine
    } catch {
        Write-Host "Failed to write to log file: $($_.Exception.Message)"
    }
}

function Assert-Admin {
    Write-Log "Checking for administrator privileges..." "INFO"
    $current = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($current)
    if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        Write-Log "This script must be run as Administrator." "ERROR"
        throw "This script must be run from an elevated PowerShell session (Run as Administrator)."
    }
    Write-Log "Administrator check passed."
}

function Remove-SystemPolicies {
    Write-Log "Removing system-wide personalisation policies..."
    $basePaths = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\PersonalizationCSP'
    )
    foreach ($b in $basePaths) {
        if (Test-Path $b) {
            Write-Log "Deleting: $b"
            Remove-Item -Path $b -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

function Remove-BrandingMaterial {
    Write-Log "Checking contents of $FullLocation for removable branding folders..."

    if (Test-Path $FullLocation) {
        # Define allowed folder names
        $AllowedFolders = @("Lockscreen", "Theme", "Wallpaper")

        # Get all subfolders in $FullLocation
        $SubFolders = Get-ChildItem -Path $FullLocation -Directory -ErrorAction SilentlyContinue

        foreach ($Folder in $SubFolders) {
            if ($AllowedFolders -contains $Folder.Name) {
                Write-Log "Removing folder: $($Folder.FullName)"
                Remove-Item -Path $Folder.FullName -Recurse -Force -ErrorAction SilentlyContinue
                Write-Log "Deleted: $($Folder.FullName)"
            } else {
                Write-Log "Skipped: $($Folder.FullName) (not in allowed list)" "INFO"
            }
        }
    } else {
        Write-Log "No files found at $FullLocation" "INFO"
    }
}

# --- MAIN ---
try {
    Assert-Admin
    Write-Log "Starting cleanup of wallpaper, lock screen, and Spotlight policies..."
    Remove-SystemPolicies
    Remove-BrandingMaterial
    Write-Log "Cleanup complete. Default Windows settings will apply at next logon."
    Write-Log "Restored default Wallpaper."
    rundll32.exe user32.dll,UpdatePerUserSystemParameters
} catch {
    Write-Log "Fatal error: $($_.Exception.Message)" "ERROR"
    Write-Error $_.Exception.Message
    exit 1
}
finally {
    Write-Log "Closing log."
}

With the scripts and folder prepared, the next thing to do is to create the Win32App and upload this to Intune for deployment.

This guide will walk through creating the Win32App / .intunewin file for deployment; How to create a Win32 App or Intunewin Package – James Vincent

With the .intunewin created, login to Intune portal and set about creating a Win32App.

For the Install and Uninstall commands, we use the following. Where Location is the name of your landing zone under ProgramData (this would likely be your company name, or simply EUC).

Install

%SystemRoot%\SysNative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File .\Set-DesktopBranding.ps1 -Location "Vini"

Uninstall

%SystemRoot%\SysNative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -File .\Uninstall-DesktopBranding.ps1 -Location "Vini"

It is important you utilise %SystemRoot%\SysNative\WindowsPowerShell\v1.0\powershell.exe, failure to do so will likely result in the registry keys being created under WOW6432Node, and problems will arise in that this script will not work.

And we throw in the Detection script to take care of the app detection. We’re simply checking for the existence of the copied across wallpaper image file.

With the App created and assigned, it’s time to sit back and wait for the magic to happen.

At next logon (after a reboot), you will find the magic has indeed happened…

and then, after logon…

This is a run once scenario, in that, the application will apply one time (unless the detection method is tweaked to allow it to run again).

If you needed to update the wallpaper, you’d simply reference WallpaperImg = “wallpaper1.jpg” in the install parameters and tweak the detection method accordingly.

With this scripted method now working and in place. We can demonstrate how the native CSP option will play nicely alongside this. Ensuring the device has an Enterprise license, if we assign an Intune policy to the device…

Within minutes, the changes will take place.

James avatar

3 responses to “Deploy and Apply Desktop Wallpaper & Lockscreen using Intune”

  1. Trey Anderson

    Excellent script and instructions. I had been trying to do something similar for a week, and my script would never execute. After viewing your guidance, I found out the way I was executing PowerShell was causing my reg keys to go to WOW6432Node. Thank you for the help!

Leave a Reply

Your email address will not be published. Required fields are marked *