Fonts.psm1
[CmdletBinding()] param() if ($PSVersionTable.PSVersion -lt '6.0') { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Compatibility with PowerShell 6.0 and newer.' )] $IsWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT' } $scriptName = 'Fonts' Write-Verbose "[$scriptName] - Importing module" #region - From [classes] Write-Verbose "[$scriptName] - [classes] - Processing folder" #region - From [classes] - [Scope] Write-Verbose "[$scriptName] - [classes] - [Scope] - Importing" enum Scope { CurrentUser AllUsers } Write-Verbose "[$scriptName] - [classes] - [Scope] - Done" #endregion - From [classes] - [Scope] Write-Verbose "[$scriptName] - [classes] - Done" #endregion - From [classes] #region - From [public] Write-Verbose "[$scriptName] - [public] - Processing folder" #region - From [public] - [Get-Font] Write-Verbose "[$scriptName] - [public] - [Get-Font] - Importing" function Get-Font { <# .SYNOPSIS Retrieves the installed fonts. .DESCRIPTION Retrieves the installed fonts. .EXAMPLE Get-Font Gets all the fonts installed for the current user. .EXAMPLE Get-Font -Name 'Arial*' Gets all the fonts installed for the current user that start with 'Arial'. .EXAMPLE Get-Font -Scope 'AllUsers' Gets all the fonts installed for all users. .EXAMPLE Get-Font -Name 'Calibri' -Scope 'AllUsers' Gets the font with the name 'Calibri' for all users. .OUTPUTS [System.Collections.Generic.List[PSCustomObject]] #> [Alias('Get-Fonts')] [OutputType([System.Collections.Generic.List[PSCustomObject]])] [CmdletBinding()] param( # Specifies the name of the font to get. [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [SupportsWildcards()] [string[]] $Name = '*', # Specifies the scope of the font(s) to get. [Parameter( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Scope[]] $Scope = 'CurrentUser' ) begin { $functionName = $MyInvocation.MyCommand.Name Write-Verbose "[$functionName]" } process { $scopeCount = $Scope.Count Write-Verbose "[$functionName] - Processing [$scopeCount] scope(s)" foreach ($ScopeItem in $Scope) { $scopeName = $ScopeItem.ToString() Write-Verbose "[$functionName] - [$scopeName] - Getting font(s)" $fontFolderPath = $script:FontFolderPathMap[$script:OS][$scopeName] Write-Verbose "[$functionName] - [$scopeName] - Font folder path: [$fontFolderPath]" $folderExists = Test-Path -Path $fontFolderPath Write-Verbose "[$functionName] - [$scopeName] - Folder exists: [$folderExists]" if (-not $folderExists) { return $fonts } $installedFonts = Get-ChildItem -Path $fontFolderPath -File $installedFontsCount = $($installedFonts.Count) Write-Verbose "[$functionName] - [$scopeName] - Filtering from [$installedFontsCount] font(s)" $nameCount = $Name.Count Write-Verbose "[$functionName] - [$scopeName] - Filtering based on [$nameCount] name pattern(s)" foreach ($fontFilter in $Name) { Write-Verbose "[$functionName] - [$scopeName] - [$fontFilter] - Filtering font(s)" $filteredFonts = $installedFonts | Where-Object { $_.BaseName -like $fontFilter } foreach ($fontItem in $filteredFonts) { $fontName = $fontItem.BaseName $fontPath = $fontItem.FullName $fontScope = $scopeName Write-Verbose "[$functionName] - [$scopeName] - [$fontFilter] - Found [$fontName] at [$fontPath]" [PSCustomObject]@{ Name = $fontName Path = $fontPath Scope = $fontScope } } Write-Verbose "[$functionName] - [$scopeName] - [$fontFilter] - Done" } Write-Verbose "[$functionName] - [$scopeName] - Done" } } end {} } Write-Verbose "[$scriptName] - [public] - [Get-Font] - Done" #endregion - From [public] - [Get-Font] #region - From [public] - [Install-Font] Write-Verbose "[$scriptName] - [public] - [Install-Font] - Importing" #Requires -Modules Admin function Install-Font { <# .SYNOPSIS Installs a font in the system .DESCRIPTION Installs a font in the system .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. #> [Alias('Install-Fonts')] [CmdletBinding(SupportsShouldProcess)] param ( # File or folder path(s) to the font(s) to install. [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [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( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Scope[]] $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 { $functionName = $MyInvocation.MyCommand.Name Write-Verbose "[$functionName]" if ($Scope -contains 'AllUsers' -and -not (IsAdmin)) { $errorMessage = @" Administrator rights are required to install fonts in [$($script:FontFolderPathMap[$script:OS]['AllUsers'])]. Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command. "@ throw $errorMessage } $maxRetries = 10 $retryIntervalSeconds = 1 } process { $scopeCount = $Scope.Count Write-Verbose "[$functionName] - Processing [$scopeCount] scopes(s)" foreach ($scopeItem in $Scope) { $scopeName = $scopeItem.ToString() $fontDestinationFolderPath = $script:FontFolderPathMap[$script:OS][$scopeName] $pathCount = $Path.Count Write-Verbose "[$functionName] - [$scopeName] - Processing [$pathCount] path(s)" foreach ($PathItem in $Path) { Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - Processing" $pathExists = Test-Path -Path $PathItem -ErrorAction SilentlyContinue if (-not $pathExists) { Write-Error "[$functionName] - [$scopeName] - [$PathItem] - Path not found, skipping." continue } $item = Get-Item -Path $PathItem -ErrorAction Stop if ($item.PSIsContainer) { Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - Folder found" Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - Gathering font(s) to install" $fontFiles = Get-ChildItem -Path $item.FullName -ErrorAction Stop -File -Recurse:$Recurse Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - Found [$($fontFiles.Count)] font file(s)" } else { Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - File found" $fontFiles = $Item } foreach ($fontFile in $fontFiles) { $fontFileName = $fontFile.Name $fontName = $fontFile.BaseName $fontFilePath = $fontFile.FullName Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Processing" # Check if font is supported $fontExtension = $fontFile.Extension.ToLower() $supportedFont = $script:SupportedFonts | Where-Object { $_.Extension -eq $fontExtension } if (-not $supportedFont) { Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Font type [$fontExtension] is not supported. Skipping." continue } $folderExists = Test-Path -Path $fontDestinationFolderPath -ErrorAction SilentlyContinue if (-not $folderExists) { Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Creating folder [$fontDestinationFolderPath]" $null = New-Item -Path $fontDestinationFolderPath -ItemType Directory -Force } $fontDestinationFilePath = Join-Path -Path $fontDestinationFolderPath -ChildPath $fontFileName $fontFileAlreadyInstalled = Test-Path -Path $fontDestinationFilePath if ($fontFileAlreadyInstalled) { if ($Force) { Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Already installed. Forcing install." } else { Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Already installed. Skipping." continue } } Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Installing font" $retryCount = 0 $fileCopied = $false do { try { $null = $fontFile.CopyTo($fontDestinationFilePath) $fileCopied = $true } catch { $retryCount++ if (-not $fileRemoved -and $retryCount -eq $maxRetries) { Write-Error $_ Write-Error "Failed [$retryCount/$maxRetries] - Stopping" break } Write-Verbose "Failed [$retryCount/$maxRetries] - Retrying in $retryIntervalSeconds seconds..." Start-Sleep -Seconds $retryIntervalSeconds } } while (-not $fileCopied -and $retryCount -lt $maxRetries) if (-not $fileCopied) { continue } if ($IsWindows) { $fontType = $script:SupportedFonts | Where-Object { $_.Extension -eq $fontExtension } | Select-Object -ExpandProperty Type $registeredFontName = "$fontName ($fontType)" Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Registering font as [$registeredFontName]" $regValue = if ('AllUsers' -eq $Scope) { $fontFileName } else { $fontDestinationFilePath } $params = @{ Name = $registeredFontName Path = $script:FontRegPathMap[$scopeName] PropertyType = 'string' Value = $regValue Force = $true ErrorAction = 'Stop' } $null = New-ItemProperty @params } Write-Verbose "[$functionName] - [$scopeName] - [$fontFilePath] - Done" } if ($item.PSIsContainer) { Write-Verbose "[$functionName] - [$scopeName] - [$PathItem] - Done" } } Write-Verbose "[$functionName] - [$scopeName] - Done" } } end { if ($IsLinux) { if ($Verbose) { Write-Verbose "Refreshing font cache" fc-cache -fv } else { fc-cache -f } } Write-Verbose "[$functionName] - Done" } } Write-Verbose "[$scriptName] - [public] - [Install-Font] - Done" #endregion - From [public] - [Install-Font] #region - From [public] - [Uninstall-Font] Write-Verbose "[$scriptName] - [public] - [Uninstall-Font] - Importing" #Requires -Modules Admin function Uninstall-Font { <# .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 #> [Alias('Uninstall-Fonts')] [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( ValueFromPipeline, ValueFromPipelineByPropertyName )] [Scope[]] $Scope = 'CurrentUser', # Name of the font to uninstall. [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName )] [SupportsWildcards()] [string[]] $Name ) begin { $functionName = $MyInvocation.MyCommand.Name Write-Verbose "[$functionName]" if ($Scope -contains 'AllUsers' -and -not (IsAdmin)) { $errorMessage = @" Administrator rights are required to uninstall fonts in [$($script:FontFolderPath['AllUsers'])]. Please run the command again with elevated rights (Run as Administrator) or provide '-Scope CurrentUser' to your command. "@ throw $errorMessage } $maxRetries = 10 $retryIntervalSeconds = 1 } process { $Name = $PSBoundParameters['Name'] $scopeCount = $Scope.Count Write-Verbose "[$functionName] - Processing [$scopeCount] scopes(s)" foreach ($scopeItem in $Scope) { $scopeName = $scopeItem.ToString() $nameCount = $Name.Count Write-Verbose "[$functionName] - [$scopeName] - Processing [$nameCount] font(s)" foreach ($fontName in $Name) { Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Processing" $fonts = Get-Font -Name $fontName -Scope $Scope Write-Verbose ($fonts | Out-String) foreach ($font in $fonts) { $filePath = $font.Path $fileExists = Test-Path -Path $filePath -ErrorAction SilentlyContinue if (-not $fileExists) { Write-Warning "[$functionName] - [$scopeName] - [$fontName] - File [$filePath] does not exist. Skipping." } else { Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Removing file [$filePath]" $retryCount = 0 $fileRemoved = $false do { try { Remove-Item -Path $filePath -Force -ErrorAction Stop $fileRemoved = $true } catch { # Common error; 'file in use'. $retryCount++ if (-not $fileRemoved -and $retryCount -eq $maxRetries) { Write-Error $_ Write-Error "Failed [$retryCount/$maxRetries] - Stopping" break } Write-Verbose $_ Write-Verbose "Failed [$retryCount/$maxRetries] - Retrying in $retryIntervalSeconds seconds..." #TODO: Find a way to try to unlock file here. Start-Sleep -Seconds $retryIntervalSeconds } } while (-not $fileRemoved -and $retryCount -lt $maxRetries) if (-not $fileRemoved) { break # Break to skip unregistering the font if the file could not be removed. } } if ($IsWindows) { Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Searching for font in registry" $keys = Get-ItemProperty -Path $script:FontRegPathMap[$scopeName] $key = $keys.PSObject.Properties | Where-Object { $_.Value -eq $filePath } if (-not $key) { Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Font is not registered. Skipping." } else { $keyName = $key.Name Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Unregistering font [$keyName]" Remove-ItemProperty -Path $script:FontRegPathMap[$scopeName] -Name $keyName -Force -ErrorAction Stop } } Write-Verbose "[$functionName] - [$scopeName] - [$fontName] - Done" } } Write-Verbose "[$functionName] - [$scopeName] - Done" } } end { if ($IsLinux) { if ($Verbose) { Write-Verbose 'Refreshing font cache' fc-cache -fv } else { fc-cache -f } } Write-Verbose "[$functionName] - Done" } } Register-ArgumentCompleter -CommandName Uninstall-Font -ParameterName Name -ScriptBlock { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $null = $commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters if ([string]::IsNullOrEmpty($fakeBoundParameters['Scope'])) { Get-Font -Scope 'CurrentUser' | Where-Object { $_.Name -like "$wordToComplete*" } | Select-Object -ExpandProperty Name } else { Get-Font -Scope $fakeBoundParameters['Scope'] | Where-Object { $_.Name -like "$wordToComplete*" } | Select-Object -ExpandProperty Name } } Write-Verbose "[$scriptName] - [public] - [Uninstall-Font] - Done" #endregion - From [public] - [Uninstall-Font] Write-Verbose "[$scriptName] - [public] - Done" #endregion - From [public] #region - From [common] Write-Verbose "[$scriptName] - [common] - Importing" $script:FontRegPathMap = @{ CurrentUser = 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' AllUsers = 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' } $script:FontFolderPathMap = @{ 'Windows' = @{ CurrentUser = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts" AllUsers = "$($env:windir)\Fonts" } 'MacOS' = @{ CurrentUser = "$env:HOME/Library/Fonts" AllUsers = '/Library/Fonts' } 'Linux' = @{ CurrentUser = "$env:HOME/.fonts" AllUsers = '/usr/share/fonts' } } $script:OS = if ($IsWindows) { 'Windows' } elseif ($IsLinux) { 'Linux' } elseif ($IsMacOS) { 'MacOS' } else { throw 'Unsupported OS' } $script:SupportedFonts = @( [pscustomobject]@{ Extension = '.ttf' Type = 'TrueType' Description = 'TrueType Font' } [pscustomobject]@{ Extension = '.otf' Type = 'OpenType' Description = 'OpenType Font' } [pscustomobject]@{ Extension = '.ttc' Type = 'TrueType' Description = 'TrueType Font Collection' } [pscustomobject]@{ Extension = '.pfb' Type = 'PostScript Type 1' Description = 'PostScript Type 1 Font' } [pscustomobject]@{ Extension = '.pfm' Type = 'PostScript Type 1' Description = 'PostScript Type 1 Outline Font' } [pscustomobject]@{ Extension = '.woff' Type = 'Web Open Font Format' Description = 'Web Open Font Format' } [pscustomobject]@{ Extension = '.woff2' Type = 'Web Open Font Format 2' Description = 'Web Open Font Format 2' } ) Write-Verbose "[$scriptName] - [common] - Done" #endregion - From [common] # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get # Define the types to export with type accelerators. $ExportableEnums = @( [Scope] ) $ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } foreach ($Type in $ExportableEnums) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Warning "Enum already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing enum '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } $ExportableClasses = @( ) $ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } foreach ($Type in $ExportableClasses) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Warning "Class already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing class '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in ($ExportableEnums + $ExportableClasses)) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'Get-Font' 'Install-Font' 'Uninstall-Font' ) Variable = '' } Export-ModuleMember @exports |