vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
qpdf structural sweep: runs `qpdf --check` on every original/resave pair and flags any
|
||||
file whose exit code worsened.
|
||||
|
||||
.DESCRIPTION
|
||||
The second half of the release validation (Compare-VeraPDF.ps1 is the first). Reads the
|
||||
resave log written by `KillerPDF.exe --batch-resave` and checks only the rows marked OK -
|
||||
skipped files were never written and have nothing to compare.
|
||||
|
||||
qpdf exit codes: 0 = clean, 2 = errors, 3 = warnings only. "Worsened" is any pair whose
|
||||
after-code is higher than its before-code. The release bar is zero worsened.
|
||||
|
||||
.\QpdfSweep.ps1 -Corpus C:\pdf-corpus -Resaved C:\pdf-corpus-resaved `
|
||||
-ResaveLog ..\resave.csv -CsvOut qpdf-results.csv
|
||||
|
||||
Exit code 0 = no worsened pairs, 1 = worsened pairs 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]$Corpus,
|
||||
[Parameter(Mandatory = $true)] [string]$Resaved,
|
||||
[Parameter(Mandatory = $true)] [string]$ResaveLog,
|
||||
[Parameter(Mandatory = $true)] [string]$CsvOut
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not (Get-Command qpdf -ErrorAction SilentlyContinue)) { Write-Error 'qpdf not on PATH'; exit 2 }
|
||||
if (-not (Test-Path -LiteralPath $ResaveLog)) { Write-Error "Resave log not found: $ResaveLog"; exit 2 }
|
||||
|
||||
function Get-QpdfCheckCode {
|
||||
param([string]$Path)
|
||||
|
||||
# qpdf writes ordinary exit-3 warnings to stderr. Under Windows PowerShell, redirecting
|
||||
# native stderr while ErrorActionPreference is Stop turns that expected diagnostic into a
|
||||
# terminating NativeCommandError before the sweep can record the exit code.
|
||||
$oldPreference = $ErrorActionPreference
|
||||
try {
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& qpdf --check $Path *> $null
|
||||
return $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $oldPreference
|
||||
}
|
||||
}
|
||||
|
||||
$rows = Import-Csv -LiteralPath $ResaveLog | Where-Object { $_.Status -eq 'OK' }
|
||||
$results = New-Object System.Collections.Generic.List[object]
|
||||
$i = 0
|
||||
|
||||
foreach ($r in $rows) {
|
||||
$i++
|
||||
if ($i % 100 -eq 0) { Write-Host ("{0} / {1}" -f $i, $rows.Count) }
|
||||
$o = Join-Path $Corpus $r.File
|
||||
$n = Join-Path $Resaved $r.File
|
||||
if (-not (Test-Path -LiteralPath $n)) {
|
||||
$results.Add([pscustomobject]@{ File = $r.File; Before = -1; After = -1; Worsened = 'MISSING' })
|
||||
continue
|
||||
}
|
||||
$b = Get-QpdfCheckCode $o
|
||||
$a = Get-QpdfCheckCode $n
|
||||
$results.Add([pscustomobject]@{ File = $r.File; Before = $b; After = $a; Worsened = ($a -gt $b) })
|
||||
}
|
||||
|
||||
$results | Export-Csv -LiteralPath $CsvOut -NoTypeInformation -Encoding UTF8
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ('Pairs checked : {0}' -f $results.Count)
|
||||
$results | Group-Object { '{0} -> {1}' -f $_.Before, $_.After } | Sort-Object Count -Descending |
|
||||
ForEach-Object { Write-Host (' {0,-10} {1}' -f $_.Name, $_.Count) }
|
||||
$worse = @($results | Where-Object { $_.Worsened -eq $true -or $_.Worsened -eq 'MISSING' })
|
||||
Write-Host ('Worsened : {0}' -f $worse.Count)
|
||||
|
||||
if ($worse.Count -gt 0) {
|
||||
Write-Host 'RESULT: FAIL - structural health worsened on at least one file.' -ForegroundColor Red
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host 'RESULT: PASS - no file''s structural health got worse.' -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
# Standards-conformance validation results - KillerPDF 1.7.5
|
||||
|
||||
veraPDF run date: 2026-08-22, against the 1.7.5 release build. This small maintenance release
|
||||
changes live annotation rotation behavior, mouse-wheel navigation, shortcuts, and localization,
|
||||
without changing the PDF serializer. The standard open/save pipeline was nevertheless run fresh
|
||||
across the complete corpus because every KillerPDF release must independently meet the same
|
||||
zero-regression bar. The run reproduces every established count exactly: 2,236 successful resaves, 671 refusals
|
||||
matching the SKIP rows one for one, 63 improvements, and the same single documented PDF/A-4
|
||||
header case as the only flagged saved file. The qpdf sweep also reproduces its table exactly:
|
||||
2,032 clean both sides, 195 improved, 9 kept preexisting warnings, 0 worsened.
|
||||
|
||||
Question under test: does saving a PDF through KillerPDF degrade its
|
||||
standards conformance? Every file in a 2,907-file public corpus was validated, resaved through
|
||||
KillerPDF's standard open/save pipeline, and validated again.
|
||||
|
||||
Result: **Zero** conformance regressions across every file KillerPDF will save, with one documented engine limitation
|
||||
(PDF/A-4's PDF 2.0 header). **63 files came out more conformant than they went in.**
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Version | Role |
|
||||
|---|---|---|
|
||||
| veraPDF | 1.30.2 | PDF/A + PDF/UA validation (the industry reference validator) |
|
||||
| qpdf | 12.3.2 | Structural check (`--check` exit codes) |
|
||||
| KillerPDF | 1.7.5 | `--batch-resave` through the standard open/save pipeline |
|
||||
| Compare-VeraPDF.ps1 | this folder | Diffs the two veraPDF reports file by file |
|
||||
| QpdfSweep.ps1 | this folder | Structural before/after sweep (`qpdf --check` exit codes) |
|
||||
|
||||
## Corpus
|
||||
|
||||
2,907 PDFs from the public conformance suites: the veraPDF test corpus (PDF/A-1, PDF/A-2,
|
||||
PDF/A-4, PDF/UA-1, PDF/UA-2), the Isartor PDF/A-1b test suite, and the TWG test files. These
|
||||
are deliberately hostile files: most are constructed to violate exactly one clause of a
|
||||
standard, so any structural damage a resave introduces shows up as a new failed rule.
|
||||
|
||||
## Method
|
||||
|
||||
1. Validate the pristine corpus: `verapdf --recurse --format json <corpus> > baseline.json`
|
||||
2. Resave every file through KillerPDF: `KillerPDF.exe --batch-resave <corpus> <resaved> --log resave.csv`
|
||||
3. Validate the resaved tree the same way into `after.json`
|
||||
4. `Compare-VeraPDF.ps1` matches files by relative path and flags any file that fails a rule
|
||||
after the resave that it did not fail before
|
||||
5. qpdf sweep: `qpdf --check` on original and resave of all 2,236 saved files; flag any file
|
||||
whose exit code worsened
|
||||
|
||||
## veraPDF results
|
||||
|
||||
| Outcome | Files |
|
||||
|---|---|
|
||||
| Corpus total | 2,907 |
|
||||
| Resaved OK | 2,236 |
|
||||
| Skipped (refused, source untouched) | 671 |
|
||||
| Resave failures | 0 |
|
||||
| Validation outcome unchanged | 2,172 |
|
||||
| Improved (noncompliant before, fully compliant after) | 59 |
|
||||
| Improved (fails fewer rules than before) | 4 |
|
||||
| Regressed | 1 (the documented PDF/A-4 header case below) |
|
||||
|
||||
The 671 skips are encrypted files and files damaged beyond parsing. KillerPDF refuses to
|
||||
resave what it cannot fully read rather than risk writing a damaged file; each one is a SKIP
|
||||
row in `resave.csv`, and all 671 files absent from the after-report cross-check exactly
|
||||
against those SKIP rows. No file went missing for any other reason.
|
||||
|
||||
The 63 improvements are a side effect, not a goal: many corpus files carry deliberately
|
||||
malformed structure (bad trailers, broken xref, wrong stream lengths), and rewriting the file
|
||||
through a clean serializer repairs that class of defect.
|
||||
|
||||
## The one known limitation: PDF/A-4
|
||||
|
||||
ISO 19005-4 (PDF/A-4) is built on PDF 2.0 and requires a `%PDF-2.0` header. KillerPDF's write
|
||||
engine serializes PDF 1.7, so the single PDF/A-4 corpus file gains ISO 19005-4:2020 clause
|
||||
6.1.3 tests 4 and 5 after a resave. This is a version-marker limitation, not structural
|
||||
damage: qpdf reports the resaved file clean. PDF 2.0 serialization is future work; KillerPDF
|
||||
does not claim PDF/A-4 output.
|
||||
|
||||
## qpdf structural sweep
|
||||
|
||||
`qpdf --check` on the original and the resave of all 2,236 saved files:
|
||||
|
||||
| Exit code before -> after | Files |
|
||||
|---|---|
|
||||
| 0 -> 0 (clean both sides) | 2,032 |
|
||||
| 3 -> 0 (warnings before, clean after) | 195 |
|
||||
| 3 -> 3 (kept preexisting warnings) | 9 |
|
||||
| Worsened | 0 |
|
||||
|
||||
No file's structural health got worse; 195 files with qpdf warnings came out clean.
|
||||
|
||||
## What had to be fixed to get here
|
||||
|
||||
The write engine is PdfSharpCore 1.3.67 (MIT), vendored under `third_party/PdfSharpCore/`
|
||||
with six patches, each marked `KillerPDF patch` in the source:
|
||||
|
||||
1. **No Producer/Creator stamping** into an imported document's Info dictionary. PDF/A
|
||||
(ISO 19005-1 clause 6.7.3) requires the Info dictionary to stay equivalent to the XMP
|
||||
metadata; silently rewriting Producer broke that on every save.
|
||||
2. **No /ModDate rewrite at open.** Same clause: the reader stamped a new modification date
|
||||
into every document the moment it was opened for modification.
|
||||
3. **No transparency /Group injected into pages.** The writer force-added
|
||||
`/Group << /S /Transparency >>` to every page; PDF/A-1 (clause 6.4) forbids transparency.
|
||||
4. **Stream /Length always matches the spec's byte count** (clause 6.1.7), including
|
||||
zero-length streams, which were serialized with no EOL between `stream` and `endstream`.
|
||||
5. **Debug verbose file layout removed.** Debug builds padded object tokens with extra
|
||||
spacing that violates the object syntax rules (clause 6.1.8).
|
||||
6. **Booleans written as the PDF keywords `true` / `false`.** .NET's `Boolean.ToString()`
|
||||
leaked into indirect boolean objects as `True`, which is not a valid PDF token
|
||||
(ISO 32000-1 clause 7.3.2). This broke `/MarkInfo /Marked` in PDF/UA files.
|
||||
|
||||
On top of the library patches, every save runs three scrubs in KillerPDF itself:
|
||||
|
||||
- **Dangling /Outlines removal** - reading `doc.Outlines` plants an empty outline dictionary
|
||||
that becomes a dangling reference (the 1.6.3 corruption bug).
|
||||
- **Degenerate /CropBox removal** - reading page boxes planted `[0 0 0 0]` boxes that Adobe
|
||||
rejects as out-of-range page dimensions (the other 1.6.3 corruption bug).
|
||||
- **Dead signature values stripped** - a digital signature's digest must cover the entire
|
||||
file, so any resave invalidates it. Leaving the stale `/V` and `/Perms /DocMDP` entries in
|
||||
place fails strict validation; the save now removes the dead values and keeps the empty
|
||||
signature fields.
|
||||
|
||||
## Reproducing this run
|
||||
|
||||
Everything needed ships in this folder or is a free download (veraPDF, qpdf, the public
|
||||
corpora). On a tree containing the corpus:
|
||||
|
||||
```
|
||||
verapdf --recurse --format json C:\pdf-corpus > baseline.json
|
||||
Start-Process -Wait KillerPDF.exe -ArgumentList '--batch-resave','C:\pdf-corpus','C:\pdf-corpus-resaved','--log','resave.csv'
|
||||
verapdf --recurse --format json C:\pdf-corpus-resaved > after.json
|
||||
.\Compare-VeraPDF.ps1 -Baseline baseline.json -After after.json `
|
||||
-BaselineRoot C:\pdf-corpus -AfterRoot C:\pdf-corpus-resaved -CsvOut compare.csv
|
||||
.\QpdfSweep.ps1 -Corpus C:\pdf-corpus -Resaved C:\pdf-corpus-resaved `
|
||||
-ResaveLog resave.csv -CsvOut qpdf-results.csv
|
||||
```
|
||||
|
||||
**The resave step must be `Start-Process -Wait`** (or otherwise blocked on): KillerPDF.exe is
|
||||
a GUI-subsystem binary, so a bare invocation returns immediately and the after-scan then
|
||||
validates a half-written tree - every not-yet-written file shows up as MISSING_AFTER (this
|
||||
burned the 1.7.0 run, twice).
|
||||
|
||||
The compare script counts every MISSING_AFTER as a regression by design, so a run with skips
|
||||
exits 1 even when clean. The release bar is: every MISSING_AFTER row cross-checks against a
|
||||
SKIP row in `resave.csv` (encrypted/unparseable files KillerPDF refuses to touch), and the
|
||||
only rule-level change is the documented PDF/A-4 header case. Anything beyond that is a real
|
||||
regression.
|
||||
Reference in New Issue
Block a user