Skip to content
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
leaf and node limits each end the walk the instant they are reported, so at most one of that
pair, plus the depth code's first occurrence, is ever retained in the same walk. Computed
lazily, on first access, and cached for the reader's lifetime. (#98)
- **A new option, `PdfReaderOptions.MaxFormXObjectDepth`, and ten new `PdfReaderDiagnosticCode`
values in a `3xx` block reserved for content streams.** The reader now has an internal
content-stream interpreter (ISO 32000-2 §7.8.2), shared machinery the two extraction milestones
still ahead of it (text, then images) will build on; a caller cannot invoke it directly in this
release, so the ten codes below cannot yet be reported to one. `MaxFormXObjectDepth` (default 32,
tighten-only like the three resource limits it joins) is the one part of this a caller sets
today: a ceiling on Form XObject recursion depth (§8.10) the interpreter enforces once something
does call it, reporting `FormXObjectDepthExceeded` and continuing rather than recursing
unboundedly. `Do` on a Form XObject brackets the form's own content in an implicit save and
restore of the graphics state, marked-content nesting, and BX/EX compatibility depth, mirroring
§8.10.1's own steps a) and e), so nothing the form does to any of the three leaks into the page
that invoked it. The rest of the interpreter follows the same policy throughout: a malformed or
unsupported construct is reported, not thrown, and interpretation continues past it. That is
what the other nine codes describe: `ContentStreamLexError`, `UnknownOperator` (`Warning`
severity: an operator this reader skipped is a best-effort reading, not proof the output is
correct), `OperandStackMalformed` for a producer-side malformation, `ContentLimitExceeded` for
this reader's own processing ceilings instead (an operand-count, array-or-dictionary-operand
token, `q`-depth, or marked-content-depth cap), `FormXObjectCycle`, `FormXObjectBudgetExceeded`, `ResourceMissing`,
`InlineImageMalformed`, and `ContentStreamTooLarge` (one 64 MiB decoded-content budget per page,
shared between its own `/Contents` and every Form XObject it draws; each invocation of a form
counts again, since the interpretation cost this bounds scales with how many times a form is
drawn), charged against the budget as each `/Contents` element or form invocation decodes rather
than only once all of them already have, so a `/Contents` array naming the same oversized stream
many times cannot hold every decode in memory before the cap gets a chance to stop it. `Do` on a
Form XObject also concatenates the form's own `/Matrix` into the graphics state's CTM (§8.10.1 b)
before interpreting its content, so a caller reading the CTM from inside the form's own content
sees the composed value, not the invoker's own CTM with the form's matrix left for it to apply
separately. (#98)

### Changed

Expand Down
34 changes: 20 additions & 14 deletions docs/reader-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ var options = new PdfReaderOptions
MaxDecodedStreamBytes = 64 * 1024 * 1024,
ReconstructionBudgetMultiplier = 4,
MaxDiagnostics = 200,
MaxFormXObjectDepth = 16,
};

using var reader = PdfReader.Open(File.OpenRead("input.pdf"), options);
Expand All @@ -80,19 +81,24 @@ real `startxref` chain left for `/Prev` to extend, and a recovered trailer's `/I
enough to carry into a new revision. Reconstruction also refuses outright the instant it finds
any sign the document is encrypted, rather than guessing at a key.

**`MaxDecodedStreamBytes`**, **`ReconstructionBudgetMultiplier`**, and **`MaxDiagnostics`** are all
**tighten-only**. None is a spec requirement — ISO 32000-2 Annex C.1 notes that "a particular PDF
processor running on a particular device and in a particular operating environment will always have
practical limits", and Annex C.3 adds that available memory is "often much less in mobile devices
than desktop computers." The defaults (512 MiB decoded-stream ceiling, an ×8 multiplier on
reconstruction's `max(1 MiB, N × file length)` work budget, a 1000-entry diagnostics cap) are this
library's own choice for a desktop host, not something Annex C mandates. A caller on a more
constrained device, or hardening against a decompression bomb, a file engineered to burn CPU across
many decoy candidates, or a document that would otherwise report the same recoverable condition on
a huge number of objects, can lower any of the three. Raising any of them above its default throws
`ArgumentOutOfRangeException` at `Open` time: nothing above the shipped defaults has been exercised
as a safe ceiling, so these options can only make the reader stricter than it already is, never
looser.
**`MaxDecodedStreamBytes`**, **`ReconstructionBudgetMultiplier`**, **`MaxDiagnostics`**, and
**`MaxFormXObjectDepth`** are all **tighten-only**. None is a spec requirement: ISO 32000-2 Annex
C.1 notes that "a particular PDF processor running on a particular device and in a particular
operating environment will always have practical limits", and Annex C.3 adds that available memory
is "often much less in mobile devices than desktop computers." The defaults (512 MiB decoded-stream
ceiling, an ×8 multiplier on reconstruction's `max(1 MiB, N × file length)` work budget, a
1000-entry diagnostics cap, 32 levels of Form XObject recursion) are this library's own choice for
a desktop host, not something Annex C mandates. A caller on a more constrained device, or hardening
against a decompression bomb, a file engineered to burn CPU across many decoy candidates, a
document that would otherwise report the same recoverable condition on a huge number of objects, or
a page that nests Form XObjects (ISO 32000-2 §8.10) deeper than a caller wants to follow, can lower
any of the four. Raising any of them above its default throws `ArgumentOutOfRangeException` at
`Open` time: nothing above the shipped defaults has been exercised as a safe ceiling, so these
options can only make the reader stricter than it already is, never looser.
`MaxFormXObjectDepth` governs an internal content-stream interpreter this package does not expose
directly yet. Text and image extraction, still ahead on the roadmap below, will be its first
callers. It has no visible effect until then, but validates at `Open` time regardless, alongside
the other three.

---

Expand Down Expand Up @@ -351,7 +357,7 @@ throws `PdfPasswordException` if the document needs a real one.
best-effort: a wrong guess at the object graph during recovery produces a wrong, but internally
consistent, decrypted copy.

**`MaxDecodedStreamBytes`, `ReconstructionBudgetMultiplier`, and `MaxDiagnostics` only go down.**
**`MaxDecodedStreamBytes`, `ReconstructionBudgetMultiplier`, `MaxDiagnostics`, and `MaxFormXObjectDepth` only go down.**
Raising any of them above its shipped default throws at `Open` time rather than silently
clamping — there is no way to ask the reader to trust a file more than its own defaults do.

Expand Down
Loading