Commit Graph
3601 Commits
Author SHA1 Message Date
Grzegorz MartensandClaude Opus 5.5 a2b2bc5e6d M2.6: test that a JPEG refused at its lookup table is left out
CodeRabbit's follow-up on #5825: the indexed JPEG test ran only with an
unlimited allowance, so nothing checked that a refused lookup-table read
leaves a JPEG out (`a_lookup_table_is_read_within_the_exports_allowance`
checks a Flate image). The test now measures what the soft mask costs
alone (no colour space, so nothing is read after it) and allows a step
less than the mask and the table: the export is refused with the
reason, and only the table is refused, nothing after it. A step short
of the mask, the mask alone is refused, so the mask is read first and
the case above is refused at the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 06:57:06 +01:00
Grzegorz MartensandClaude Opus 5.5 0c9cd267f5 M2.6: a JPEG's colour space is read once for an export
CodeRabbit on #5825: a JPEG that isn't inverted read its colour space to
know whether it is CMYK, and then, with a soft mask, a stencil mask or a colour key keeping it
from being written as it is, read it again for its samples. An indexed one
(not valid, written as it is in the end) paid for its lookup table twice,
and could be refused with an allowance that covers the image and one reading.
The first reading is kept and used.

a_jpegs_lookup_table_is_paid_for_once: an indexed JPEG with a soft mask pays
one reading of its table, and is written as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:51:14 +01:00
Grzegorz MartensandClaude Opus 5.5 05300ea5bc M2.6: say what was measured, not why, and test the other block zlib-rs refuses
The Claims review of the branch: the docs said miniz_oxide "builds a block's
tables for every block", which a timing doesn't show. They now say what was
timed: about 1.7 us for every fixed-Huffman block, empty or not. "The worst
comes to about 7 ns a step" was per byte read; a stage's start, about 0.25 us
under zlib-rs (1.6 us for eight over an empty stream, release), isn't priced,
since every way to repeat one takes bytes of the file. The letterhead test's
doc promised more than it checks.

The same review found a second shape zlib-rs refuses where miniz_oxide read
on: a dynamic block whose code has no end-of-block symbol. flate.rs's quirks
and the README row say so, and a_dynamic_code_without_an_end_is_damage checks
that nothing of it is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 f8788dff11 M2.6: say what zlib-rs reads differently, and test which backend inflates
The Consumers review of the commit before this one: zlib-rs refuses a back-reference to before
the start of the output, which miniz_oxide filled with zeros and went on
from. Read strictly such a stream is now refused, and read tolerantly what
came before it is kept, as zlib has it. flate.rs's quirks and the filters
README say so, and a_copy_from_before_the_start_is_damage checks it, which
also fails under miniz_oxide, so it shows which backend runs: the read price
is set for zlib-rs.

The same review: other dependencies still build flate2's default miniz_oxide
backend, and zlib-rs runs because flate2 prefers it when both are built, not
because it is the only one. The Cargo.toml comment says so. The miniz_oxide
measured was 0.9.1, flate2's (0.8.9 is png's); FLATE_READ_WORK's doc says so.
The filters README named miniz_oxide as the backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 6771ddffe8 M2.6: inflate with zlib-rs everywhere, so reading Flate costs 2 steps a byte
The Resources review of the commits before this one found that pricing what Flate reads at 128
steps a byte refused real redactions. Redaction decodes a picture once for
each page that draws it, so 240 pages sharing a 1 MB scan-like Flate picture,
a mark on each, went from Ok (240 cleared, 20.3 s) to ImagesPastAllowance, its
allowance paying for 188 decodes: each paid about 27 times what it had.

The cost being priced is miniz_oxide's: about 1.7 us for every fixed-Huffman
block, empty or not. flate2's zlib-rs backend took about 11 to 13 ns a fixed block,
and the Windows build already used it (craft-update turns
on zip's deflate-flate2-zlib-rs, and with both backends on, flate2 inflates
with zlib-rs, measured). Every platform now builds flate2 with zlib-rs, which
it prefers to the miniz_oxide other dependencies still build.
In a release build (zlib-rs 0.6.8), per byte read: empty fixed blocks 9 ns,
fixed blocks of one literal 6, empty dynamic 21, empty stored 1.3, against
1,383, 768, 106 and 2 before. Each backend inflating what its own
encoder made, 16 MiB of text took 2.6 ms against 6.3 to 13.7, and a
scan-like picture 38 ms against 53.

FLATE_READ_WORK is now 2: per byte read, the worst shape comes to about 7 ns a
step, and a picture an encoder made pays twice its stored length more than unpriced
(an_image_an_encoder_made_pays_little_for_its_reading: at most half again).

The same review: export and compare's Office export read an indexed image's
lookup table outside the allowance, once per image, even with nothing left
(1.4 s each for 1 MiB of empty blocks under miniz_oxide). It is read through
ImageBudget::decode_filters now, and refused past it
(a_lookup_table_is_read_within_the_exports_allowance).

filters, cos, optimize, redact, create, edit, export and engine pass with the
new backend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 f16c7db8f4 M2.6: a preset dictionary is refused whatever follows its header
Mutation testing of the commit before this one left the preset-dictionary check in flate.rs
alive: the test's bytes after the header fail to inflate either way. A header
with FDICT set followed by data that inflates as raw deflate is refused too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
Grzegorz MartensandClaude Opus 5.5 10172d7a94 M2.6: a Flate stage pays for what it reads
The image allowance charged a Flate stage the bytes it made, and its input
only as what the stage before it made. Inflating does work for every block it
reads, whether or not the block makes anything: a stream of empty
fixed-Huffman blocks, ten bits each, took 1,383 ns a byte to make nothing
(miniz_oxide 0.9.1, release), one of a literal each 768 ns a byte, empty
dynamic blocks 106, against the 3 ns a step budget.rs states. A 250-byte
stream of four Flates over empty blocks took 47 s and was charged about
34 million steps, so a review of #5698 worked out about 7 hours before a
240 KB file of them was refused, through clearing, Optimize, export, compare
and redaction's images and dictionaries alike.

Where decoding is paid for (decode_tolerant_within, decode_within), a Flate
stage now pays FLATE_READ_WORK, 128 steps, for each byte it reads, before it
runs: a stage the allowance can't pay for doesn't start. Per byte read, the
worst of the shapes measured comes to about 11 ns a step at that price, inside
what a JBIG2 step takes. The raw retry of a zlib header that only looked valid reads the
bytes again, so it pays again, makes no more than is left once it has, and is
refused when it can't be paid for. Byte counts for a byte allowance
(decode_counted, decode_tolerant_counted, as a page's content uses) are
unchanged.

With a tiny file's allowance (2^34 steps), the 250-byte stream is decoded
three times in 141 s, and the fourth stops before its last stage reads the
empty blocks (8.2 ns a step, release). Data an encoder makes took 0.4 to 3.2 ns a byte it
made, reading included, and each byte of a file adds 64 times the price to an
operation's allowance.

Tests: a_flate_stage_pays_for_what_it_reads (empty blocks: 129 steps a byte,
refused a step short with nothing inflated) and a_second_reading_is_paid_for;
the exact costs in filters, cos and optimize tests add what Flate reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-03 00:14:52 +01:00
FredApps 8f1441a3f2 fix(ui): keep pane resize edges right of the border, off the page scrollbar (#5730)
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.
2026-11-02 16:59:26 +01:00
FredApps 1997b8ed94 Merge remote-tracking branch 'origin/main' into pr5740 2026-11-02 16:29:14 +01:00
FredApps e261518727 Merge pull request #5569 from Piohrun/feat/jbig2-run-allowance
M2.6: the images of a run, and a page's JBIG2 images, share one allowance of decoding
2026-11-02 15:11:32 +01:00
FredApps 1303684851 Merge pull request #5639 from RiceDee/fix/rail-page-zoom-fields
fix(ui): the rail's page box fits, and the zoom field looks like a field (#5636)
2026-11-02 14:46:11 +01:00
FredApps 2d4e317a58 Merge pull request #5638 from RiceDee/fix/quick-bar-centred-in-margin
fix(ui): centre the quick-tools bar in the margin beside the page (#5635)
2026-11-02 14:45:58 +01:00
FredApps ec25e7d327 M2.6: images decode outside the renderer only through an ImageBudget
`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.
2026-11-02 14:24:21 +01:00
FredApps 4ffe5908c8 Merge origin/main into feat/jbig2-run-allowance
# Conflicts:
#	vendor/README.md
2026-11-02 14:15:33 +01:00
RiceDee ecd1822525 fix(ui): the size field shows "Mixed" whole in a tooltip (#5634)
A long translation scrolls in the capped field, so the tooltip gives the full status.
2026-11-02 23:37:29 +11:00
RiceDee 89efc79ecd fix(ui): the size field stays within its room in the row in every language (#5634)
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.
2026-11-02 23:23:20 +11:00
Harsh Verma ec2f59305b Merge branch 'storytold:main' into fix/panel-resize-scrollbar-5730 2026-11-02 12:23:11 +00:00
RiceDee 808ba71287 Merge main into fix/rail-page-zoom-fields
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.
2026-11-02 23:11:59 +11:00
RiceDee bcff9a3948 Merge main into fix/quick-bar-centred-in-margin
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.
2026-11-02 22:58:09 +11:00
RiceDee f711b163c6 Merge main into fix/format-text-fields-fit-pane
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.
2026-11-02 22:43:31 +11:00
Combiler01 8baae14c34 fix(ui): prevent panel resize handle collision with scrollbar (#5730) 2026-11-02 16:36:17 +05:30
FredApps 2f8a8fb625 Preferences: reconcile current main before final validation 2026-11-02 11:23:23 +01:00
NetterSilverandClaude Haiku 5.5 d7dd1de647 Reject '+' sign in page selection numbers (#5715)
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>
2026-11-02 05:07:42 -05:00
4a0b1dd158 Repair scan: an object whose header names another generation is still found (#5580) (#5584)
* 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>
2026-11-02 05:02:12 -05:00
Combiler01 d266527b98 ui: fold dotted and dotless i in preferences text search 2026-11-02 14:41:41 +05:30
Combiler01 b5e847d3b9 ui: scroll to matches in segmented pill options, toggle pills, and theme cards 2026-11-02 14:18:21 +05:30
Combiler01 7f42c375dd ui: translate search match-count text and remove unused toggle_pill 2026-11-02 12:45:46 +05:30
Combiler01 80ffea6df4 ui: remove misleading cancel button from preferences dialog 2026-11-02 12:28:19 +05:30
Combiler01 0c822af3b1 ui: implement browser-style in-situ text search and highlight in preferences 2026-11-02 12:00:53 +05:30
Harsh Verma fa818ccc08 Merge branch 'storytold:main' into feat/preferences-cards-refresh 2026-11-02 06:00:41 +00:00
Combiler01 6aa88171d3 ui: register theme card focus upfront and assert TwoUp continuous preservation 2026-11-02 11:24:53 +05:30
Combiler01 dac706f3b2 ui: fix vertical layout containment for theme preview cards 2026-11-02 11:02:02 +05:30
Combiler01 8867d15d18 ui: align navigation rail accessibility labels with existing integration test 2026-11-02 10:46:10 +05:30
Combiler01 ec533af1bb ui: fix two-page continuous scrolling and add keyboard focus indicators 2026-11-02 10:43:27 +05:30
d3384340a6 M7: make page-mark replacement transactional (#5657) (#5704)
* 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>
2026-11-01 23:49:18 -05:00
8faebd0d96 Fix Matrix::invert rejecting valid matrices with a small scale (#5703)
* 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>
2026-11-01 23:49:14 -05:00
f035218cb4 fix(ui): don't check recent files on the UI thread at startup (#5682) (#5692)
* 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>
2026-11-01 23:49:10 -05:00
Combiler01 5f07da4419 ui: modernize preferences dialog with card-based layout
- Replace cluttered settings list with clean, modular card containers
- Integrate interactive real-time search box flush with card boundary
- Combine page display and continuous scrolling into a unified segmented pill
- Enable smooth scrolling with dedicated animation duration
- Enhance vertical scrollbar spacing and layout geometry
- Update unit and integration tests across layout, appearance, and theme
- Credit @Combiler01 in contributors/people.toml
2026-11-02 09:58:24 +05:30
FredApps f89a5917ed M2.6: streams made in memory enlarge a run's image allowance as a file's bytes do 2026-11-01 23:52:37 +01:00
FredApps 5eb82d1f68 M2.6: an image pays for the pixels its data holds, not those its size declares 2026-11-01 23:52:07 +01:00
FredApps 74d51ebf4a M2.6: a JBIG2 image stored again without its globals decodes within the run's allowance 2026-11-01 23:50:39 +01:00
FredApps bc4acf7bfc Merge origin/main into feat/jbig2-run-allowance 2026-11-01 23:50:04 +01:00
FredApps 0cba87ae6f Merge pull request #5677 from Arch-amon/arclight/freehand-hotbar
feat(ui,annot): customizable hotbar strip, freehand highlight, and moveable navigation rail
2026-11-01 23:34:38 +01:00
FredApps 0b2f93538d test(ui): target the Bookmarks submenu item explicitly (#5677) 2026-11-01 22:57:53 +01:00
FredApps 616ddca371 M8: images stored again without shared JBIG2 globals are stored as a cleared copy is
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.
2026-11-01 22:48:29 +01:00
FredApps 54d0ea7c59 Merge branch 'main' into feat/redact-bilevel-codec 2026-11-01 22:44:58 +01:00
FredApps 4d8eede62a Merge pull request #5472 from Piohrun/feat/mono-jbig2
M11: Monochrome Images reads JBIG2 scans: averaged like any, and stored again as JBIG2 (optimize.mono-compression)
2026-11-01 22:44:16 +01:00
FredApps 2850d52825 Merge pull request #5471 from Piohrun/feat/mono-indexed-polarity
M11: Monochrome Images averages indexed black-and-white images, the palette saying which sample is dark (optimize.downsample-images)
2026-11-01 22:43:06 +01:00
FredApps d35be6fdac fix(ui): use the shared rail viewport fix (#5677) 2026-11-01 22:40:04 +01:00
FredApps 99e63d4238 Merge pull request #5461 from Piohrun/feat/redact-jbig2
M8: redaction clears the covered pixels of JBIG2 scans instead of removing them (protect.redact-image-pixels)
2026-11-01 22:39:07 +01:00