vendor: import KillerPDF 1.7.5 source (GPL-3.0) as the base for MMD PDF
This commit is contained in:
@@ -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