ci.yml had no permissions block, so its token got the repository
default. Its jobs only check out code, restore caches and upload
artifacts, so contents: read is enough.
The permissions half of item 6 in #1114.
Reduce File Size removes named destinations that nothing refers to. It
collected the referenced names by reading /Dest and a GoTo's /D
directly, so when a link's /Dest was an indirect reference to the name
string (valid PDF) rather than the string itself, the name was not
counted. The destination it used was then treated as unreferenced and
deleted, silently breaking the link.
referenced_names now resolves /Dest and /D before reading the name, so
an indirectly referenced destination is kept.
Test: a link whose /Dest is a reference to the name string keeps its
destination through optimize(); an unused destination is still removed.
Reported-by: BS.ehlas.cz
* #2028: test that Edit text & images is the first Modify page item
Edit text & images was listed under Add content, though it changes what is already on the page. The test checks that the Edit a PDF catalogue lists it first under Modify page and not under Add content. With the item back under Add content it fails on the section assertion (left: Add content, right: Modify page).
* #246: test that start-up and the panic hook never use eprintln
eprintln! panics when standard error is a broken pipe, and a panic inside the panic hook aborts the process. The test scans the product code of the desktop main.rs (everything before the test module) and fails on any eprintln! outside a comment. With the old eprintln! calls restored in the panic hook and the log-file warning, it fails listing main.rs:188 and main.rs:227.
* #843: test that text typed into an open paragraph counts as unsaved work
An open paragraph with a draft that was not applied yet was not counted as unsaved work, so closing the tab or quitting skipped the save prompt. The test opens a paragraph, types into it, and checks that first_dirty() reports the tab (and not before typing, nor after the original text is typed back). Without the line_editor check in has_unsaved_work, first_dirty() stays None and the assertion fails (left: None, right: Some(0)).
* test(sign): serialize the Keychain search-list tests
Both ignored Keychain tests save the user's keychain search list, change it
and restore what they saved. Run together (`--ignored` runs them in
parallel), one could restore its list while the other needed its temporary
keychain, or put back a keychain the other had already deleted. A shared
lock now runs them one at a time.
With the lock, the issuer test still failed about one run in ten when it
came second: the Security framework caches the search list in-process and
learns of `security list-keychains` asynchronously, so `keychain::find` did
not yet look in the new keychain. The test now waits up to five seconds for
the identity to appear.
Follows up a CodeRabbit review comment on #3716.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(sign): restore the keychain search list before deleting the keychain
If deleting the temporary keychain panicked, the test left it on the user's
search list, and the next Keychain test (which accepts the poisoned lock)
saved that list and later restored it. Restore the list first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(sign): fail when the keychain search list isn't restored
Both Keychain tests ignored the result of the `security list-keychains` that
restores the user's search list, so a failed restore went unnoticed and left
the deleted temporary keychain on the list. They now check it after deleting
the keychain.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(sign): keep the issuer test's keychain at one path
trustd remembers which keychain file it found an issuer in. Each run of
`keychain_identities_carry_their_issuers` created its keychain at a path with
the process id and deleted it afterwards, so the next run's trust evaluation
kept looking in the deleted file and built an empty chain, until trustd
forgot it. The test failed every run after the first for a while.
The keychain now lives at the same path every run, and a copy left by a
killed run is deleted first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: ffishman <20211677+ffishman@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2526-2532 (UnitValue per_in table; UnitType at :2510-2516)
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Redact: refuse Redact marks written in place; remove replies and pop-ups listed on
other pages; delete form fields by object, not name; resolve Do by its last operand
like the renderer; sanitize drops PCAdded and flattens widgets before hidden-content
cleanup. Sign: freed signed objects and edits to an already-signed widget are
modifications. Cos: only reading trusts /Type /XRef to skip encryption.
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 1f8811761a)
A frame count is not a time: on a busy runner the worker had not finished
the open, or the page text had not arrived, within 30 frames or 50 hover
rounds. support::step_until (#[must_use]) steps frames until a condition
holds or 30 s pass, sleeping briefly between frames, and says whether it
held; each test asserts on that with its own message.
- control_open_replies_pending_while_the_worker_opens_and_focuses_once_it_has
and mode_tabs_keep_the_hand_over_page_text_scrolled_under_them wait with it,
and so does control.rs call() for its reply.
- support::settle waits for every pending render and fails if the pages are
not rendered within 30 s. It replaces all the render-pending settle loops
(40 to 3000 frames) and the local settle/wait_for_pages wrappers, except
text_select's double-click wait, which is held to 8 frames on purpose to
stay inside egui's multi-click window, and view.rs's loops that already
wait by a deadline.
- wait_until is built on step_until, keeping its diagnostic message.
- tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value already
waited 30 s for its file. It failed because its "same-frame" case shared
its scratch directory with typing_and_saving_in_the_same_frame_saves_the_last_keystrokes,
which runs in the same test process (since ee1ee704, #1045) and deleted
the directory mid-save. forms' and pickers' scratch() now give every call
a directory of its own.
Fixes#4186
Review follow-up on #2957:
- free_name picked a default name (Text1) that a parent field left empty
by a move still held at the top of /Fields, and attach then refused it
for every new field of that kind. The own names (/T) of the top-level
/Fields nodes are now skipped too.
- the editor sends nested fields' full names, so renaming address.city to
address.town took the move path: it copied the inherited keys onto the
field and moved it to the end of the parent's /Kids. A full name whose
parents are the field's current parents now renames only its own /T.
Field tree per ISO 32000-2 12.7.4 (Arlington Field*.tsv: /Parent, /Kids,
/T; FT, Ff, V, DV, DA, MaxLen inheritable).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
The dependency-advisory gate rejects ctutils 0.4.2 (non-constant-time
mask generation and u128 comparisons, fixed in 0.4.4). It comes in
through crypto-bigint for the signing crates. Only its version and
checksum change in Cargo.lock.
(cherry picked from commit 8a687ee2c6012e3d18399633d2fb9fb3742f02ae)
Review follow-ups on filling a field converting inline /DR fonts:
- A signature field whose /V sits on its parent counted as unsigned, so
filling another field rewrote /DR in a signed form. The check now
follows the /Parent chain (bounded at 64, like the field walk).
- A catalog holding /AcroForm as a direct dictionary was skipped; its
inline fonts are now converted and written back into the catalog.
Both tests failed before the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The panel now has an "Attach a file…" button and a "Delete attachment" button; the side-panel
snapshot test (every_panel_matches_its_snapshot) still expected the read-only panel.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Crop is only listed in the Organize pages panel, which is shown together with the thumbnail grid; activating it there must bring the document view back, otherwise the tool is selected but no page is on screen to drag a crop rectangle on.
In the "page.crop" command arm, close the organize grid of the active view before arming the Crop tool, the way opening a page from the grid does. The left panel then falls back to the workspace panel on the next frame, as it already does when the grid closes by other means.
Test: the_crop_tool_leaves_the_page_grid_so_a_page_can_be_cropped failed before the change and passes after.
A multi-line or multi-style draft is laid out by rewrite_styled_block, which broke rows only at \n: a CRLF left a stray CR before the break and a lone CR broke nothing. The CR of a CRLF is now skipped and a lone CR breaks the row, keeping the draft's character positions, and a draft whose only break is a lone CR takes the same path as one with \n.
Review follow-up for #3551: two ExtGStates at the same opacity are the same
Paint, so a dashed outline after a solid one joined its line and was
rewritten with the first run's selections. Ts tracks the effective dash
pattern and line cap, from d and J and from an ExtGState's /D and /LC, and
same_paint compares them as values for stroked text, so states with the
same settings under other names still continue a line. A dash array longer
than 32 entries, or with an operand that isn't a finite number, and a cap
outside 0-2 are ignored.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Review follow-up for #3551, rebased on the gs grouping of #3310:
- The styled rewrite draws each run in its own q/Q from the state where
the rewrite is drawn: a gs has no inverse, so one run's ExtGState stayed
in force for the runs after it. Two ExtGStates at the same opacity share
a paragraph (Paint), but either may set what Paint doesn't compare.
- The gs arm applies an ExtGState's /LW to the line width; paint_ops
restates the effective width after any replayed gs.
- A run whose visible Tr (or, for stroked text, stroke colour and width)
differs no longer joins the line before it, so a line is not rewritten
in its first run's state. Clipping and invisible modes still join, as
the #3447 refusal reads the whole line; gs changes are already kept
apart by Paint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A rewritten paragraph is drawn before its text object and restated only the
font, spacing and fill. Anything the text object set before its first show was
lost: `1 Tr 1 0 0 RG 2 w` inside BT turned red outline text into filled black
text, and `/GS1 gs` inside BT made semi-transparent text opaque, even when the
text was unchanged.
The interpreter now tracks stroke colour, line width and the ExtGStates
selected (bounded to 32 distinct names), and the rewrite restates whatever
differs from the state in force where it is drawn. Styled runs restate it per
run, too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewriting a paragraph in one style split its text at white space and
joined the words with one space, so a run of spaces, an indent or a
trailing space typed in the editor was saved as one space or none. The
text is now kept as typed (other white space becomes a space, and CR or
CRLF become line breaks), and the wrapper keeps each run of spaces,
dropping only the spaces where a row wraps.
A justified (stretched) row still drops its indent and the spaces after
its last word, as before, so word spacing reaches only the spaces
between its words; keeping an indent on a justified row is a possible
follow-up. Centred and right-aligned rows, and the editor's draft box,
are measured to the last word, and a paragraph at 0 Tz is not justified.
Fixes#4199Fixes#3797
Agents read the tool description to plan a call. The description of
ocr_recognize_files now says that two inputs which would write the
same output file are refused before anything is read or written, so
an agent does not expect a partial batch.
Checked: cargo test -p pdfcraft-automation --test automation -- tools
(86 passed, 0 failed). The text is the only change in this commit.
The ocr_recognize_files tool wrote every input's searchable copy to
the same name in the output folder. Two inputs with one file name
(a/scan.pdf and b/scan.pdf) therefore wrote the same output: the
second replaced the first while both results reported success.
Reproduced on the unchanged tree with the OCR models installed: both
results pointed at out/scan.pdf, and the folder held one file.
The whole batch is now refused before anything happens. The output
folder is not created, nothing is read, and the OCR engines are not
loaded. The error names the colliding file names and says what to do.
Names are compared the way the default filesystem compares them:
without regard to case on Windows and macOS, exactly elsewhere, and on
Windows a trailing dot or space is not part of the name. The compared
name is the one the output is written under, after the separator
replacement the writer already applies. A request without collisions
runs as before.
Not changed here: the batch dialog in crates/ui-egui (ocr_ui.rs) writes
its outputs the same way and needs its own change.
Checked: the three ocr_recognize_files tests in
crates/automation/tests/automation.rs and the unit test
shared_output_names_follow_the_default_filesystem, run with the models
installed and without them. Each pinned line was reverted in turn and
the test that pins it failed; the file was restored after each step.
Catalan, Finnish, Hindi, Korean, Indonesian, Romanian and Albanian get the strings this PR adds
(AI-assisted translations from English, not separately native-reviewed), so the catalogue coverage
tests pass. ATTRIBUTION.toml's sha256 for each file is refreshed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Optimize reads a JPEG's header again for every group of fragments it tries,
and one image can be drawn many times. The scan skipped stray bytes between
segments one at a time, so a JPEG with megabytes of them after SOI cost a
full walk on every call: 64 references to one such image delayed the first
progress call by seconds.
The scan now treats a stray byte as an unreadable header, which leaves the
image as it is, and stops after 4096 markers and fill bytes. Each step jumps
over a whole segment, so the work no longer grows with the stream's size.
The covered text walks every run of a covered line, and a run can draw
no words (`( ) Tj`), so a line now costs its word boxes or its runs,
whichever are more. A word followed by many empty runs can no longer
be walked once per cover outside the 50-million-comparison bound.
Review on #4198 (CodeRabbit).