* Automation: check the read and write roots on every path a tool reads or writes
* CLI: add --automation-read-root and --automation-write-root to mcp
* Docs: document --automation-read-root and --automation-write-root for mcp
* #1448: count main's doc_batch helpers as resolved by their caller
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>
Adding a word to a multi-line paragraph merged or reflowed its lines.
Two causes:
- Chrome/Skia subset fonts leave glyphs of the /DW width out of /W.
has_glyph treated those CIDs as missing, so words containing them
measured short, run calibration inflated, unchanged rows rewrapped
and new letters fell back to Helvetica. A composite subset with an
explicit /DW now counts a ToUnicode-mapped CID absent from /W as
present.
- Text drawn turned on the page (landscape pages drawn a quarter turn
round) was joined into lines and rows along the page's x axis, so the
whole paragraph became one line. Lines, rows and blocks are now found
along each text's own baseline (identical values for upright text), a
turned paragraph is rewritten in its own text space, and that rewrite
keeps explicit line breaks. Formatting part of turned text is refused
instead of being misplaced.
Claude-Session: https://claude.ai/code/session_01VFaMKaTDYNrroco63W74EQ
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Keep selected form values and page content through later edits (#1319, #1305, #1324, #1306)
An XFA radio item whose text contains a space was written back as an appearance-state name, so the next layout selected nothing. Replace Pages filled the gap between a split selection. Marking a figure decorative replaced every content stream on the page. Control clicks and drags accepted coordinates that overflow to infinity.
* Move the decorative-figure regression out of pdfcraft-edit
edit and a11y are both layer 4, so a dev-dependency from edit to a11y fails the layer check. The same regression now runs in the engine, which already depends on both.
* Drop pdfcraft-a11y from the edit crate's lockfile entry
* Run a tool after a background open of the file it asked for
A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.
* Wait for background opens in the command tests
Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.
* Compare batch copies to the PDF that was written
A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M10: Windows print jobs show per-sheet progress; printers list faster
Follow-up to #944, carrying the two parts main doesn't have yet.
- The progress card follows a job being spooled and counts its sheets
as they reach the spooler ("Sending sheet 2 of 3 to Office…"), using
craft-print's new submit_with_progress. It replaces the "Sending to…"
toast, leaves another long job's card alone, and goes when the job
ends; the spooler's answer is still a notice.
- craft-print bumped to storytold/craft-libs#27, which also lists
Windows printers through .NET's PrinterSettings (the API jobs spool
with) instead of a CIM query of Win32_Printer.
- "Sending sheet {s} of {n} to {printer}…" in all 20 catalogs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M7: share Windows local-link fixture portability fixes (#1265)
Use the identical test-only prerequisite already proposed in #1263: Unix-only mutable vectors, ordinary Windows drive paths and escaped PDF literal strings. This is shared CI support for the separate xref overflow fix; production link handling is unchanged.
Generated with Codex.
* M1: return an error for exhausted xref object numbers (#1337)
Check the next-object addition and return CosError at the xref offset rather than overflowing or wrapping to object zero. Classic-table and stream regressions reproduced the original panic; ordinary, zero-offset and repaired-range controls preserve catalog access and allocation.
Validation: 2732 passed, 0 failed, 13 ignored; strict all-target Clippy, fmt, 31 WASM crates, layering, assets, deny and parity passed. The rebuilt CLI opened and rendered a clean synthetic page.
Generated with Codex.
* M1: place overflow notes beside stable milestone entries (#1337)
Keep the tested code unchanged while avoiding progress-note conflicts with newly landed main updates. Formatting and diff checks passed.
Generated with Codex.
* Merge main: adapt exhausted-xref repair to the compact xref index
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>
Reuse the existing alignment picker for choice properties, keeping existing /Q and choice values until changed. Cover cancel, undo/redo, save/reopen and retained options/flags through the real UI.
Generated with Codex.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: add selected-text Copy to page and Edit menus (#1225)
Register selected page-text Copy with the existing document permission gate and expose it in Edit and the ordinary page context menu. Route floating Copy through the same gate while preserving focused-input and Organize clipboard ownership.
Four regressions fail behaviorally on the exact base; five candidate regressions pass. All eight cargo xtask ci stages pass: 2575 tests passed, 12 ignored, and 31 WASM crate checks. Native synthetic checks verify Unicode clipboard output, disabled no-selection/restricted actions, and permissions-password unlock using presented Xvfb/Vulkan software-rendered frames. Physical display and hardware GPU coverage are not claimed; the optional cargo-deny audit was unavailable.
Generated with Codex for FredApps.
* Tests: scope GoToR case mutation to Unix for Windows Clippy
Keep the common directory/non-PDF cases immutable. Shadow them in the Unix-only block so FIFO coverage can append its case without an unused-mut warning on Windows. No assertion or platform coverage is removed.
This repairs the exact hosted Windows Clippy failure inherited from upstream. Source review and whitespace checks passed; no local build, test or formatter was run during the pause. Automatically triggered hosted CI will validate this head separately.
Generated with Codex for FredApps.
* Tests: scope document link target mutation to Unix for Windows Clippy
Preserve directory coverage on all native platforms and device coverage on Unix. Static diff, whitespace and conflict checks passed; no local build, test or formatter was run during the pause.
Generated with Codex for FredApps.
* Tests: escape GoToR fixture file paths as PDF literal strings
Windows absolute paths were interpreted as PDF escape sequences, introducing control characters and failing the hosted local GoToR root test. Escape backslashes and parentheses in the fixture helper without changing product behavior or assertions. Static diff and whitespace checks passed; hosted CI will validate the repair. No local builds, tests or formatters ran.
Generated with Codex for FredApps.
* Tests: use ordinary absolute paths in the GoToR fixture
Build the absolute target from the original sandbox base rather than its canonicalized Windows verbatim path. Assert absolute-path form and canonical identity with the sandbox target. Preserve production UNC/device/verbatim rejection and existing containment and destination assertions. Static diff and whitespace checks passed; no local build, test or formatter ran.
Generated with Codex for FredApps.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Under a full workspace test run the background find could still be searching after 60 short
frames: 12 of 15 matches were in when the assertion ran. The PAGE re-run had the same race.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M1: keep unknown trailer entries on a full save (#1338)
A full rewrite built a new trailer from a few known keys, so accepted custom metadata disappeared without a repair note. Copy every other entry and write objects that only those entries reference.
* Run a tool after a background open of the file it asked for
A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.
* Wait for background opens in the command tests
Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.
* Compare batch copies to the PDF that was written
A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.
* Drop the unused batch fixture left by the merge
Main already keeps the written PDF in original, so the older source binding is unread and fails -D warnings.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M5: Make Properties Default in Comment Properties
Acrobat's Comment Properties dialog has a Make Properties Default box
beside Locked. Ticking it and pressing OK makes the edited colour,
opacity, line width (and, for shapes, fill) the default for new comments
of that kind, e.g. new strikethroughs take the chosen colour.
The dialog applies the edited look, so the box also works when only the
default changes. Opacity and width go through the clamping setters: the
dialog allows 0% opacity, which would make an invisible default.
The box is translated in all 21 catalogues and the comment.default-properties
parity entry names the new test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Drawing a rectangle or oval selects it and paints its eight handles, but only the Select tool
answered them: with the shape tool still on, dragging a handle drew a second shape on top of the
first. The handles were a promise the tool did not keep, and anyone marking up a drawing had to
switch tools after every shape to fix its size.
A shape tool now hands a press on the selected comment's handles to the same select_input the
Select tool uses, as the Fill & Sign tools already do for their marks. Only the handles: a drag
anywhere else, over the shape included, still draws, so drawing one shape inside another keeps
working, and a polygon being clicked out keeps its clicks. The Eraser is left out. The handle
test is shared with `grabs` rather than copied.
Lines, arrows and polygons are unchanged: they have no resize handles with the Select tool
either.
Fixesstorytold/pdfcraft#2338
Claude-Session: https://claude.ai/code/session_01DsT1y8xBvkXeBxEUQqdDtj
Co-authored-by: Claude <noreply@anthropic.com>
Adds crates/crypt/tests/proptests.rs (464 lines): eight properties over the
payload and ciphertext side of the standard security handler. Tests only - no
product code, no new dependency (proptest was already a dev-dependency here).
Upstream's src/tests.rs covers this handler well with fixed vectors (including
files qpdf wrote) and one property over malformed dictionaries. Its encrypted
round trips run three shapes - a 30-byte text, a 1000-byte ramp and a 10-byte
attachment. Not walked before: empty content, block boundaries (15/17), bytes
handed to the decoder at lengths it never encrypted, key widths only key_len
decides (5/16/16/32), /Perms verified by nothing, and Algorithm 2 step f for
unencrypted metadata on revisions below 4.
Evidence: 25 one-line breakages of crypt/src, each run against these properties
AND upstream --lib; restored and byte-compared per row; verdicts from cargo exit
codes. 16 seen here, several by nothing else in the repository (/Num fold width
both directions, RC4-40 key length, /EFF selection, Identity name guard, IV
scoping, step f, /Perms tail). Named blind spots: four hash_r6 breaks stay
green because create() and open() share that function - the companion branch
quotes qpdf's own numbers to close them.
Gates: cargo test -p pdfcraft-crypt 25 passed / 0 failed; clippy --all-targets
-D warnings clean; fmt clean. Default 256 cases, nothing lowered.
Co-authored-by: Nick Nyssen <314538108+amoisttowe1@users.noreply.github.com>
The Print dialog dimmed the colour page instead of showing the monochrome
output that CUPS or the Windows spooler will print. Preview requests now
carry a grayscale bit when the box is checked; the canvas converts the
raster to luminance on receipt. Premultiplied RGB is unmultiplied first,
so translucent content keeps its paper-relative luminance, and toggling
the box drops colour textures for the correctly tagged replacement.
bookmark_list and link_list add a destination: its type (XYZ, Fit, FitH,
FitV, FitR) and its position in the same top-left points as every rect.
A link through a named destination now gives its page (to_page, named)
instead of only the name.
Co-authored-by: Brandon Thomas <echelon@gmail.com>
accessibility_tags returned every structure tag at once. A 20-page tagged
guide printed by Chromium has 400 to 630 tags, 70,000 to 110,000
characters of JSON, more than an MCP client takes in one tool result.
It now pages like bookmark_list: offset, limit (default 200, at most
1000) and next, with count the number of tags that match. types, pages
and max_depth keep only some tags (the headings, one page, the top
levels).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M0: update rustls to 0.23.46
Co-authored-by: Codex <codex@openai.com>
* M2: requeue render requests after parse cancellation
Retire the active request key after releasing cancelled parse ownership,
allowing current demand to be queued again. Cover renewal before and after
the abort with deterministic queue-admission and completion regressions.
Co-authored-by: Codex <codex@openai.com>
---------
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
* M3: arrow keys follow the Scroll speed preference (#985)
The preference set how far a wheel notch scrolls, but the arrow keys
always moved 40 points. They now move 40 points times the preference's
factor (half at Slow, double at Fast), Page Down / Page Up still move a
screen, and the preference is labelled for both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(print): the Print dialog waits for the printer list instead of offering Save as PDF (#160)
The first time the dialog opens, the printer list is read in the
background and no printer is selected yet, so the dialog showed Save as
PDF and its button saved a PDF. It now says it is looking for printers
and the button waits until the list arrives or the user chooses Save as
PDF. An empty list says that no printers were found. The button's
Print / Save as PDF label is translated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Four ui_group harnesses waited at most one wall-clock second (200 x 5 ms)
for the page to render, and stopped as soon as nothing was pending, which
can be before the text layer is even asked for. Under load the text
layer came later and column-select, identity and update-check tests
failed. They now wait through support::wait_until for what they use (a
glyph on screen, the highlight in the document), with its 30-second
deadline. The highlighter quick-click test of #1277 itself did not fail
on this base in 256 runs.
Claude-Session: https://claude.ai/code/session_01PCdPYhYkggLJVYoME2kr5R
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- Retain tiles of on-screen pages at both eviction sites in
crates/ui-egui/src/canvas.rs, so a pan or zoom that moves the viewport no
longer discards tiles that were rendered sharp moments earlier.
- Stop dropping a zoom's tiles when zooming out far enough shows the page
whole; that made returning to a visited zoom a cold miss, which re-blurred
the page and re-interpreted it once per tile.
- Bound older-zoom retention by bytes (TILE_BYTES) as well as by count
(OLD_TILES): a tile of a large scan can be 4096 squared, about 67 MiB, so a
count alone ran past the tile allowance.
- Add returning_to_a_visited_zoom_reuses_its_cached_tiles, which fails on the
reverted fix, and cite both view tests in view.deep-zoom-tiles; the entry
stays partial because the 8192 px-per-side cap is untouched.
* M14: Italian catalog: complete the missing entries
it.tsv lacked 265 keys that the other catalogs carry. The 201 the code
looks up are added in one dated section at the end of the catalog: the
sentences the accessibility checker, preflight, signature validation and
page-range parser word (their WORDINGS), the certificate rows and calendar
weekday and month labels (their tl_ctx contexts), digital-ID, docking and
print messages, and five plurals (one|other). Italian now has 2,528
entries.
The new rows follow the wording the catalog already uses: history labels
say "Modifica ..." like "Change {key}", and the Creator and Producer keys
that label fills in say "Applicazione" and "Produttore PDF", as the
Document Properties dialog names those fields. The Optimize PDF summary
says "eliminato" like its Discard options, the checker and preflight
sentences say "ha rilevato" and "Viene usato", the trust messages say
"nel tuo elenco di certificati attendibili", the password and certificate
rows say "non corrispondono" and "rilasciato" like the existing "The
passwords don't match." and "Issued by" rows, and the print hint quotes
the "Disattivato" option as the dialog shows it.
The other 64 are left out because no non-test code looks them up:
- 10 tooltip texts with a fixed macOS shortcut ("Zoom in (⌘+)"); the code
fills the shortcut in through "{key}" rows Italian already has;
- 9 older wordings the code no longer uses: the five Combine folder
messages, "Save (M4)", " on page {label}", the owner-only security
message and the old "Text recognition isn't installed" message (the
code now says "its model files are missing");
- 45 generic labels and plurals only the Lithuanian catalog has. The code
says "{n} file|{n} files" as two plain strings, "1 file" and
"{n} files", which Italian has; "{n} comment|..." and "{n} result|..."
are used nowhere.
The entries are AI-assisted translations written from the meaning of the
English (clean-room; no proprietary localisation consulted), and keep
their placeholders and trailing ellipsis. The catalog's SHA-256 in
ATTRIBUTION.toml is updated by hand, as the other catalog changes do.
localization-parity.md, the ROADMAP progress log, the M14 rows of
docs/roadmap.md and docs/localization.md record the new count, and
parity/acrobat-features.toml now says Italian translates the engine's
sentences too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: kcobelli-pixel <257471167+kcobelli-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
`page_render` only returns PNG. A host that paints the page straight onto a canvas (a browser, a
GPU texture) encodes a PNG only to decode it again, on every scroll. `render_pixels(doc, page,
scale)` uses the same renderer and the same per-document cache as `page_render` and returns the
width, the height and the RGBA pixels; `scale` is pixels per point (1.0 = 72 dpi). `Pixels` is
re-exported so the caller can name the type.
It validates like the tool does: the scale must be finite and within the tool's dpi limit, the page
must exist (1-based), and the document must be open. The test checks the size agrees with
`page_render` at 72 dpi, the buffer is RGBA, the scale doubles the width, and each refusal.
This is an embedder API, not a user-facing feature, so it has no tool and no parity entry.
Co-authored-by: Paulo Junior <195025778+bonfim6P@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
On wasm32-unknown-unknown the engine has neither a clock (`now()` is empty) nor a time zone
(`local_utc_offset()` is 0), so in a browser Fill & Sign stamps 1 Jan 1970 and `/ModDate` is never
written. The one way to inject a clock, `with_clock`, is a test shortcut: it takes the injected
instant AS local time and zeroes the zone, so a host that injects only that gets either the local
dates or the UTC ones wrong by its offset (21:30 in São Paulo, UTC-3, is already the next day
in UTC).
`Session::with_wall_clock(clock, utc_offset)` keeps the two separate: `clock` is the UTC instant
(what the file records) and `utc_offset` is asked on every use, because the zone changes with
daylight saving. `Automation::with_wall_clock` forwards it. Nothing changes for a native build, or
for `with_clock`.
The test pins the contrast: at 21:30 in São Paulo the stamped day is the 14th, `signing_date`
carries `-03'00'`, the file's instant stays 00:30Z on the 15th, and the plain test clock still
answers the 15th.
Co-authored-by: Paulo Junior <195025778+bonfim6P@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A page that draws a discretionary hyphen through WinAnsiEncoding's 0xAD extracts, in our map, as
U+00AD SOFT HYPHEN. That character is invisible, so text the reader can plainly see does not match
a search for it, and `redact`'s pattern matching -- which runs on exactly this map -- does not
match it either. The same silent-failure shape as reading Symbol as Latin: the text is there, the
tool cannot see it.
Annex D.2 gives codes 0x2D and 0xAD the same glyph name, so the byte a Windows codepage calls a
soft hyphen draws, and should extract as, an ordinary hyphen. Changed in the reading table only:
writing is unaffected, because win_ansi_byte maps 0xA0..0xFF by identity, so '-' still takes the
canonical 0x2D and a caller holding a literal U+00AD still writes 0xAD.
The round-trip test could no longer hold for that code -- two codes now mean one character -- so
it states the aliasing instead of asserting a bijection, and a new test pins the behaviour and the
reason.
**This needs the owner's reading of the spec before merging.** The evidence is not unanimous: a
PDF library's documentation cites Annex D.2 for the glyph name `hyphen` at 0xAD, and Poppler
extracts U+002D, but the vendored hayro maps 0xAD to the glyph name `sfthyphen` and reports
U+00AD, normalising to `hyphen` only when looking the glyph up in the font program. Two
implementations disagree and I cannot read the normative table from here.
Note this does not change `pdfcraft-cli text`, which maps through hayro, not this table; the
user-visible extraction path carries the same question separately.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Clicking straight into another text box while one was open for typing replaced the editor and
dropped the typed text: only a click on empty canvas (the editor losing focus) or Cmd+Enter
applied it (#2022). The click now queues the open paragraph's edit and asks for the clicked box
to open on the next frame, from the document that edit has been applied to, so the box's index
and geometry are the new inventory's, not the one the click came with. A click on the open box's
own frame keeps the editor and its draft instead of resetting it.
The regression test types into the top of two boxes, clicks straight into the bottom one, and
requires the first box's text applied ("Top edited"), the edit on the undo stack, and the second
box open. On the old code it failed with ["Top", "Bottom"] — the draft had been dropped.
every_registered_command_is_implemented skips a hand-kept list of commands
that open file pickers. file.pin_folder and create.multiple_separate are not
on it, so every run of the ui-egui tests showed two real file dialogs on the
developer's desktop (seen through the xdg portal on Linux). The test now
answers any picker as a cancel through pick_override.
The line is drawn half a gap left of the first tab, which is outside the
scroll area's clip rect, so it was clipped away. Clamp it into the clip rect.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
uses_device_colour found content streams by their dictionaries and split
their bytes on whitespace. It skipped form XObjects with /Type /XObject and
tiling patterns, missed `cs`/`CS` with a device space, inline images and
operators before a delimiter, and took a `k` in a string, a comment, inline
image data, a font program or an ICC profile for DeviceCMYK. A CMYK logo in a
form therefore got an sRGB output intent and passed pdfa_verify.
Content streams are now found by role (page /Contents, Type 3 glyphs, forms,
appearance streams, tiling patterns) and read with pdfcraft_content::parse.
Reading stops once all three device spaces are seen.
Found while testing crates/edit/src/lib.rs:445
Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Holding Shift while dragging a corner handle now preserves the annotation's
current proportions, unless the annotation has its own fixed aspect ratio
(image signatures). This matches Acrobat behaviour for comments, stamps,
and form fields.
- Added helper to compute the effective ratio from fixed
aspect, current proportions with Shift held, or none.
- Updated in comments.rs and in prepare.rs to
pass the Shift modifier.
- Updated signature_drag.rs to read and pass Shift.
- Added tests for the new Shift behaviour in comments.rs and prepare.rs.
- Updated docs/ui-parity.md and parity/acrobat-features.toml.
Fixes#2199
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M4: lay out Organize pages as Acrobat's panel
Organize pages gets Acrobat's panel: Pages selected (typed, or by kind
from its menu: all, even, odd, landscape, portrait); page options as a
row of icon buttons (rotate both ways, delete, extract) with Insert (a
blank page, pages from a file, duplicates), Replace, Split and Bates
numbering; page setup (boxes, crop, transitions, templates, printing);
and page labels and properties. Items PdfCraft doesn't have yet are
listed muted and say they are planned.
The panel shows whenever the grid does, however Organize was opened (the
tool, the command, --organize), and its Close ends Organize. The grid's
own toolbar keeps the selection count, moving pages, their size, Save
pages and Close; the page actions moved into the panel with the same
labels, and queue on the view as before. Two new strings are translated
in every catalogue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: Pages selected shows the current page when nothing is selected
The empty field showed the example "1, 12, 16-18" in grey, which read as a
selection. It now shows the page the actions apply to, as Acrobat does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(i18n): refresh the catalogue hashes in ATTRIBUTION.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: leaving Organize goes back to the workspace's panel
Opening a page from the grid (or the grid's Close, or the panel's Close)
always showed All tools, so Organize opened from the Edit panel dropped
the user out of Edit. It now goes back to the workspace's own panel: Edit
under the Edit tab, Convert's and E-Sign's under theirs, All tools
otherwise. Back still goes to All tools.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: box the Organize panel's page edit (clippy large_enum_variant)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M4: parity notes for the Organize pages panel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Refresh catalog hashes after merging main
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
A page turned a quarter (or any landscape page) was fitted with the
portrait margins and pinned to the top of its cell, so it showed as a
small strip with empty space below. The page picture is now as large as
the cell's picture area allows and centred in it, as Acrobat draws it;
the sides narrow only when a picture that wide would reach the selection
checkbox. Damaged page sizes (zero, NaN, infinite) still give a finite
rectangle inside the cell.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The boxes round paragraphs and images were drawn by the same call that
takes the page's pointer input, and that call was skipped whenever
something else owned the input: selected objects, added content, or page
input blocked because the window lost focus or a dialog or the palette
was open. Switching to another window made every box vanish until the
pointer came back. The boxes are now drawn whenever the tool is on; only
the input waits.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M7: the Edit tab turns on Edit text & images, as in Acrobat
Entering Edit boxes every paragraph and image on the page at once, so text
is editable with one click instead of first finding Edit text & images in
the tool list. The floating toolbar gets an Edit text & images button while
Edit is open or the tool is in use. Leaving Edit (or turning the tool off
there) applies a paragraph still open in the editor, as clicking away from
it does; left open, its page kept showing the paragraph's text removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: parity notes for the Edit tab turning on Edit text & images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M7: the panel-reveal test looks for the Edit panel's own row
With Edit text & images on, its name is on the floating toolbar as well as in the panel, so
the label alone matched twice and no longer showed that the Tools tab was revealed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
unsaved_changes_are_kept_until_reload_is_chosen looked for the English notice while the app
followed the developer's system language (Auto), so it failed on a non-English Windows.
Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(redact): size image clearing with checked arithmetic (#2177)
An ICCBased image's /N comes from the file and was cast with `as usize`, then multiplied
unchecked into the row size. /N 2^60 overflowed it (debug: overflow panic; release: a zero row
emptied the buffer and the write loop indexed out of bounds), and a negative /N wrapped.
/N is now limited to 1, 3 or 4 (ISO 32000-2 §8.6.5.5), every size product is checked, and the
pixel writes use `get_mut`. An image that fails any of these is refused, and the caller already
removes it whole (fail-closed). Regression test covers /N 2^60, -1 and 2.
* redact tests: pass the pixel array by value (clippy needless_borrows_for_generic_args)
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>
* M3: Save asks before writing over a changed file, one tab per file, locked autosaves (#2166)
Save compares the file on disk with the version the document came from
(its open, reload or last save) and asks before writing over another
one, with Save as... to keep it; the changed-on-disk notice says what
Reload and Save each do. A file that is open already comes to the front
instead of opening twice. A running instance holds an exclusive lock
file in the recovery folder and names it in its snapshots, so a second
instance neither offers nor discards what the first is autosaving.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* M3: the pending save exists only where files do (#2166)
The web build has no file to compare against, so the struct and field
are desktop-only; the wasm check gained no warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
* Refresh catalog hashes after merging main
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M9: harden .p12 digital IDs (#1847)
New digital IDs are written with 600,000 PBKDF2-HMAC-SHA-256 rounds (OWASP)
for both encrypted parts and the MAC, instead of 2048. The MAC gets the same
count because checking it alone tells whether a guessed password is right.
Opening a .p12 now spends at most 10 million rounds of key derivation across
the whole file, charged before any hashing. The file chose its own iteration
counts unchecked (`u64 as u32`), so a crafted file could hang the password
check for about an hour, or wrap 2^32+1 to one round.
Creating an ID needs at least 8 characters (NIST SP 800-63B), from one
constant, pkcs12::MIN_PASSWORD_CHARS, shared by the app, sign_id_create and
its MCP schema. Opening an existing ID takes any password.
* Refresh catalog hashes after merging main
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
New files from Combine, Extract, Split and Create, signed, timestamped and
LTV-embedded files and the OCR batch output reopen with their pages before
anyone writes them, as saves already did. The desktop app, the automation
tools (with or without --root) and the CLI's edit, combine, extract and
split write through pdfcraft_platform::staging::write_atomically, so a
failed write or --out naming the input never leaves a half-written file.
Special files such as /dev/stdout are still written as given.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6: sort multi-select choice indices
Write ascending /I option indices without reordering /V selections.
Add forms and engine regressions for reverse selection and save/reopen.
Co-authored-by: Codex <codex@openai.com>
* M0: update rustls to 0.23.46
Update only the resolved rustls version and registry checksum.
Keep the existing choice-index feature changes and advisory policy.
Co-authored-by: Codex <codex@openai.com>
---------
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>