Skip to content

Instantly share code, notes, and snippets.

@glektarssza
Created June 3, 2025 06:04
Show Gist options
  • Save glektarssza/9e581969e8073c4a030f7efc2d068636 to your computer and use it in GitHub Desktop.
Save glektarssza/9e581969e8073c4a030f7efc2d068636 to your computer and use it in GitHub Desktop.
Install Windows Fonts without Elevated Permissions
<#
.SYNOPSIS
Install fonts.
.DESCRIPTION
Install the fonts to the system.
.PARAMETER Path
The paths to the font files to install.
.PARAMETER FontExtensions
A list of file extensions to consider as font files for installation.
.PARAMETER DryRun
Whether to perform a "dry run" and not actually perform any operations.
.PARAMETER Force
Whether to force an attempt at installation of the font even if it's already
installed.
.EXAMPLE
Install-Font -Path ./path/to/font.ttf
# Install a single font file.
.EXAMPLE
Install-Font -Path ./path/to/font.ttf -Force
# Install a single font file, overwriting any existing font already named
# "font.ttf".
.EXAMPLE
Install-Font -Path ./path/to/font.ttf,./path/to/another/font.otf
# Install multiple font files.
.EXAMPLE
Get-ChildItem -File -Path ./fonts/ | Install-Font
# Install all font files from a given folder.
#>
function Install-Fonts {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
[ValidateNotNullOrWhiteSpace()]
[String]
$Path,
[Parameter(Mandatory = $false)]
[ValidateNotNullOrWhiteSpace()]
[String[]]
$FontExtensions = @(".ttf", ".otf", ".woff", ".woff2", ".eot"),
[Parameter(Mandatory = $false)]
[switch]
$DryRun,
[Parameter(Mandatory = $false)]
[switch]
$Force
)
Begin {
$stagingDirPath = "C:\Windows\Temp\Fonts";
Write-Debug "Font staging directory is at '$stagingDirPath'";
if (-not $DryRun) {
$stagingDir = New-Item -Path $stagingDirPath -ItemType Directory -Force;
}
$stagedFonts = [System.Collections.Generic.List[string]]::new();
$installedFonts = [System.Collections.Generic.List[System.IO.FileInfo]]::new();
Get-ChildItem -Path "$env:LOCALAPPDATA\Microsoft\Windows\Fonts" -Recurse -File | ForEach-Object {
$file = $_;
if (([System.Linq.Enumerable]::Any([string[]]$FontExtensions, [System.Func[string, bool]] {
param($ext)
return $file.Name.ToLower().EndsWith($ext.ToLower());
}))) {
Write-Debug "Found installed font '$($file.Name)' at '$($file.FullName)'";
$installedFonts.Add($file) | Out-Null;
}
}
Get-ChildItem -Path "C:\Windows\Fonts" -Recurse -File | ForEach-Object {
$file = $_;
if (([System.Linq.Enumerable]::Any([string[]]$FontExtensions, [System.Func[string, bool]] {
param($ext)
return $file.Name.ToLower().EndsWith($ext.ToLower());
}))) {
Write-Debug "Found installed font '$($file.Name)' at '$($file.FullName)'";
$installedFonts.Add($file) | Out-Null;
}
}
}
Process {
if (-not (Test-Path -Path $Path -PathType Leaf)) {
Write-Warning "Font file '$Path' is not a file!";
return;
}
$file = Get-Item -Path $Path;
Write-Debug "Considering font '$($file.FullName)' for installation...";
if (-not ([System.Linq.Enumerable]::Any([string[]]$FontExtensions, [System.Func[string, bool]] {
param($ext)
return $file.Name.ToLower().EndsWith($ext.ToLower());
}))) {
Write-Debug "Font '$($file.Name)' failed the extension check, skipping!";
return;
}
if ( ([System.Linq.Enumerable]::Any($installedFonts, [System.Func[System.IO.FileInfo, bool]] {
param($installedFont)
return ($file.BaseName.ToLower() -eq $installedFont.BaseName.ToLower());
})) -and (-not $Force)) {
Write-Debug "Font '$($file.Name)' is already installed, skipping!";
return;
}
if (-not $DryRun) {
Write-Debug "Staging font '$($file.Name)' for installation...";
Copy-Item -Path $file -Destination $stagingDir;
}
else {
Write-Debug "Would have staged font '$($file.Name)' for installation...";
}
$stagedFonts.Add((Join-Path -Path $stagingDirPath -ChildPath $file.Name)) | Out-Null;
}
End {
Write-Debug "Starting font install process...";
Write-Debug "Getting font install directory...";
# NOTE: Little Windows trick to get the special font folder object
$fontDestination = (New-Object -ComObject Shell.Application).Namespace(0x14);
$fontDestinationPath = $local:fontDestination.Self.Path;
Write-Debug "Font install directory is at '$fontDestinationPath'";
if (-not $DryRun) {
Write-Debug "Installing fonts from staging directory at '$stagingDirPath'...";
if ($stagedFonts.Count -le 0) {
Write-Warning "All fonts requested to for install are already installed!";
} else {
$fontDestination.CopyHere((New-Object -ComObject Shell.Application).Namespace($stagingDirPath).Items(), 0x10);
Write-Debug "Cleaning up staging directory...";
$stagedFonts | ForEach-Object {
Remove-Item -Path $_ -Force -ErrorAction SilentlyContinue;
}
}
}
else {
$stagedFonts | ForEach-Object {
Write-Debug "Would have installed '$([System.IO.Path]::GetFileName($_))'";
}
}
Write-Debug "Done font install process";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment