Set custom wallpaper in Windows 10 during OSD with ConfigMgr

191_0

Windows 10 has been around for a while now, and so have various methods for branding the operating system, like importing custom registry files, executing batch files and so on. If you’ve visited my site before, you should know by now that I’m no fan of using legacy methods (even though they may work just fine, my goal is to find new ways by using modern technologies). A good friend of mine, Jörgen Nilsson, has made an excellent blog post about how to set a custom wallpaper (background) when deploying Windows 10. Jörgens method triggered me to attempt converting it into a PowerShell script instead.

Windows 10 wallpapers

For those of us that have been branding Windows for years now, we’re familiar with the process of replacing img0.jpg in C:\Windows\Web\Wallpaper\Windows. However, with the release of Windows 10, support for 4K and 8K screen resolutions was introduced, in addition to a method that automatically detects the current resolution and attempts to set a wallpaper that matches the width and height. So in Windows 10, we now have two places that we need to configure in order to properly set a custom wallpaper:

  • C:\Windows\Web\Wallpaper\Windows
  • C:\Windows\Web\4K\Wallpaper\Windows

If you browse the 4K folder, you’ll see that the default Windows 10 wallpaper are present in various resolutions. From my understanding, Windows will attempt to find a suitable wallpaper in this folder that matches the current resolution. If there’s no match, it will default back to the wallpaper in C:\Windows\Web\Wallpaper\Windows. This means, that we also have to replace our custom wallpaper with the defaults in the 4K folder.

Bonus: I’ve not yet tested what would happen if the wallpapers in the 4K folder were simply removed, but my guess is that Windows would default to the img0.jpg in C:\Windows\Web\Wallpaper\Windows.

Script

So in order to set a custom wallpaper in Windows 10, I’ve converted Jörgens method (originally a batch-script) into a PowerShell script with some more logic and error handling. In addition, I’ve also added some logging in a file called SetDefaultWallpaper.log that will be created in C:\Windows\Temp. You’ll find the script below and on my GitHub repository. For the script to properly work, you’ll need to create the following folder structure:
191_1

Below is an explanation of the various components required by the Set-DefaultWallpaper.ps1 script for it to work properly:

  • 4K folder
    • Place your 4K wallpapers in this folder. Make sure that you name them accordingly as the defaults in C:\Windows\Web\4K\Wallpaper\Windows.
  • Modules folder
  • img0.jpg
    • This is the default wallpaper that you want to used when none of the wallpapers in the 4K folder suits the resolution.

If you do not include the NTFSSecurity module in the proper folder, the whole process will break since the PowerShell script relies on this module to take and set ownership of the existing wallpaper files (this is required, and well documented in Jörgens post).

Save the below script as Set-DefaultWallpaper.ps1 and put it in the same folder where you’ve created the required components as described above.

# Functions
function Write-LogFile {
	param(
		[parameter(Mandatory=$true, HelpMessage="Name of the log file, e.g. 'FileName'. File extension should not be specified")]
		[ValidateNotNullOrEmpty()]
		[string]$Name,
		[parameter(Mandatory=$true, HelpMessage="Value added to the specified log file")]
		[ValidateNotNullOrEmpty()]
		[string]$Value,
		[parameter(Mandatory=$true, HelpMessage="Choose a location where the log file will be created")]
		[ValidateNotNullOrEmpty()]
		[ValidateSet("UserTemp","WindowsTemp")]
		[string]$Location
	)
	# Determine log file location
	switch ($Location) {
		"UserTemp" { $LogLocation = ($env:TEMP + "\") }
		"WindowsTemp" { $LogLocation = ($env:SystemRoot + "\Temp\") }
	}
	# Construct log file name and location
	$LogFile = ($LogLocation + $Name + ".log")
	# Create log file unless it already exists
	if (-not(Test-Path -Path $LogFile -PathType Leaf)) {
		New-Item -Path $LogFile -ItemType File -Force | Out-Null
	}
	# Add timestamp to value
	$Value = (Get-Date).ToShortDateString() + ":" + (Get-Date).ToLongTimeString() + " - " + $Value
	
	# Add value to log file
	Add-Content -Value $Value -LiteralPath $LogFile
}

# Stage NTFSSecurity module
try {
    $NTFSSecurityModulePath = Join-Path -Path $env:WINDIR -ChildPath "System32\WindowsPowerShell\v1.0\Modules\NTFSSecurity"
    if (-not(Test-Path -Path $NTFSSecurityModulePath -PathType Container)) {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Staging NTFSSecurity module in: $($NTFSSecurityModulePath)"
        Copy-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath "Modules") -Destination $NTFSSecurityModulePath -Recurse -ErrorAction Stop
    }
}
catch [System.Exception] {
    Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to stage required PowerShell module: NTFSSecurity" ; break
}

# Import NTFSSecurity module
try {
    Import-Module -Name NTFSSecurity -ErrorAction Stop
}
catch [System.Exception] {
    Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to import required PowerShell module: NTFSSecurity" ; break
}

# Scripts variables
$NewOwner = "$($env:COMPUTERNAME)\Administrator"
$SystemContext = "NT AUTHORITY\SYSTEM"
$DefaultWallpaperRootPath = Join-Path -Path $PSScriptRoot -ChildPath "img0.jpg"

# Process C:\Windows\WEB\Wallpaper\Windows\img0.jpg
try {
    # Take ownership
    $DefaultWallpaperImagePath = Join-Path -Path $env:WINDIR -ChildPath "WEB\Wallpaper\Windows\img0.jpg"
    $CurrentOwner = Get-Item -Path $DefaultWallpaperImagePath | Get-NTFSOwner
    if ($CurrentOwner.Owner -notlike $NewOwner) {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Setting new owner of '$($NewOwner)' on: $($DefaultWallpaperImagePath)"
        Set-NTFSOwner -Path $DefaultWallpaperImagePath -Account $NewOwner -ErrorAction Stop
    }

    # Grant NT AUTHORITY\SYSTEM and Local Administrator Full Control access
    try {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Granting '$($SystemContext)' Full Control on: $($DefaultWallpaperImagePath)"
        Add-NTFSAccess -Path $DefaultWallpaperImagePath -Account $SystemContext -AccessRights FullControl -AccessType Allow -ErrorAction Stop
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Granting '$($NewOwner)' Full Control on: $($DefaultWallpaperImagePath)"
        Add-NTFSAccess -Path $DefaultWallpaperImagePath -Account $NewOwner -AccessRights FullControl -AccessType Allow -ErrorAction Stop
    }
    catch [System.Exception] {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to grant required Full Control permissions on: $($DefaultWallpaperImagePath)" ; break
    }

    # Replace wallpaper
    try {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Replacing default wallpaper in: $($DefaultWallpaperImagePath)"
        Remove-Item -Path $DefaultWallpaperImagePath -Force -ErrorAction Stop
        Copy-Item -Path $DefaultWallpaperRootPath -Destination $DefaultWallpaperImagePath -Force -ErrorAction Stop
    }
    catch [System.Exception] {
        Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to replace default wallpaper: $($DefaultWallpaperImagePath)" ; break
    }
}
catch [System.Exception] {
    Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to take ownership of: $($DefaultWallpaperImagePath)" ; break
}

# Process C:\Windows\WEB\4K\Wallpaper\Windows recursively
$HDWallpaperRoot = Join-Path -Path $PSScriptRoot -ChildPath "4K"
$HDWallpapers = Get-ChildItem -Path $HDWallpaperRoot
if (($HDWallpapers | Measure-Object).Count -ge 1) {
	$LocalHDWallpapersPath = Join-Path -Path $env:WINDIR -ChildPath "WEB\4K\Wallpaper\Windows"
	$LocalHDWallpapers = Get-ChildItem -Path $LocalHDWallpapersPath -Recurse -Filter *.jpg
	foreach ($LocalHDWallpaper in $LocalHDWallpapers) {
		# Take ownership
		$CurrentOwner = Get-Item -Path $LocalHDWallpaper.FullName | Get-NTFSOwner
		if ($CurrentOwner.Owner -notlike $NewOwner) {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Setting new owner of '$($NewOwner)' on: $($LocalHDWallpaper.FullName)"
			Set-NTFSOwner -Path $LocalHDWallpaper.FullName -Account $NewOwner -ErrorAction Stop
		}

		# Grant NT AUTHORITY\SYSTEM Full Control access
		try {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Granting '$($SystemContext)' Full Control on: $($LocalHDWallpaper.FullName)"
			Add-NTFSAccess -Path $LocalHDWallpaper.FullName -Account $SystemContext -AccessRights FullControl -AccessType Allow -ErrorAction Stop
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Granting '$($NewOwner)' Full Control on: $($LocalHDWallpaper.FullName)"
			Add-NTFSAccess -Path $LocalHDWallpaper.FullName -Account $NewOwner -AccessRights FullControl -AccessType Allow -ErrorAction Stop
		}
		catch [System.Exception] {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to grant required Full Control permissions on: $($LocalHDWallpaper.FullName)" ; break
		}

		# Remove default wallpaper
		try {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Removing default wallpaper: $($LocalHDWallpaper.FullName)"
			Remove-Item -Path $LocalHDWallpaper.FullName -Force -ErrorAction Stop
		}
		catch [System.Exception] {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to remove default wallpaper: $($LocalHDWallpaper.FullName)" ; break
		}
	}

	# Copy default wallpapers
	foreach ($HDWallpaper in $HDWallpapers) {
		try {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Copying '$($HDWallpaper.FullName)' wallpaper to: $($LocalHDWallpapersPath)"
			Copy-Item -Path $HDWallpaper.FullName -Destination $LocalHDWallpapersPath -Force -ErrorAction Stop
		}
		catch [System.Exception] {
			Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to copy default wallpaper '$($HDWallpaper.FullName)' to: $($LocalHDWallpapersPath)" ; break
		}
	}
}
else {
	Write-LogFile -Name "SetDefaultWallpaper" -Location WindowsTemp -Value "Unable to locate wallpapers in 4K root folder when processing, skipping the 4K wallpapers"
}

Using the script in ConfigMgr

With the above described folder structure in place and the script saved, copy it to a folder in your Content Library in ConfigMgr. The process described below is pretty straight forward and consists of creating a Package in ConfigMgr with the source content prepared earlier in this post and finally adding a Run PowerShell Script step to your Windows 10 task sequence at some point after the Setup ConfigMgr and Windows step.

I’ll not cover the steps of creating a Package, as that should be something everyone is familiar with (if not, you have some reading to do). Simply create a regular Package with no Program, and point the content source to the prepared folder with all the contents. Edit your Windows 10 task sequence and add a step like shown below, where you point to your Package and simply just call the PowerShell script Set-DefaultWallpaper.ps1:
191_2

Remember to set the PowerShell execution policy to Bypass, or the script will not execute properly. Below is a snippet from how the SetDefaultWallpaper.log file will look after a successful deployment:
191_3

Once you deploy Windows 10 from now on, you’ll get your custom wallpaper instead of the default one.

3 Comments

  1. Nathan McNulty

    Nickolaj, an even easier way to do it is to overwrite the files after the WIM image has been applied but before the next reboot (so create the step between Apply Operating System and Apply Windows Settings). This way you don’t have change any permissions, and Windows will apply the proper permissions to your files on first boot!

    Reply
    1. NickolajNickolaj (Post author)

      Thanks for sharing! 🙂

      Regards,
      Nickolaj

      Reply
  2. Carl

    unable to stage required Powershell Module Windowss 10 Ent v1607 can not seem to make it work Any idea ?

    Reply

Leave a Comment

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