# Extra run: GIMP from the Microsoft Store and GIMP Portable (PortableApps.com): where each keeps its settings, # and whether PhotoGIMP (3.1 + our fix) takes effect there. Helpers are the same as windows.ps1. # PhotoGIMP on a real Windows machine: installs GIMP 3.2.6, applies PhotoGIMP the ways users do # (wrong folder, README way, our repair, upstream master), uninstalls it, and tests the Chocolatey # package. Every step writes facts and screenshots to verify/$env:VERIFY_OUT/. $OutName = $(if ($env:VERIFY_OUT) { $env:VERIFY_OUT } else { 'results' }) . "$PSScriptRoot\lib.ps1" $GimpUrl = 'https://download.gimp.org/gimp/v3.2/windows/gimp-3.2.6-setup.exe' $GimpSha = '9337cccbc01d4098ee7a3dab215b3afbe6ece99c5287c92441d6f12cf541ebca' $PgUrl = 'https://github.com/Diolinux/PhotoGIMP/releases/download/3.1/PhotoGIMP.zip' $PgSha = 'de6a17ffb70000051372d84141b7e69fc2e60a0af3bd3b3100c9afd5e0ff3ae6' $MasterCommit = 'fc61a8f' $Image = Join-Path $repo 'assets\moraine-lake.jpg' $Desk = [Environment]::GetFolderPath('Desktop') $Cfg = Join-Path $env:APPDATA 'GIMP' $facts.os = (Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, BuildNumber, OSArchitecture) $facts.arch = $env:PROCESSOR_ARCHITECTURE $facts.runner = $env:VERIFY_OUT $facts.date = (Get-Date).ToString('yyyy-MM-dd HH:mm') Set-Screen; Dismiss-Oobe; Hide-Consoles foreach ($e in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, (New-Object System.Windows.Automation.PropertyCondition($UIA::NameProperty, 'Performance Options')))) { try { $e.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close(); Log 'closed Performance Options' } catch { } } function Step($n) { Log "===== $n =====" } function Tree($p) { if (Test-Path $p) { Get-ChildItem -Force $p | ForEach-Object { $_.Name } } else { @() } } function Hash($p) { if (Test-Path $p) { (Get-FileHash $p -Algorithm SHA256).Hash.ToLower() } else { $null } } $script:GimpExe = $null function Start-Gimp([string]$tag, [switch]$WithImage, [int]$Settle = 25, [switch]$Splash) { $a = @('--console-messages'); if ($WithImage) { $a += "`"$Image`"" } $err = Join-Path $out "$tag-stderr.txt"; $o = Join-Path $out "$tag-stdout.txt" $p = Start-Process -FilePath $script:GimpExe -ArgumentList $a -PassThru -RedirectStandardError $err -RedirectStandardOutput $o $t0 = Get-Date if ($Splash) { foreach ($s in 2, 4, 6, 9) { while (((Get-Date) - $t0).TotalSeconds -lt $s) { Start-Sleep -Milliseconds 200 }; Shot "$tag-splash-${s}s" } } $end = (Get-Date).AddSeconds(300) while ((Get-Date) -lt $end) { $p.Refresh() if ($p.HasExited) { Log "$tag GIMP exited early with code $($p.ExitCode)"; break } if ($p.MainWindowHandle -ne 0 -and ($p.MainWindowTitle -match ([string][char]0x2013 + ' GIMP$') -or $p.MainWindowTitle -eq 'GNU Image Manipulation Program')) { break } Start-Sleep 1 } Log ("$tag main window after {0:N0}s: '{1}'" -f ((Get-Date) - $t0).TotalSeconds, $p.MainWindowTitle) Start-Sleep $Settle Hide-Consoles Close-Welcome $tag if ($p.MainWindowHandle -ne 0) { [N]::ShowWindow($p.MainWindowHandle, 3) | Out-Null; [N]::SetForegroundWindow($p.MainWindowHandle) | Out-Null; Start-Sleep 3 } return $p } $script:KeepWelcome = $false function Close-Welcome([string]$tag) { $hs = [N]::Find('Welcome to GIMP') Log "$tag welcome windows found: $($hs.Count)" if ($hs.Count -gt 0) { $facts["welcome_$tag"] = $true if ($script:KeepWelcome) { return } $h = $hs[0] [N]::SetForegroundWindow($h) | Out-Null; Start-Sleep 1 Key @(0x1B); Start-Sleep 2 if ([N]::Find('Welcome to GIMP').Count -gt 0) { $r = New-Object N+RECT if ([N]::GetWindowRect($h, [ref]$r)) { Click ($r.Right - 56) ($r.Bottom - 31); Log "$tag clicked Close at $($r.Right - 56),$($r.Bottom - 31)"; Start-Sleep 2 } } if ([N]::Find('Welcome to GIMP').Count -gt 0) { [N]::PostMessage($h, 0x0010, [IntPtr]::Zero, [IntPtr]::Zero) | Out-Null; Start-Sleep 2 } Log "$tag welcome closed: $([N]::Find('Welcome to GIMP').Count -eq 0)" return } $w = $null foreach ($e in $UIA::RootElement.FindAll([System.Windows.Automation.TreeScope]::Children, [System.Windows.Automation.Condition]::TrueCondition)) { if ($e.Current.Name -like 'Welcome to GIMP*') { $w = $e; break } } if (-not $w) { return } $facts["welcome_$tag"] = $true if ($script:KeepWelcome) { return } Shot "$tag-welcome" $b = FindName 'Close' 5 $w if ($b -and (Invoke-El $b)) { Log "$tag closed Welcome dialog"; Start-Sleep 2; return } try { $w.GetCurrentPattern([System.Windows.Automation.WindowPattern]::Pattern).Close(); Log "$tag closed Welcome window" } catch { Log "$tag could not close Welcome: $_" } Start-Sleep 2 } function Stop-Gimp($p, [string]$tag) { if (-not $p -or $p.HasExited) { return } $p.Refresh() if ($p.MainWindowHandle -ne 0) { [N]::SetForegroundWindow($p.MainWindowHandle) | Out-Null; Start-Sleep 1; Key @(0x11, 0x51) } if (-not $p.WaitForExit(20000)) { $p.CloseMainWindow() | Out-Null } if (-not $p.WaitForExit(20000)) { Log "$tag GIMP did not close in 30s; looking for a quit dialog" $q = FindName 'Quit' 3; if ($q) { Invoke-El $q | Out-Null; $p.WaitForExit(15000) | Out-Null } } if (-not $p.HasExited) { Log "$tag killing GIMP"; Stop-Process -Id $p.Id -Force } # Leave the GIMPPortable launcher running: it copies the settings back into Data after GIMP exits. Get-Process -Name 'gimp*' -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -notlike 'GIMPPortable*' } | Stop-Process -Force -ErrorAction SilentlyContinue Start-Sleep 3 } function Set-NoWelcome($dir) { # Same effect as unticking "Show on Start" in GIMP's Welcome dialog; only for clean screenshots. $rc = Join-Path $dir 'gimprc' if (Test-Path $rc) { $c = Get-Content $rc -Raw; if ($c -notmatch 'show-welcome-dialog') { Add-Content $rc "`n(show-welcome-dialog no)" } } else { Set-Content $rc '(show-welcome-dialog no)' } } function Close-Explorer { Get-Process explorer -ErrorAction SilentlyContinue | Out-Null $shell = New-Object -ComObject Shell.Application foreach ($w in @($shell.Windows())) { try { $w.Quit() } catch { } } Start-Sleep 2 } function Snapshot-Config($tag) { $d = Join-Path $Cfg '3.2' $facts["config_$tag"] = [ordered]@{ appdata_gimp = @(Tree $Cfg); files_32 = @(Tree $d) toolrc = (Hash (Join-Path $d 'toolrc')); sessionrc = (Hash (Join-Path $d 'sessionrc')); shortcutsrc = (Hash (Join-Path $d 'shortcutsrc')) toolrc_has_vector = $(if (Test-Path (Join-Path $d 'toolrc')) { [bool](Select-String -Path (Join-Path $d 'toolrc') -Pattern 'gimp-vector-tool' -Quiet) } else { $null }) } if (Test-Path (Join-Path $d 'toolrc')) { Copy-Item (Join-Path $d 'toolrc') (Join-Path $out "$tag-toolrc.txt") -Force } } function Copy-Contents($from, $to) { New-Item -ItemType Directory -Force $to | Out-Null; Copy-Item -Path (Join-Path $from '*') -Destination $to -Recurse -Force } function Repair-32($d) { # Our repair for PhotoGIMP 3.1 on GIMP 3.2: the "vectors" names were renamed to "path" in GIMP 3.2. (Get-Content (Join-Path $d 'toolrc') -Raw) -replace '"gimp-vector-tool"', '"gimp-path-tool"' | Set-Content (Join-Path $d 'toolrc') -NoNewline (Get-Content (Join-Path $d 'sessionrc') -Raw) -replace '"gimp-vectors-list"', '"gimp-path-list"' | Set-Content (Join-Path $d 'sessionrc') -NoNewline (Get-Content (Join-Path $d 'shortcutsrc') -Raw) -replace '"tools-vector"', '"tools-path"' | Set-Content (Join-Path $d 'shortcutsrc') -NoNewline } function Reset-Stock { Remove-Item -Recurse -Force (Join-Path $Cfg '3.2') -ErrorAction SilentlyContinue Remove-Item -Recurse -Force (Join-Path $Cfg '3.0') -ErrorAction SilentlyContinue $p = Start-Gimp 'reset' -Settle 10; Stop-Gimp $p 'reset' Set-NoWelcome (Join-Path $Cfg '3.2') } function Wait-Gimp([string]$tag, [int]$Timeout = 300, [int]$Settle = 20) { $end = (Get-Date).AddSeconds($Timeout); $p = $null while ((Get-Date) -lt $end) { $p = Get-Process -Name 'gimp*' -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowHandle -ne 0 -and ($_.MainWindowTitle -match ([string][char]0x2013 + ' GIMP$') -or $_.MainWindowTitle -eq 'GNU Image Manipulation Program') } | Select-Object -First 1 if ($p) { break }; Start-Sleep 2 } if (-not $p) { Log "$tag no GIMP window after $Timeout s"; $facts["${tag}_window"] = $false; return $null } $facts["${tag}_window"] = $true; $facts["${tag}_exe"] = $p.Path Log "$tag window: '$($p.MainWindowTitle)' from $($p.Path)" Start-Sleep $Settle; Hide-Consoles; Close-Welcome $tag [N]::ShowWindow($p.MainWindowHandle, 3) | Out-Null; [N]::SetForegroundWindow($p.MainWindowHandle) | Out-Null; Start-Sleep 3 return $p } function Find-Settings { # Every folder that looks like a GIMP settings folder (contains gimprc or toolrc), anywhere GIMP might write. $roots = @((Join-Path $env:APPDATA 'GIMP'), (Join-Path $env:LOCALAPPDATA 'Packages'), 'C:\pa') $found = @() foreach ($r in $roots) { if (-not (Test-Path $r)) { continue } Get-ChildItem $r -Recurse -Include 'gimprc', 'sessionrc' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DirectoryName -Unique | Where-Object { $_ -notmatch '\\etc\\gimp\\' } | ForEach-Object { $d = $_; $t = Join-Path $d 'toolrc' $found += [ordered]@{ dir = $d; toolrc = (Hash $t) } } } return $found } function Fixed-Copy($dst) { Copy-Contents $src30 $dst; Repair-32 $dst } try { Step 'download PhotoGIMP 3.1' $zip = Join-Path $env:TEMP 'PhotoGIMP.zip' Invoke-WebRequest -Uri $PgUrl -OutFile $zip -UseBasicParsing $facts.photogimp_zip_sha_ok = ((Hash $zip) -eq $PgSha) $ex = Join-Path $env:TEMP 'PhotoGIMP'; Expand-Archive $zip -DestinationPath $ex -Force; $src30 = Join-Path $ex '3.0' Step 'Microsoft Store: install GIMP with winget (msstore source)' if ($env:ONLY -ne 'portable') { try { $facts.winget = (winget --version 2>&1 | Out-String).Trim() $srch = (winget search --name GIMP --source msstore --accept-source-agreements 2>&1 | Out-String); Set-Content (Join-Path $out 'store-search.txt') $srch $m = [regex]::Match($srch, '(?m)^GIMP\s+([0-9A-Z]{12})\s'); $sid = $(if ($m.Success) { $m.Groups[1].Value } else { '9PNSJCLXDZ0V' }); $facts.store_id = $sid $o = (winget install --id $sid --source msstore --accept-package-agreements --accept-source-agreements --silent 2>&1 | Out-String) Set-Content (Join-Path $out 'store-install.txt') $o; $facts.store_install_exit = $LASTEXITCODE $pkg = Get-AppxPackage -Name '*GIMP*' | Select-Object -First 1 if ($pkg) { $facts.store_package = [ordered]@{ name = $pkg.Name; version = "$($pkg.Version)"; family = $pkg.PackageFamilyName; arch = "$($pkg.Architecture)" } $appId = ([xml](Get-Content (Join-Path $pkg.InstallLocation 'AppxManifest.xml'))).Package.Applications.Application | Select-Object -First 1 -ExpandProperty Id $aumid = "$($pkg.PackageFamilyName)!$appId"; $facts.store_aumid = $aumid Start-Process "shell:AppsFolder\$aumid" $p = Wait-Gimp 'store-first' if ($p) { Shot 'store-first' $p.MainWindowHandle; Stop-Gimp $p 'store-first' } $facts.store_settings_after_first = @(Find-Settings) $facts.store_appdata_gimp = @(Tree (Join-Path $env:APPDATA 'GIMP')) # A: our way (paste into %APPDATA%\GIMP\3.2), as the path finder says for the installer version $appdata32 = Join-Path $env:APPDATA 'GIMP\3.2' New-Item -ItemType Directory -Force $appdata32 | Out-Null; Fixed-Copy $appdata32 Start-Process "shell:AppsFolder\$aumid"; $p = Wait-Gimp 'store-appdata' if ($p) { Shot 'store-appdata' $p.MainWindowHandle; Stop-Gimp $p 'store-appdata' } $facts.store_settings_after_appdata_copy = @(Find-Settings) # B: into the folder the Store GIMP really used (if it is a private package folder) $priv = @($facts.store_settings_after_first | Where-Object { $_.dir -like '*\Packages\*' } | ForEach-Object { $_.dir }) | Select-Object -First 1 $facts.store_private_dir = $priv if ($priv) { Fixed-Copy $priv Start-Process "shell:AppsFolder\$aumid"; $p = Wait-Gimp 'store-private' if ($p) { Shot 'store-private' $p.MainWindowHandle; Stop-Gimp $p 'store-private' } $facts.store_settings_after_private_copy = @(Find-Settings) } } else { $facts.store_package = $null } } catch { Log "store failed: $_"; $facts.store_error = "$_" } } Get-Process -Name 'gimp*' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Remove-Item -Recurse -Force (Join-Path $env:APPDATA 'GIMP') -ErrorAction SilentlyContinue Step 'GIMP Portable (PortableApps.com)' # Every call that can block has a time limit, and facts are saved after each step, so a stuck step still # leaves results. The package's silent install (/S) never finished in our first run (10 minutes), so we # unpack it with 7-Zip: the PortableApps.com installer is an NSIS archive and puts the same files in place. function Run-Limited($exe, $argList, $secs, $tag) { $pp = Start-Process -FilePath $exe -ArgumentList $argList -PassThru -WindowStyle Hidden -RedirectStandardOutput (Join-Path $out "$tag.txt") -RedirectStandardError (Join-Path $out "$tag-err.txt") if (-not $pp.WaitForExit($secs * 1000)) { Log "$tag still running after $secs s; stopping it"; Stop-Process -Id $pp.Id -Force -ErrorAction SilentlyContinue; return 'timeout' } return $pp.ExitCode } function Wait-LauncherExit([int]$secs) { $end = (Get-Date).AddSeconds($secs) while ((Get-Date) -lt $end) { if (-not (Get-Process -Name 'GIMPPortable' -ErrorAction SilentlyContinue)) { return $true }; Start-Sleep 2 } Log "launcher still running after $secs s"; return $false } $TS = [System.Windows.Automation.TreeScope]; $AE = [System.Windows.Automation.AutomationElement] function Installer-Window { Get-Process | Where-Object { $_.MainWindowHandle -ne 0 -and $_.MainWindowTitle -like 'GIMP Portable*' -and $_.MainWindowTitle -notlike '*Launcher*' } | Select-Object -First 1 } function Drive-Installer([int]$procId, [int]$secs) { # Like a user who accepts every default: press Enter on each page (language OK, Next, I Agree, Install, Finish). # The destination is preset with the installer's own /DESTINATION= option. $end = (Get-Date).AddSeconds($secs); $n = 0; $done = @(); $logged = $false while ((Get-Date) -lt $end) { $w = Installer-Window if (-not $w) { if (-not (Get-Process -Id $procId -ErrorAction SilentlyContinue) -and (Test-Path 'C:\pa\GIMPPortable\GIMPPortable.exe')) { return $done } if (-not $logged -and ((Get-Date) -gt $end.AddSeconds(20 - $secs))) { $logged = $true; Log "no installer window yet; processes with windows: $((Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { $_.ProcessName + ':' + $_.MainWindowTitle }) -join ' ;; ')" } Start-Sleep 2; continue } $n++; Shot ('portable-installer-{0:D2}' -f $n) Log "installer window '$($w.MainWindowTitle)' (pid $($w.Id), $($w.ProcessName)): Enter" [N]::SetForegroundWindow($w.MainWindowHandle) | Out-Null; Start-Sleep -Milliseconds 600; Key @(0x0D) $done += $w.MainWindowTitle; Start-Sleep 4 if ($n -gt 25) { break } } Log "installer: giving up; top windows: $((Get-TopWindows) -join ' ;; ')"; Shot 'portable-installer-stuck'; return $done } function Press-El($el) { # A real mouse click on the button's centre; Invoke alone did not close these NSIS dialogs. try { $w = $el; $r = $el.Current.BoundingRectangle; Click ([int]($r.X + $r.Width / 2)) ([int]($r.Y + $r.Height / 2)) } catch { Invoke-El $el | Out-Null } } function Answer-Integrity { # The PortableApps.com launcher asks this when the package was not installed by its own installer. $d = Get-Process | Where-Object { $_.MainWindowTitle -like '*PortableApps.com Launcher*' } | Select-Object -First 1 if ($d) { Shot 'portable-launcher-dialog'; $facts.portable_launcher_dialog = $true; [N]::SetForegroundWindow($d.MainWindowHandle) | Out-Null; Start-Sleep -Milliseconds 600; Key @(0x59); Log 'launcher dialog: pressed Y' } } function Start-Portable([string]$tag) { Start-Process $launcher; Start-Sleep 8; Answer-Integrity $p = Wait-Gimp $tag 240 if (-not $p) { Shot "$tag-nowindow" } return $p } function Portable-Settings { @(Find-Settings | Where-Object { $_.dir -like 'C:\pa\*' -or $_.dir -like "$env:APPDATA*" }) } try { $page = (Invoke-WebRequest 'https://portableapps.com/apps/graphics_pictures/gimp_portable' -UseBasicParsing -TimeoutSec 60).Content $file = ([regex]::Match($page, 'GIMPPortable_[0-9A-Za-z._-]+?\.paf\.exe')).Value $facts.portable_file = $file $paf = Join-Path $env:TEMP $file $facts.portable_download = Run-Limited 'curl.exe' @('-sSL', '--max-time', '600', '-o', "`"$paf`"", "https://downloads.sourceforge.net/portableapps/$file") 660 'portable-download' $facts.portable_sha256 = Hash $paf; $facts.portable_bytes = (Get-Item $paf).Length; Save 'facts.json' $facts; Log "downloaded $($facts.portable_bytes) bytes" $pi = Start-Process $paf -ArgumentList '/DESTINATION=C:\pa\' -PassThru $facts.portable_installer_steps = @(Drive-Installer $pi.Id 600) Start-Sleep 5 if (-not $pi.HasExited) { Stop-Process -Id $pi.Id -Force -ErrorAction SilentlyContinue } # If the Finish page started GIMP Portable, close it so every start below is ours. Get-Process -Name 'gimp*', 'GIMPPortable*' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue; Start-Sleep 3 $facts.portable_install_way = 'PortableApps.com installer, clicked through' if (-not (Test-Path 'C:\pa\GIMPPortable\GIMPPortable.exe')) { $7z = @('C:\Program Files\7-Zip\7z.exe', 'C:\ProgramData\chocolatey\bin\7z.exe') | Where-Object { Test-Path $_ } | Select-Object -First 1 $facts.portable_unpack = Run-Limited $7z @('x', "`"$paf`"", '-oC:\pa\GIMPPortable', '-x!$PLUGINSDIR', '-y', '-bd') 600 'portable-7z' $facts.portable_install_way = 'installer failed; unpacked with 7-Zip' } Save 'facts.json' $facts $facts.portable_tree = @(Tree 'C:\pa\GIMPPortable'); $facts.portable_app = @(Tree 'C:\pa\GIMPPortable\App'); Save 'facts.json' $facts Log "unpacked: $($facts.portable_tree -join ', ')" $ini = 'C:\pa\GIMPPortable\App\AppInfo\appinfo.ini' if (Test-Path $ini) { $facts.portable_appinfo = @(Get-Content $ini | Select-String 'PackageVersion|DisplayVersion|^Name=' | ForEach-Object { $_.Line }) } $lini = Get-ChildItem 'C:\pa\GIMPPortable\App\AppInfo\Launcher' -Filter '*.ini' -ErrorAction SilentlyContinue | Select-Object -First 1 if ($lini) { Copy-Item $lini.FullName (Join-Path $out 'portable-launcher.ini') } $launcher = 'C:\pa\GIMPPortable\GIMPPortable.exe' Remove-Item -Recurse -Force (Join-Path $env:APPDATA 'GIMP') -ErrorAction SilentlyContinue Log 'portable: first start' $p = Start-Portable 'portable-first' if ($p) { Shot 'portable-first' $p.MainWindowHandle; Stop-Gimp $p 'portable-first' } $facts.portable_launcher_exited = Wait-LauncherExit 120 $facts.portable_settings_after_first = Portable-Settings; $facts.portable_appdata_gimp = @(Tree (Join-Path $env:APPDATA 'GIMP')) $facts.portable_data = @(Tree 'C:\pa\GIMPPortable\Data'); Save 'facts.json' $facts $dir = @($facts.portable_settings_after_first | Where-Object { $_.dir -like 'C:\pa\*' -and $_.dir -notlike '*\DefaultData\*' -and $_.dir -notlike '*\Other\*' } | ForEach-Object { $_.dir }) | Select-Object -First 1 $facts.portable_dir = $dir; Log "portable settings folder: $dir" if ($dir) { Fixed-Copy $dir; $facts.portable_installed_toolrc = Hash (Join-Path $dir 'toolrc') Log 'portable: start with PhotoGIMP' $p = Start-Portable 'portable-installed' if ($p) { Shot 'portable-installed' $p.MainWindowHandle; Stop-Gimp $p 'portable-installed' } $facts.portable_launcher_exited2 = Wait-LauncherExit 120 $facts.portable_settings_after_install = Portable-Settings; Save 'facts.json' $facts Log 'portable: second start' $p = Start-Portable 'portable-second' if ($p) { Shot 'portable-second' $p.MainWindowHandle; Stop-Gimp $p 'portable-second' } Wait-LauncherExit 120 | Out-Null $facts.portable_settings_after_second = Portable-Settings } } catch { Log "portable failed: $_"; $facts.portable_error = "$_" } } catch { Log "FAILED: $_"; $facts.error = "$_" } finally { Get-Process -Name 'gimp*' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue Save 'facts.json' $facts Log 'done' }