Open a damaged or truncated PDF in C#, see what OnePDF repaired, and save a clean copy, or reject damaged files outright with strict mode.
The input here is a PDF whose last lines were cut off, and with them startxref, the pointer to its cross-reference table.
using OnePdf; // Lenient by default: what can be repaired is repaired, and each repair is recorded. using (PdfDocument doc = PdfDocument.Open("damaged.pdf")) { Console.WriteLine($"{doc.PageCount} pages, title: {doc.Info.Title}"); doc.Save("repaired.pdf"); // a clean file with a new cross-reference table foreach (PdfDiagnostic d in doc.Diagnostics) // severity, object number, offset, message Console.WriteLine(d); } // Strict: the first problem throws instead. try { using PdfDocument strict = PdfDocument.Open("damaged.pdf", new PdfOpenOptions { Strict = true }); } catch (PdfFormatException e) { Console.WriteLine($"strict: {e.Message}"); } // TryOpen never throws for a file that will not open. if (PdfDocument.TryOpen("not-a-pdf.txt", out PdfDocument? other, out PdfException? error)) other.Dispose(); else Console.WriteLine($"not-a-pdf.txt: {error.Message}");
6 pages, title: Harbor trial report Warning: The cross-reference data is unusable (There is no startxref near the end of the file.); rebuilt by scanning the file for objects. strict: The cross-reference data is unusable (There is no startxref near the end of the file.); rebuilt by scanning the file for objects. not-a-pdf.txt: The file has no usable cross-reference data, trailer or document catalog.
qpdf --check finds no errors in repaired.pdf.
The repair chain
- Trust the cross-reference data. Read the table or stream that
startxrefnames, then the earlier ones. - Fix single objects by scanning. An entry that doesn’t lead to its object is found by scanning the file for object headers. A stream whose
/Lengthis wrong ends at itsendstream. - Rebuild. When the cross-reference data can’t be read at all, the whole file is scanned and rebuilt from the objects it holds, as here.
Each repair is a PdfDiagnostic on doc.Diagnostics, with the severity, the object number and the byte offset when known. Some repairs only happen when an object is first read, so read the list after the work you need, as the sample does after Save.
Good to know
Savewrites a clean file: new cross-reference data, correct lengths, no unreachable objects. Content that was truly lost stays lost: a reference to a missing object is written asnull.- Don’t use
SaveIncrementalon a repaired file. A file that was rebuilt by scanning has no cross-reference section to append to, so it throws. - A page that can’t be parsed throws only when you use that page, so one bad page doesn’t stop you reading the others.
- Limits on decoded size, nesting, object count and more are on by default, and are never relaxed by repair: a file that exceeds them throws
PdfLimitException. Strict = trueturns every repair into aPdfFormatException, for pipelines that should reject damaged input.
Every rule is in Reading and repair rules.