Files
mmd-pdf/validation/RESULTS.md
kua-agent 6610acfa44 feat: rebrand to MMD PDF, single corporate theme, and remove original text under an edit
- Services/PdfRedactText.cs: strip text whose origin falls inside a CoverAnnotation
  from the page content stream at save time, hooked into PdfBurn.DrawAnnotationsIntoDoc.
  Fixes edited values staying recoverable by text extraction.
- Themes/MMD.xaml replaces all thirteen themes; picker and accent strip removed; no dark mode.
- Rename KillerPDF -> MMD PDF across code, resources, packaging and locale strings; new icon.
- Remove the upstream author credit and the in-app install button.
2026-08-27 07:37:09 +02:00

145 lines
7.6 KiB
Markdown

# Standards-conformance validation results - MmdPdf 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 MmdPdf 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 MmdPdf degrade its
standards conformance? Every file in a 2,907-file public corpus was validated, resaved through
MmdPdf's standard open/save pipeline, and validated again.
Result: **Zero** conformance regressions across every file MmdPdf 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) |
| MmdPdf | 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 MmdPdf: `MmdPdf.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. MmdPdf 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. MmdPdf'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; MmdPdf
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 `MmdPdf 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 MmdPdf 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 MmdPdf.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): MmdPdf.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 MmdPdf refuses to touch), and the
only rule-level change is the documented PDF/A-4 header case. Anything beyond that is a real
regression.