Found in review:
- **A press where two pages touch.** With the page gaps hidden, pages can share a row of pixels,
and a press there was claimed by both: the first page acted on it, then the second took it
over, and the text layer could count it twice. Only a press just begun is claimed now, by the
first page that does. This was so on `main` too.
- **The guard** listed who asks `PageHits::at` and `context_at`, but not `PageHits::hits`, which
they wrap. A new call of it in `page_input.rs` now fails, and the guard's doc says what it
still can't see.
- **Docs.** `Hit::declines_drag` (a link's drag with the Link tool), `over_mark`, and when the
Select tool still unselects under another tool's frame.
Tests: two pages claiming one press, seen failing without the fix. A planted call of
`PageHits::hits` fails the guard. The router's unit tests now begin a press before claiming it,
as `draw_pages` does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review:
- **A double-click another tool took finished a polygon.** Round 5 kept commenting tools off a
press another tool had taken, but a polygon, polyline or cloud finishes on a double-click or
Enter whoever has the press. With Prepare open, a double-click on a field opened its properties
and committed the polygon being clicked out. Added text opened for editing did the same with
the Edit panel open. The commenting tools now skip such a frame entirely, as `main`'s consumed
chain did. The comments' own presses and a click's unselecting still run.
- **The menu's anchor after Escape.** While a press Escape cancelled is held, the page's tools
are skipped, and a right-click left the page menu on the page right-clicked before. It now
anchors where it is, as every other right-click does.
- **The guard.** It now lists who asks the page view's hits at a point (`PageHits::at` and
`context_at`), so a new place in `page_input.rs` can't test a point again. It takes a file
that uses the comments' page context (`PageCx`) for a page module. Its doc of the point tests
it doesn't list is now true.
- **Docs and visibility.** True docs for `PagePress::Free`, where a press is off the pages, and
the camera's hit. `link_ui::screen_rect` is private.
Tests: a polygon left unfinished by a double-click on a Prepare field, and a right-click after
Escape anchoring the menu. Each was seen failing without its fix. A planted ask of the page
view's hits, and a planted unlisted file using `PageCx`, each fail the guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review:
- **Comments acted on presses another tool had taken.** Comments run every frame now, and a
commenting tool acted on any press nobody held, so with Prepare open a Rectangle drag drew a
rectangle as well as Prepare's selection rectangle. Added content, with the Edit panel open,
could likewise open a note composer under the Note tool. `main` skipped comments when another
module took the frame. They are now told (`taken`), and their drawing and placing tools leave
such a press alone. A click there still unselects.
- **The page menu's anchor went stale.** A right-click while the primary button was held skipped
the call that also anchors the page menu, so Delete page, Paste and Copy Image acted on the page
right-clicked before. Every right-click on a page now anchors the menu there, whoever takes it,
and only the selections wait for the release. That includes the right-clicks Add text, Edit
text, the Link tool and a field tool take, which used to keep the previous anchor.
- **Edit text's right-clicks.** Edit text took right-clicks on added content too. It now takes
those on its paragraphs, pictures and the objects selected together, and comments see a
right-click on added text, as before.
- **Camera corners.** A camera corner at the page's edge showed its grab cursor past the edge,
where a press is no page's. Its hit now stops at the page, as a link's does.
- One source for a comment's layer, dead code in Add text's right-click, and true docs for
`PagePress::Elsewhere` and the "Page presses" rule.
Tests: a shape-tool drag with Prepare open, a right-click with the button held on another page,
a right-click on added text with Edit text, the menu anchored by an Edit text right-click, and a
camera corner at the page's edge. Each was seen failing with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review:
- **Right-clicks during a press.** A comment being moved kept its selection only while its
gesture state was live, so a gesture left over (a release the pages never saw) blocked every
later right-click. Comments and Prepare now leave their selection alone while the primary
button is held. egui ends the press's own interaction (and its drag) when another button goes
down, so the button is what tells. A touch's long press, which egui reports as a right-click
with the finger still down, still opens its menu.
- **A click that began beside the page** unselected the selected comment, though the Link tool and
Prepare now leave theirs alone for such a click. Comments do the same.
- **The guard** took a file as a page module only if it named `PagePointer`; a file using
`PageToolContext`, `PagePress` or the router's hits or owners is one too. Its message about
tooltips and release checks now points at the page view's own hits, and its doc of the router's
readers names link following.
Tests: a right-click with a gesture left over, a click that began beside the page, and a long
press on a comment. Each was seen failing with its fix
reverted. A planted page-router name in a module the guard didn't list fails the guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review:
- **The Link tool's click** acted on any press: with Prepare open, a click on a field also
unselected the selected link, and a click that began off the page did too. It now acts only on
a press on a link, or one nobody holds.
- **A right-click while a comment is moved** selected the comment under it mid-drag, though the
release still moved the first. Comments now keep their selection during a move, resize or
reshape, as Prepare does.
- The hits under the pointer before the tools act are taken only on the pointer's page, and the
Link tool's right-click reuses them.
- The router has one source for the press's page (`Capture` no longer carries its own).
- The guard lists which functions may ask which comment lies under a point (`comment_at`); the
guard finds hit tests by name, and its docs say so.
- Docs made true: what `PagePress::Mine` promises, what `tool_input` gates, and that a dialog does
not cancel the pages' press (only Escape and automation's cancel do).
Tests: the two fixes, and ruling 1 for added text (a click on it unselects the comment), each seen
failing with its fix reverted; the drawing-tool right-click test now checks the comment's menu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Routing right-clicks changed a few more cases beyond the rulings, found in review:
- **The Link tool.** A right-click on a link also unselected the selected comment and moved the
page menu's anchor; the Link tool used to take it whole. On a document it can't change, its
right-click on a note over a link opened the plain page menu instead of the note's. Now the Link
tool's right-click is what its press there would take, on a document it can change, and on a
link it leaves comments alone.
- **The camera.** A corner, on the handle layer, took right-clicks from a comment under it,
though it has no menu. Corners are no longer right-click targets.
- **The Link tool's hand** showed in the 2 px margin past the page's edge, where a press is no
page's. `link_ui::hit` now finds nothing off the page.
Docs made true: what a comment's press is (`comments::page_input`), what reaches the free-press
gate (`page_tool_input`), and when every page is `PagePress::Free`.
Tests: those three, and two that pin right-clicks kept as before: Edit text's on a paragraph
under a comment, and a Prepare field tool's. Each seen failing with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The modules that are now told what a press is to their page (forms, Prepare, comments, the Link
tool, the camera, the stamps and the other free-press tools) left the press-handler list, whose
check was only that each read `router.owner(` and called no hit test of its own. Nothing kept
them from reading the router again, though CLAUDE.md said the guard enforced it.
- Rule (e) now: `router.press_for(`, `router.owner(` and `router.keeps_drag(` are read only by
`ROUTER_READERS`: the page view, and the Edit text group and text layer until 4b tells them
too. An unused entry fails the test, so 4b removes them one by one.
- A hit test called as a method counts as a call: `cx.hit(origin)` or `self.foo_hit(p)` in a
press handler passed. `PageCx::hit`, which finds a comment and makes no `Hit`, is renamed
`comment_at`.
- An entry of `PAGE_HIT_CALLS` that no page module calls any more fails the test.
Each was seen: a `view.router.owner(page)` and a `cx.hit(origin)` planted in forms' `page_input`
pass the guard before and fail it after, and a stale `PAGE_HIT_CALLS` entry fails it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
egui reports a drag once the pointer has moved a few points from the press. A press near a page's
bottom edge, flicked onto the next page in one frame, is seen as a drag only with the pointer on
the next page. Redact (and the area Highlight), the Link tool and Prepare returned before their
drag start unless the pointer was on their own page, and the next page is `Elsewhere`, so the
press drew nothing anywhere. (On main the next page took it, with the box's start on the page
before.)
They now start the gesture from where the press began on their own page, as zoom, snapshot, crop
and Sign area already did, so the box, link rectangle, marquee, field drawn or field moved belongs
to the page the press began on. Tests: one per tool, each seen failing with the fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page's hover (the top hit under the pointer) was only worked out when nothing was drawn over
the pages, so with the palette open a field beside it lost its tooltip and its hover outline,
and comments their outline. The palette keeps the page's cursor and tips away, which is what
that gate is for; hover now ignores it, as the outlines and the field tooltip did before.
The hits under the pointer were also asked up to four times a page each frame, each pass running
every module's hit test: for a click's release, for Fill & Sign's preview, for a right-click, and
for the cursor. A click's release, Fill & Sign's preview and a right-click's tool checks now share
one pass, taken before the tools act and only on frames that need it. A right-click's own pass
(comments by `context_hit`) stays, and the cursor and hover keep theirs, after the tools act.
A unit test pins that cancelling the pages' gestures lets go of a camera corner. The kittest for
Escape mid corner drag passes on `main` too, because Escape leaves the camera tool first.
Tests: `a_fields_tooltip_shows_beside_the_open_palette`,
`cancelling_pointer_gestures_lets_go_of_a_camera_corner`; each seen failing with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Routing right-clicks by what lies on top under the pointer changed three cases beyond the
rulings:
- **Prepare.** A right-click while a field was dragged selected the field under the pointer, and
the release then moved that one. Prepare used to ignore right-clicks during a drag; it does
again (`prepare::context` returns while a grab is held).
- **The Link tool.** A note over a link took the right-click, though the Link tool's press there
is the link's. The Link tool's right-click on a link is the link's again, whatever lies over it.
- **Add text.** Comments took right-clicks away from paragraphs and added items. Add text used to
take every right-click on a page it can change; it does again.
Tests: those three, and two that pin ruling 6's right-click half with the Select tool: no link
menu on a link with no action, and none on a form field over a link. Each was seen failing with
its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page guards (pointer reads, cursor writes, and presses) left a file's tests out by cutting it
at its first `#[cfg(test)] mod`. `edit_text_ui/image.rs` has a test module at line 231, so its
lines 245-1116 were never read: `crop_input`, `image_input` and the picture hit tests among
them. A hit test called there, a router write or a raw pointer read passed every guard.
`outside_test_modules` now leaves out each test module up to its closing brace, and the guards
read the rest. Nothing in the newly read code breaks a rule. A unit test pins the scanner, and a
bare `crop_hit(` planted in `image_input` now fails the press guard (it passed before).
The guard's messages and docs, CLAUDE.md's "Page presses" line, and the docs of
`page_tool_input`, `PageHits::hits` and `PageRouter::cancel` now say what the code does: modules
are told what a press is to their page (`PagePress`), and `PageHits::hits` is the one function
that asks the hit tests. `comments::context_hit(` joins the hit tests a press handler must not
call.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forms and Prepare read the router themselves, and forms tested the pointer twice more: for a
field's tooltip, and for whether a click came up over the widget it was pressed on. Following a
link tested the pointer again too.
Both are now told the page's `PagePress`. Prepare's Select tool draws a selection rectangle, or
unselects, only on a press nobody holds, and a field tool draws only on such a press. Whether a
click came up over what its press began on is answered once, from the hits under the pointer
(`released_over`), for fields and for following links. The field tooltip (`forms_ui::tooltip`)
and the field hover outline come from the page's hover, the top hit under the pointer.
`PageHits::hits` is now the only function that calls a hit test, and the press-handler check no
longer lists forms or Prepare.
Behaviour changes: a field's tooltip shows only where a press would take the field, not under
a comment drawn over it (the user's ruling, 2026-10-31); a field's hover outline follows the
same rule, so it shows only with the Select tool, which fills fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tools that act wherever on the page each decided for themselves whether a press was
theirs: Sign area, zoom and snapshot, crop, redact and the stamps asked the router, and Measure,
Fill & Sign, the camera and the Link tool's rectangle acted on any press. Stamp placement lived
inline in the page view.
`tool_input` is now the one gate. Measure, the Stamp tool and custom stamps, Fill & Sign, Sign
area, zoom and snapshot, the camera and crop are each told whether the press is the tool's (it
began on their page and no hit won it), and act only then; their cursors and gestures run as
before. Redact's boxes and the Link tool's rectangle are told the same where they are called.
Stamp placement moves to `stamps_ui::place` and `place_custom`, beside custom stamps. Measure
is told its clicks (`measure_ui::Clicks`), Sign area takes a `PageToolContext`.
The camera's corners are hits (`camera_ui::hit`, `Owner::CameraCorner` on the handle layer), so
a corner's drag is an owned press like any other: it starts on the corner's own press, ends on
`drag_stopped() || !primary_down`, and `cancel_pointer_gestures` drops it. Until now only the
order of two key handlers kept Escape mid-drag from leaving a corner following the pointer:
Escape leaves the camera tool before the pages see it. The kittest for Escape mid-drag can't
fail with that order; a unit test of the cancel comes later in this series.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments ran last in the page view's chain of `||`, so only when no earlier module had taken the
frame, and asked the router themselves (`grabs`, `fill_grabs`, `draw_grabs`), with a hit test of
their own for hover. A link, a field or added text taking the frame first hid a comment's
gesture end, its Escape and the click that unselects it.
Comments now run on every frame and are told the page's `PagePress`:
- a press on a comment (its body, a handle or a point) goes to `select_input` with the part,
with the Select tool, a shape tool (the selected shape's handles) or Fill & Sign (its marks);
- the drawing tools act only on a press nobody holds (`Gestures::free`);
- a click on anything but a comment leaves comments unselected.
`grabs`, `fill_grabs`, `draw_grabs` and `cancelled_drag` (with the raw button read in `keys` it
needed) are gone; `grabs_points` is `reaches_points`. Fill & Sign leaves the press to a mark
when the press is someone's, or, with no button down, when a mark is the top hit under the
pointer. The page's hover is that top hit (`PageInput.hover`), and the comments' hover outline
follows it, so it no longer shows under a field or added text that would take a press.
Behaviour change (the user's ruling, 2026-10-31): a click on a form field, or on added text,
leaves the selected comment unselected, as a click on a link or the page already did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each module asked the router `owner(page).is_none()` to learn whether a press was the active
tool's. That reads a press held on another page, or begun between the pages, as the tool's
too.
The router now remembers the page a press began on (`begin` on every new press, then `press`
on the page it lands on), and `press_for(page)` answers what a page has to do with it:
- `Mine(owner)`: the press began on this page and the owner's hit won it;
- `Free`: no button is down, or the press began on this page and no hit won it, so it is the
active tool's;
- `Elsewhere`: the press began on another page, or between the pages.
The page view asks once per page, and the stamps and the Link tool use the answer. The other
modules move to it in the next commits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each right-click handler tested the pointer itself, and the chain's order decided which ran:
comments only when no earlier module took the frame, Prepare by its own widget scan.
A right-click now goes to what lies on top under the pointer (`PageHits::context_at`), by the
layer ranking a press uses, with two differences. Comments count with every tool, as a
comment's menu opens whatever tool is active (`comments::context_hit`). A highlight over a link
is the highlight's: a left click there follows the link, a right-click opens the highlight's
menu. On every right-click the page's menu is anchored at the pointer, and the comment it is
on is selected, or none (`comments::context`); a Prepare field is selected (`prepare::context`);
links take theirs from the same answer. Edit text's own paragraphs and pictures, and a Prepare
field tool, keep their right-clicks as before.
Behaviour change (the user's ruling, 2026-10-31): a right-click on a highlight over a link opens
the highlight's menu, not the link's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three link tests disagreed. The Select tool's hand and click (`link_hit`) took the topmost link
the renderer lists, with no margin, by its place in that list. The Link tool's selection and
Select's right-click took the bottom one of every link the engine lists, 2 px around each, by
`/Annots` index. Comments had a fourth (`on_link`).
Now `link_ui::hit` is the one test, and `Owner::Link` carries the `/Annots` index:
- the Link tool finds every link the engine lists, 2 px around each, and takes the topmost; a
drag from a link still draws a new one (the hit declines the drag);
- every other tool finds the links the renderer can follow, with no margin (so Select shows no
hand over a link with no action, as before), and takes the topmost;
- following a link looks its target up by that index;
- `comments::hit` is told whether a link is under the point.
The Link tool is told the link its press is on, and the one a right-click is on, instead of
testing the pointer itself. A right-click goes to the top hit under the pointer, so with the
Select tool a right-click on a note over a link selects the note and opens its menu, and one
just beside a link no longer opens the link's.
Behaviour changes (the user's rulings, 2026-10-31): the Link tool takes the topmost of stacked
links; Select's right-click has no margin; a right-click on a note over a link is the note's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What the tools do with the pointer on one page now lives in its own file: `PageInput`,
`PageToolContext` and `page_tool_input`; `PageHits`, `edit_inventory`, `link_hit` and
`cancel_on_escape`; the text layer, `text_layer` to `finish_text_pointer_gesture`; and
`page_overlays`, which follows links and picks images. `cached_edit_images` stays in canvas.rs,
where painting and the menus use it too. No change in behaviour. canvas.rs goes from 4,073 to
3,265 lines.
The guard's page modules gain `canvas/page_input.rs`, and its allowlist entries move with the
functions.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#5426 took the page pointer router from #5300 at 11bc8c56, before #5300 added its guard. The
guard on main checks seven named press handlers, eight hit tests called by path and one
`router.press(` caller. Planted inside `#[cfg(any())]`, it caught one of five violations: a
second `link_hit` in `page_tool_input`, a bare `hit(` in comments' `page_after_text`,
`view.router.cancel()` and `press_cancelled = true` in redact_ui all passed.
This brings in #5300's rules beside main's press-handler check, in one test,
`page_modules_act_on_a_press_only_through_the_router` (from aef301d82):
- only `page_tool_input`, `draw_pages` and `cancel_pointer_gestures` press, settle or cancel
the router;
- only `cancel_on_escape` ignores a press until the button is up;
- every `hit` and `…_hit` call, bare or by path, is in a function allowed to make one, with why;
- a `Hit` is made only in a hit test;
- an allowlist entry that is no longer used fails the test.
It caught all five planted violations, and a stale ROUTER_WRITERS entry. CLAUDE.md's "Page
presses" line names the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crop mode owns every press on its page, the controls beside its frame included (0595d4d39).
`PageHits::at` made that hit itself. A `Hit` comes from a module's hit test, named `hit` or
`…_hit`, so the cursor, the tip and the press agree, and the Page presses guard holds the page
modules to it. The picture module's `crop_hit` makes it now, and `PageHits::at` asks it first,
as before. No change in behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The centred 4 pt strip still covered the right 3 pt of the page's
scrollbar beside a right-docked pane. Register the host's resize edge
again from the border rightwards, on the host's own layer, and leave an
inert edge inert. The test now hovers and drags both scrollbars beside a
right-docked Tools pane (CodeRabbit) and checks the width is unchanged.
`ImageBudget::decode` is now the one public way to decode an image's
CCITT fax or JBIG2 data: `Document::decoded_image` and
`decoded_image_within`, which decoded with no limit on the work, are
test-only in pdfcraft-cos, and pdfcraft-filters exports only
`decode_ccitt_within` and `decode_jbig2_within`, which spend from a
caller's allowance (`decode_jbig2` is gone, `decode_ccitt` private).
So an operation over many images can't decode them without sharing an
allowance; the compiler holds new callers to it. Doctests that must not
compile check both crates; with the old entry points public they fail.
Tests outside the crates decode through an unlimited ImageBudget.
Streams in general (`Stream::decoded`) are left for a follow-up.
It reserves room for a translated "Mixed", and in Czech or French that pushed the alignment
button up to 54 pt past the Tools pane. The field is now no wider than SIZE_FIELD, and a longer
translation scrolls in it. The language test checks the size row too.
Conflict with main's rail docking, resolved by keeping both: the rail follows the side chosen
for it (main) at 49 pt with 1 pt padding, and main's dock button stays above the page display
button, while zoom out and in stay side by side under the zoom field.
Conflict with main's hotbar, resolved by keeping both: the bar keeps main's offset past a hotbar
docked on the left or top, and centres in the margin that starts after it.
Conflicts with main's font menu, size field and accessible names, resolved by keeping both:
the font list keeps main's searchable installed-fonts menu inside its outline; the line spacing
list and number fields keep their names. The size field now reserves room for "Mixed", so
MIN_ROW counts it (62 pt, measured for English), and the spacing columns take their width from
the font list so a longer translation of "Mixed" can't widen them.
Found while testing crates/ui-egui/src/panels/page_selection_ui.rs:24-27 (parse, `number` closure)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* A rebuilt xref finds an object whose header names another generation (#5580)
`poppler-85140-0.pdf` is 340 bytes with no cross-reference table, and the
header of its one page reads `3 18446744073709551616 obj` while the page tree
points at `3 0 R`. Before #427 the digits wrapped and 2^64 wrapped to exactly
0, so the header read as `3 0 obj` and the file opened by accident. Once an
overflowing integer became a real, the object was unreachable and the file
stopped opening at all, which `cargo xtask check` reports as a regression.
The generation is not really the point. `3 99999 obj` with the same page tree
is refused in exactly the same way on main -- the control for the new test
fails on that row first, with "page 1 does not exist" -- so this is a
mismatched generation in a rebuilt table, not an integer overflow. A table
rebuilt by scanning the file has no revision history to tell two generations of
an object apart, and a file that needs rebuilding references everything as
`N 0 R` whatever its headers say.
So, in the reconstruction path only:
- a generation number that cannot be read is taken as 0, the value such a
file's references use (`read_object_header`). A generation is at most
65,535 (ISO 32000-2 §7.5.4), so there is nothing to lose by it;
- every scanned object is also filed under generation 0, with `or_insert` so
a real `N 0 obj` is never displaced by the alias;
- the object is read back behind the same lenient header, since reading it
parses the header again and the strict identifier check rejected it
(`read_repaired_body`, gated on `repaired`).
Reading numbers anywhere else is unchanged, and a table read from the file's
own xref keeps the exact identifier match. The scan already files objects found
in object streams under generation 0, and cos's table parser already takes an
unreadable generation as 0, so this follows what the code does elsewhere.
The test covers generations 0, 99999 and 2^64 with no table and no `startxref`,
and asserts the page is found at its own size. Its fixture is built in the test
rather than taken from the corpus, so it holds without the pinned checkout.
* The generation-0 stand-in is one key, not a second object
Two consequences of filing a scanned object under generation 0 as well,
both from review.
`XRef::objects` walks the table's keys, so an object with a stand-in came out
once per key: a document whose page tree cannot be read is recovered by looking
for objects that are pages, and it reported the page twice. Enumeration now
yields one object per location, after the sort it already does. Both keys stay
for lookups; only this walk is deduplicated.
And `or_insert` never replaces, so a later `3 1 obj` moved the exact `3 1` key
to the later body while the `3 0` stand-in stayed on the earlier one -- `3 0 R`
and `3 1 R` would then read different objects. The scan now tracks which
generation-0 keys are stand-ins: a real `N 0 obj` takes the key and keeps it,
and a later header for the same number replaces a stand-in.
The tests check both through what comes out: page 2 must not exist for a
single-page file recovered without a page tree, and with two `3 1 obj` headers
the page comes back at the later body's 300x150 rather than the earlier
200x100.
* M2: the repair scan keeps its place on a non-header, and an object stream's first member is enumerated (#5580)
- read_object_header's lenient path works on a clone, so `42 3 0 obj` no longer costs the
header after the stray number (CodeRabbit, minor).
- XRef::objects deduplicates by location and object number: an object stream and its first
member share (offset, 0) and are two objects (CodeRabbit, minor).
- vendor/README.md: hayro-syntax patch (34) and its tests.
Both new tests fail with their fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>