Fonts.psm1

<#
.SYNOPSIS
    Installs a font in the system

.DESCRIPTION
    Installs a font in the system

.PARAMETER Force
    Force will overwrite existing fonts

.EXAMPLE
    Install-Font -Path C:\FontFiles\Arial.ttf

    Installs the font file 'C:\FontFiles\Arial.ttf' to the current user profile.

.EXAMPLE
    Install-Font -Path C:\FontFiles\Arial.ttf -Scope AllUsers

    Installs the font file 'C:\FontFiles\Arial.ttf' so it is available for all users. This requires administrator rights.

.EXAMPLE
    Install-Font -Path C:\FontFiles\Arial.ttf -Force

    Installs the font file 'C:\FontFiles\Arial.ttf' to the current user profile. If the font already exists, it will be overwritten.

.EXAMPLE
    Install-Font -Path C:\FontFiles\Arial.ttf -Scope AllUsers -Force

    Installs the font file 'C:\FontFiles\Arial.ttf' so it is available for all users. This requires administrator rights. If the font already exists, it will be overwritten.

.EXAMPLE
    Get-ChildItem -Path C:\FontFiles\ -Filter *.ttf | Install-Font

    Gets all font files in the folder 'C:\FontFiles\' and installs them to the current user profile.

.EXAMPLE
    Get-ChildItem -Path C:\FontFiles\ -Filter *.ttf | Install-Font -Scope AllUsers

    Gets all font files in the folder 'C:\FontFiles\' and installs them so it is available for all users. This requires administrator rights.

.EXAMPLE
    Get-ChildItem -Path C:\FontFiles\ -Filter *.ttf | Install-Font -Force

    Gets all font files in the folder 'C:\FontFiles\' and installs them to the current user profile. If the font already exists, it will be overwritten.

.EXAMPLE
    Get-ChildItem -Path C:\FontFiles\ -Filter *.ttf | Install-Font -Scope AllUsers -Force

    Gets all font files in the folder 'C:\FontFiles\' and installs them so it is available for all users. This requires administrator rights. If the font already exists, it will be overwritten.
#>

function Install-Font {
    [CmdletBinding()]
    param (
        # File or folder path(s) to the font(s) to install.
        [Parameter(
            Mandatory,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [Alias('FullName')]
        [string] $Path,

        # Scope of the font installation.
        # CurrentUser will install the font for the current user only.
        # AllUsers will install the font so it is available for all users on the system.
        [Parameter()]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string] $Scope = 'CurrentUser',

        # Recurse will install all fonts in the specified folder and subfolders.
        [Parameter()]
        [switch]$Recurse,

        # Force will overwrite existing fonts
        [Parameter()]
        [switch]$Force
    )

    begin {
        $fontFolderPath = $Scope -eq 'CurrentUser' ? "$env:LOCALAPPDATA\Microsoft\Windows\Fonts" : "$($env:windir)\Fonts"
        $fontRegistryPath = $Scope -eq 'CurrentUser' ? 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' : 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Fonts'

        if ($Scope -eq 'AllUsers' -and -not (Test-Administrator)) {
            throw "Administrator rights are required to install fonts in '$fontFolderPath'. Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command."
        }
    }

    process {
        if (-not (Test-Path -Path $Path)) {
            Write-Error "File [$Path] does not exist."
            return
        }

        $Item = Get-Item -Path $Path -ErrorAction Stop

        if ($Item.PSIsContainer) {
            Write-Verbose "[$($Item.FullName)] - Gathering font(s) to install."
            $FontFiles = Get-ChildItem -Path $Item.FullName -ErrorAction Stop -File -Recurse:$Recurse
            Write-Verbose "[$($Item.FullName)] - Gathering font(s) to install. - [$($FontFiles.Count)] font(s) found."
        } else {
            $FontFiles = $Item
        }

        $shell = New-Object -ComObject Shell.Application

        foreach ($FontFile in $FontFiles) {
            $fontFileDestinationPath = Join-Path $fontFolderPath $FontFile.Name
            $fontFileAlreadyInstalled = Test-Path -Path $fontFileDestinationPath
            if ($fontFileAlreadyInstalled) {
                if ($Force) {
                    Write-Verbose "[$($FontFile.Name)] - Already installed. Forcing install."
                } else {
                    Write-Verbose "[$($FontFile.Name)] - Already installed. Skipping."
                    continue
                }
            }

            $fontType = switch ($FontFile.Extension) {
                '.ttf' { 'TrueType' } # TrueType Font
                '.otf' { 'OpenType' } # OpenType Font
                '.ttc' { 'TrueType' } # TrueType Font Collection
                '.pfb' { 'PostScript Type 1' } # PostScript Type 1 Font
                '.pfm' { 'PostScript Type 1' } # PostScript Type 1 Outline Font
                '.woff' { 'Web Open Font Format' } # Web Open Font Format
                '.woff2' { 'Web Open Font Format 2' } # Web Open Font Format 2
            }

            if ($null -eq $fontType) {
                Write-Warning "[$($FontFile.Name)] - Unknown font type. Skipping."
                continue
            }

            Write-Verbose "[$($FontFile.Name)] - Installing font - [$Scope]"

            $shellFolder = $shell.Namespace($FontFile.Directory.FullName)
            $shellFile = $shellFolder.ParseName($FontFile.name)
            $fontName = $shellFolder.GetDetailsOf($shellFile, 21)



            Write-Verbose "[$($FontFile.Name)] - Installing font - [$fontName]"

            $maxRetries = 10
            $retryIntervalSeconds = 1
            $retryCount = 0
            $fileCopied = $false

            do {
                try {
                    $destinationFilePath = Join-Path $fontFolderPath $FontFile.Name
                    Copy-Item -Path $FontFile.FullName -Destination $destinationFilePath -Force -ErrorAction Stop
                    $fileCopied = $true
                } catch {
                    $retryCount++
                    if (-not $fileRemoved -and $retryCount -eq $maxRetries) {
                        Write-Error $_
                        Write-Error "[$($FontFile.Name)] - Installing font - [$fontName] - Failed. Attempt [$retryCount/$maxRetries]. Stopping."
                        break
                    }
                    Write-Warning "[$($FontFile.Name)] - Installing font - [$fontName] - Failed. Attempt [$retryCount/$maxRetries]. Retrying in $retryIntervalSeconds seconds..."
                    Start-Sleep -Seconds $retryIntervalSeconds
                }
            } while (-not $fileCopied -and $retryCount -lt $maxRetries)

            if (-not $fileCopied) {
                continue
            }
            $registeredFontName = "$fontName ($fontType)"
            Write-Verbose "[$($FontFile.Name)] - Registering font as [$registeredFontName]"
            $regValue = $Scope -eq 'AllUsers' ? $FontFile.Name : $destinationFilePath
            New-ItemProperty -Name "$fontName ($fontType)" -Path $fontRegistryPath -PropertyType string -Value $regValue -Force -ErrorAction stop | Out-Null
        }
    }

    end {}
}

<#
.SYNOPSIS
    Retrieves the installed fonts.

.DESCRIPTION
    Retrieves the installed fonts.

.EXAMPLE
    Get-InstalledFont

    Gets all the fonts installed for the current user.

.EXAMPLE
    Get-InstalledFont -Name 'Arial*'

    Gets all the fonts installed for the current user that start with 'Arial'.

.EXAMPLE
    Get-InstalledFont -Scope 'AllUsers'

    Gets all the fonts installed for all users.

.EXAMPLE
    Get-InstalledFont -Name 'Calibri' -Scope 'AllUsers'

    Gets the font with the name 'Calibri' for all users.

.OUTPUTS
    System.Management.Automation.PSCustomObject[]

#>

function Get-InstalledFont {
    [OutputType([pscustomobject[]])]
    [CmdletBinding()]
    param(
        # Specifies the name of the font to get.
        [Parameter()]
        [SupportsWildcards()]
        [string] $Name = '*',

        # Specifies the scope of the font(s) to get.
        [Parameter()]
        [validateSet('CurrentUser', 'AllUsers')]
        [string] $Scope = 'CurrentUser'
    )

    $fontRegistryPath = $Scope -eq 'CurrentUser' ? 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' : 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Fonts'

    $fontProperties = Get-ItemProperty -Path $fontRegistryPath
    $filteredFontProperties = $fontProperties.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' -and $_.Name -like $Name }
    $fonts = @()
    foreach ($fontProperty in $filteredFontProperties) {
        $font = [PSCustomObject]@{
            Name  = $fontProperty.Name
            Path  = $Scope -eq 'AllUsers' ? (Join-Path "$($env:windir)\Fonts" $fontProperty.Value) : $fontProperty.Value
            Scope = $Scope
        }
        $fonts += $font
    }

    return $fonts
}

<#
.SYNOPSIS
    Uninstalls a font from the system.

.DESCRIPTION
    Uninstalls a font from the system.

.EXAMPLE
    Uninstall-Font -Name 'Courier New'

    Uninstalls the 'Courier New' font from the system for the current user.

.EXAMPLE
    Uninstall-Font -Name 'Courier New' -Scope AllUsers

    Uninstalls the Courier New font from the system for all users.

.OUTPUTS
    None
#>

function Uninstall-Font {
    [CmdletBinding()]
    param (
        # Scope of the font to uninstall.
        # CurrentUser will uninstall the font for the current user.
        # AllUsers will uninstall the font so it is removed for all users.
        [Parameter(
            Position = 0,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true
        )]
        [ValidateSet('CurrentUser', 'AllUsers')]
        [string] $Scope = 'CurrentUser'
    )

    DynamicParam {
        $runtimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

        $parameterName = 'Name'
        $parameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $parameterAttribute.Mandatory = $true
        $parameterAttribute.Position = 1
        $parameterAttribute.HelpMessage = 'Name of the font to uninstall.'
        $parameterAttribute.ValueFromPipeline = $true
        $parameterAttribute.ValueFromPipelineByPropertyName = $true
        $attributeCollection.Add($parameterAttribute)
        $parameterValidateSet = (Get-InstalledFont -Scope $Scope).Name
        $validateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($parameterValidateSet)
        $attributeCollection.Add($validateSetAttribute)
        $runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($parameterName, [string], $attributeCollection)
        $runtimeParameterDictionary.Add($parameterName, $runtimeParameter)
        return $runtimeParameterDictionary
    }

    begin {
        if ($Scope -eq 'AllUsers' -and -not (Test-Administrator)) {
            throw "Administrator rights are required to uninstall fonts. Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command."
        }
    }

    process {

        $Name = $PSBoundParameters['Name']

        Write-Verbose "[$Name] - Uninstalling font - [$Scope]"
        $font = Get-InstalledFont -Name $Name -Scope $Scope
        $filePath = $font.path

        Write-Verbose "[$Name] - Removing file [$filePath]"

        $maxRetries = 10
        $retryIntervalSeconds = 1
        $retryCount = 0
        $fileRemoved = $false

        do {
            try {
                # Try to remove the file
                Remove-Item -Path $filePath -Force -ErrorAction Stop
                $fileRemoved = $true
            } catch {
                # Handle any exceptions here (e.g., file in use)
                $retryCount++
                if (-not $fileRemoved -and $retryCount -eq $maxRetries) {
                    Write-Error $_
                    Write-Error "[$Name] - Removing file [$filePath] - Failed. Attempt [$retryCount/$maxRetries]. Stopping."
                    break
                }
                Write-Warning "[$Name] - Removing file [$filePath] - Failed. Attempt [$retryCount/$maxRetries]. Retrying in $retryIntervalSeconds seconds..."
                Start-Sleep -Seconds $retryIntervalSeconds
            }
        } while (-not $fileRemoved -and $retryCount -lt $maxRetries)

        if (-not $fileRemoved) {
            return
        }

        $fontRegistryPath = $Scope -eq 'CurrentUser' ? 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' : 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Fonts'
        Write-Verbose "[$Name] - Unregistering font [$fontRegistryPath]"
        Remove-ItemProperty -Path $fontRegistryPath -Name $Name -Force -ErrorAction Stop
    }
}

<#
.SYNOPSIS
    Test if the current user is an administrator.
.DESCRIPTION
    Test if the current user is an administrator.
.OUTPUTS
    Boolean
.EXAMPLE
    Test-Administrator

    Returns true if the current user is an administrator.
#>

function Test-Administrator {
    [OutputType([Boolean])]
    $user = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal($user)
    $isAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    return $isAdmin
}