In `a_shared_jbig2_dictionary_that_cant_leave_refuses_the_redaction`, the case of a copy of
the dictionary whose `/Filter` is given by reference may end `Ok`, the image stored again
without it. That branch checked only that the dictionary, object 9, left, and that its marker
was nowhere in the file. But `assert_gone` can't decode the copy, object 10, with its filter by
reference, and its bytes are Flate, so a run that never found the copy, and left it in the
file, passed. The branch now checks that object 10 left too, through `reachable_objects`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
One conflict, in redact sanitize.rs. Main's #6021 (930b44131) split `remove_hidden` in two:
- it now counts only what the chosen categories need (`scan_only`);
- it hands the removal to `remove_counted`, which tests also call with `every_page`.
This branch had added one line to the old body: `kept_before`, what a full save keeps before
anything is removed. Main's side is taken whole, and that line is now the first thing
`remove_counted` does after finding the catalog, before any change to the document. So every
caller of `remove_counted` has it. `unshare_unmarked` stays last, after `unlink_undrawn`, and
merged cleanly there. The only early return after a change is `NothingToApply` when nothing
was removed and nothing unlinked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Main's #1143 (863af864a), merged in 77b9cfa31, has cos accept both generations of a slot whose
cross-reference row and object header disagree: `8 0` in the row and `8 1 obj` in the file, a
reference of either reads the object. `unshare` stored an image again with `Document::set` under
the row's generation, which keeps that one alone. A page that named the image by its header's,
`/Im 8 1 R`, then read null, in the document and after a full save: a redaction on another page
took the image off a page no mark reached.
It now stores it with `Document::set_existing`, which keeps both. It was the one object this
PR's code writes back after reading.
Test: `an_image_stored_again_keeps_the_generation_its_header_gives` patches the second page's
image to `8 1 obj` behind a row of generation 0, and its page to `/Im 8 1 R`. After a redaction
on the first page, that page still draws the scan, in the document and after `write_full` and
reopening, and the dictionary is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Main's #1143 (863af864a), merged in 77b9cfa31, makes cos read a reference whose generation isn't
its object's as null. The walk that finds what shares a /JBIG2Globals dictionary with what left
fetched through `Document::get`, but remembered what it read by object number alone, so a
reference of another generation and the object's own one shared what the first of them read:
- What a parameters value names (`Run::names_in`): `/DecodeParms [10 1 R 10 0 R]` on an image
that left, object 10 naming dictionary 9, read 10 1 R as null, named nothing, and remembered
that for object 10. The 10 0 R after it found it, so nothing named 9, and the dictionary
stayed in the file with the image another page kept.
- A size, filters or parameters an image is stored again with (`Run::follow`): an image whose
`/Width 11 1 R` reads as null can't be stored again, and refused the run alone. Once another
image had read 11 0 R, it took that width and was stored with it.
What an image names is now read by object number, whatever generation the reference gives
(`Run::fetch_numbered`), in `names_in` and in the `/JBIG2Globals` chain alike: a reader that
doesn't compare generations draws with that object, so what an image that left names this way
may hold its shapes. That is how the chain already named `9 1 R` (the "another generation" way
out), and how both read before #1143. What is read by number is remembered by number. The
values an image is stored again with are read as cos reads them, and remembered by number and
generation, so a reference of another generation reads its own way.
`kept`'s comment said a reference reads the object of its number whatever its generation, which
is no longer so for a full save. It says how `reachable_objects` reads one now. The README and
`Run`'s comments say which reads take the number and which the generation too.
Tests:
- `a_redacted_scans_shared_jbig2_dictionary_leaves_the_file` gains three ways out:
- parameters through another generation, then their own;
- parameters through another generation alone;
- a `/JBIG2Globals` reference to a reference of another generation.
- `a_size_through_another_generation_isnt_the_one_its_own_reference_reads` stores no image
with the width another reference read. It refuses instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Conflicts, and how they were resolved:
- cos writer.rs and lib.rs: main now numbers a full save in `full_save_order` and exports
`reachable_objects`, the object numbers a full save writes, for the pattern guard. This
branch had added the same walk as `walk` and `reachable`. Main's is kept, with its alias
handling, and this branch's is dropped: `globals::kept` is now `reachable_objects`, so the
JBIG2 cleanup and the pattern guard read one walk. `kept` returns the walk's error (a save
that can't number its objects), which redaction and Remove Hidden Information pass on. The
cos test of what is reachable, and two redaction tests, read `reachable_objects` instead.
The exports keep this branch's `DECODE_PARMS_KEYS` and main's `TextChars`,
`civil_from_days` and `new_file_id`.
- redact lib.rs: main unlinks pruned patterns from `/Pages` nodes after `unlink_doomed`; this
branch replaced `restore_shared_jbig2` with `globals::unshare`. Both stay, the unlink first:
an image a pruned pattern's cell drew leaves the file with it, and is a user of a dictionary
that left too.
- redact sanitize.rs: main's `strip_outline_actions` and this branch's `unshare_unmarked` both
stay. At the end of `remove_hidden`, `unshare_unmarked` runs after main's `unlink_undrawn`,
so an image unlinked for being drawn nowhere takes a shared dictionary with it, and is
followed by main's `require_full_save_with_new_id`.
- redact image.rs: `MAX_PIXELS` is `pub(crate)` on both sides; the comment names
`globals::without_dictionary`, where this branch moved `without_globals`.
Tests: `an_undrawn_image_unlinked_takes_a_shared_jbig2_dictionary_too` (Remove Hidden
Information, through `unlink_undrawn`) and
`an_image_a_pruned_patterns_cell_drew_takes_a_shared_jbig2_dictionary_too` (a redaction,
through `unlink_pruned_patterns`) pin the order of the two new routes before `unshare`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY