diff --git a/.github/workflows/powershell-ci.yml b/.github/workflows/powershell-ci.yml new file mode 100644 index 0000000..2ef148b --- /dev/null +++ b/.github/workflows/powershell-ci.yml @@ -0,0 +1,53 @@ +name: PowerShell CI + +on: + push: + pull_request: + +jobs: + analyze: + name: Script analysis (PSScriptAnalyzer) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + Set-PSRepository -Name 'PSGallery' -InstallationPolicy Trusted + Install-Module PSScriptAnalyzer -Scope CurrentUser -Force + + - name: Run PSScriptAnalyzer (errors only) + shell: pwsh + run: | + Invoke-ScriptAnalyzer -Path . -Recurse -EnableExit -Severity Error + + syntax-check: + name: Basic syntax check (-DryRun) + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: PowerShell 7 check + shell: pwsh + run: | + pwsh -NoProfile -ExecutionPolicy Bypass -File ./FontSharpener.ps1 -DryRun -Verbose + + - name: Windows PowerShell 5.1 check + shell: powershell + run: | + powershell -NoProfile -ExecutionPolicy Bypass -File ./FontSharpener.ps1 -DryRun -Verbose + + publish-readme: + name: Publish README artifact + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Upload README + uses: actions/upload-artifact@v4 + with: + name: README + path: README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30f48b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# OS junk +.DS_Store +Thumbs.db + +# IDE/editor +.vscode/ +.idea/ +*.code-workspace + +# Logs and temp +*.log +*.tmp +~$* + +# PowerShell metadata (optional modules) +*.psd1 +*.psm1 + +# Node/other common folders (not used here but harmless) +node_modules/ +dist/ +build/ diff --git a/FontSharpener.ps1 b/FontSharpener.ps1 new file mode 100644 index 0000000..3b2b10d --- /dev/null +++ b/FontSharpener.ps1 @@ -0,0 +1,330 @@ +#Requires -Version 5.1 +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] +param( + [Parameter(Mandatory = $false, HelpMessage = 'Scaling percent to apply. Supported: 100, 125, 150, 175')] + [ValidateSet(100,125,150,175)] + [int] + $ScalingPercent = 100, + + [Parameter()] + [switch] + $DryRun, + + [Parameter(HelpMessage = 'Directory or file path to save backup. If directory, a timestamped filename will be created.')] + [string] + $BackupPath, + + [Parameter(HelpMessage = 'Path to a previously created backup file to restore (JSON created by this script).')] + [string] + $Restore, + + [Parameter(HelpMessage = 'Skip interactive prompts.')] + [switch] + $Force +) + +# Constant registry path and keys +$RegPath = 'HKCU:\Control Panel\Desktop' +$RegistryKeys = @('DpiScalingVer','Win8DpiScaling','LogPixels','FontSmoothing') + +function Test-IsAdministrator { + try { + $current = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]::new($current) + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + catch { + Write-Error "Failed to determine elevation state: $($_.Exception.Message)" + return $false + } +} + +function Get-TargetValues { + param( + [Parameter(Mandatory)] + [ValidateSet(100,125,150,175)] + [int] $Percent + ) + + $logPixels = switch ($Percent) { + 100 { 96 } + 125 { 120 } + 150 { 144 } + 175 { 168 } + default { throw "Unsupported scaling percent: $Percent" } + } + + [ordered]@{ + DpiScalingVer = 0x00001000 + Win8DpiScaling = 0x00000001 + LogPixels = [int]$logPixels + FontSmoothing = 0x00000001 + } +} + +function Get-CurrentValues { + $result = @{} + foreach ($k in $RegistryKeys) { + try { + $item = Get-ItemProperty -Path $RegPath -Name $k -ErrorAction Stop + $result[$k] = [int]($item.$k) + } + catch { + $result[$k] = $null + } + } + return $result +} + +function Ensure-BackupDirectory { + param( + [string] $Path + ) + if (-not (Test-Path -LiteralPath $Path)) { + $null = New-Item -Path $Path -ItemType Directory -Force + } +} + +function Get-DefaultBackupPath { + $docs = [Environment]::GetFolderPath('MyDocuments') + if (-not $docs) { $docs = $env:USERPROFILE } + $dir = Join-Path -Path $docs -ChildPath 'FontSharpener-Backups' + Ensure-BackupDirectory -Path $dir + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + return (Join-Path -Path $dir -ChildPath ("FontSharpener-backup-$stamp.json")) +} + +function Resolve-BackupFilePath { + param( + [string] $InputPath + ) + if ([string]::IsNullOrWhiteSpace($InputPath)) { + return Get-DefaultBackupPath + } + + if (Test-Path -LiteralPath $InputPath) { + $attr = Get-Item -LiteralPath $InputPath + if ($attr.PSIsContainer) { + Ensure-BackupDirectory -Path $InputPath + $stamp = Get-Date -Format 'yyyyMMdd-HHmmss' + return (Join-Path -Path $InputPath -ChildPath ("FontSharpener-backup-$stamp.json")) + } + else { + return $InputPath + } + } + + $parent = Split-Path -Path $InputPath -Parent + if (-not [string]::IsNullOrWhiteSpace($parent)) { + Ensure-BackupDirectory -Path $parent + } + return $InputPath +} + +function Save-Backup { + param( + [string] $Path, + [hashtable] $CurrentValues + ) + $backup = [ordered]@{ + Created = (Get-Date).ToString('s') + ComputerName = $env:COMPUTERNAME + UserName = $env:USERNAME + RegistryPath = $RegPath + Values = $CurrentValues + } + + $json = $backup | ConvertTo-Json -Depth 5 + Set-Content -LiteralPath $Path -Value $json -Encoding UTF8 + Write-Verbose "Backup saved to: $Path" +} + +function Backup-RegistryUnderscoreCopies { + param( + [hashtable] $CurrentValues + ) + foreach ($key in $RegistryKeys) { + $val = $CurrentValues[$key] + if ($null -ne $val) { + New-ItemProperty -Path $RegPath -Name ("{0}_" -f $key) -PropertyType DWord -Value ([int]$val) -Force | Out-Null + } + } +} + +function Apply-Values { + param( + [hashtable] $ValuesToApply + ) + foreach ($k in $ValuesToApply.Keys) { + New-ItemProperty -Path $RegPath -Name $k -PropertyType DWord -Value ([int]$ValuesToApply[$k]) -Force | Out-Null + } +} + +function Verify-Values { + param( + [hashtable] $Expected + ) + $curr = Get-CurrentValues + $ok = $true + foreach ($k in $Expected.Keys) { + $cv = $curr[$k] + $ev = [int]$Expected[$k] + if ($cv -ne $ev) { + Write-Error "Verification failed for $k. Current=$cv Expected=$ev" + $ok = $false + } + } + return $ok +} + +function Show-PlannedChanges { + param( + [hashtable] $Current, + [hashtable] $Target + ) + Write-Output 'Planned changes:' + foreach ($k in $Target.Keys) { + $cv = $Current[$k] + $ev = [int]$Target[$k] + if ($cv -ne $ev) { + Write-Output (" - {0}: {1} -> {2}" -f $k, ($cv -as [string]), $ev) + } + else { + Write-Output (" - {0}: already {1}" -f $k, $ev) + } + } +} + +function Invoke-SelfElevationIfNeeded { + if (Test-IsAdministrator) { return } + + Write-Warning 'This script is not running elevated. Attempting to relaunch as Administrator...' + + try { + $hostPath = (Get-Process -Id $PID).Path + $args = @('-NoProfile','-ExecutionPolicy','Bypass','-File',('"{0}"' -f $PSCommandPath)) + foreach ($name in $PSBoundParameters.Keys) { + if ($name -eq 'Verbose') { continue } + $value = $PSBoundParameters[$name] + if ($null -eq $value) { continue } + if ($value -is [switch]) { + if ($value.IsPresent) { $args += ('-{0}' -f $name) } + } + else { + $args += ('-{0}' -f $name) + $args += ('"{0}"' -f $value) + } + } + if ($PSBoundParameters.ContainsKey('Verbose')) { $args += '-Verbose' } + + Start-Process -FilePath $hostPath -ArgumentList $args -Verb RunAs | Out-Null + Write-Output 'Relaunched with elevation. This instance will exit.' + exit 0 + } + catch { + Write-Error "Failed to relaunch elevated: $($_.Exception.Message). Please run this script from an elevated PowerShell (Run as Administrator)." + exit 1 + } +} + +try { + if ($PSBoundParameters.ContainsKey('Restore') -and -not [string]::IsNullOrWhiteSpace($Restore)) { + $restorePath = $Restore + if (-not (Test-Path -LiteralPath $restorePath)) { + throw "Restore file not found: $restorePath" + } + + $json = Get-Content -LiteralPath $restorePath -Raw -Encoding UTF8 | ConvertFrom-Json + if (-not $json -or -not $json.Values) { + throw 'Restore file is invalid or missing Values section.' + } + + $targetFromBackup = @{} + foreach ($k in $RegistryKeys) { + if ($null -ne $json.Values.$k) { + $targetFromBackup[$k] = [int]$json.Values.$k + } + } + if ($targetFromBackup.Keys.Count -eq 0) { + throw 'Restore file does not contain any known keys to restore.' + } + + $current = Get-CurrentValues + if ($DryRun) { + Write-Output ("[DRY-RUN] Would restore registry values from backup: {0}" -f $restorePath) + Show-PlannedChanges -Current $current -Target $targetFromBackup + exit 0 + } + + Invoke-SelfElevationIfNeeded + + $backupFile = Resolve-BackupFilePath -InputPath $BackupPath + Save-Backup -Path $backupFile -CurrentValues $current + Backup-RegistryUnderscoreCopies -CurrentValues $current + + if (-not $Force) { + $answer = Read-Host 'Proceed to restore values from backup? (Y/N)' + if ($answer -notmatch '^(?i)y') { Write-Output 'Aborted by user.'; exit 0 } + } + + Apply-Values -ValuesToApply $targetFromBackup + if (-not (Verify-Values -Expected $targetFromBackup)) { + throw 'Verification after restore failed.' + } + + Write-Output 'Restore completed successfully.' + Write-Output 'A sign out or reboot may be required for changes to fully apply.' + exit 0 + } + + $target = Get-TargetValues -Percent $ScalingPercent + $currentValues = Get-CurrentValues + + $diff = @{} + foreach ($k in $target.Keys) { + if ($currentValues[$k] -ne $target[$k]) { $diff[$k] = $target[$k] } + } + + if ($DryRun) { + Write-Output ("[DRY-RUN] Scaling Percent: {0}%" -f $ScalingPercent) + Show-PlannedChanges -Current $currentValues -Target $target + $plannedBackup = Resolve-BackupFilePath -InputPath $BackupPath + Write-Output ("[DRY-RUN] A backup would be saved to: {0}" -f $plannedBackup) + exit 0 + } + + if ($diff.Count -eq 0) { + Write-Output 'All target values are already applied. No changes necessary.' + exit 0 + } + + Invoke-SelfElevationIfNeeded + + $backupPathResolved = Resolve-BackupFilePath -InputPath $BackupPath + Save-Backup -Path $backupPathResolved -CurrentValues $currentValues + Backup-RegistryUnderscoreCopies -CurrentValues $currentValues + + if (-not $Force) { + Write-Output ("The following changes will be applied for scaling {0}%:" -f $ScalingPercent) + Show-PlannedChanges -Current $currentValues -Target $target + $answer2 = Read-Host 'Proceed? (Y/N)' + if ($answer2 -notmatch '^(?i)y') { Write-Output 'Aborted by user.'; exit 0 } + } + + Apply-Values -ValuesToApply $diff + + if (-not (Verify-Values -Expected $target)) { + throw 'Verification failed. Not all values were applied.' + } + + Write-Output ("Scaling settings applied successfully for {0}%" -f $ScalingPercent) + Write-Output 'A sign out or reboot may be required for changes to fully apply.' + exit 0 +} +catch { + Write-Error ("Failure: {0}" -f $_.Exception.Message) + exit 1 +} diff --git a/README.md b/README.md index 792be33..69d13c5 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,103 @@ -# font_sharpener -DPI Scaling Fix for Clear Fonts in Windows -Улучшает чёткость шрифтов в Windows через настройку реестра. -По мотивам https://actika.livejournal.com/5313.html - - -Склонируйте репозиторий или скачайте файл Set-DpiScaling.ps1: -sh -git clone https://github.com/ваш-репозиторий.git - - -Перейдите в папку с скриптом: - -sh -cd registry-dpi-scaling-tool - - -Запуск скрипта - -Откройте PowerShell от имени администратора - -(Нажмите Win + X → "Терминал Windows (администратор)") - - -Разрешите выполнение скриптов (если нужно): - -powershell -Set-ExecutionPolicy RemoteSigned -Scope CurrentUser -Force -Запустите скрипт: - -powershell -.\Set-DpiScaling.ps1 - - -Что делает скрипт? -Создает резервные копии текущих значений реестра (добавляя _ к именам ключей): - -DpiScalingVer → DpiScalingVer_ - -Win8DpiScaling → Win8DpiScaling_ - -LogPixels → LogPixels_ - -FontSmoothing → FontSmoothing_ - -Устанавливает новые значения для улучшения масштабирования: - -reg -DpiScalingVer = 0x00001000 - -Win8DpiScaling = 0x00000001 - -LogPixels = 0x00000060 (96 DPI) - -FontSmoothing = 0x00000001 (Включено) - -Проверяет, что изменения применились. - - -Важно! - -Требуются права администратора - -После применения изменений может потребоваться перезагрузка - -Рекомендуется создать точку восстановления системы перед запуском - - - +# FontSharpener + +Windows DPI scaling and font smoothing helper. Adjusts registry values to improve font clarity on scaling presets (100/125/150/175%). Based on community guidance and documented registry keys. + +Repository contains one script: `FontSharpener.ps1`. + +English | Русский (см. ниже) + +--- + +## What it does (EN) +- Backs up your current DPI-related registry values (two ways): + - Creates a timestamped JSON backup file in Documents/FontSharpener-Backups by default (path configurable with `-BackupPath`). + - Stores underscore copies in the registry (e.g., `LogPixels_`). +- Applies DPI scaling defaults for the selected preset and enables font smoothing: + - DpiScalingVer = 0x00001000 + - Win8DpiScaling = 1 + - LogPixels = 96/120/144/168 (for 100/125/150/175%) + - FontSmoothing = 1 +- Verifies changes and suggests signing out or rebooting. + +Important: +- This script edits your registry under `HKCU\Control Panel\Desktop`. Use at your own risk. +- Administrator privileges are required to modify the registry. The script will attempt to relaunch elevated; otherwise, it will instruct you to run as Administrator. +- You can always run with `-DryRun` first to see what would change without writing to the registry. + +## Requirements +- Windows PowerShell 5.1 or PowerShell 7+ +- Run from an elevated PowerShell when applying changes (not required for `-DryRun`). + +## Usage +Open PowerShell as Administrator, then run: + +- Apply 125% scaling: + - `powershell` (Windows PowerShell 5.1): `powershell -NoProfile -ExecutionPolicy Bypass -File .\FontSharpener.ps1 -ScalingPercent 125` + - `pwsh` (PowerShell 7+): `pwsh -NoProfile -ExecutionPolicy Bypass -File .\FontSharpener.ps1 -ScalingPercent 125` + +- Dry-run (no changes; shows planned diffs): + - `pwsh -NoProfile -File .\FontSharpener.ps1 -ScalingPercent 150 -DryRun -Verbose` + +- Custom backup directory: + - `pwsh -File .\FontSharpener.ps1 -ScalingPercent 175 -BackupPath D:\Backups` + +- Restore from backup file created by this script: + - `pwsh -File .\FontSharpener.ps1 -Restore .\FontSharpener-Backups\FontSharpener-backup-20250101-101234.json -Force` + +Parameters: +- `-ScalingPercent <100|125|150|175>`: Target scaling preset. Default: 100. +- `-DryRun`: Do not change registry; print planned changes and backup location. +- `-BackupPath `: Directory or file path for the JSON backup. If a directory, a timestamped filename is created. +- `-Restore `: Restore values from a JSON backup created by this script. +- `-Force`: Skip interactive prompts. +- `-Verbose`: Show additional details. + +Supported presets and LogPixels mapping: +- 100% -> 96 +- 125% -> 120 +- 150% -> 144 +- 175% -> 168 + +After applying changes, sign out or reboot to ensure the settings take effect. + +--- + +## Что делает (RU) +- Создаёт резервную копию текущих значений (двумя способами): + - Пишет JSON-файл с бэкапом (по умолчанию в Документы/FontSharpener-Backups; путь настраивается через `-BackupPath`). + - Создаёт копии значений с подчёркиванием в реестре (`LogPixels_` и т.п.). +- Применяет значения масштабирования и сглаживания шрифтов для выбранного пресета: + - DpiScalingVer = 0x00001000 + - Win8DpiScaling = 1 + - LogPixels = 96/120/144/168 (для 100/125/150/175%) + - FontSmoothing = 1 +- Проверяет применённые значения и предлагает выйти из системы или перезагрузиться. + +Важно: +- Скрипт изменяет реестр в `HKCU\\Control Panel\\Desktop`. Используйте на свой риск. +- Для записи в реестр требуются права администратора. Скрипт попробует перезапуститься с повышенными правами или подскажет, как запустить «От имени администратора». +- Рекомендуется сначала выполнить `-DryRun` (без изменений) и посмотреть, что будет изменено. + +## Требования +- Windows PowerShell 5.1 или PowerShell 7+ +- Для применения изменений запустите PowerShell «От имени администратора» (для `-DryRun` не требуется). + +## Примеры +- Применить масштаб 125%: + - `powershell -NoProfile -ExecutionPolicy Bypass -File .\\FontSharpener.ps1 -ScalingPercent 125` +- Пробный запуск (без изменений): + - `pwsh -NoProfile -File .\\FontSharpener.ps1 -ScalingPercent 150 -DryRun -Verbose` +- Указать папку для резервной копии: + - `pwsh -File .\\FontSharpener.ps1 -ScalingPercent 175 -BackupPath D:\\Backups` +- Восстановить из ранее созданного файла: + - `pwsh -File .\\FontSharpener.ps1 -Restore .\\FontSharpener-Backups\\FontSharpener-backup-20250101-101234.json -Force` + +Параметры: +- `-ScalingPercent <100|125|150|175>` — целевое масштабирование. По умолчанию 100. +- `-DryRun` — ничего не меняет; показывает планируемые изменения и путь бэкапа. +- `-BackupPath <путь>` — папка или файл для JSON-бэкапа. Если указана папка — имя файла будет с отметкой времени. +- `-Restore <путь>` — восстановление значений из JSON-бэкапа. +- `-Force` — не задавать вопросов (подтверждений). +- `-Verbose` — подробные сообщения. + +После применения изменений может потребоваться выход из системы или перезагрузка. diff --git "a/\320\257\321\221\321\202\320\272\320\270\320\265 \321\210\321\200\320\270\321\204\321\202\321\213.ps1" "b/\320\257\321\221\321\202\320\272\320\270\320\265 \321\210\321\200\320\270\321\204\321\202\321\213.ps1" deleted file mode 100644 index 27f3eef..0000000 --- "a/\320\257\321\221\321\202\320\272\320\270\320\265 \321\210\321\200\320\270\321\204\321\202\321\213.ps1" +++ /dev/null @@ -1,93 +0,0 @@ -<# -.SYNOPSIS - Скрипт для настройки параметров масштабирования в реестре -.DESCRIPTION - 1. Создает резервную копию текущих значений реестра (добавляя _ к именам ключей) - 2. Устанавливает новые значения для оптимального масштабирования -.NOTES - Требует запуска от имени администратора -#> - -# Путь к разделу реестра -$regPath = "HKCU:\Control Panel\Desktop" - -# Ключи для резервного копирования и настройки -$registryKeys = @( - "DpiScalingVer", - "Win8DpiScaling", - "LogPixels", - "FontSmoothing" -) - -# Новые значения для установки -$newValues = @{ - "DpiScalingVer" = 0x00001000 - "Win8DpiScaling" = 0x00000001 - "LogPixels" = 0x00000060 - "FontSmoothing" = 0x00000001 -} - -# Функция для создания резервной копии -function Backup-RegistryKeys { - Write-Host "`nСоздание резервных копий текущих значений..." -ForegroundColor Cyan - - foreach ($key in $registryKeys) { - $originalValue = Get-ItemProperty -Path $regPath -Name $key -ErrorAction SilentlyContinue - - if ($originalValue -ne $null) { - $backupKeyName = $key + "_" - $originalValueData = $originalValue.$key - - # Создаем резервную копию - Set-ItemProperty -Path $regPath -Name $backupKeyName -Value $originalValueData -Type DWORD -Force - Write-Host "Резервная копия: $key -> $backupKeyName (Значение: $originalValueData)" -ForegroundColor Green - } else { - Write-Host "Ключ $key не найден, резервная копия не создана" -ForegroundColor Yellow - } - } -} - -# Функция для установки новых значений -function Set-NewRegistryValues { - Write-Host "`nУстановка новых значений..." -ForegroundColor Cyan - - foreach ($key in $newValues.Keys) { - $value = $newValues[$key] - Set-ItemProperty -Path $regPath -Name $key -Value $value -Type DWORD -Force - Write-Host "Установлено: $key = $value" -ForegroundColor Green - } -} - -# Функция для проверки изменений -function Verify-Changes { - Write-Host "`nПроверка установленных значений..." -ForegroundColor Cyan - - foreach ($key in $newValues.Keys) { - $currentValue = (Get-ItemProperty -Path $regPath -Name $key -ErrorAction SilentlyContinue).$key - $expectedValue = $newValues[$key] - - if ($currentValue -eq $expectedValue) { - Write-Host "$key - OK (Текущее: $currentValue, Ожидаемое: $expectedValue)" -ForegroundColor Green - } else { - Write-Host "$key - ОШИБКА (Текущее: $currentValue, Ожидаемое: $expectedValue)" -ForegroundColor Red - } - } -} - -# Основной код скрипта -try { - # Создаем резервные копии - Backup-RegistryKeys - - # Устанавливаем новые значения - Set-NewRegistryValues - - # Проверяем изменения - Verify-Changes - - Write-Host "`nНастройка завершена успешно!`nДля применения изменений может потребоваться выход из системы." -ForegroundColor Green -} -catch { - Write-Host "`nОшибка при выполнении скрипта: $_" -ForegroundColor Red - exit 1 -} \ No newline at end of file