Add-Font

Installing a new font in Windows.

This can’t be difficult, right? It is just drag and drop. Get your newest shiny font, open c:\windows\Fonts and drop it there. This should also be very easy to automate then. Well, ouch! There would be no post article then:)

Gathering the pieces

I started with simplest possible solution – I tried copying file to C:\Windows\Fonts. I was requested by providing administrative credentials (I’m not running with scissors 🙂 ). Good. Confirmed that drag and drop is working as expected. That won’t be a viable solution for 20+ workers uploading different fonts daily. Oh, and they don’t have admin rights either!

I tried copying the file with Copy-Item -Credential. It worked but font was not registered. I’ve created proper registry entry in

HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts

but that didn’t do the trick either. Sure enough the font was visible in Fonts folder but was not accesible by any application.

Let the hunt begin

I’ve used some google-fu here and there and it seems that:

  • copying the file using Shell.Application methods and proper copy flag options
  • creating the registry entry manually

is enough. The road to victory seemed cleared. Just one last monster to slay – to add proper registry key I needed the TRUE font name, not the file name. How to retrieve additional properties of a font file (those from ‘details’ tab)?

Those properties are not accessible by any built-in PowerShell cmdlet. I could either use another Shell.Application object and retrieve the Title (or iterate through the 288 properties… Mick Pletcher has a nice article about it here), or I could use System.Drawing assembly for that.

Got the meat, let’s plan

I had all the information I needed to get the cooking planning going. First function flow looked like this:

I wanted to be able to either add one file or point to a directory with fonts, but not both. What about some failsafes? I wanted to be sure that only appropriate file types will be selected and copied to C:\Windows\Fonts folder. Also I wanted to copy the file only if it wasn’t there already. Imagine a user having a folder with bunch of fonts and adding new ones there, while keeping the old one “just for reference”.

So the final flow is something like:

Let the coding begin

Let’s start the fun part. To accept only one parameter I’ve used ParameterSetName in Param() block for each parameter. Parameter $Path accepts only containers (folders) and I labeled it ‘Directory’. Parameter $FontFile accepts only files and is labeled ‘File’. To have ‘Directory’ set as default I added an additional declaration in CmdletBinding() section

[CmdletBinding(DefaultParameterSetName='Directory')]
Param(
[Parameter(Mandatory=$false,
ValueFromPipeline=$True, ValueFromPipelineByPropertyName=$True)]
[Parameter(ParameterSetName='Directory')]
[ValidateScript({Test-Path $_ -PathType Container })]
[System.String[]]
$Path,
[Parameter(Mandatory=$false,
ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[Parameter(ParameterSetName='File')]
[ValidateScript({Test-Path $_ -PathType Leaf })]
[System.String]
$FontFile
)

The Begin{} block initializes variables. To create a Shell.Application with proper flags ReadOnly variable $Fonts is created with special ID (0x14). I’m also using copyFlag options 4+16 which means ‘Do not display a progress dialog’ (which it still does!) and ‘Respond with “yes to all” for any dialog box that is displayed’ to avoid bothering popups during the process.

begin {
Set-Variable Fonts -Value 0x14 -Option ReadOnly
$fontRegistryPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts"
$shell = New-Object -ComObject Shell.Application
$folder = $shell.NameSpace($Fonts)
$objfontFolder = $folder.self.Path
#$copyOptions = 20
$copyFlag = [string]::Format("{0:x}",4+16)
$copyFlag
}

Next interesting thing is getting font files whether directory or file was provided. I’ll need object with properties (Full Path, Name) so I’ll Get-ChildItem through, but only if file has allowed extension:

process {
switch ($PsCmdlet.ParameterSetName) {
"Directory" {
ForEach ($fontsFolder in $Path){
$fontFiles = Get-ChildItem -Path $fontsFolder -File -Recurse -Include @("*.fon", "*.fnt", "*.ttf","*.ttc", "*.otf", "*.mmm", "*.pbf", "*.pfm")
}
}
"File" {
$fontFiles = Get-ChildItem -Path $FontFile -Include @("*.fon", "*.fnt", "*.ttf","*.ttc", "*.otf", "*.mmm", "*.pbf", "*.pfm")
}
}

While in the foreach-object loop and not-yet-registered font condition, I’ll retrieve ‘Details’ properties from the font file with System.Drawing type object:

Add-Type -AssemblyName System.Drawing
$objFontCollection = New-Object System.Drawing.Text.PrivateFontCollection
$objFontCollection.AddFontFile($item.FullName)
$FontName = $objFontCollection.Families.Name

Still in the loop, I’ll copy the file and create registry entry if needed:

$folder.CopyHere($item.FullName, $copyFlag)
$regTest = Get-ItemProperty -Path $fontRegistryPath -Name "*$FontName*" -ErrorAction SilentlyContinue
if (-not ($regTest)) {
New-ItemProperty -Name $FontName -Path $fontRegistryPath -PropertyType string -Value $item.Name
}

That’s all Folks.

Additional information

Add-Font function is a part of PPoShTools module which you can find on GitHub. I like verbosity of my code- it is way easier to debug what went wrong with your automation tools just by looking at the logs. I’m using Write-Log (also a function from PPoShTools module). By default it writes to screen, but with setting a proper context (Set-LogConfiguration ) I can have all my functions in current running workspace log to file or event log instead. No need to rewrite the code!

As always, if you have any comments – feel free to contact me.

P.S. How to allow non-admin user write/run code that requries administrative rights? Check out my previous post.

2 thoughts on “Add-Font

  1. Great post and nice script, but I must disagree in one point. Font filename IS and has always been enough to install the font using Shell.Application’s .CopyHere method, so the script utilizing System.Drawing is interesting, however it seem to be overcomplicated for this task.

    To prove the point I’ve stripped your script from System.Drawing and registry adding sections:

    # Add-Type -AssemblyName System.Drawing
    # $objFontCollection = New-Object System.Drawing.Text.PrivateFontCollection
    # $objFontCollection.AddFontFile($item.FullName)
    # $FontName = $objFontCollection.Families.Name

    # $regTest = Get-ItemProperty -Path $fontRegistryPath -Name “*$FontName*” -ErrorAction SilentlyContinue
    # if (-not ($regTest)) {
    # New-ItemProperty -Name $FontName -Path $fontRegistryPath -PropertyType string -Value $item.Name
    # Write-Host “Registering font {$($item.Name)} in registry with name {$FontName}”
    # }

    And compared the results. It works same way with above commented out – new font is installed, visible in Windows\Fonts as well as visible in app (notepad.exe). Tested with some random TTF @ W7x64. Please test yourself and let me know your result.

    Reading font property via System.Drawing is still great, and hope I’ll find it useful for other tasks some day, so many thanks for that.

    Like

    1. Hi
      I was testing that with Windows 10 and without properly editing the registry – it was not working. While the filename is enough to add it to registry – it is added with the filename and not the actual font name (if it differs). When I sweep through the registry (i.e. cleaning up, fixing) I like to have it simple – and having the registration of font with proper name – helps a lot.
      Thanks for your comment. Will have to test on Win7 when I’ll have some time 😉

      Like

Leave a comment