Runtime/OSDStageFont.ps1
|
function Initialize-OSDStageFontNativeApi { [CmdletBinding()] param() if ('OSDStage.FontNativeApi' -as [type]) { return } Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; namespace OSDStage { public static class FontNativeApi { [DllImport("gdi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern int AddFontResourceEx(string lpszFilename, uint fl, IntPtr pdv); [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] public static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, UIntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult); } } '@ -ErrorAction Stop } function Get-OSDStageFontRegistrationName { [CmdletBinding()] param([Parameter(Mandatory)][System.IO.FileInfo]$File) $suffix = if ($File.Extension.ToLowerInvariant() -eq '.otf') { 'OpenType' } else { 'TrueType' } '{0} ({1})' -f [IO.Path]::GetFileNameWithoutExtension($File.Name), $suffix } function Install-OSDStageFontFile { [CmdletBinding()] param( [Parameter(Mandatory)][System.IO.FileInfo]$File, [string]$FontsDirectory = (Join-Path $env:WINDIR 'Fonts'), [string]$RegistryPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', [switch]$SkipNativeActivation ) $extension = $File.Extension.ToLowerInvariant() if ($extension -notin @('.ttf','.otf')) { return [pscustomobject]@{ FileName=$File.Name;RegistrationName=$null;Status='Ignored';Outcome='Unsupported' SourceSha256=$null;Destination=$null;ActivationLoaded=$null;Error=$null } } $destination = Join-Path $FontsDirectory $File.Name $registrationName = Get-OSDStageFontRegistrationName -File $File $sourceHash = (Get-FileHash -LiteralPath $File.FullName -Algorithm SHA256 -ErrorAction Stop).Hash $copied = $false $registrationCreated = $false try { New-Item -ItemType Directory -Path $FontsDirectory -Force -ErrorAction Stop | Out-Null if (-not (Test-Path -LiteralPath $RegistryPath)) { New-Item -Path $RegistryPath -Force -ErrorAction Stop | Out-Null } $registry = Get-ItemProperty -LiteralPath $RegistryPath -ErrorAction Stop $registrations = @( $registry.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } ) $matchingRegistration = @( $registrations | Where-Object { [string]$_.Value -ieq $File.Name } ) $namedRegistration = @( $registrations | Where-Object { [string]$_.Name -ieq $registrationName } ) if (Test-Path -LiteralPath $destination -PathType Leaf) { $destinationHash = (Get-FileHash -LiteralPath $destination -Algorithm SHA256 -ErrorAction Stop).Hash if ($destinationHash -ne $sourceHash) { return [pscustomobject]@{ FileName=$File.Name;RegistrationName=$registrationName;Status='Failed';Outcome='FileConflict' SourceSha256=$sourceHash;Destination=$destination;ActivationLoaded=$null Error='A different font file with the same destination filename already exists.' } } if ($matchingRegistration.Count -gt 0) { return [pscustomobject]@{ FileName=$File.Name;RegistrationName=[string]$matchingRegistration[0].Name Status='AlreadyPresent';Outcome='AlreadyPresent' SourceSha256=$sourceHash;Destination=$destination;ActivationLoaded=$null;Error=$null } } } if ($namedRegistration.Count -gt 0 -and [string]$namedRegistration[0].Value -ine $File.Name) { return [pscustomobject]@{ FileName=$File.Name;RegistrationName=$registrationName;Status='Failed';Outcome='RegistrationConflict' SourceSha256=$sourceHash;Destination=$destination;ActivationLoaded=$null Error='The derived machine font registration name already points to another file.' } } if (-not (Test-Path -LiteralPath $destination -PathType Leaf)) { Copy-Item -LiteralPath $File.FullName -Destination $destination -ErrorAction Stop $copied = $true } New-ItemProperty -LiteralPath $RegistryPath -Name $registrationName -Value $File.Name -PropertyType String -Force -ErrorAction Stop | Out-Null $registrationCreated = $true $activationLoaded = $null if (-not $SkipNativeActivation) { try { Initialize-OSDStageFontNativeApi $activationLoaded = ([OSDStage.FontNativeApi]::AddFontResourceEx($destination, 0, [IntPtr]::Zero) -gt 0) $broadcastResult = [UIntPtr]::Zero [void][OSDStage.FontNativeApi]::SendMessageTimeout( [IntPtr]0xffff, 0x001D, [UIntPtr]::Zero, [IntPtr]::Zero, 0x0002, 1000, [ref]$broadcastResult ) } catch { # Registry + %WINDIR%\Fonts are the persistent machine-wide contract. # Session activation is best-effort and must not convert a durable install into failure. $activationLoaded = $false } } [pscustomobject]@{ FileName=$File.Name;RegistrationName=$registrationName;Status='Installed';Outcome='Installed' SourceSha256=$sourceHash;Destination=$destination;ActivationLoaded=$activationLoaded;Error=$null } } catch { if ($registrationCreated) { try { Remove-ItemProperty -LiteralPath $RegistryPath -Name $registrationName -ErrorAction SilentlyContinue } catch {} } if ($copied) { try { Remove-Item -LiteralPath $destination -Force -ErrorAction SilentlyContinue } catch {} } [pscustomobject]@{ FileName=$File.Name;RegistrationName=$registrationName;Status='Failed';Outcome='Failed' SourceSha256=$sourceHash;Destination=$destination;ActivationLoaded=$null;Error=$_.Exception.Message } } } function Invoke-OSDStageFontPackage { [CmdletBinding()] param( [Parameter(Mandatory)][string]$PayloadRoot, [Parameter(Mandatory)][string]$PackageId, [Parameter(Mandatory)][string]$DeploymentId, [Parameter(Mandatory)][string]$ResultPath, [string]$FontsDirectory = (Join-Path $env:WINDIR 'Fonts'), [string]$RegistryPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', [switch]$SkipNativeActivation ) $fontFiles = @( Get-ChildItem -LiteralPath $PayloadRoot -File -Recurse -ErrorAction Stop | Where-Object { $_.Extension.ToLowerInvariant() -in @('.ttf','.otf') } | Sort-Object FullName ) if ($fontFiles.Count -eq 0) { throw "Font package '$PackageId' contains no supported .ttf or .otf files." } $items = [System.Collections.Generic.List[object]]::new() foreach ($font in $fontFiles) { $items.Add((Install-OSDStageFontFile -File $font -FontsDirectory $FontsDirectory -RegistryPath $RegistryPath -SkipNativeActivation:$SkipNativeActivation)) } $failed = @($items | Where-Object Status -eq 'Failed').Count $record = [ordered]@{ SchemaVersion=2 DeploymentId=$DeploymentId Id=$PackageId Status=if($failed -gt 0){'Failed'}else{'Succeeded'} FontCount=$items.Count InstalledCount=@($items | Where-Object Status -eq 'Installed').Count AlreadyPresentCount=@($items | Where-Object Status -eq 'AlreadyPresent').Count FailedCount=$failed Items=@($items) Error=if($failed -gt 0){"$failed font(s) failed."}else{$null} } New-Item -ItemType Directory -Path (Split-Path $ResultPath -Parent) -Force | Out-Null $record | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $ResultPath -Encoding UTF8 [pscustomobject]@{ ExitCode=if($failed -gt 0){1}else{0} Integrity='Sha256Validated' ResultPath=$ResultPath Items=@($items) } } |