Scripts/Debug-FoPipelineSteps.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Run an image pipeline step-by-step and compare output after each step to find corruption. .DESCRIPTION Mirrors Invoke-FoPluginChain execution but compares the working file against an untouched copy of the input after every plugin step. Reports the first step where visual compare fails. Uses the same compare stack as image integration tests (Compare-FoImage, JPEG SSIM fallback, GIF/APNG frame compare, ICO largest-icon compare). .PARAMETER Path Image file to optimize. .PARAMETER ProfileName Settings profile from Tests/ImageTestProfiles.psd1 (default LosslessDefault). .PARAMETER CompareMode Pixel, SSIM, or SSIMOnly. Defaults to the profile CompareMode when omitted. .PARAMETER PluginPath Plugin directory. Defaults to FO_TEST_PLUGIN_PATH or Get-FoDefaultPluginPath. .PARAMETER WorkDirectory Folder for before copy, per-step snapshots, and failure diffs. .PARAMETER AllowMissingDssim Allow PNG pixel compare to fall back to ImageMagick AE when dssim is absent. .PARAMETER ContinueOnFailure Run all steps even after the first compare failure (default stops at first failure). .EXAMPLE .\Scripts\Debug-FoPipelineSteps.ps1 .\Tests\Fixtures\Images\pngsuite\basn2c08.png .EXAMPLE .\Scripts\Debug-FoPipelineSteps.ps1 .\photo.jpg -ProfileName LossyHighQuality -CompareMode SSIMOnly #> [CmdletBinding()] param( [Parameter(Mandatory)] [Alias('FullName')] [string]$Path, [string]$ProfileName = 'LosslessDefault', [ValidateSet('Pixel', 'SSIM', 'SSIMOnly')] [string]$CompareMode, [string]$PluginPath, [string]$WorkDirectory, [switch]$AllowMissingDssim, [switch]$ContinueOnFailure ) $ErrorActionPreference = 'Stop' $moduleRoot = Split-Path -Parent $PSScriptRoot $script:FoModuleRoot = $moduleRoot Import-Module (Join-Path $moduleRoot 'FileOptimizer.psd1') -Force Import-Module (Join-Path $moduleRoot 'Tests\FoTestSupport\FoTestSupport.psd1') -Force . (Join-Path $moduleRoot 'Public\Resolve-FoPluginExecutable.ps1') . (Join-Path $moduleRoot 'Private\Handlers\Invoke-FoNativeHandlers.ps1') . (Join-Path $moduleRoot 'Private\Get-FoStepRequiredExecutables.ps1') . (Join-Path $moduleRoot 'Private\Invoke-FoPlugin.ps1') . (Join-Path $moduleRoot 'Private\Get-FoLevelFlags.ps1') . (Join-Path $moduleRoot 'Private\Get-ExtensionByContent.ps1') . (Join-Path $moduleRoot 'Private\Test-FoFileGate.ps1') . (Join-Path $moduleRoot 'Pipelines\_Helpers.ps1') Get-ChildItem -Path (Join-Path $moduleRoot 'Pipelines\*.ps1') -Exclude '_Helpers.ps1' | ForEach-Object { . $_.FullName } . (Join-Path $moduleRoot 'Public\Get-FoPipeline.ps1') function Invoke-FoDebugStepVisualCompare { param( [Parameter(Mandatory)][string]$Before, [Parameter(Mandatory)][string]$After, [Parameter(Mandatory)][string]$CompareMode, [Parameter(Mandatory)][string]$PluginPath, [string]$DiffOutputPath, [double]$SSIMDissimilarityMaximum = -1, [switch]$AllowMissingDssim, [string]$WorkDirectory ) $ext = [System.IO.Path]::GetExtension($After) $decisions = Get-FoImageTestDecisions if ($ext -match '(?i)^\.gif$') { $beforeFrames = Get-FoGifFrameCount -Path $Before -PluginPath $PluginPath $afterFrames = Get-FoGifFrameCount -Path $After -PluginPath $PluginPath if ($beforeFrames -gt 1 -or $afterFrames -gt 1) { $frameDir = Join-Path $WorkDirectory 'frames' $frameCompare = Compare-FoGifFrames -Before $Before -After $After -PluginPath $PluginPath ` -WorkDirectory $frameDir -DiffOutputPath $DiffOutputPath return [PSCustomObject]@{ Pass = $frameCompare.Pass CompareTool = 'GifFrames' MetricValue = if ($frameCompare.FrameResults) { ($frameCompare.FrameResults | Measure-Object -Property MetricValue -Maximum).Maximum } else { $null } CompareMode = $CompareMode Detail = $frameCompare } } } if ($ext -match '(?i)^\.ico$') { $icoCompare = Compare-FoIcoLargest -Before $Before -After $After -PluginPath $PluginPath ` -WorkDirectory (Join-Path $WorkDirectory 'ico') -DiffOutputPath $DiffOutputPath return [PSCustomObject]@{ Pass = $icoCompare.Pass CompareTool = 'IcoLargest' MetricValue = $icoCompare.MetricValue CompareMode = $CompareMode Detail = $icoCompare } } if ($ext -match '(?i)^\.png$') { $beforeApng = 1 $afterApng = 1 try { $beforeApng = Get-FoApngFrameCount -Path $Before -PluginPath $PluginPath -WorkDirectory (Join-Path $WorkDirectory 'apng-probe-before') } catch { Write-Debug $_.Exception.Message } try { $afterApng = Get-FoApngFrameCount -Path $After -PluginPath $PluginPath -WorkDirectory (Join-Path $WorkDirectory 'apng-probe-after') } catch { Write-Debug $_.Exception.Message } if (($beforeApng -gt 1) -or ($afterApng -gt 1)) { $apngCompare = Compare-FoApngFrames -Before $Before -After $After -PluginPath $PluginPath ` -WorkDirectory (Join-Path $WorkDirectory 'apng') -DiffOutputPath $DiffOutputPath return [PSCustomObject]@{ Pass = $apngCompare.Pass CompareTool = 'ApngFrames' MetricValue = if ($apngCompare.FrameResults) { ($apngCompare.FrameResults | Measure-Object -Property MetricValue -Maximum).Maximum } else { $null } CompareMode = $CompareMode Detail = $apngCompare } } } $modeEffective = if ($CompareMode -eq 'SSIMOnly') { 'SSIM' } else { $CompareMode } $compareParams = @{ Before = $Before After = $After Mode = $modeEffective PluginPath = $PluginPath } if ($DiffOutputPath) { $compareParams['DiffOutputPath'] = $DiffOutputPath } if ($modeEffective -eq 'SSIM' -and $SSIMDissimilarityMaximum -ge 0) { $compareParams['SSIMDissimilarityMaximum'] = $SSIMDissimilarityMaximum } if ($modeEffective -eq 'Pixel') { $compareParams['PngDssimDissimilarityMaximum'] = $decisions.PngDssimDissimilarityMaximum } if ($AllowMissingDssim) { $compareParams['AllowMissingDssim'] = $true } $compare = Compare-FoImage @compareParams if ($CompareMode -eq 'Pixel' -and -not $compare.Pass -and $ext -match '(?i)^\.jpe?g$') { $jpegCompare = Test-FoJpegImageCompare -Before $Before -After $After -PluginPath $PluginPath ` -SSIMDissimilarityMaximum $SSIMDissimilarityMaximum -DiffOutputPath $DiffOutputPath ` -AllowMissingDssim:$AllowMissingDssim $compare = $jpegCompare.Compare $modeEffective = $jpegCompare.CompareMode } return [PSCustomObject]@{ Pass = $compare.Pass CompareTool = $compare.CompareTool MetricValue = $compare.MetricValue CompareMode = $modeEffective Detail = $compare } } function Debug-FoPipelineSteps { [CmdletBinding()] param( [Parameter(Mandatory)] [string]$Path, [string]$ProfileName = 'LosslessDefault', [ValidateSet('Pixel', 'SSIM', 'SSIMOnly')] [string]$CompareMode, [string]$PluginPath, [string]$WorkDirectory, [switch]$AllowMissingDssim, [switch]$ContinueOnFailure ) $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) { throw "File not found: $Path" } $settings = Get-FoImageTestProfile -Name $ProfileName -PluginPath $PluginPath if ($settings.LogLevel -lt 2) { $settings['LogLevel'] = 2 } $effectiveCompareMode = if ($PSBoundParameters.ContainsKey('CompareMode')) { $CompareMode } else { Get-FoImageTestProfileCompareMode -Name $ProfileName } $ssimThreshold = -1 if ($effectiveCompareMode -in @('SSIM', 'SSIMOnly')) { $ssimThreshold = Get-FoImageTestLossyThreshold -ProfileName $ProfileName ` -ImagePath $resolvedPath -PluginPath $settings.PluginPath } $workRoot = if ($WorkDirectory) { [System.IO.Path]::GetFullPath($WorkDirectory) } else { Join-Path $env:TEMP ("FoPipelineDebug_{0}" -f (Get-Random)) } if (-not (Test-Path -LiteralPath $workRoot)) { New-Item -ItemType Directory -Path $workRoot -Force | Out-Null } $fileName = [System.IO.Path]::GetFileName($resolvedPath) $beforePath = Join-Path $workRoot "before_$fileName" $workFile = Join-Path $workRoot "work_$fileName" Copy-Item -LiteralPath $resolvedPath -Destination $beforePath -Force Copy-Item -LiteralPath $resolvedPath -Destination $workFile -Force $plan = Get-FoExecutionPlan -Path $workFile -Settings $settings if ($plan.Plans.Count -eq 0) { throw "No pipeline groups for '$resolvedPath'." } Write-Host "Debugging $($plan.Plans.GroupName -join ', ') ($($plan.Plans.Steps.Count) steps) [CompareMode=$effectiveCompareMode]" -ForegroundColor Cyan Write-Host "Work directory: $workRoot" $stepResults = [System.Collections.Generic.List[object]]::new() $firstFailure = $null $stepIndex = 0 foreach ($p in $plan.Plans) { foreach ($step in $p.Steps) { $stepIndex++ $stepLabel = $step.Name $pluginResult = Invoke-FoPlugin -Step $step -InputFile $workFile -Settings $settings ` -PluginPath $settings.PluginPath -SearchMode $settings.PluginSearchMode $fileExt = [System.IO.Path]::GetExtension($workFile) $safeLabel = $stepLabel -replace '[^\w\-]', '_' $snapshot = Join-Path $workRoot ("step_{0:D2}_{1}{2}" -f $stepIndex, $safeLabel, $fileExt) Copy-Item -LiteralPath $workFile -Destination $snapshot -Force # dssim appends "-0.png" (etc.) to -o paths; omit extension so names stay clean. $diffPath = Join-Path $workRoot ("diff_{0:D2}" -f $stepIndex) $compareError = $null try { $compare = Invoke-FoDebugStepVisualCompare -Before $beforePath -After $workFile ` -CompareMode $effectiveCompareMode -PluginPath $settings.PluginPath ` -DiffOutputPath $diffPath -SSIMDissimilarityMaximum $ssimThreshold ` -AllowMissingDssim:$AllowMissingDssim -WorkDirectory $workRoot } catch { $compareError = $_.Exception.Message $compare = [PSCustomObject]@{ Pass = $false CompareTool = 'Error' MetricValue = $null CompareMode = $effectiveCompareMode Detail = $compareError } } $entry = [PSCustomObject]@{ Index = $stepIndex Step = $stepLabel Group = $p.GroupName Accepted = $pluginResult.Accepted Skipped = $pluginResult.Skipped SizeBefore = $pluginResult.SizeBefore SizeAfter = $pluginResult.SizeAfter ComparePass = $compare.Pass CompareTool = $compare.CompareTool MetricValue = $compare.MetricValue CompareError = $compareError SnapshotPath = $snapshot DiffPath = if ($compare.Pass) { $null } else { $diffPath } } $stepResults.Add($entry) $statusColor = if ($compare.Pass) { 'Green' } else { 'Red' } $metricText = if ($null -ne $compare.MetricValue) { " metric=$($compare.MetricValue)" } else { '' } $errorText = if ($compareError) { " error=$compareError" } else { '' } Write-Host (" [{0}] {1}: compare={2}{3}{4} size {5} -> {6}" -f $stepIndex, $stepLabel, $compare.Pass, $metricText, $errorText, (Format-FoFileSize $pluginResult.SizeBefore), (Format-FoFileSize $pluginResult.SizeAfter)) -ForegroundColor $statusColor if (-not $compare.Pass -and -not $firstFailure) { $firstFailure = $entry if (-not $ContinueOnFailure) { Write-Warning "First failing step: $stepLabel (snapshot: $snapshot, diff: $diffPath)" break } } } if ($firstFailure -and -not $ContinueOnFailure) { break } } $summary = [PSCustomObject]@{ Path = $resolvedPath ProfileName = $ProfileName CompareMode = $effectiveCompareMode WorkDirectory = $workRoot BeforePath = $beforePath FinalPath = $workFile StepResults = @($stepResults) FirstFailure = $firstFailure AllPassed = ($null -eq $firstFailure) Groups = @($plan.Plans.GroupName) } if ($summary.AllPassed) { Write-Host 'All steps passed visual compare.' -ForegroundColor Green } else { Write-Host ("First failure at step {0}: {1}" -f $firstFailure.Index, $firstFailure.Step) -ForegroundColor Yellow } return $summary } if (-not (Test-FoPluginsAvailable)) { throw 'Plugin binaries required. Set FO_TEST_PLUGIN_PATH or run Install-Plugins.ps1.' } Debug-FoPipelineSteps @PSBoundParameters |