How OnePDF opens a file, and what it does when the file is damaged. Every repair adds a PdfDiagnostic (severity
Warning, with object number and offset where known) to PdfDocument.Diagnostics. With PdfOpenOptions.Strict each of
them throws PdfFormatException instead. Informational notes (Info) never throw.
The fallback chain
- Trust the cross-reference data. Find the last
startxrefin the final 8 KiB, read the section it names, then follow/Prevback to the first section. Sections are tables (xref...trailer) or cross-reference streams (ISO 32000-2 7.5.8). In a hybrid file the table's/XRefStmsupplies entries for objects the table lists as free (7.5.8.4). Each section is one revision (PdfDocument.Revisions, oldest first). - Fix individual objects by scanning. When an entry's offset does not lead to
n g objwith the expected number, the file is scanned once (shared by all later lookups) for object headers and the object is read from where it is found. Diagnostic: "the cross-reference offset ... does not point at the object". An object that cannot be found reads as null (severity Error). - Rebuild. If the xref data cannot be read at all (no
startxref, no section at the offset, a damaged or truncated table or trailer, a/Prevloop, a subsection claiming more entries than the file can hold) or the trailer's/Rootdoes not lead to a dictionary, the whole file is scanned and the cross-reference data is replaced by what the scan finds. Diagnostic: "the cross-reference data is unusable (...); rebuilt by scanning the file for objects".
Limit violations (PdfLimitException) are never repaired: they propagate.
Rules in detail
- Header.
%PDF-is looked for in the first 1024 bytes. Bytes before it are ignored (Info). A missing header is a warning. Offsets instartxrefand the xref data are tried as absolute file positions, then relative to the header; the first that lands on a section is used for the whole file (Info). - Table entries. Entries are read as tokens, not fixed 20-byte lines, so 19-byte and CR-only lines work. An entry
of type
nwith offset 0 is free. A first subsection that starts at 1 although its first entry is the free head (0000000000 65535 f) is renumbered from 0 (warning). - Trailer. The newest trailer is used;
Root,Info,IDandEncryptit lacks are taken from older trailers. Each revision keeps its own trailer. - Stream length (7.3.8.2).
/Length(direct, or indirect) is trusted only ifendstreamfollows the data after at most 16 white-space bytes. Otherwise the data ends at the nextendstream, elseendobj, else the end of the file, less one end-of-line (CRLF, LF or CR). Diagnostic: "the stream's /Length ... is wrong". A/Lengththat is indirect and cannot be resolved takes the same path. The length of an object stream or xref stream is never read from inside an object stream. - Stream keyword.
streammay be followed by spaces before its end-of-line; CR alone is accepted. - Missing
endobj. Any token other thanendobjafter an object's value is a warning; reading continues. - Unterminated containers. An array or dictionary cut off by
endobj,stream,obj,xref,trailer,startxrefor the end of data is closed there (warning). A stray]or>>is ignored. A dictionary key that is not a name skips that token; a key without a value is dropped. Null values are dropped (a missing key and a null value are the same, 7.3.7). - Numbers. Leading signs, a second decimal point, and a minus after digits are tolerated (
--5is -5,1.2.3is 1.2,5-3is 5). Integers beyond 64 bits become reals. - Strings and names. Unterminated strings run to the end of data. Bad
#xxescapes in names stay literal. Name bytes that are not valid UTF-8 are read as Latin-1. - Object streams. The entry's index is checked against the stream's header; a wrong index is found by number
(warning).
/Nand/Firstare validated against the decoded size before anything is allocated. When/Nor/Firstis missing, negative, too large, or/Firstlies inside the header, the header is salvaged (warning "invalid /N or /First"): the leading run ofnumber offsetinteger pairs is the header (at most/Npairs when/Nis sane, else as many as the data allows), and/Firstis taken to be where that run ends. A header that stops early keeps the pairs read so far and the rest of the stream's objects read as null (warning). An object stream that is missing or not a stream makes its objects read as null (warning). Strict mode throws for each of these. - References. A reference to a missing, free or null object resolves to null. A chain of references to references
longer than
PdfLimits.MaxRecursionDepth(or a loop) reads as null (warning). A load that needs itself is aPdfFormatException. - Page tree.
Pages(andPageCount) walk/Pagesiteratively once; the inheritable attributesResources,MediaBox,CropBoxandRotatecome from the nearest ancestor that has them. A page without a usableMediaBoxis US Letter (warning); aCropBoxis clipped to theMediaBox; aRotatethat is not a multiple of 90 is 0 (warning). Nesting beyondMaxNestingDepthis skipped (warning). A node reached twice is skipped (warning); a kid that is not a dictionary is skipped (warning); a node without/Kidsis a page unless/Typeis/Pages. - External streams. A stream with
/Freads as empty (warning). Nothing outside the document is ever opened. - Encryption.
/Encryptis read as soon as the cross-reference data is, before the catalog or any object stream is trusted (a file rebuilt by scanning authenticates before its object streams are opened).PdfOpenOptions.Passwordis tried as user then owner password; the empty password is always tried. A wrong or missing password isPdfPasswordException; an unsupported or malformed handler isPdfEncryptionException. Everything parsed before authentication is dropped from the cache. Strings are decrypted when their object is parsed, stream data when it is read (before filters), under the object number and generation of the containing indirect object. Exempt: the/Encryptdictionary, cross-reference streams, strings inside object streams (the stream is encrypted as a whole) and the trailer/ID. ACryptfilter names the crypt filter to use (Identityleaves the data alone) and is not part of the decode chain. A metadata stream is left in the clear when/EncryptMetadatais false. Damaged ciphertext (bad padding, partial block) decrypts as far as it goes with a warning.
The rebuild scan
One pass over the file, byte by byte, no whole-file buffer:
objpreceded by<number> <generation>and not byend, followed by a delimiter or white space, starts an object. The object is parsed; for a stream the body is skipped using a trusted/Lengthor the nextendstream, so bytes inside stream data (an embedded PDF, say) are not taken for objects.- The last definition of a number wins.
trailerfollowed by a dictionary is a trailer candidate; so is the dictionary of any/Type /XRefstream. - The trailer used is the last candidate with a
/Root;Root,Info,IDandEncryptare each taken from the latest candidate that has them. With no candidate, the last/Type /Catalogobject is used (warning). - Objects inside object streams are added after the scan. An object stream counts as defined where it sits in the file, so a later direct definition of the same number wins over its contents and a later object stream wins over an earlier definition. An object stream that cannot be read is skipped (warning).
- The result is a single revision. More than
PdfLimits.MaxObjectsobjects raisesPdfLimitException.
Limits checked while reading
| Limit | Checked |
|---|---|
MaxObjects | declared subsection and /Index counts before reading entries; headers during the rebuild scan; page-tree nodes |
MaxNestingDepth | on entering each array or dictionary level (explicit stack, no recursion) |
MaxDecodedStreamBytes, MaxExpansionRatio | by the filters while they run; raw stored size before reading it |
MaxRecursionDepth | reference-to-reference chains |
The typed layer
Name trees, number trees, outlines, form field trees and action chains are walked iteratively with a visited set, so a
node reached twice (a cycle) is skipped with one warning, and with MaxObjects / MaxNestingDepth bounds. A malformed
tree key, a non-dictionary page or annotation entry, a destination that leads nowhere, an unparseable date or an XMP
packet that is not well-formed XML never throws: the value reads as null or empty (XMP keeps its raw bytes). DTDs in XMP
are refused, never expanded. Stream-backed text (embedded files, XFA, XMP) is size-checked against the limits before it is
read; over the limit the item is ignored with a warning, except PdfEmbeddedFile.ReadContent, which throws
PdfLimitException because the caller asked for it.