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>
* M7: make page-mark replacement transactional (#5657)
* ROADMAP: drop the header line (the log entry stays)
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>
* Fix Matrix::invert rejecting valid matrices with a small scale
Found while testing crates/content/src/lib.rs:732-735 (Matrix::invert)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Matrix::invert: a matrix whose inverse overflows has none
A subnormal determinant passes the relative check, and dividing by it
overflows (CodeRabbit on #5703).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(ui): don't check recent files on the UI thread at startup (#5682)
Restoring settings dropped recent files whose files no longer exist by
asking the file system about each one on the UI thread, before the first
frame. On a network mount that has stopped answering (davfs2 in the
report) that call blocks in the kernel, so the window never appears.
Settings now keep every recent entry when they load. Once the window is
up, a worker checks the restored paths and each frame drops the ones it
has found gone. An entry it hasn't reached, or can't tell about (the
mount answers with an error), stays listed. A worker stuck on a dead
mount stays stuck, but nothing waits for it. This follows the pinned
folders' worker.
Fixes#5682
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Recent files: a file opened while the check is pending stays listed
The worker's answer that a restored path is gone is out of date once the
user opens that path; keep the new entry (CodeRabbit on #5692).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
An image that shared a redacted image's /JBIG2Globals is now stored
again as the smallest of Flate, Group 4 or JBIG2 coded alone (smallest),
like a cleared copy, rather than always Flate, so the other pages of a
scan don't balloon either. The shared-globals test's second page is a
ragged scan and checks it is no longer Flate and is smaller.
A JBIG2 image's /JBIG2Globals hold the symbol dictionary of every image that
shares them (jbig2enc -s and most scanners give one to all of a scan's pages),
so the bitmaps of marks drawn only under a redaction stayed in the file while
another page's image used them. Every image using the globals of a removed or
replaced image (or of one in a removed or replaced form) is now decoded and
stored again in place as Flate without them; a full save then drops them. One
that can't be decoded for certain refuses the redaction
(RedactError::SharedJbig2Globals). Report::images_restored counts them.
- Add Freehand-Highlight tool with 100pt line width support, Lucide highlighter icon, and PDF Multiply blending.
- Add customizable floating and docked hotbar with command picker, size slider, full-side strip chrome integration, and settings gear icon anchored at the bar end.
- Add left/right docking options for the navigation rail with toggle button and preferences setting.
* M2: a shrunk image on a turned or mirrored page snaps to whole pixels (#5530)
hayro patch 14 (`snap_minified_image`) shrinks an interpolated image once
to its whole-pixel device rectangle and draws that 1:1, instead of
shrinking to a rounded-up size and resampling again at a fractional
scale and offset. It only accepted an upright transform (a > 0, d > 0,
b = c = 0), so every image on a page under /Rotate 90, 180 or 270 -- and
any image placed turned or mirrored by its own matrix -- still took the
double resample. That is #5530: a scanned page that matches PDFium
upright drifts and softens when the same page is rotated.
The snap now accepts every transform whose axes lie along the device's
(the eight quarter-turns and mirrors) and returns the 1:1 transform
that draws the shrunk raster from the snapped corner, one source-axis
step a whole device pixel, in whichever direction that axis runs.
Skewed and freely rotated images keep the old path.
On a synthetic page (crop box and scale on whole pixels, image at
fractional coordinates), /Rotate 90, 180 and 270 renders turned back
differed from the upright render in 72% of pixels, by up to 200 grey
levels; now they are identical. Non-interpolated images were already
identical and stay so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: snap only when the dropped skew is invisible, and only a resize that succeeded
Review of #5640:
- The snap judged an image axis-aligned by a tolerance on its
coefficients (1e-4 of the scale). For a very wide image a skew that
small still moves the far edge by more than a pixel (40 000 samples
at 4e-5: 1.6 px), and the snap flattened it. It now measures what it
drops: how far each off-axis term moves the far edges, at most a
hundredth of a device pixel (SNAP_SKEW). Upright and turned alike.
- A resize that fails hands back the source raster at source size; the
1:1 snapped transform then drew it at that size. The snapped
transform is now used only when the shrink produced the snapped
size; otherwise the full transform, as before.
- The mirrored case of the edge test drew a uniform image, which looks
the same mirrored; it now also mirrors a half-black image and checks
which side the black lands on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M2: the turned-page test draws every kind of image the snap reaches
The /Rotate oracle test drew one 8-bit grey image. Stencil masks (CCITT
scans often are one), 1-bit images and images with a soft mask reach
the snap through other paths, so it now draws all five: 8-bit and
1-bit grey, a stencil mask, a soft mask of the image's size and one of
another size. On main, every interpolated kind is off by 113-217 levels
over 63-73% of the image when turned, and so is a non-interpolated
image with a smaller soft mask (110 levels, 16%): hayro patch 15
filters such a mask whatever /Interpolate says, so it is snapped
upright and was not when turned. All are identical with the fix; the
other non-interpolated kinds were already identical and stay as guards.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M13: make MCP aligns enums Kimi-compatible
Fixes#5588
* Drop the progress-log lines that conflict with main
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>
An end that is missing or isn't a number sorted below any number, so [1],
or [1 7 0 R] with 7 leading nowhere, read as inverted on a JPEG or a stencil
mask, and so did an end too long to hold, which parses as infinite. Such an
array now leaves the default mapping, as it does on the lossless path.
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M8: typing the last page of a range keeps what is typed (#5574)
The page-range dialogs (Redact pages, Rotate pages, Duplicate field,
Number pages) raised the end to the start on every frame, so the
half-typed "1" of "12" became the start before the "2" arrived.
The end is now ordered only once its field is no longer being typed
into. Number pages no longer lowers the start to a half-typed end, and
its preview cannot underflow while the end is below the start.
* M8: a test types the last page of Rotate Pages' range (#5574)
Rotate Pages' From and to fields are labelled, and a test types "12" after a
first page of 5 (pages 5-12 are turned) and "3" left by clicking OK (page 5
alone), as croquete1 asked on #5579.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
* M8: page ranges stay typed while the window is in the background, and OK orders them (#5574)
Review follow-up. The end field's typing check used Response::has_focus,
which is false while another window is in front, so switching apps
halfway through "12" still raised the typed "1" to the start. It now
asks egui's memory (widgets::typing_into). OK pressed while the end is
still focused (a screen reader's click) orders the range first in Rotate
pages, Duplicate field and Redact pages, and Number pages builds its edit
from the ordered end, so a reversed range never reaches the edit.
---------
Co-authored-by: Claude <noreply@anthropic.com>
* M14: the comments panel lays out only the cards in view (#5488)
With more than 100 comments, a card or page header out of view that
was measured at the current width only takes up its height instead of
being laid out every frame. The selected card is always drawn, so it
still scrolls itself into view. Shorter lists are laid out whole and
keep every card in the accessibility tree.
The 500-comment panel in tests/perf.rs drops from about 23 ms to 5 ms
per frame locally, well inside the 40 ms budget it overran on CI.
* M14: the comments panel measures a card again when it changes (#5488)
A card's kept height is now per document and per what the card shows (its
thread's text, author, date, status, colour and lock, and whether it is the
selected card), so another document at the same width, an edit, an undo or a
redo never lays the cards below it out at a stale height (CodeRabbit on #5583).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
* M14: a comment card with an editor open is never skipped (#5488)
A card with an inline editor open on it or on one of its replies is laid out
wherever it is, and its kept height is apart from the one without the editor,
so the cards below make room for the editor (CodeRabbit on #5583).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
---------
Co-authored-by: Claude <noreply@anthropic.com>
* M10: Create ▸ Blank page uses the system region's paper (#5644)
A new blank document was always US Letter. It now takes the paper of the
system region: Windows' own Region paper size (iPaperSize) first, then the
system locale on macOS or LC_ALL/LC_PAPER/LANG, A4 everywhere outside CLDR's
US-Letter regions, and Letter when the region can't be told (web).
The region parsing is shared with the Units preference.
* M10: the first locale variable set decides the paper; extensions are not regions (#5644)
LC_ALL, LC_PAPER and LANG are read in POSIX order and the first one set
decides, so LC_ALL=C no longer falls through to LANG's region; and a locale
tag's region is read from its base tag only, so en-US-u-nu-latn is US, not
NU (CodeRabbit on #5651).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
* M10: the blank page test covers A4 as well as Letter (#5644)
The app keeps the paper Create ▸ Blank page makes (blank_paper, the system
region's at start), so the test makes a blank page on A4 and on Letter
whatever the host's region (CodeRabbit on #5651).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
---------
Co-authored-by: Claude <noreply@anthropic.com>
After switching back to the window, the windowing layer sends no
pointer move until the mouse moves, so egui knew no pointer and the
wheel scrolled nothing. When a frame turns the wheel with no known
pointer and no move of its own, the raw-input hook now asks the system
where the pointer is (drag_pointer::in_window, already used for file
drags) and puts that move first.
The test timed the 100-field form three times, then the 800-field form
three times, and compared the best of each. A busy spell on a shared
runner that fell on the second half slowed only the large form, and the
ratio went past 16x (16.6x on a gate run) though it is about 7x.
Each of five rounds now opens the small form and then the large one, and
the best round's ratio is checked against the same 16x, after one
untimed opening of each. With the values filled one by one again (the
quadratic code before #4638) it still fails, at about 48x.
The list was fixed at 12. Preferences ▸ Documents and view now takes a
number from 0 to 50, with 12 as the default. At 0 Home has no Recent
section and File ▸ Open Recent is disabled.
The list is cut when Preferences closes, so the number can pass through
0 on its way to another. Until then Home and saved settings already
leave out what is past it. A settings file can't ask for more than 50.
The control channel and launch options take it as recent-files. The two
new strings go in every catalog as English copies.
A region's location, a refinement's offset, a symbol's corner, a Huffman
range line and a decoded integer of 2^31 could each overflow: a panic
wherever overflow is checked. The last four wrap, as a release build did.
Bitmap::combine saturates, which comes to the same: a side is at most
65,535, so a location that overflows is past the end of any page. What is
drawn doesn't change. Patch (12), after the numbers #5491 and #5569 take.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
Convert context-paste user coordinates into AddImage display coordinates and
identify the inserted image through its new owning stream and XObject. Keep
legacy added-image metadata aligned with the active image controller, route
Delete to the visible selection, and clear hidden image selections when leaving
the controller while preserving text selections and drafts.
Separate selection-dependent Copy from whole-page Snapshot and document Export
permissions. Add synthetic UI regressions covering transformed page placement,
identity collisions, shared image references, saved deletion results, tool
transitions, and permission controls.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Fold a leading transform inside the image scope as cm followed by the local transform; an enclosing transform uses the opposite multiplication order. This preserves the complete requested image and crop displacement under an outer scale. The exact crop-arrange-group-move fixture previously moved only five points for a ten-point request; saved geometry and rasters now match an independent direct Transform control across both axes and signed offsets. Full scoped Linux native, wasm and repository gates pass.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Track the text-state operators with saved drawing state, restoring inherited spacing, scaling, leading, rendering mode, rise and known font before relocated Form artwork. Emit restoration inside BT/ET, and leave a font selected by a replayed ExtGState authoritative over an older direct Tf. The exact inherited 12 Tc fixture failed its saved-PDF raster comparison before this fix; zero-spacing and destination-state controls now preserve rendering. Scoped Linux native, wasm, clippy and formatting checks pass.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Replay repeated ExtGState names in last-application order. Compare each complete soft-mask value and its establishment matrix instead of only its group reference, and retain cached shared definitions across saved states. Saved-PDF raster regressions cover Alpha versus Luminosity over the same group, repeated aliases at different matrices, and ordinary sequence/single-mask controls. Definition checks also cover backdrop and transfer parameters. Actual baseline and reverted-comparator tests fail; scoped Linux native, wasm, clippy and formatting checks pass.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Project the visible user-space bounds centre for the drag preview, matching the RotateBy engine operation. The full image centre differs for cropped artwork and caused the preview to jump on release.
The regression compares actual painted preview corners with saved/reopened image corners and verifies a requested 90-degree turn on cropped and uncropped controls, including page rotation. Linux scoped clippy, 106 tests, wasm, formatting, layering and assets pass.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Invert the actual affine basis instead of projecting onto assumed orthogonal axes. Capture the displayed crop frame for handles and apply its relative transform to the full artwork, preserving author clips on rotated pages. Exact out-and-back gestures preserve bytes and undo state, including off-centre presses.
Synthetic headless regressions verify sheared and rotated images, crop-frame anchors, press-offset independence and saved/reopened placement. Degenerate and nonfinite bases are refused; the inverse guard has a negative-control test.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
Only request a script conversion from the panel; the caller decides whether conversion changed the text. Do not create a paragraph rewrite merely because the button was clicked. A hidden QQQ fixture checks both script buttons preserve document bytes, undo history and saved rendering, while supported conversions remain covered. Actual reverted-fix raster failure and scoped Linux native, wasm, clippy and repository checks passed.
Co-Authored-By: Codex GPT-6.1 <noreply@openai.com>
One eight-byte search would miss a clearing that took only the first byte
of it: the window is gone and seven covered bytes stay. The saved picture
now has to be the one with every pixel under the mark cleared and no other
changed, on both coded forms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
They use std::thread and std::sync::mpsc, so they take the same gate, and
so does the helper only they use. The crate's test build doesn't compile
without std on main either, for reasons of its own; these add nothing to
that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY