## What this changes
lopdf's fallback loader (used by `inspect` when the lazy reader fails, and always by `attachment_data`) parsed every object stream eagerly, capping only decoded bytes. A stream of `0 0 0 …` inside the 256 MiB limit built ~120M `Object`s (10+ GB) and aborted. This bounds what it builds:
- **Per top-level object:** at most 4,194,304 objects (`parser::MAX_OBJECT_NODES`, the same as cos's `MAX_NODES`), counting every array element and dictionary value at any depth. One budget is threaded through `array` / `_dictionary` / `_direct_object`. An object is charged once it is built, so failed `alt` branches cost nothing, and an exhausted budget is a nom `Failure`.
- **Per object stream:** at most 1,000,000 index entries (cos's `/N` clamp), and the index list is no longer allocated from the whole block.
- **Per load:** at most 8,388,608 objects across all object streams, in an atomic `NodeBudget` shared by rayon workers. A member reserves up to its own byte length (every object takes at least one byte) and refunds what it doesn't use, so parallel members can't hold the budget between them. Once the budget is spent, the remaining members are skipped with a warning.
- **`convert` in `crates/render/src/structure.rs`:** the depth limit is now 128, one level per array or dictionary, matching cos's parser. Before, it was 100 with two levels per dictionary, so ~51 nested dictionaries alone forced the lopdf fallback.
Logged as lopdf patch (7) in `vendor/README.md`.
Fixes#5083
## How you verified it
`cargo fmt --check` on `pdfcraft-render` and `cargo clippy -p pdfcraft-render --all-targets -- -D warnings` (which compiles the vendored lopdf). Regression tests in `crates/render/src/inspect/tests.rs`, where lopdf patches are tested in-tree (the vendored lopdf's own tests need its missing `assets/example.pdf`):
- `object_stream_members_over_the_node_cap_are_skipped` (croquete1): a member that is one array of 4.3M elements is skipped at the per-object cap; a small member is kept.
- `object_stream_index_entries_past_the_clamp_are_ignored`: of 1,000,001 distinct members, the one past the 1,000,000-entry clamp is never built.
- `object_streams_of_one_load_share_their_node_budget`, `object_streams_loaded_in_parallel_share_their_node_budget` and `a_malformed_object_stream_member_refunds_its_reservation` (Harrison-Booth): a small shared `NodeBudget` bounds the members of several streams, serially and across threads, and a malformed member refunds what it didn't build. `NodeBudget` and `ObjectStream::new_within` are now public for this.
- [x] `cargo fmt --check`
- [x] clippy with `-D warnings` on the crates I changed
- [x] tests pass on the crates I changed (`cargo test -p pdfcraft-render --lib object_stream`: 9 passed, including the five new tests)
## Never-crash
- [x] No new `unwrap`/`expect`/`panic!`/`unreachable!`/`todo!` outside tests
- [x] No indexing or slicing with input-derived positions; the budget uses `checked_sub` and a `min`-bounded reservation; index entries and objects built are capped; nesting stays bounded (128 in `convert`)
- [x] No `unsafe`
- [x] The abort this fixes has synthetic regression tests (above)
## Provenance
- [x] No assets, fonts or copied code; the limits follow `pdfcraft_cos`'s own caps
## Security
- [x] No change to `--root`, MCP, the control channel or network access
## Parity and reach
- Not applicable: an internal robustness fix to the fallback loader, with no user-facing feature or parity entry. The vendored-patch log (`vendor/README.md`, lopdf (7)) is updated.
## Anything a reviewer should look at first
- `vendor/README.md` now lists the tests above for patch (7).
- Ordinary (not compressed) indirect objects are capped at 4M objects each but aren't charged to the per-load budget; their total is bounded only by file size.
- `get_compressed_object` still decodes and re-parses the whole stream per lookup (pre-existing); it now gets one object's budget.
- Objects nested 101–128 deep now reach the Inspector through the lazy reader rather than being skipped by the fallback. The Inspector's own walkers cap depth at 32 or less.
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
<!-- coderabbit-ai-generated:v1 -->
## Summary by CodeRabbit
* **Bug Fixes**
* Added limits to PDF object parsing and object-stream processing. Loading now parses only selected members, while lookups parse only the requested member. Members that exceed available limits are skipped in a deterministic order.
* Increased the supported nesting depth for dictionary and object conversion, allowing more deeply nested content to be processed.
* Documented that conversion failures can trigger eager loading as a fallback.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
* Render: an isolated CMYK group of any size blends in CMYK, in bands
hayro patch (27) blended an isolated DeviceCMYK transparency group in CMYK only up to 2^24
device pixels (a 4096 x 4096 square); a larger one was blended in RGB. Print as image renders a
whole page at once, up to 8192 px on the long side, so a page enlarged to 11 x 17 in at 600 dpi
(about 43 MP) blended any group covering most of it in RGB, and a print test file's CMYK blend
patches drew their X's. The same applied to exporting a large page as an image.
A larger group is now blended in bands of whole rows, each within the old limit: both ink passes
and the tint conversion run per band, so their memory stays as before, and only the finished
group (4 bytes a pixel) is kept whole. Every band is blended before any is drawn, so a group a
pass has no tints for is still drawn as before, whole.
The regression test lowers the band to 7 rows (set_cmyk_band_pixels, hidden, per thread like
set_skip_offscreen_paths) and checks the banded group matches the whole one across an
anti-aliased edge, and is CMYK's green where magenta is under black with Difference.
* Test: CMYK green through the profile is (0, 166, 81); check green against RGB's magenta
The view.find parity entry keeps main's new notes and evidence (combining
marks and Thai or Indic word gaps, #6517) with this branch's ligature and
line-end hyphen sentence and tests added.
Main took hayro-interpret patch (76) for non-embedded Arial and Times New Roman
CIDs, so the View auto state becomes patch (103) in the ledger and the code
comments. The view.layers-panel note no longer lists the Print event's auto
state as missing: main applies it when printing (print.print-ready-pdf).
Keeps main's Callout font size preference with the shared callout_knee(),
main's on_text() beside the callout box resizing, and both new inspector
helpers.
Main moved a link's action reading into Inspector::action_target, shared
with bookmarks, and made the link/attachment text budget start with the
outline. Resolution:
- annotations.rs: link() calls action_target and then resolves a URI
target against /URI /Base, spending what the base adds from the same
budget (or keeping the raw address); QuadPoints reading unchanged.
The budget doc comment covers bookmarks, links, quads and attachments.
- core.rs: keeps the branch's uri_base read; the outline goes through
main's budgeted outline_window (the branch's old call is dropped).
- tools.rs: link_list keeps main's "to_page with its zoom" and the
branch's resolved_url / active_areas text.
relative_uri_actions_resolve_against_the_catalog_base re-proven red with
the resolution reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Main's #6663 added InterpreterWarning/RenderWarning::Type3WidthsMissing (patch
90) and a decoded-image cache that records warnings as flags (patch 91). Both
warning variants are kept in every match (renderer, automation, CLI), and
MalformedIndexedImage takes flag 32 in warning_bit/warnings_in. The ledger
keeps main's rows with (80) added; both sides' raster test modules are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA
Main's #6569 (hayro-interpret patch 101) reads a symbolic TrueType font that
names no glyphs and keeps the Macintosh order as Mac OS Roman; this branch's
patch (79) reads one that names its glyphs by those names. Both are kept: the
renderer struct holds both fields, and the editor's unicode_map applies the
glyph-name reading only to a font whose post table names glyphs, then the Mac
OS Roman one, so each font takes the reading the renderer gives it. The ledger
keeps main's rows with (79) added; the editor reuses descriptor_flags and
MAX_SFNT.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA