Scripts/Invoke-FoImageCorpusSweep.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Batch-optimize image corpus fixtures and export compare metrics to CSV. .DESCRIPTION L3 regression helper (tagged Slow in Pester). Runs Invoke-FoImageOptimizationTest across Tier A manifest fixtures or all image files under a downloaded Tier B/C/D corpus. .PARAMETER Tier A — manifest fixtures under Tests/Fixtures/Images/. B, C, D — recursive scan under Tests/Fixtures/Corpus/tier-{b,c,d}/ (download via Get-ImageTestCorpus.ps1). .PARAMETER ProfileName Settings profile from Tests/ImageTestProfiles.psd1. Each profile declares a preferred CompareMode (Pixel or SSIMOnly); the sweep uses that unless -CompareMode is supplied. .PARAMETER CompareMode Visual verification mode passed to Invoke-FoImageOptimizationTest: Pixel — lossless pixel/dssim compare (PNG uses dssim when installed). SSIM / SSIMOnly — ImageMagick SSIM dissimilarity; SSIMOnly skips pixel compare first. When omitted, the value from ImageTestProfiles.psd1 for -ProfileName is used. .PARAMETER PluginPath Plugin directory (magick.exe, oxipng.exe, …). Defaults to FO_TEST_PLUGIN_PATH or module plugins/. .PARAMETER OutputCsv CSV path for results. Default: ./corpus-sweep-tier{tier}-{profile}-{timestamp}.csv under current directory. .PARAMETER SkipCompare Size-only regression; skip visual compare. .PARAMETER AllowMissingDssim Allow PNG pixel compare to fall back to ImageMagick AE when dssim is not installed. Default: require dssim (also opt out with FO_COMPARE_ALLOW_MISSING_DSSIM=1). .PARAMETER MaxFiles Limit files processed (0 = all). Useful for smoke runs. .PARAMETER CorpusRoot Override FO_TEST_CORPUS_PATH for Tier B/C/D scans. .PARAMETER WorkDirectory Root folder for per-file work dirs and failure artifacts. Defaults to a temp folder. .EXAMPLE .\Scripts\Invoke-FoImageCorpusSweep.ps1 -Tier A -ProfileName LosslessDefault .EXAMPLE .\Scripts\Invoke-FoImageCorpusSweep.ps1 -Tier A -ProfileName LossyHighQuality Uses SSIMOnly compare mode from the LossyHighQuality profile. .EXAMPLE .\Scripts\Invoke-FoImageCorpusSweep.ps1 -Tier A -ProfileName LossyHighQuality -CompareMode Pixel Force pixel compare despite the profile default (not recommended for lossy profiles). .EXAMPLE .\Scripts\Get-ImageTestCorpus.ps1 -Tier B .\Scripts\Invoke-FoImageCorpusSweep.ps1 -Tier B -MaxFiles 20 -OutputCsv .\tier-b-sweep.csv #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('A', 'B', 'C', 'D')] [string]$Tier, [string]$ProfileName = 'LosslessDefault', [string]$OutputCsv, [string]$PluginPath, [ValidateSet('Pixel', 'SSIM', 'SSIMOnly')] [string]$CompareMode, [switch]$SkipCompare, [switch]$AllowMissingDssim, [int]$MaxFiles = 0, [string]$CorpusRoot, [string]$WorkDirectory ) $ErrorActionPreference = 'Stop' $moduleRoot = Split-Path -Parent $PSScriptRoot Import-Module (Join-Path $moduleRoot 'FileOptimizer.psd1') -Force Import-Module (Join-Path $moduleRoot 'Tests\FoTestSupport\FoTestSupport.psd1') -Force if (-not (Test-FoPluginsAvailable)) { throw 'Plugin binaries required. Set FO_TEST_PLUGIN_PATH or install plugins under .\plugins\.' } $resolvedPluginPath = if ($PluginPath) { [System.IO.Path]::GetFullPath($PluginPath) } else { Get-FoTestPluginPath } Write-FoTestPluginVersions -PluginPath $resolvedPluginPath -Verbose $effectiveCompareMode = if ($PSBoundParameters.ContainsKey('CompareMode')) { $CompareMode } else { Get-FoImageTestProfileCompareMode -Name $ProfileName } if (-not $SkipCompare -and $effectiveCompareMode -eq 'Pixel' -and -not (Test-FoCompareAllowMissingDssim -AllowMissingDssim:$AllowMissingDssim.IsPresent)) { Assert-FoDssimCompareAvailable -PluginPath $resolvedPluginPath } $settings = Get-FoImageTestProfile -Name $ProfileName -PluginPath $resolvedPluginPath $imageExtensions = @('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tif', '.tiff', '.ico', '.avif') function Get-FoCorpusSweepTargets { param( [string]$TierName, [string]$RootOverride ) $targets = @() if ($TierName -eq 'A') { $manifest = Get-FoImageTestManifest foreach ($entry in @($manifest.Tiers.A.Files)) { $targets += [PSCustomObject]@{ Id = $entry.Id RelativePath = $entry.Source Format = $entry.Format Path = Get-FoImageTestFixturePath -Id $entry.Id LossySSIMMaximum = if ($null -ne $entry.LossySSIMMaximum) { [double]$entry.LossySSIMMaximum } else { -1 } } } return $targets } $presence = Test-FoImageTestFixturesPresent -Tier $TierName -CorpusRoot $RootOverride if (-not $presence.Present) { throw "Tier $TierName corpus not present at '$($presence.Root)'. Run Get-ImageTestCorpus.ps1 -Tier $TierName first." } $tierRoot = $presence.Root foreach ($file in Get-ChildItem -LiteralPath $tierRoot -Recurse -File) { if ($imageExtensions -notcontains $file.Extension.ToLowerInvariant()) { continue } $relative = $file.FullName.Substring($tierRoot.Length).TrimStart('\', '/') $targets += [PSCustomObject]@{ Id = $null RelativePath = $relative Format = $file.Extension.TrimStart('.').ToUpperInvariant() Path = $file.FullName LossySSIMMaximum = -1 } } return $targets } function Get-FoCorpusSweepWorkDirectoryName { param( [string]$RelativePath, [string]$FullPath ) $label = if ($RelativePath) { $RelativePath } else { [System.IO.Path]::GetFileName($FullPath) } Get-FoCorpusSweepSafeLabel -Value $label } function Get-FoCorpusSweepSafeLabel { param( [Parameter(Mandatory)] [string]$Value ) ($Value -replace '[\\/:*?"<>|]', '_') } function New-FoCorpusSweepResultRow { param( [string]$Tier, $Target, $Result, [string]$ErrorMessage ) if ($Result) { return [PSCustomObject]@{ Tier = $Tier FixtureId = $Target.Id RelativePath = $Target.RelativePath Format = $Target.Format Status = $Result.Optimization.Status OriginalSize = $Result.Optimization.OriginalSize FinalSize = $Result.Optimization.FinalSize BytesSaved = $Result.Optimization.BytesSaved OptimizeDurationMs = if ($null -ne $Result.OptimizeDurationMs) { $Result.OptimizeDurationMs } else { $Result.Optimization.DurationMs } CompareDurationMs = $Result.CompareDurationMs CompareMode = $Result.CompareMode ComparePass = if ($Result.Compare) { $Result.Compare.Pass } else { $null } MetricValue = if ($Result.Compare) { $Result.Compare.MetricValue } else { $null } Pass = $Result.Pass Error = if ($Result.CompareError) { $Result.CompareError } else { $null } FailureArtifacts = if ($Result.FailureArtifacts) { $Result.FailureArtifacts.Root } else { $null } WorkDirectory = $Result.WorkDirectory } } return [PSCustomObject]@{ Tier = $Tier FixtureId = $Target.Id RelativePath = $Target.RelativePath Format = $Target.Format Status = 'Error' OriginalSize = $null FinalSize = $null BytesSaved = $null OptimizeDurationMs = $null CompareDurationMs = $null CompareMode = $null ComparePass = $null MetricValue = $null Pass = $false Error = $ErrorMessage FailureArtifacts = $null WorkDirectory = $null } } $targets = @(Get-FoCorpusSweepTargets -TierName $Tier -RootOverride $CorpusRoot) if ($MaxFiles -gt 0) { $targets = @($targets | Select-Object -First $MaxFiles) } if ($targets.Count -lt 1) { throw "No image files found for Tier $Tier." } $sweepRoot = if ($WorkDirectory) { [System.IO.Path]::GetFullPath($WorkDirectory) } else { Join-Path $env:TEMP ("FoCorpusSweep_{0}_{1}" -f $Tier, (Get-Date -Format 'yyyyMMdd_HHmmss')) } New-Item -ItemType Directory -Path $sweepRoot -Force | Out-Null Write-Host ("Corpus sweep: Tier {0}, {1} file(s), profile {2}, compare {3}{4}" -f ` $Tier, $targets.Count, $ProfileName, $(if ($SkipCompare) { 'skipped' } else { $effectiveCompareMode }), ` $(if (-not $SkipCompare -and -not $PSBoundParameters.ContainsKey('CompareMode')) { ' (from profile)' } else { '' })) Write-Host "Work directory: $sweepRoot" $rows = @() $index = 0 foreach ($target in $targets) { $index++ $label = if ($target.Id) { $target.Id } else { $target.RelativePath } Write-Host ("[{0}/{1}] {2}" -f $index, $targets.Count, $label) Write-Progress -Activity 'Corpus sweep' -Status $label -PercentComplete (($index / $targets.Count) * 100) $itemWorkDir = Join-Path $sweepRoot (Get-FoCorpusSweepWorkDirectoryName -RelativePath $target.RelativePath -FullPath $target.Path) New-Item -ItemType Directory -Path $itemWorkDir -Force | Out-Null $params = @{ FixturePath = $target.Path Settings = $settings CompareMode = $effectiveCompareMode WorkDirectory = $itemWorkDir SkipCompare = $SkipCompare.IsPresent } if ($AllowMissingDssim) { $params['AllowMissingDssim'] = $true } if ($effectiveCompareMode -eq 'SSIMOnly') { $format = Resolve-FoImageTestLossyFormat -Format $(if ($target.Format) { $target.Format } else { 'Default' }) if ($format -eq 'AVIF') { $params['SSIMDissimilarityMaximum'] = (Get-FoImageTestDecisions).AvifDefaultSSIMDissimilarityMaximum } else { $fixtureOverride = Get-FoImageTestLossyFixtureOverride -RelativePath $target.RelativePath -FixtureId $target.Id $thresholdParams = @{ ProfileName = $ProfileName Format = $format ImagePath = $target.Path PluginPath = $resolvedPluginPath FixtureOverride = $fixtureOverride } try { $params['SSIMDissimilarityMaximum'] = Get-FoImageTestLossyThreshold @thresholdParams } catch { $thresholdParams['Format'] = 'Default' $params['SSIMDissimilarityMaximum'] = Get-FoImageTestLossyThreshold @thresholdParams } } } try { $result = Invoke-FoImageOptimizationTest @params if ($result.CompareError) { Write-Warning "Compare failed for '$label': $($result.CompareError)" } $rows += New-FoCorpusSweepResultRow -Tier $Tier -Target $target -Result $result } catch { if (Test-FoCompareDssimRequiredError -Message $_.Exception.Message) { throw } Write-Warning "Sweep error on '$label': $($_.Exception.Message)" $rows += New-FoCorpusSweepResultRow -Tier $Tier -Target $target -ErrorMessage $_.Exception.Message } } Write-Progress -Activity 'Corpus sweep' -Completed if (-not $OutputCsv) { $profileLabel = Get-FoCorpusSweepSafeLabel -Value $ProfileName $OutputCsv = Join-Path (Get-Location) ("corpus-sweep-tier{0}-{1}-{2}.csv" -f $Tier.ToLower(), $profileLabel, (Get-Date -Format 'yyyyMMdd-HHmmss')) } $OutputCsv = [System.IO.Path]::GetFullPath($OutputCsv) $csvDir = Split-Path -Parent $OutputCsv if ($csvDir -and -not (Test-Path -LiteralPath $csvDir)) { New-Item -ItemType Directory -Path $csvDir -Force | Out-Null } $rows | Export-Csv -LiteralPath $OutputCsv -NoTypeInformation -Encoding UTF8 $passed = @($rows | Where-Object { $_.Pass }).Count $failed = $rows.Count - $passed $errors = @($rows | Where-Object { $_.Error }).Count Write-Host ("Done: {0} passed, {1} failed ({2} with errors). CSV: {3}" -f $passed, $failed, $errors, $OutputCsv) Write-Host "Failure artifact roots under: $sweepRoot" if ($failed -gt 0) { exit 1 } exit 0 |