Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active June 20, 2024 03:07
Show Gist options
  • Save Jaykul/d53a16ce5e7d50b13530acb4f98aaabd to your computer and use it in GitHub Desktop.
Save Jaykul/d53a16ce5e7d50b13530acb4f98aaabd to your computer and use it in GitHub Desktop.
Temporary Font Install?

Did you know you can install fonts without elevation?

The catch is that they're only available for the duration of your session. They are, however, available in all apps across the system.

Someone asked about how to do it on Facebook this week, and at first, I just pointed them at the install script for PowerLineFonts which loops through all the fonts in a folder and install them.

I've used this more than a few times to install some fonts, including the PowerLine ones, which are great:

$sa = New-Object -ComObject Shell.Application
$fonts =  $sa.NameSpace("shell:fonts")

foreach($font in Get-ChildItem -Recurse -Include *.ttf, *.otg) { 
    $fonts.CopyHere($font.FullName)
}

The problem is that this seems to require you to have administrative rights (it's actually putting the fonts in C:\Windows\Fonts), and this guy didn't have that, and was looking for a way to install the fonts just for a single session. It turns out there is a Win32 API for that.

Now, when you see a windows API that simple (it just takes a string), you can just write the DllImport yourself. However, if you don't know where to start, there's something even simpler. Look it up on PInvoke and you can just copy paste the C# declaration. You just have to call Add-Type and specify the type name, and an empty namespace, and make sure that the declaration has public on the front. This one looks like this:

add-type -name Session -namespace "" -member @"
[DllImport("gdi32.dll")]
public static extern int AddFontResource(string filePath);
"@

$null = foreach($font in Get-ChildItem -Recurse -Include *.ttf, *.otg) {
    [Session]::AddFontResource($font.FullName)
}

That's all there is to it. It adds the font to your session, and it'll be gone after reboot.

Technically, you're supposed to call RemoveFontResource to unload the fonts, and notify everyone when you've added (or removed) fonts by sending a WM_FONTCHANGE broadcast message. That gets a little more complicated, but the whole thing is actually there as the C# code for an executable on that PInvoke page.

We could map that to PowerShell wrapping Get-ChildItem if we wanted to, and even stick it into a Fonts module ;-)

add-type -name Session -namespace "" -member @"
[DllImport("gdi32.dll")]
public static extern bool AddFontResource(string filePath);
[DllImport("gdi32.dll")]
public static extern bool RemoveFontResource(string filePath);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam = 0, int lParam = 0);
"@
function Get-Font {
[CmdletBinding(DefaultParameterSetName='Items', SupportsTransactions=$true, HelpUri='http://go.microsoft.com/fwlink/?LinkID=113308')]
param(
[Parameter(ParameterSetName='Items', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralItems', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath},
[Parameter(Position=1)]
[string]
${Filter},
[string[]]
${Include},
[string[]]
${Exclude},
[Alias('s')]
[switch]
${Recurse},
[uint32]
${Depth},
[switch]
${Force},
[switch]
${Name}
)
dynamicparam
{
try {
$targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-ChildItem', [System.Management.Automation.CommandTypes]::Cmdlet, $PSBoundParameters)
$dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
if ($dynamicParams.Length -gt 0)
{
$paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
foreach ($param in $dynamicParams)
{
$param = $param.Value
if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
{
$dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
$paramDictionary.Add($param.Name, $dynParam)
}
}
return $paramDictionary
}
} catch {
throw
}
}
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$PSBoundParameters['Include'] = "*.fon", "*.fnt", "*.ttf", "*.ttc", "*.fot", "*.otf", "*.mmm", "*.pfb", "*.pfm"
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Get-ChildItem', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
if($MyInvocation.InvocationName -eq "Add-Font") {
$scriptCmd = {& $wrappedCmd @PSBoundParameters | Microsoft.PowerShell.Core\ForEach-Object {
if([Session]::AddFontResource($_.FullName)) {
Write-Verbose "Added Font: $(Resolve-Path $_.FullName -Relative)"
}
else {
Write-Warning "Failed on $(Resolve-Path $_.FullName -Relative)"
}
} }
} elseif($MyInvocation.InvocationName -eq "Remove-Font") {
$scriptCmd = {& $wrappedCmd @PSBoundParameters | Microsoft.PowerShell.Core\ForEach-Object {
if([Session]::RemoveFontResource($_.FullName)) {
Write-Verbose "Removed Font: $(Resolve-Path $_.FullName -Relative)"
}
else {
Write-Warning "Failed on $(Resolve-Path $_.FullName -Relative)"
}
} }
}
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
$EVERYONE = New-Object IntPtr 0xffff
$FONTCHANGE = 0x1D
$NULL = [Session]::PostMessage($EVERYONE, $FONTCHANGE)
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Management\Get-ChildItem
.ForwardHelpCategory Cmdlet
#>
}
Set-Alias Add-Font Get-Font
Set-Alias Remove-Font Get-Font
Export-ModuleMember -Function "Get-Font" -Alias "Add-Font", "Remove-Font"
@Halkcyon
Copy link

Halkcyon commented Nov 14, 2018

A suggestion: use -TypeDefinition on Add-Type and don't worry about all those extra parameters (it is the default paramset):

Add-Type @'
using System;
using System.Runtime.InteropServices;

public static class Font
{
    public const int HWND_BROADCAST = 0xFFFF;
    public const int WM_FONTCHANGE = 0x001D;

    [DllImport("gdi32.dll")]
    public static extern int AddFontResource(string filePath);

    [DllImport("user32.dll")]
    public static extern int RegisterWindowMessage(string lpString);

    [DllImport("user32.dll")]
    public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam);
}
'@

@aalexgabi
Copy link

aalexgabi commented Feb 22, 2022

My take on this script:

  • Will fail/throw if it cannot register/unregister font.
  • Broadcasts WM_FONTCHANGE to inform all windows that fonts have changed
  • Does not require administrator privileges
  • Does not install fonts in Windows, only makes them available for all programs in current session until reboot
  • Has verbose mode for debugging
  • Does not work with font folders

Usage:

register-fonts.ps1 [-v] [-unregister <PATH>[,<PATH>...]] [-register  <PATH>[,<PATH>...]] # Register and unregister at same time
register-fonts.ps1 [-v] -unregister <PATH>
register-fonts.ps1 [-v] -register <PATH>
register-fonts.ps1 [-v] <PATH> # Will register font path
Param (
  [Parameter(Mandatory=$False)]
  [String[]]$register,

  [Parameter(Mandatory=$False)]
  [String[]]$unregister
)

# Stop script if command fails https://stackoverflow.com/questions/9948517/how-to-stop-a-powershell-script-on-the-first-error
$ErrorActionPreference = "Stop"

add-type -name Session -namespace "" -member @"
[DllImport("gdi32.dll")]
public static extern bool AddFontResource(string filePath);
[DllImport("gdi32.dll")]
public static extern bool RemoveFontResource(string filePath);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool PostMessage(IntPtr hWnd, int Msg, int wParam = 0, int lParam = 0);
"@

$broadcast = $False;
Foreach ($unregisterFontPath in $unregister) {
  Write-Verbose "Unregistering font $unregisterFontPath"
  # https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-removefontresourcea
  $success = [Session]::RemoveFontResource($unregisterFontPath)
  if (!$success) {
    Throw "Cannot unregister font $unregisterFontPath"
  }
  $broadcast = $True
}

Foreach ($registerFontPath in $register) {
  Write-Verbose "Registering font $registerFontPath"
  # https://docs.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-addfontresourcea
  $success = [Session]::AddFontResource($registerFontPath)
  if (!$success) {
    Throw "Cannot register font $registerFontPath"
  }
  $broadcast = $True
}

if ($broadcast) {
  # HWND_BROADCAST https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
  $HWND_BROADCAST = New-Object IntPtr 0xffff
  # WM_FONTCHANGE https://docs.microsoft.com/en-us/windows/win32/gdi/wm-fontchange
  $WM_FONTCHANGE  = 0x1D

  Write-Verbose "Broadcasting font change"
  # Broadcast will let other programs know that fonts were changed https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea
  $success = [Session]::PostMessage($HWND_BROADCAST, $WM_FONTCHANGE)
  if (!$success) {
    Throw "Cannot broadcase font change"
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment