vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF

This commit is contained in:
2026-08-27 06:58:22 +02:00
commit 532485a830
577 changed files with 149058 additions and 0 deletions
+241
View File
@@ -0,0 +1,241 @@
<#
.SYNOPSIS
Compares two veraPDF JSON batch reports and flags any file whose
validation outcome changed between the two runs.
.DESCRIPTION
Used to verify that resaving PDFs through KillerPDF does not degrade
standards conformance. Workflow:
1. Baseline the corpus:
verapdf --recurse --format json C:\pdf-corpus > baseline.json
2. Resave every corpus file through KillerPDF into a mirror folder,
preserving relative paths (see --batch-resave).
3. Validate the resaved tree:
verapdf --recurse --format json C:\pdf-corpus-resaved > after.json
4. Compare:
.\Compare-VeraPDF.ps1 -Baseline baseline.json -After after.json `
-BaselineRoot C:\pdf-corpus -AfterRoot C:\pdf-corpus-resaved
Files are matched by path relative to the given roots. The check that
matters for release: zero regressions. A regression is any of:
- NEW_FAIL: compliant in baseline, non-compliant after
- rules added: a file fails rules after that it did not fail before
(even if it already failed others)
- PARSE_ERROR_AFTER: veraPDF parsed the baseline file but not the resave
- MISSING_AFTER: file present in baseline report but absent in after
Exit code 0 = no regressions, 1 = regressions found, 2 = usage/input error.
.NOTES
Compatible with Windows PowerShell 5.1 and PowerShell 7.
Part of the KillerPDF validation harness (validation/).
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)] [string]$Baseline,
[Parameter(Mandatory = $true)] [string]$After,
[Parameter(Mandatory = $true)] [string]$BaselineRoot,
[Parameter(Mandatory = $true)] [string]$AfterRoot,
[string]$CsvOut,
[switch]$ShowUnchanged
)
$ErrorActionPreference = 'Stop'
function Get-VeraJobs {
param([string]$Path, [string]$Root)
if (-not (Test-Path -LiteralPath $Path)) {
Write-Error "Report not found: $Path"
exit 2
}
$raw = Get-Content -LiteralPath $Path -Raw
try {
$json = $raw | ConvertFrom-Json
} catch {
Write-Error "Could not parse JSON in ${Path}: $($_.Exception.Message)"
exit 2
}
if (-not $json.report -or -not $json.report.jobs) {
Write-Error "No report.jobs found in $Path - is this a veraPDF --format json report?"
exit 2
}
$rootNorm = $Root.TrimEnd('\', '/').ToLowerInvariant()
$jobs = @{}
foreach ($job in $json.report.jobs) {
$name = [string]$job.itemDetails.name
$key = $name.ToLowerInvariant()
if ($rootNorm.Length -gt 0 -and $key.StartsWith($rootNorm)) {
$key = $key.Substring($rootNorm.Length).TrimStart('\', '/')
}
# Parse failures produce a job with no validationResult entry.
$vr = $null
$vrProp = $job.PSObject.Properties['validationResult']
if ($null -ne $vrProp -and $null -ne $job.validationResult) {
$vrArr = @($job.validationResult)
if ($vrArr.Count -gt 0) { $vr = $vrArr[0] }
}
$compliant = $null
$failedRules = @()
if ($null -ne $vr) {
$compliant = [bool]$vr.compliant
if ($vr.details -and $vr.details.ruleSummaries) {
foreach ($rs in @($vr.details.ruleSummaries)) {
if ([string]$rs.ruleStatus -eq 'FAILED') {
$failedRules += ('{0} clause {1} test {2}' -f $rs.specification, $rs.clause, $rs.testNumber)
}
}
}
}
$jobs[$key] = [pscustomobject]@{
Name = $name
ParseError = ($null -eq $vr)
Compliant = $compliant
FailedRules = @($failedRules | Sort-Object -Unique)
}
}
return $jobs
}
$baseJobs = Get-VeraJobs -Path $Baseline -Root $BaselineRoot
$afterJobs = Get-VeraJobs -Path $After -Root $AfterRoot
$results = New-Object System.Collections.Generic.List[object]
$regressionCount = 0
$unchangedCount = 0
foreach ($key in @($baseJobs.Keys | Sort-Object)) {
$b = $baseJobs[$key]
if (-not $afterJobs.ContainsKey($key)) {
$results.Add([pscustomobject]@{
File = $key; Change = 'MISSING_AFTER'; Regression = $true
Detail = 'In baseline report but absent from after report (resave failed or file skipped)'
})
$regressionCount++
continue
}
$a = $afterJobs[$key]
if ($b.ParseError -and $a.ParseError) {
# Unparseable before and after: no change, nothing to compare.
$unchangedCount++
if ($ShowUnchanged) {
$results.Add([pscustomobject]@{
File = $key; Change = 'UNCHANGED_PARSE_ERROR'; Regression = $false
Detail = 'veraPDF could not parse this file in either run'
})
}
continue
}
if (-not $b.ParseError -and $a.ParseError) {
$results.Add([pscustomobject]@{
File = $key; Change = 'PARSE_ERROR_AFTER'; Regression = $true
Detail = 'Baseline validated, but veraPDF could not parse the resaved file'
})
$regressionCount++
continue
}
if ($b.ParseError -and -not $a.ParseError) {
$results.Add([pscustomobject]@{
File = $key; Change = 'PARSE_ERROR_RESOLVED'; Regression = $false
Detail = 'Baseline was unparseable, resave validates. Not a regression, but verify the resave kept the file intact'
})
continue
}
$added = @($a.FailedRules | Where-Object { $b.FailedRules -notcontains $_ })
$removed = @($b.FailedRules | Where-Object { $a.FailedRules -notcontains $_ })
if ($added.Count -eq 0 -and $removed.Count -eq 0) {
$unchangedCount++
if ($ShowUnchanged) {
$results.Add([pscustomobject]@{
File = $key; Change = 'UNCHANGED'; Regression = $false
Detail = ('compliant={0}, failedRules={1}' -f $b.Compliant, $b.FailedRules.Count)
})
}
continue
}
$change = 'RULES_CHANGED'
if ($b.Compliant -and -not $a.Compliant) { $change = 'NEW_FAIL' }
elseif (-not $b.Compliant -and $a.Compliant) { $change = 'NOW_COMPLIANT' }
$isRegression = ($added.Count -gt 0)
if ($isRegression) { $regressionCount++ }
$detailParts = @()
if ($added.Count -gt 0) { $detailParts += ('ADDED: ' + ($added -join ' | ')) }
if ($removed.Count -gt 0) { $detailParts += ('removed: ' + ($removed -join ' | ')) }
$results.Add([pscustomobject]@{
File = $key; Change = $change; Regression = $isRegression
Detail = ($detailParts -join ' ; ')
})
}
foreach ($key in @($afterJobs.Keys | Sort-Object)) {
if (-not $baseJobs.ContainsKey($key)) {
$results.Add([pscustomobject]@{
File = $key; Change = 'MISSING_BASELINE'; Regression = $false
Detail = 'In after report but not in baseline (extra output file?)'
})
}
}
# ---- Output ----------------------------------------------------------------
$changed = @($results | Where-Object { $_.Change -notlike 'UNCHANGED*' })
Write-Host ''
Write-Host ('Baseline jobs : {0}' -f $baseJobs.Count)
Write-Host ('After jobs : {0}' -f $afterJobs.Count)
Write-Host ('Unchanged : {0}' -f $unchangedCount)
Write-Host ('Changed : {0}' -f $changed.Count)
Write-Host ('Regressions : {0}' -f $regressionCount)
Write-Host ''
if ($results.Count -gt 0) {
# NOTE: keep $toShow a plain array. Wrapping the generic List in @(...) and reading .Count
# makes the Windows PowerShell 5.1 binder throw "Argument types do not match".
$toShow = $changed
if ($ShowUnchanged) { $toShow = $results.ToArray() }
if ($toShow.Count -gt 0) {
# Plain padded strings instead of Format-Table (its 5.1 formatter has its own quirks);
# the CSV is the real record anyway.
$fileW = 60
foreach ($r in $toShow) { if ($r.File.Length -gt $fileW) { $fileW = $r.File.Length } }
Write-Host (('{0,-' + $fileW + '} {1,-20} {2,-5} {3}') -f 'File', 'Change', 'Reg', 'Detail')
Write-Host (('{0,-' + $fileW + '} {1,-20} {2,-5} {3}') -f '----', '------', '---', '------')
foreach ($r in $toShow) {
Write-Host (('{0,-' + $fileW + '} {1,-20} {2,-5} {3}') -f $r.File, $r.Change, $r.Regression, $r.Detail)
}
Write-Host ''
}
}
if ($CsvOut) {
$results | Export-Csv -LiteralPath $CsvOut -NoTypeInformation -Encoding UTF8
Write-Host ('Full results written to {0}' -f $CsvOut)
}
if ($regressionCount -gt 0) {
Write-Host 'RESULT: FAIL - KillerPDF resave introduced regressions.' -ForegroundColor Red
exit 1
} else {
Write-Host 'RESULT: PASS - no conformance regressions introduced.' -ForegroundColor Green
exit 0
}