chore: sync upstream (554 Commits) #9

Merged
alf merged 554 commits from chore/sync-upstream-20261026 into main 2026-10-26 08:47:42 +00:00
Owner

Upstream-Sync von upstream/main (554 Commits). Fast-Forward.

  • 64e0868b feat(ui): menus from craft-menu, native on macOS (#4752)
  • 01b696df test(ui): CJK interface tests skip without craft-fonts (#4706)
  • 13bfbd07 Merge pull request #4737 from storytold/chore/deps-safe-refresh
  • fc578a0b chore(deps): bump 11 safe dependency updates
  • 83ba2868 build: pin craft-update to craft-libs main (495e597) (#4728)
  • cda2d019 build: pin craft-update to craft-libs main (495e597) (#4721)
  • 833cd63c proposal(redact): prevent retention of confidential vector paths behind clipping (F-008) (#4577)
  • 86a4120a feat: startup update prompt modal and direct self-updating (#4654)
  • e90f7882 Migrate PdfCraft to egui 0.37 (#4591)
  • d764a8eb refactor(ui): side panels on craft-ui task panes and the shared rail (#4606)
  • 71b47b51 M3: a Page tools menu in the Pages panel (#4415) (#4583)
  • 8708f35b M6: with NeedAppearances, check boxes and radio buttons draw their /MK caption (ISO 32000-2 Table 224) (#4454)
  • 58d368f1 M10: Word export keeps an italic run inside its sentence's paragraph (#4158) (#4411)
  • 0c124edc M3: an attachment's row opens on a double-click and has a right-click menu (#3466) (#4513)
  • 7dd44301 M2: a luminosity soft mask in a DeviceCMYK group converts as ISO 32000-2 10.4.2 says, so 100% K masks fully (#4423)
Upstream-Sync von `upstream/main` (554 Commits). Fast-Forward. - 64e0868b feat(ui): menus from craft-menu, native on macOS (#4752) - 01b696df test(ui): CJK interface tests skip without craft-fonts (#4706) - 13bfbd07 Merge pull request #4737 from storytold/chore/deps-safe-refresh - fc578a0b chore(deps): bump 11 safe dependency updates - 83ba2868 build: pin craft-update to craft-libs main (495e597) (#4728) - cda2d019 build: pin craft-update to craft-libs main (495e597) (#4721) - 833cd63c proposal(redact): prevent retention of confidential vector paths behind clipping (F-008) (#4577) - 86a4120a feat: startup update prompt modal and direct self-updating (#4654) - e90f7882 Migrate PdfCraft to egui 0.37 (#4591) - d764a8eb refactor(ui): side panels on craft-ui task panes and the shared rail (#4606) - 71b47b51 M3: a Page tools menu in the Pages panel (#4415) (#4583) - 8708f35b M6: with NeedAppearances, check boxes and radio buttons draw their /MK caption (ISO 32000-2 Table 224) (#4454) - 58d368f1 M10: Word export keeps an italic run inside its sentence's paragraph (#4158) (#4411) - 0c124edc M3: an attachment's row opens on a double-click and has a right-click menu (#3466) (#4513) - 7dd44301 M2: a luminosity soft mask in a DeviceCMYK group converts as ISO 32000-2 10.4.2 says, so 100% K masks fully (#4423)
alf added 554 commits 2026-10-26 08:47:40 +00:00
Acrobat's Edit on a scanned page first converts it to editable text and images.
In PdfCraft the words had to be recognized through Scan & OCR before Edit text &
images could open them. Turning Edit text & images on for a page that is a
picture (an image covering at least half of a page with no text) now runs the
recognition for that page first: once per page per window, one undoable
"Recognize text" step, with the language of the Recognize Text dialog. Pages
with text, read-only files and other tools are left alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Edit tab opens the Edit panel without picking Edit text & images, so the
recognition added in the previous commit did not run for people who just
clicked Edit. It now runs whenever Edit is on by the page view's own test: the
Edit panel is open, or Edit text & images or Add text is the tool.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pages with a comment or text box over a scanned picture count as having text,
so the recognition run skipped them ("the page already contains text") and
Edit did nothing for them. The run started by Edit now also reads the picture
of such a page, as Acrobat does, and the words the page's text already has are
not added twice.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
Review of #1609: the Edit-triggered recognition also read pages that have real
text (a flyer or slide export with a full-page background picture), adding an
undo step and marking the file modified just because Edit was turned on.

It now runs with skip_text_pages on, so such a page is left alone. A new
OcrSettings::native_text_only decides what counts as the page's text: its own
content and form fields, not comments and text boxes (which are also not drawn
into the picture that is read), so a scan with a few comments on it is still a
scan, as in Acrobat. The doc comment now says what the code does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pdfcraft_organize::add_attachment puts a file in /Names /EmbeddedFiles as a
Filespec with a Flate-compressed EmbeddedFile stream (/Subtype, /Size,
/ModDate), the name made unique; delete_attachment removes one, the last taking
the tree with it. Edit::AddAttachment / Edit::DeleteAttachment (undoable),
Edit ▸ Attach a file… with a button at the top of the Attachments panel and a
trash button on each document-level row, tools attachment_add and
attachment_delete, rows in every language, parity attachments-add and
attachments-delete shipped.

Fixes set_attachments (Combine): a duplicate name was numbered by appending
ASCII bytes, corrupting UTF-16 keys; names are now numbered as text. An
indirect /Names is updated in place instead of replaced, so its /Dests and
/JavaScript stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cargo xtask assets checks each catalog's sha256; the rows this branch adds
changed the files without updating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ATTRIBUTION.toml: the 21 language files' sha256 recomputed from the
merged files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The note was cut at an escaped quote when it was ported, which left
parity/acrobat-features.toml unparseable (cargo xtask parity failed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: the language catalogs (both sides' rows kept), ATTRIBUTION.toml's checksums
(recomputed from the merged files), and tests both sides appended at the same place.
Upstream's new Vietnamese catalog gets this change's labels.
An expand button at the top-left of the All tools sidebar opens a view that
fills the workspace, like Acrobat's tools page: every tool as a tile, filed
under five categories (Create & Edit, Review & Compare, Forms & Signatures,
Protect & Standardize, Customize & Automate), with a search field that matches
tool names and the items inside each tool, in English or the UI language.
Choosing a tile opens that tool's panel; Collapse, Esc or another workspace
puts the view away. `--tools full` opens it from the command line and the
control channel.

The categories live in the engine catalogue (TOOL_CATEGORIES), with a test
that every tool group is in exactly one. Six new strings are translated in
all 22 catalogues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/i18n/lt.tsv
#	crates/ui-egui/src/i18n/vi.tsv
The sidebar's expand button was labelled "Expand", the same as the Combine
grid's per-file Expand on its hover bar. With both on screen, the Combine
grid tests' lookup by that label found two buttons and 13 of them failed.
The buttons are now "Expand all tools" and "Collapse all tools", translated
in all 22 catalogues, and the labels are tl! literals the coverage scan sees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Description tab's plain values (File, Application, PDF producer) now
start where the text inside the Title/Author fields does, the fields use
the dialog's full width instead of a fixed 420 points, and Subject and
Keywords wrap onto more lines instead of clipping mid-word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Properties ▸ Space Audit shows the same bytes-by-kind table as the PDF
Optimizer's Audit Space Usage, which now shares its table code. The
audit walks the whole file, so it runs when the tab opens and again only
after an edit (keyed on the document and its edit generation). The tab
is also reachable as `--dialog space-audit`.

The audit table no longer overflows its dialog: egui's default minimum
column width stretched the narrow Bytes column past the computed widths,
widening the card by about 5 points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shared dialog footer laid its buttons out right-to-left in all the
height left, centred. After a taller Document Properties tab the modal
kept that tab's height, so Close floated far below a shorter tab's last
row. The footer is now one row high.

Page hovers (form fields, comments, the link tool, image editing) set the
cursor from the raw pointer position, so a hand or text cursor showed
over fields under a dialog's backdrop, where nothing takes clicks. While
a modal is open the cursor starts from the arrow each frame and only the
modal's own widgets change it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/i18n/es.tsv
Conflicts: ATTRIBUTION.toml, crates/ui-egui/src/i18n/es.tsv, crates/ui-egui/src/i18n/vi.tsv. Catalogs keep upstream's rows, then this branch's rows upstream lacks; ATTRIBUTION.toml is upstream's with every sha256 recomputed; progress lines keep both sides, upstream's first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Header and Footer, Watermark and Background dialogs drew their
preview on a blank sheet, so you could not see where the marks would
land on the page's text and pictures. The preview now paints the page's
own thumbnail, rendered off the UI thread like the pages panel's, with a
blank sheet until it arrives. It shows the current page when the page
range selects it, otherwise the first page the range selects, at its
displayed size (after /Rotate), which is the frame the engine lays the
marks out in. A background colour tints the page picture instead of
covering it, as a background behind the content looks.

Fixes #2837.
Optimize decoded DCT images with the generic JPEG decoder, which converts
three components from YCbCr to RGB whenever the JPEG itself doesn't say
otherwise, and then dropped /DecodeParms. An RGB JPEG stored untransformed
(/ColorTransform 0 and no Adobe marker) therefore came out in other colours
when recompressed, resampled or merged from strips.

The samples are now read with the colour transform ISO 32000-2 §7.4.8
gives them: an Adobe APP14 marker's flag, else /ColorTransform (default 1).
Untransformed samples are taken as stored, and the result is written as a
plain YCbCr JPEG (or Flate), which needs no /DecodeParms. When the marker
and /ColorTransform disagree, a value is out of range, the parameters are
not paired with the filter as §7.4 pairs them, or the decoder reads the
components otherwise (named R, G and B), the image is left as it is.

The tests draw each image with PdfCraft's renderer before and after
Optimize: RGB JPEGs with and without an Adobe marker under each
/ColorTransform, CMYK JPEGs (still left alone) and JPEG strips merged into
one picture.

Part of #1201.
Notes and file attachments are saved with the NoRotate flag (/F bit 5),
but nothing honoured it: on a page with /Rotate the icon was fitted to
/Rect and turned with the page, and sat away from where it was placed.

The vendored renderer now honours NoRotate (hayro-interpret patch 37):
the appearance fitted to /Rect is turned against the page's rotation
about the rectangle's upper-left corner, which stays put (ISO 32000-2
12.5.3). Form fields and links stay in /Rect, where their click targets
come from. A counterrotated appearance, as stamps use, would be turned
twice by viewers that honour the flag.

Annotation::shown_rect gives the box the note is drawn in. The UI uses
it for hit-testing, outlines, moving, the hover pop-up, the Comments
panel flash and the edit composer; a resize maps it back to /Rect.
comment_add anchors an icon's displayed top-left at `at`, and
comment_list, doc_info and comment_edit use the drawn box.

This also covers the sticky-note part of #1869.
The tool panel's back arrow (Back to all tools, in the Edit panel among
others) and Organize pages' back arrow set the workspace to All tools
directly. Pressed from the Edit tab, they left Edit text & images on
under All tools, every paragraph still boxed, and a paragraph open in
the editor still open over the page with its edit not applied.

Both arrows now go through enter_mode, as the mode bar and the Read mode
command do (#3014). Organize calls it once the panel's action has been
handed to the view, since the view borrows the app until then.

Test: editing.rs::the_edit_panel_back_arrow_applies_an_open_paragraph_and_puts_edit_text_down
Test: editing.rs::the_organize_back_arrow_applies_an_open_paragraph_and_puts_edit_text_down
# Conflicts:
#	ATTRIBUTION.toml
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/comments_panel.rs
#	vendor/README.md
# Conflicts:
#	ATTRIBUTION.toml
Review follow-ups for the expanded All tools view:

- Escape closed the view even when a dialog or prompt over it owned the key.
  It now checks modal_open() first.
- set_option("mode", …) changed workspace without clearing the view, so
  leaving and coming back to All tools reopened it. Another workspace now puts
  it away, as switch_mode does.
- Right-to-left translations are stored in display order, so a multi-word
  Arabic query typed in logical order missed its tool. Such a query now matches
  word by word, as the command palette's search does.

Each test was seen to fail with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: ATTRIBUTION.toml, crates/organize/src/tests.rs (code resolved by hand: crates/organize/src/tests.rs). Catalogs keep upstream's rows, then this branch's rows upstream lacks; ATTRIBUTION.toml is upstream's with every sha256 recomputed; progress lines keep both sides, upstream's first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On top of the merge with 932bf9c6; no conflicts. ATTRIBUTION.toml is upstream's with every sha256 recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The layout revision replaces the placeholder pages and adds the fields,
so usage rights (/Perms /UR3) signed over the form as published no
longer match it, and a viewer may then open the form with every field
locked. write_form now takes them off with the helper strip already
needed, and the layout report says so.
Layout notes are lowercase clauses joined with semicolons, and the bar
put the first right after the layout's own sentence ('11 fields. the
form's usage rights...'). The first note now starts with a capital.
# Conflicts:
#	ROADMAP.md
CodeRabbit: Edit marked a page as tried before checking that it was a scan, so
a page that was not one when Edit first looked (no picture over it yet) was
never looked at again, even after a picture of text was added to it. A page
found not to be a scan is now remembered with the document's edit generation
and judged again only after the document changes (not every frame: listing a
page's images parses it); a page whose recognition has started is still never
started again, so Undo sticks.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	parity/acrobat-features.toml
CodeRabbit: the recognition Edit starts used the same notifications as a run the
user asked for, so turning Edit on for a page with text of its own (skipped by
the run) showed "Recognizing text..." and then "Recognized 0 words on 0 pages;
1 page already had text". The run Edit starts is now marked automatic: it shows
no progress toast, and no completion toast when it added no words. A run the
user asks for, and an automatic run that did read words, notify as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeRabbit: Read mode leaves the Edit tool picked, so the recognition Edit
starts could run in Read mode and change a scanned document; and it could run
while a dialog or prompt was open, which PdfCraftApp::modal_open says nothing
may act beneath. It now waits for Edit to be on outside Read mode with no modal
open, and is tried again once they are gone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: crates/automation/tests/automation.rs (code resolved by hand: crates/automation/tests/automation.rs). Catalogs keep upstream's rows, then this branch's rows upstream lacks; ATTRIBUTION.toml is upstream's with every sha256 recomputed; progress lines keep both sides, upstream's first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
attachment_add compared every attachment name, including files attached to pages as comments, so a page comment with the same file name made the reported key empty. It now compares only the document-level name-tree keys. Deleting the last document attachment when the catalog's /Names is an indirect dictionary left an empty /Names behind; it is now removed from the catalog, as the direct case already did.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Scrolling continuously, Edit > Select All (Cmd+A) selects the text of
every page of the document; one page at a time it still selects the page
shown, as does a document of one page. Shown pages highlight as their
text arrives, and Copy gets all of it in page order, reading the text of
pages not read for display (text far away is let go to bound memory, so
it isn't kept for this). A press on a page or Escape ends it. Select All
still returns whether text is selected.

The Select All parity entry covers both modes; the viewer and copy
security tests run one page at a time, which is what they check.
A form past the pass's depth limit is removed rather than walked. That
is the fail-safe choice for a redaction -- marked content inside a form
we cannot read would otherwise survive -- but it also removes whatever
else that form drew, marked or not. At a limit of 12, a page with a
13-deep chain lost the text of its innermost form with nothing in it
marked.

Measured on a page that draws HEAD, a chain of forms whose innermost
draws DEEP, then TAIL, with only HEAD marked:

    chain   before            after (12)      after (50)
    1       HEAD DEEP TAIL    DEEP TAIL       DEEP TAIL
    12      HEAD DEEP TAIL    DEEP TAIL       DEEP TAIL
    13      HEAD DEEP TAIL    TAIL            DEEP TAIL
    14      HEAD DEEP TAIL    TAIL            DEEP TAIL
    50      HEAD DEEP TAIL    TAIL            DEEP TAIL
    51      HEAD TAIL         TAIL            TAIL

The limit is now 50, which is what the renderer follows
(MAX_NESTED_INTERPRETATION_DEPTH) and what Edit text's reader follows
(text::MAX_FORM_DEPTH). So anything a reader can see, this pass can
walk. The last row is why 50 and not more: at 51 the renderer does not
draw DEEP either, so removing that form costs nothing visible and the
fail-safe still holds.

Fixes #3645's data loss. The other half of that report -- that nothing
tells the user when a form is removed -- is untouched: Report.forms_removed
is counted but the engine's edit pipeline does not carry it out, and the
one channel that reaches the result (doc.edit_notices) is typed
TextNotice, for text that could not be drawn. Widening that type or
adding a channel is a design choice for the maintainers rather than
something to slip into this fix.
- Copy checks the document's copying permission first, then reads the
  pages whose text wasn't read for display on a worker thread
  (DocumentCopy; in place on the web), keeping only each page's text as
  it goes; the clipboard gets the text on a later frame. A new copy or
  the document closing cancels it.
- Any press on a page ends Select All of the document, whatever the tool
  and whether its text is read; so do switching to one page at a time, a
  page selection, and an edit of the document.
A whole-document copy reads its pages off the UI thread, and a copy made
meanwhile (a page selection, a text field's, a comment's, an image) went
to the clipboard first and was then replaced by the document's text.
Any later copy now drops the pending one: copy_selected_text and the
image copies drop it directly, and finish_document_copy, now run at the
end of the frame, drops it when anything else copied that frame.

DocumentCopyJob::of classifies each page from text_failed (a set)
instead of looking it up in pages_without_text's Vec, which was
quadratic in the page count.
Three of them, measured on main:

    pdfcraft-cli extract f.pdf --pages 1 --out --password
    (exit 0, and the PDF is in a file called `--password`)

    pdfcraft-cli check-one --edit f.pdf
    pdfcraft-cli: check-one: missing file

    pdfcraft-cli info /tmp/café.pdf          (that é as one Latin-1 byte)
    thread 'main' panicked at library/std/src/env.rs:878:51:
    called `Result::unwrap()` on an `Err` value

- An option whose value is a **file or folder** refuses a value that is another
  option, and names the way out for a file that really starts with `-`:
  `./--name`, as `--root` already offered. That also closes the gap the option
  walk left for those: an unknown option in a value position was skipped along
  with the option it followed, so `combine --out --nonsense` wrote `--nonsense`
  instead of saying what `--nonsense` was.

  Only those options. A password or a title can be any string, `--page`
  included — `an_option_shaped_password_is_not_parsed_as_a_page_flag` pins
  that, and a bad `--page` or `--dpi` is reported by the command in its own
  terms either way.

- `positional` asks the option table which tokens are values, instead of
  skipping the one after every option. The token after a flag that takes none
  is an argument: `check-one --edit f.pdf` read the file as `--edit`'s operand
  and then reported it missing.

- Arguments come through `args_os`, so one that is not UTF-8 is a usage error
  rather than a panic. `std::env::args` panics while collecting them, so even
  `--help` ended that way.

tests/options.rs covers each, and each fails without its fix (the file called
`--password` comes back; "check-one: missing file"; exit 101 with that report).
FredApps: CI never fetches the OCR models, so the tests that recognize something
skip there and the recognition paths this PR adds went untested in CI.

The decision is split out of auto_ocr_scanned_page into scan_to_read, which
needs no models (the run itself judges the page's own text), with unit tests
that run everywhere: a picture page is read and a text or blank page is not;
only Edit being on starts it (its tab counts); Read mode and an open dialog
wait; a page already read is not read again and a page found no scan is judged
again once the document changes. Two mutants (no Read/modal guard; a no-scan
page never judged again) each fail one of them. The tests that recognize words
still need the models.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CodeRabbit: Edit refuses to start recognition in Read mode or beneath a dialog,
but a run started a moment earlier could finish after the user had switched to
Read mode or opened a dialog, and poll_ocr applied its result there. The result
of a run Edit started is now left until both are gone; a run the user asked for
completes as before. The unit test uses a made-up result, so it needs no OCR
models, and fails without the check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The picker passed no target, so the staleness guard never ran and a file
chosen after switching tabs was attached to the newly active document. Pass
the active document like Import data and the image picker do. A test covers
the tab switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/js/src/formcalc.rs
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mechanical merge (ATTRIBUTION.toml): main's copy of the pinned files, hashes recomputed, both sides' catalog rows kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Merged cleanly, no conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	parity/acrobat-features.toml
# Conflicts:
#	crates/js/src/tests.rs
# Conflicts:
#	crates/engine/src/tests.rs
#	crates/xfa/src/tests.rs
# Conflicts:
#	crates/engine/src/tests.rs
#	crates/js/src/tests.rs
#	crates/ui-egui/tests/forms.rs
Resolved by the keeper:
- ATTRIBUTION.toml: upstream's copy, hashes recomputed
- crates/organize/src/tests.rs: additions from both sides, upstream's first

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
page_extract (and Split, which extracts each part) wrote the copied
pages without any outline, even when every page was extracted, though
the tool says bookmarks come along.

extract_pages now reads the source's bookmarks with the destinations
mapped to the copies, as Combine already does, and writes them as the
new document's outline. A bookmark whose page was left out goes, unless
a bookmark under it stays. A source without bookmarks still gets no
outline.

Fixes #4020
Conflicts:
- crates/ui-egui/src/panels.rs: main made `panel_header` pub(crate) with
  `back: Option<&str>` (the Calculation order page uses it too); this branch
  needs its leading button to be Expand on All tools. It now takes
  `lead: Option<(&str, &str)>`, the button's icon and tooltip, and the three
  callers pass theirs (calc_order_ui.rs included).
- i18n catalogues: both sides appended entries at the same place; kept both.
- ATTRIBUTION.toml: catalogue hashes recomputed.

Main added eleven catalogues since this branch was cut (ca, ckb, fi, hi, id,
ko, lo, nb, ro, sq, th), and their coverage tests require every UI literal, so
the eight All tools view strings are translated in them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a fixed font size, a text field's appearance cuts a long value at
the edge of its box, but form_fill only answered "filled": n. A person
sees the cut; an agent filling forms through the CLI or MCP did not.

The text appearance builder now notes when the text it lays out does
not fit: one line wider than the box, or wrapped lines below it.
forms::appearance::value_overflow asks that for a field's widget,
Document::field_overflow for a field by name, and form_fill adds a
warning for each filled field that is cut off. The value is still saved
whole and drawn as before; auto-sized fields shrink to fit and are not
reported.

The automation tests' inline PDF builder moves to a shared pdf_of.

Fixes #4018
On top of #3011's page unit, the Drawing scale block gets a "1:N" field (1:50,
1/100, 50, 1:2,5 are read; zero, negative, huge and malformed values are
refused with a message) and a combo of common architect scales (1:1 to
1:1000). Either one fills the existing fields (1 mm of paper = N mm, shown in
metres) and applies the scale to the page without a second click, only when
the document allows annotation. Apply scale is unchanged.
Merged cleanly, no conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review of #3670: with the depth limit at 50, depth was the only bound on the
form walk. A form that draws the next level twice, or a cycle of two forms,
costs 2^depth interpretations — at 12 that was 4096, at 50 it is a hang on a
file anyone can send.

The pass now also counts the forms it enters, and the budget is Edit text's own
`MAX_FORM_VISITS`, referenced rather than copied. Running out is treated as a
form that cannot be read, which is what the depth limit already did: removed in
Apply, the page left alone in Verify.

Measured with the new test:

  depth 50, fan-out and a 50-deep cycle, with the budget   5.9 s
  depth 22, fan-out, budget removed                        still running after
                                                           5 minutes

A `const` assertion ties this pass's depth limit to Edit text's, so the build
fails rather than a redaction quietly removing what a reader still shows (the
review's third point). Checked by setting the limit back to 12:

  error[E0080]: evaluation panicked: assertion failed:
  MAX_DEPTH >= pdfcraft_edit::text::MAX_FORM_DEPTH

Tests: a page whose forms each draw the next twice, and the same with the last
form drawing the first again — both must finish and still redact the page.
Retain compatibility regression coverage with upstream content-boundary and image-size tests.
Two cases still drew a value cut off without saying so. A single-line
value at a fixed font size taller than the box (12 pt text in an 8 pt
field) was checked only for width, so it fit sideways and was cut at the
top and bottom. In a multiline field, a character wider than the box
becomes a line of its own, which fits vertically but is cut at the sides.

The single-line and comb branch now compares the line's ascent to
descent against the clip box (a new `Overflow::High`), and the multiline
branch reports the widest wrapped line that is still wider than the box.
An empty value draws nothing, so it is never reported, however small the
box.

Fixes #4018
Review follow-ups for the expanded All tools view:

- Opening a document left the view on, either over the new document (All
  tools as the default workspace) or waiting to reappear on the next switch
  back (Read as the default, or a mode override). Activating a document now
  puts it away, and the reset lives in enter_mode, which every workspace
  change goes through.
- The sidebar's expand button and `--tools full` assigned the mode directly.
  From Read the right panel saved on entry was not restored, and from Edit the
  Edit text tool stayed active. Both now go through switch_mode, as the mode
  bar does, so the sidebar shows the All tools list again on collapse.

Each new test was seen to fail with the fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The expanded view kept covering the workspace after a control client chose a
tool (`tool=organize`) or went Home (`home`), so the chosen panel or the Home
view never showed. The same held for a document tab click, Open Recent on an
open file, and the Combine files tab.

select_tool now clears the view, so every way of choosing a tool does. The
view also remembers which tab was in front when it opened, and another tab
coming forward puts it away, whichever path brought it there.

Each new test was seen to fail with the fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With no document open the expanded view covers Home itself, so the control
option `home` changed no tab and the view stayed up (CodeRabbit's remaining
merge-risk note on 8a8bb92d). The option now puts the view away itself.
`going_home_with_no_document_puts_the_expanded_all_tools_away` was seen to
fail with the fix reverted.

Also doc comments on the view's `show` and `tile` and the sidebar's
`all_tools`, for CodeRabbit's docstring-coverage check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	parity/acrobat-features.toml
# Conflicts:
#	parity/acrobat-features.toml
# Conflicts:
#	parity/acrobat-features.toml
The ArtCraft community card on Home gets a close button. Closing it hides
the card until the next launch; the choice is deliberately not remembered,
and the same links stay in the top bar's Discord button and the Help menu.
A UI regression covers closing and the fresh-launch default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The theme preference defaults to System: dark on a dark desktop, light
where the desktop says nothing. Saved settings and --theme still win.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The click-cycle test assumed a fresh install starts Light; it starts on
System now, so the cycle is System, Light, Dark.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- ratio_text is derived from the scale fields (one function, sync_ratio_text):
  on page seeding and after a hand edit of drawing points, real distance, unit
  or page unit it shows the matching 1:N, or is emptied when the fields are not
  a plain m/mm drawing scale. The "Common scales" highlight follows it.
- format_ratio no longer rounds (1:1.234, 1:0.001), so the field shows exactly
  the ratio that applies.
- The smallest accepted ratio is 1:0.001, keeping the real distance within the
  minimum of its field; set_ratio refuses out-of-range ratios too.
- "Scale", "Common scales", "Enter a scale such as 1:50." and a hint that
  applying a scale switches units to m / mm are in every i18n catalog, with a
  completeness test.

Co-Authored-By: Claude <noreply@anthropic.com>
Conflict in parity/acrobat-features.toml: main moved the integration
tests into tests/it/, so the evidence paths take main's side and this
branch's two dialog_chrome tests are added on the new paths; the notes
keep this branch's wording. The custom-properties entry is now partial
through the tools, but the dialog still has no Custom tab, so the
"No Custom tab" note stays true.

Also fixes a break the merge could not show as a conflict: the cursor
test's form.pdf fixture is ../data/form.pdf from tests/it/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged cleanly, no conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Extract pages only took the selected pages. Its dialog now has a Pages
field for ranges such as "1-5, 8, 11-13", read like Combine files' and
Print's ranges, in the order typed and each page once. Left empty (as
the dialog opens), it takes the selection as before, shown as the hint.
A range it can't use says why and turns Extract off; Extract and delete
removes the typed pages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tab caches the audit per document and edit generation. Pin both
halves: frames with the file unchanged keep the same rows, and an edit
made while the tab is open (a new bookmark) shows up on the next frame.
With the cache keyed on the document alone, the test fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ATTRIBUTION.toml
Review of #3849: both tests asserted `stderr.contains("No such file")`, which
is the Unix wording. On Windows the same error reads "The system cannot find
the file specified. (os error 2)", so `test (windows-latest)` failed on them.

Neither test is about the wording. What they are about:

- `a_flag_that_takes_no_value_does_not_swallow_the_file`: the error is about
  `missing.pdf`, and is not "check-one: missing file" -- the file reached the
  command instead of being eaten as `--edit`'s operand.
- `a_file_whose_name_starts_with_dashes_is_still_reachable`: the error names the
  file the command could not open (`missing.pdf`, or `./--dash.pdf` for the
  `info` case), and is not a usage error about a value or an unknown option.

Both now assert exactly that, plus the absence of our own usage phrases. Every
string they match is one this CLI prints, so the OS has no say in it.
A paragraph selected by its frame (or objects taken by an area selection) answered the
arrow keys and Escape but not Delete, so the only way to remove text was one typed
character at a time in the inline editor. Delete or Backspace now removes the whole
selection as one undo step: paragraphs and images last-first in their inventories, added
content after them, under the existing "Delete content" history label.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a comment selected, the arrow keys scrolled the view (↑ ↓) or turned the page (← →), so
a text box, rectangle or line could only be placed with the mouse. Now an arrow key moves the
selected comment a screen pixel, and ⌘/Ctrl or Shift and an arrow ten, as Edit text nudges
objects; up is up on screen whatever the zoom, the page's rotation and the view's. Each press
is one undo step (Edit::MoveAnnotation). The keys are taken before the view's navigation,
which has them back once nothing is selected. Text markup stays with its text, and a locked
comment where it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the English placeholders added during the #2210 integration
(calculation order, text formatting, print, thumbnails, layers, comment
reshape and signature checks, plus the two signature wordings from
3e180944) with Finnish, and updates the fi.tsv sha256 and the entry
count in docs/localization.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/js/src/lib.rs
Review on #4111: Delete cleared the selection before the edit ran, so a delete the engine
refuses (text used as a clipping path) left the document as it was but dropped what the user
had selected. The selection now stays until the edit applies; `ObjectSelection::prepare`
already clears it when the document changes and keeps it when an edit is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page whose /Annots is an object of its own keeps its dictionary when a
later revision adds a signature widget or a comment: only the array
changes. That array fell through to "other changes", so countersigning
such a document (as many signing services do) turned every earlier
signature invalid, including PdfCraft's own second signature.

A changed array that was a signed page's /Annots is now classified like
an inline /Annots: by the annotations it gained (signature, form fill or
comments), under the same DocMDP rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PAdES LTV writers add the /DSS and, in the same revision, declare the
ESIC developer extension in the catalog's /Extensions (ISO 32000-2
§7.12). Any catalog change outside a short key list was "document
structure", so the signature such an update protects showed as invalid.

/Extensions may now gain declarations after signing; one that was
signed may still not change or go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mechanical merge (ATTRIBUTION.toml): main's copy of the pinned files, hashes recomputed, both sides' catalog rows kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review follow-up: an indirect /Annots array that drops a signed
annotation while gaining a signature widget was judged by the widget
alone, so a form-fill certification let a signed comment be deleted.
A removed comment is now a comment change, and a removed widget is a
change no permission covers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Reviewers flatten their comments and Fill & Sign signature once done, so
nothing on the signed or reviewed pages moves any more. The command was
only in All tools and the palette; it now also ends the page's
right-click menu (with or without a comment selected, while the document
has comments) and the Comments panel's More options menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The notice that a document is secured came back on every open; Dismiss
lasted only for that view. Its new "Don't show again" turns it off in
every document, kept in the settings as show_secured_notice and turned
back on in Preferences (or --secured-notice). Only the notice changes:
the restrictions, the encryption and Document Properties > Security stay
as they are. The two labels are translated in all 33 catalogues, whose
ATTRIBUTION hashes and provenance notes are refreshed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review of #4096: when a secured form's scripts were turned off, the
bar shows that message instead of the security one, but still offered
"Don't show again", which would turn off a notice the user had not seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description read Title, Author, Subject and Keywords only from /Info. A file
that states them only in its XMP packet (/Info is deprecated in PDF 2.0, and
some writers leave it out) showed empty fields, while Acrobat shows them.
organize::info now falls back to the packet's dc:title, dc:creator,
dc:description, pdf:Keywords, xmp:CreatorTool and pdf:Producer: a language
alternative gives its x-default item, and several authors are joined with
"; ". The engine fills the inspection's missing fields the same way, so the
read-only Description rows and doc_info agree with the editable fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`get_property` returned the raw child markup when a property had no
`rdf:li` items, so `<dc:title><rdf:Alt/></dc:title>` with no /Info title
showed "<rdf:Alt/>" in Document Properties. Character data that still
holds a `<` is markup, never a value: an empty Alt/Seq/Bag, or an item
holding a nested structure, now reads as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged cleanly, no conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Quitting from the Dock menu (or logging out, or an agent's ui.quit) while
another app is in front opens "Save changes?" in a window the user can't see,
so it looked as if PdfCraft hadn't quit. The prompt now sends a critical
user-attention request on the frame it opens while the window isn't focused:
the Dock icon bounces until PdfCraft is active, the Windows taskbar button
flashes until the window is in front, and X11 sets the urgency hint. Coming
back, or answering, sends Reset. This is what AppKit does by itself when an
inactive app presents a modal panel; PdfCraft's prompt is drawn by egui, so
AppKit never sees it.
Quitting with several unsaved documents moves from one question to the next
without the prompt closing, so a question that opened in the background after
one the user saw asked for nothing (review feedback on #4155).
close_and_continue now marks the next document's question as new. A request
that still stands carries over to it instead of being sent again.
hayro-interpret's map_code tried a symbolic font's cmap subtables in the order the font lists
them, by platform ID, so Macintosh (1, 0) came before Windows (3, 0). ISO 32000-2 §9.6.5.4
gives (3, 0) first and (1, 0) only otherwise. SAS 9.4 embeds AMT fonts whose (1, 0) table is
wrong, so "8" drew as "p", "+" as "V", and "1" and "%" not at all.

The Windows (3, 0)/(3, 1) records are now tried first; Macintosh and Unicode ones still serve
codes those leave out. The test font is built in memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #4157 (CodeRabbit): the symbolic branch of EmbeddedKind::map_code is taken whenever the
Nonsymbolic flag is not set; it doesn't look at /Encoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	parity/acrobat-features.toml
A CUPS job that names no paper gets the queue's PPD default (A4 on the
reporter's Xerox queue), whatever the sheets were laid out on. Two gaps
remained after #1583:

- cups_media had no name for 13 of the 21 PAPERS sizes (A6, ARCH A-E1,
  ANSI C-E, A0-A2), so the Print dialog sent no media for them. They now
  use PWG 5101.1 names (the IANA-registered na_arch-e1_30x42in for ARCH
  E1), which carry their dimensions: CUPS feeds the driver's size of those
  dimensions whatever the driver calls it, and driverless IPP printers use
  them natively. Checked with cupsfilter against a synthetic PPD whose
  sizes use vendor keywords (ARCHD, AnsiD, A0...): each name produced its
  size's bounding box. The 8 sizes that already worked keep their PPD
  keywords.
- doc_print to a printer (CLI run, MCP) sent no paper at all. It now
  resolves the paper to its PAPERS index and sends the same media option
  as the dialog, after the driver options. The job is built in spool_job
  so it can be tested without printing.

print::media_option is the one place both get the option from. Both new
tests were seen failing without the fix. The macOS tray half of #3752
(Auto Select instead of the PPD's default tray) is a craft-print change.
ARCH A-E went out as arch_a_9x12in .. arch_e_36x48in; PWG 5101.1 names
them na_arch-a_9x12in .. na_arch-e_36x48in (ARCH E1's IANA-registered
na_arch-e1_30x42in already had the prefix). CUPS parses either from the
dimensions, which is why the cupsfilter check passed, but a driverless
printer may accept only the names it advertises. Re-checked the corrected
names with cupsfilter against the synthetic PPD.

The paper test now pins every size's exact name, and fails on the old
spelling. Doc comments for doc_print and PrintDraft::job.

Review: storytold/pdfcraft#4161 (CodeRabbit).
30 x 42 in is `na_wide-format_30x42in` in PWG 5101.1-2023 and the IANA
IPP registry (checked against both). `na_arch-e1_30x42in` was asked for
on the IPP list in 2021 but never registered. CUPS maps the new name to
the PPD's 30 x 42 in size (cupsfilter, synthetic PPD), and the paper
test pins it and fails on the old name.

Doc comments for bad, printers and two paper tests, for the review's
docstring check.

Review: storytold/pdfcraft#4161 (CodeRabbit).
# Conflicts:
#	crates/xfa/src/tests.rs
A field's own (partial) name can't contain a period (ISO 32000-2
§12.7.4.2), so Prepare a form refused MASTER.first. Acrobat reads such a
name as a full name: the field `first` under a parent field `MASTER`.

- add_field: a dotted name makes or joins the parent fields it names.
- set_props: a dotted name is the full name and moves the field there,
  copying what it inherited onto it first; a plain name still renames
  the field within its parent. Either way the new full name may not be
  taken by, or hold or sit inside, another field.
- A field joining an existing parent doesn't take its Ff or Q.
- Field Properties and the Prepare a form name tags show the full name
  instead of the last part.
- Names are capped at 32 parts; empty parts are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2957. The field list holds only terminal fields, so a parent
left empty by a move wasn't seen and a second field of its name could be
made beside it, by adding or by renaming. And a new field joining a parent
that holds /V, /DV or /MaxLen inherited them, with no neutral value of its
own to turn them off: such a parent now takes no field that doesn't hold
its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2957: set_props took the field out of the tree before attach
checked the destination, so a refused move (a parent holding /MaxLen, say)
left it out of /Fields for a direct caller of the forms API. The read-only
check is now its own function, run before the field is detached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2957: own_inherited wrote what the field inherits onto it before
check_place could refuse the move, so a refused move still changed the field.
What it inherits is now read first, counted as its own by the check, and
written only once the move is allowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now renames a field in the calculations that read it. After rebasing,
that runs for a dotted name too, which moves the field under new parents: the
sum and the script that read `a` read `Group.a`, and still compute after a
save and reopen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2957 (benandez, points 2 and 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #2957, point 1. Both entry points already trim the whole name
before splitting it, and a space beside a period stays in its part,
which matches black-box observation of Acrobat and ISO 32000-2 12.7.4.2
(only a period is barred from a partial name). The test pins that down;
three mutants (no trim in add_field, no trim in set_props, untrimmed
empty-part check) each fail it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/engine/src/tests.rs
Opening a centred or right-aligned Add text box drew its text at the left
edge, a layout the box never had, and Ctrl+Z inside the box (which goes to
the text, not the document) left it showing that way. The editor now lays
the text out with the box's alignment. Justified text is still shown
left-aligned while editing (TextEdit can't justify).
# Conflicts:
#	crates/engine/src/tests.rs
The editor is at least 60 points wide on screen, so a short box can be
clicked into. With the box's alignment applied, right-aligned or centred
text in a box narrower than that was placed against the wider editor's
edge, past the box's right edge. The editor now reaches past a narrow
box away from the edge its text keeps, so the text stays where the page
draws it.
A text field PdfCraft added showed its typed text in Acrobat only while
the field had focus: as soon as the user clicked elsewhere the text
vanished (it was still stored, and printed). PDF-XChange Editor and
PDFgear were fine, and the same field made by MuPDF was fine in Acrobat.

Bisected in Acrobat with one-change variants of the same file: only the
variants whose /DR fonts are objects (or that have no /DR) keep the text.
The appearance stream's own fonts, /P, the appearance layout and /DA 0
made no difference. PdfCraft wrote Helv, TiRo and Cour inline in /DR.

ensure_form now adds them as objects, and turns fonts an earlier save
wrote inline into objects too (same dictionaries). An indirect /DR or
/Font stays the object it is. The XFA-to-AcroForm writer does the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up (CodeRabbit on #3488): a form an earlier PdfCraft made
should work in Acrobat once it has been filled here too, not only once a
field is added or edited. set_value now runs the same conversion (shared
with ensure_form through inline_fonts_as_objects and store_form_fonts);
it adds nothing, only changes where the existing fonts are stored.

Not in a form a signature covers (a signature field with /V): there the
file stays as the signer saw it apart from the value. PdfCraft's own
change check counts a /DR change after a form-fill certification as form
fill, but another validator may not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recognize text (one document) and Recognize text in multiple files ran
their worker unguarded. A panic in the recognizer (third-party inference)
or a worker that died holding the progress lock left the run in place: the
progress notice stayed, and every later run was refused with "Text
recognition is already running" until restart.

- Both workers run inside `pdfcraft_engine::guard`; a panic becomes the
  run's error ("Recognize text failed: …", in the UI's language).
- `poll_ocr` treats a poisoned progress lock as a failure: it clears the
  run or batch and says so.

The batch's loop moved unchanged into `recognize_files` so it can run
inside the guard. A thread that can't be spawned is #1871's (#1819), and
Export and the Action Wizard are #4043's; this leaves both alone.

Tests (crates/ui-egui/src/ocr_ui.rs): a panicking recognizer ends with
the error; a page run and a batch whose worker died holding the lock are
cleared with a notice. Both fail without the change. The OCR integration
tests pass with the real models (PDFCRAFT_MODELS).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floating comment card (new note, text box, edit comment) was an egui
Area constrained to the whole window. When the page scrolled its comment
under the mode bar, the card was pushed back into the window and sat over
the mode bar and toolbar until the whole page left the screen.

It now shows only while its anchor is inside the document view, and is
kept inside that view. While the comment is out of sight the draft is
kept, and the editor takes the focus again when it comes back.

Test: tests/comments.rs the_composer_hides_while_its_comment_is_scrolled_away
scrolls a sticky note's anchor above the document with its page still on
screen; it fails without the change (the card sat at y=43, over the
toolbar, with the document starting at y=134) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the whole page left the screen, `composer` returned before it set
`focus`, so the editor didn't take the keyboard again when the page came
back (review on #4095). That path now asks for the focus too.

Test `the_composer_takes_the_keyboard_back_after_its_page_was_off_screen`
scrolls page 1 off the screen and back; it fails without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text line joins runs painted on one baseline, and the audit reported
the line's whole text for a cover over part of it, including words
painted beside the cover that stay visible (`SECRETtail` for a cover
over `SECRET`).

TextLine now keeps each joined run's text with the end of its word
boxes (TextLine::runs), and the audit reports the runs whose words
the cover overlaps. A single run, a line reordered for reading
(right-to-left or visual-order Indic), or a cover that overlaps the
line's box but none of its words still reports the whole line.

Fixes #4164
A text field with the Password flag kept its typed value in /V, so Save, a full
rewrite, Reduce File Size and the recovery snapshots all wrote it in plain text.
ISO 32000-2 §12.7.5.3, Table 231 (note to the Password flag): PDF processors
should never store the value in the PDF file. Each of those paths now writes a
copy without the values; the open document keeps them in memory, and the masked
appearance is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: the helpers checked only the field dictionary's own /V. A value
inherited through /Parent or carried by a separate widget was missed, so the
document could be written as it was. Detection now uses the resolved value and
the widgets; clearing removes /V from the field, its widgets and every ancestor
only password fields inherit from, and gives the field an empty value of its own
where a shared ancestor value has to stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: Extract Pages copies the fields of the pages it takes, and
write_new (Combine, Extract, Split, Create) wrote that copy without the writable()
boundary saves use, so the new file kept a typed Password value. write_unchecked, which
write_new and Split's size measuring both go through, now writes the sanitized copy. The
other write_full/write_incremental calls in the engine build in-memory bytes (display,
previews, the working copy, print rasters), not files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3943: sign, sign_with_timestamp, timestamp_document and dss::embed
each write a new incremental file from editor.cos, outside the writable() boundary
saves use, so a typed Password value was signed into the file. Each now writes from
the writable() copy. The new test signs a form with a typed value and checks the
signed file holds the masked appearance but not the value; the three TSA/DSS paths
need a timestamp server or validation data, and use the same one-line change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #3943. A black-box test of Acrobat (synthetic form, Save As and
Ctrl+S, every decoded object searched) showed how it saves a Password
field: the typed value is nowhere in the file; a field with /DV is saved
holding /DV, one without is left empty; the asterisks are drawn from the
saved value (5 for a 5-letter default, not 13 for the typed password);
/DV and /AA are kept.

clear_password_values now does the same: it restores the (inherited)
/DV as the value, or leaves the field empty, and redraws the widgets, so
the appearance no longer shows how long the typed password was. A field
already at its default no longer counts as holding a password, so such
a form needs no cleaned copy. The parity note for form.fill-text says so
and lists the two limits Acrobat shares (a script copying the password
into an ordinary field, and an earlier revision kept by an incremental
save).

Mutants, each failing a test: no /DV restore, no redraw, the default
ignored in password_fields, emptiness ignored (an empty string in an
array). Dropping the empty-default filter was equivalent, so the filter
is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #3943, point 1. has_password_values ran a full fields() walk
on every save and recovery snapshot, and clear_password_values walked
every field's ancestors again for each ancestor of each target.

has_password_flag now answers first, reading only /Ff, /Kids and /Parent:
the field tree from /Fields (seen set, 32 levels) and, as fields() does,
the widgets on the pages with the fields above them. A form without such
a field returns before any Field is built. clear_password_values finds
each field's ancestors once.

The test puts a page-only widget, a flag inherited from an unlisted
parent and a nested field whose widget is on no page each alone in a
document. Mutants that drop the page scan, the parent chain, the tree
walk or the descent into kids each fail it, as does treating every
ancestor as exclusive. Dropping the seen set or the early-out changes
only speed (equivalent).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TextLine::runs also says whether joining put a space before each run,
and covered_words uses it: a word shown in two runs with nothing
between them (`SE` `CRET`) is reported as one word, not `SE CRET`.
Runs with uncovered runs left out between them are still spaced.
The regression test writes its content streams on one line each.

Review on #4198 (CodeRabbit).
crates/organize/src/import.rs: upstream's name_tree_entries (seen-node set, each node read once) kept whole; this PR's only change to it, pub(crate) for attachments.rs, applied on top.
crates/organize/src/attachments.rs: its name_tree_entries call passes the new seen set.
crates/automation/tests/attachment_key.rs, crates/ui-egui/tests/attach_picker_target.rs: moved into tests/it/ and registered in main.rs, following upstream's one-binary layout (organize keeps its single test file, as upstream's rule says).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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).
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.
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>
# Conflicts:
#	ROADMAP.md
#	crates/optimize/src/images.rs
# Conflicts:
#	crates/ui-egui/src/marks_ui.rs
#	crates/ui-egui/tests/it/marks.rs
#	parity/acrobat-features.toml
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.
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.
# Conflicts:
#	ROADMAP.md
#	crates/annot/src/lib.rs
#	crates/automation/src/comments.rs
#	crates/engine/src/lib.rs
#	crates/engine/src/tests.rs
#	crates/render/src/inspect.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/comments_panel.rs
#	crates/ui-egui/tests/it/comments.rs
#	parity/acrobat-features.toml
#	vendor/README.md
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 #4199
Fixes #3797
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>
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>
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>
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.
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.
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>
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 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-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
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
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)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #3985: a document script over the size or nesting limit made `run`
return before any script ran, so the field script failed as before. It is now
skipped (never evaluated, as the limits require), reported in its place in
doc_errors, and the other scripts and the field script still run. The field
script's own limits still refuse it outright.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #3985 (benandez): JsOutput::absorb reported a failing document script once, but append
added every error again, so merging two outputs repeated it. pdfcraft_js now says whether an
error is a document script's (is_doc_error, next to the doc_error format), and append skips one
already reported; each field script error is still its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up on #3985: JsOutput deduplicated document-script errors by
their text (document script N: ...), so a field script that threw the
same words was dropped by append, and a document script's error was
dropped by absorb when a field script had already reported that text.
JsOutput now records which errors came from document scripts
(doc_errors) and deduplicates only those; pdfcraft_js::is_doc_error,
which guessed from the wording, is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
# Conflicts:
#	crates/js/src/lib.rs
#	crates/js/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	crates/js/src/lib.rs
#	crates/js/src/tests.rs
#	parity/acrobat-features.toml
# Conflicts:
#	ATTRIBUTION.toml
A content-only edit re-reads Document::added() and marks() only when the
edit itself is one that changes them. Edit::Batch has Scope::Content when
all its parts do, but content_refresh_lists never looked inside it, so a
batch of AddText/DeleteContent or AddWatermark edits left both lists stale
until the next full refresh or undo. It now answers for its parts.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
# Conflicts:
#	parity/acrobat-features.toml
Local CodeRabbit review follow-ups for the expanded All tools view:

- The search field promises tools and commands, but an entry with a menu
  (Watermark: Add, Update, Remove) matched only by its own name, so
  "remove watermark" found nothing. Entries now also match by their menu
  commands' names, in English or the UI language.
- With the command palette open over the view, Escape closed the view as
  well as the palette. The palette now owns Escape, as dialogs do.

Each new test was seen to fail with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replies were matched to their parent by /NM across the whole document.
/NM need not be unique: PyMuPDF names the first comment on every page
`fitz-A0`, so every reply in such a file was filed under page 1's
comment, and the other pages showed none. Both comment readers (render
inspect on open, annot summaries after an edit) now resolve /IRT to the
parent's page and /Annots position by object reference, and the
Comments panel, comment properties, the canvas thread, the engine's
comment summary and the comment_list tool thread by that.

The Comments header now counts comments without their replies, as
Acrobat does (28 rather than 44 for the reported file), and a reply's
thread line runs from the avatar above it (the comment's or the
previous reply's) to its own instead of a 12 px stub.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KvugvPrjZtPVudhh7cMa54
The header keeps Acrobat's count of comments and adds the replies in
small text below it ("16 replies"), left out when there are none and
counting the whole document rather than the current search. Status
changes and checkmarks are not replies. The plural label is in all 33
catalogues with each language's plural forms; ATTRIBUTION.toml records
the new hashes and that the label is an AI-assisted translation not yet
reviewed by native speakers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KvugvPrjZtPVudhh7cMa54
Follow-up to threading by /IRT reference, from review:

- An /IRT that reaches no listed comment (the annotation itself, a
  loop, a pop-up, an object no page lists) left the annotation a reply
  shown nowhere, though still counted. `link_replies` now settles the
  links in both readers (inspect on open, the engine's list after an
  edit): a reply keeps its parent only when the chain reaches a comment
  within 64 steps, and anything else stays a comment of its own.
- A reply to a reply was never shown in the Comments panel (only
  comment_list followed the chain, #806), but the new header line
  counted it. The panel now files every text reply under the comment
  its thread starts from (`thread_tops`); a status or checkmark stays
  with the annotation it was set on.
- A Replace Text caret (/RT /Group) is grouped with its strikeout, not
  a reply to it, so the header no longer counts it (new `grouped`
  field on both readers).
- The engine summary and comment_list gather replies once instead of
  rescanning every annotation per reply (quadratic on large reviews).
- Inspect takes the first listing in page order instead of a min over
  an unordered map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The comment tools resolved `id` (the comment's /NM, as comment_list
reports it) to the first annotation with that name. /NM need not be
unique: in a PyMuPDF file every page's first comment is `fitz-A0`, so
comment_reply, comment_set_status and the rest acted on page 1's
comment whatever page the caller meant. A shared id is now refused
with a message pointing at `page` + `index`, which always name one
comment. A new end-to-end test also checks that comment_list threads
such a file by reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A status or checkmark is a state reply whose /IRT names the annotation
it applies to (ISO 32000-2 §12.5.6.2). comment_list gathered state
replies at any depth of a thread (since #839 listed nested replies), so
a checkmark or status set on a reply was reported as the comment's own,
while the Comments panel, comment properties and the page's context
menu showed the comment unmarked. comment_list now files them under the
annotation they were set on, as the panel does.

A UI regression pins the card and the page's menu (it also fails if the
menu counts checkmarks at any depth), and an automation one pins
comment_list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit filed a status or checkmark under the annotation it
was set on, but comment_list only reported states for comments, so one
set on a reply dropped out of the listing. Each reply in `replies` now
carries its own `status` and `marked`, read the same way as a
comment's, and the tool description says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From review:

- `link_replies` only finds parents among the annotations both readers
  list, and neither lists links, form widgets, pop-ups or other
  non-markup annotations (`is_comment_subtype`), so an /IRT to one of
  those leaves its annotation a comment of its own. Its doc comment now
  says so; the UI edge-case test adds an /IRT to a link and to a form
  field, and the engine test an /IRT to a link (after an edit too).
  Both fail if a reader lists links as comments.
- The Comments panel's grouping comment now says that a status or
  checkmark set on a reply, at any depth, is that reply's: the card's
  badge and checkmark leave it out, and comment_list reports it on the
  reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`link_replies` makes an annotation whose /IRT leads nowhere a comment of
its own. For a status or checkmark that meant a card in the Comments
panel (and an invisible hit area on the page, and a root in
comment_list) that the header's count left out; before threading by
reference, most of these were hidden. The realistic case is a comment
deleted from another page than its status replies.

`Annotation::is_comment` (not a reply, not a status or checkmark) is now
the one test for "listed as a comment": the panel's cards, page badges,
filter menus and count, the page's hit-testing, comment_list's roots and
the summary all use it. A Replace Text caret still shows in its
strikeout's card: it holds the replacement text, and the count already
treats the pair as one comment.

From CodeRabbit review on #4061.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebasing onto main met its annotation listing fixes (c82dc91a, dfde9078):
inspect now lists each annotation once, at the first page and position
that lists it, told apart by the object its reference chain ends at,
and reads a page-shared /Annots array under its first page only. The
rebase collects reply slots in that same pass and matches /IRT by the
object it resolves to, so a reply finds its comment wherever the
listing keeps it.

The test fails with the slots keyed by the raw /Annots reference (as
before the rebase) and with /IRT matched by its raw reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit: a recognition run reads a snapshot of the document and its result
names a page by index. If the document changed while it ran (a page deleted or
moved), Session::apply_ocr would put the words on whatever page now has that
index. An Edit-started run now remembers the page and the document's edit
generation (AutoOcr); when its result arrives after a change it is dropped and
the page's "tried" mark is cleared so the page in view is judged again. A run
the user asked for is unchanged. The unit test uses a made-up result (no OCR
models) and fails without the check.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Clear all wrote `/Annots []` on a page that had no annotations and replaced
an indirect `/Annots` array with an inline copy. It also left replies to a
mark in place, so their `/IRT` kept the cleared mark in the document (and in
the comment list, as a reply to nothing).

`clear_marks` now writes the list through the annot crate's `set_annots`
(made public), as deleting a comment does, and removes each mark's replies
and pop-ups with it. The "what goes with a deleted comment" walk moves from
`delete_annotation` into `with_replies_and_popups`, which both use; it visits
each annotation once instead of sweeping the list until nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An incremental save wrote every object added during the session, whether
anything still referred to it or not. A redaction mark added and then
cleared (or a comment added and deleted) was therefore written into the
file as an unreferenced object, with its QuadPoints, overlay text and
author.

`SaveOptions::drop_unlinked` writes such additions as free entries instead:
objects whose numbers the file never used and that nothing the update
writes refers to, walked from the trailer and the file's changed objects
through additions only. `/Size` and the numbering stay as before, so undo
past the save can bring an object back under its number. The option is off
by default because the standalone document timestamp is found by scanning,
not by reference; `Session::save_bytes` (Save, and the automation save)
turns it on. Full saves already left these objects out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`/Annots` may hold an annotation dictionary inline instead of a reference
(the standard asks for references, but files do it and PdfCraft accepts
them). Deleting a comment or clearing redaction marks only matched
referenced entries, so an inline reply (`/IRT`) or pop-up (`/Parent`) of
what went stayed behind, pointing at nothing. The pop-up an inline reply
or an inline comment names (`/Popup`) stayed too. This predates the shared
walk.

`goes_with` matches an entry against what is going, inline entries
included; `delete_annotation` and `clear_marks` filter with it. The walk
follows the pop-up an inline reply names, and deleting an inline comment
takes the pop-up it names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The page counts the spacing after every character, the last of a line
included; epaint only puts it between characters, so a line could fit in
the editor and wrap in the saved text.
Edit on a Run a JavaScript action loads its script into the editor;
Update replaces it on the same trigger, Discard leaves it. Saving the
tab now keeps actions that did not change as they are, and an edited
script keeps the actions chained after it (/Next).
A /XYZ destination with a null (or unusable) left or top went to the top of the
target page. ISO 32000-2 §12.3.2.2 says a null value keeps the current one, and
Acrobat Reader, observed on a synthetic two-page fixture at 400 %, keeps the
user-space point at the window's top-left on the new page at the same zoom.
CAD exports (WSCAD) write every cross-reference as [page /XYZ null null 0], so
following one zoomed in landed on the sheet's title row instead of where the
reader was looking.

go_to_dest now reads the place at the window's top-left before the jump and
uses it for the null coordinates, along each axis the page overflows; a page
that fits keeps the layout's own placement. Null /FitH top and /FitV left
still go to the page edge.

Tests: a unit test (zoomed, partly given, view turned a quarter, page that
fits) and an end-to-end click on such a link at 400 %, both seen failing with
the fix reverted. The malformed-bookmark case in
bookmark_destinations_scroll_to_their_position now expects the kept place, as
DestView documents unusable operands as null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2328-2330 ("format" number-picture branch, `if num < 0.0 { text = format!("-{text}") }`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The actions chained after a trigger's first one (/Next) are kept
whatever replaces it, not only another script, and the row reads
"…, then N more actions". Edit is disabled on every row while a
script is open, so it can't be overwritten.
The signing saves wrote with default options, so a redaction mark added
and cleared in the session (or a comment added and deleted) reached the
file through a signature, a timestamped signature, embedded LTV evidence
or a document timestamp, with its QuadPoints, overlay text and authors.
A signed revision can't be changed afterwards, so nothing could take it
out again.

`sign`, `sign_with_timestamp` and `dss::embed` now save with
`drop_unlinked`: everything they add is reachable (the signature through
the field's `/V`, the store from the catalog), and the ByteRange
placeholder is found in the written bytes.

`timestamp_document` can't: its `/DocTimeStamp` dictionary has no field
pointing to it, so the option would drop it and every document timestamp
would fail with "ByteRange placeholder not found". It frees the leftovers
first with the new `Document::free_unlinked_additions`, which frees
exactly what the option leaves out, and then adds the dictionary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tabbing_back_to_the_same_field_then_saving_keeps_the_committed_value (its
same-frame case) and typing_and_saving_in_the_same_frame_saves_the_last_keystrokes
both asked scratch() for "same-frame". The tests of the it binary share one
process id and run in parallel, so they got the same folder, and one could
clear it while the other was saving into it: the save then failed to read
back with NotFound. scratch() now adds a number taken per call.
# Conflicts:
#	crates/ui-egui/src/i18n/mod.rs
# Conflicts:
#	ATTRIBUTION.toml
Edit text set a paragraph whose font lacks a typed character (an accent
missing from a subset) in a standard font, always under the resource
name PCEdHelv, whatever face it stood for. Rewriting a bold paragraph
after a regular one replaced PCEdHelv with Times-Bold, so every
paragraph set in the earlier stand-in turned bold, and the editor's
draft measurement saw those paragraphs change and grew its frame over
them.

The stand-in is now named after its face (PCEdTimesBold, ...), as the
line and region rewrites already name theirs.

Fixes #4280
CodeRabbit on #4316: the edit.font-reuse-substitution note said Helvetica
stands in for missing glyphs, but the face follows the source font's
family (Helvetica, Times or Courier) and its weight and slant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.

pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.

The content_copy tool drives it headlessly, across pages and documents.

The desktop keys and menus come in follow-up PRs.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
Review: copying an added JPEG 2000 image failed even within its own
document, because the exporter can't write JPEG 2000 yet. Add Image stores
such a picture as the file it was given, so that file is the copy: it
pastes into the same document as the same object and into another as one
embedded image. The notes now also say that a CMYK picture stored
losslessly turns to RGB in another document.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
The calendar under a date field had no way to pick today: from another
month the only way back was the arrows. A Today button now sits under
the days, centred under the grid. It picks today from whichever month
the calendar shows, exactly as a click on today's cell would: written
with the field's own format through format_date, then the same commit
path, so the field's scripts treat it as they treat a clicked day, the
pick is one undoable edit, and the field and its calendar close. A year the field's date can't hold offers
no button. Hovering it shows the full date. One UI string in all 33
catalogues, marked AI-assisted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:

- the fourteen standard names of Table 184 draw that stamp with
  PdfCraft's stamp drawing (eight from the palette, six with labels of
  their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
  is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
  frame with both diagonals, so the stamp stays visible and selectable.

The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
dialog hides Two-sided while Booklet is chosen and prints the booklet two-sided itself
(Using Acrobat X Pro, Print dialog box options: "Acrobat automatically enables duplex
printing, if available, for booklet printing"; later manuals and the current help no
longer say so, and on a current Acrobat the Two-sided control is shown for Size and
hidden for Booklet with the same duplex-capable printer). print::booklet_duplex gives a
booklet's setting: both sides flip on the short edge, the fold of a 2-up sheet turned
sideways; front or back only is one pass of a manual duplex and prints one-sided. The
Print dialog hides Two-sided while Booklet is chosen and sends that setting, leaving the
Two-sided choice as it was for the other layouts (and as #2001 remembers it); doc_print
applies the same rule. This replaces the first version, which rewrote Two-sided to the
short edge when Booklet was chosen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An AP-less stamp whose /Name has no drawing got its placeholder frame in
the annotation's /C colour (/C [1 0 0] drew a red frame), while the doc
comment and parity note promise a neutral grey one. The frame now always
strokes PLACEHOLDER_GREY; the fourteen Table 184 names keep /C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
main moved the ui-egui integration tests into tests/it, so xtask parity
could no longer find print.booklet's ui-egui evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Found while testing crates/print/src/range.rs:69-76

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A file opened again shows the page, zoom and page display it was last
shown with, as Acrobat's "Restore last view settings when reopening
documents" does: remembered by path (the last 100 files) when its tab
closes or settings are saved with it open, applied over the file's own
initial view, on by default and switched off in Preferences ▸ Documents
and view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up on #2195.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalan, Finnish, Hindi, Indonesian, Korean, Romanian and Albanian have coverage
tests the new preference failed after rebasing onto main; Norwegian Bokmål and Thai
get it too. AI-assisted, marked as not reviewed by a native speaker. Lao and Central
Kurdish fall back to English for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeRabbit on #2195: a file opened while full screen was on entered the presentation
first, and the remembered view then replaced its one fitted page, while leaving full
screen went back to the display from before the restore; and a tab closed (or settings
saved) in full screen remembered full screen's temporary fit and display. The remembered
view is now applied before the presentation starts, and the view remembered is the
normal display (DocView::normal_display: the one presenting replaced) at the page
reached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up on #2195: the last view stored ViewDefaults::zoom_name(),
which rounds 62.5% to 63%. A fixed zoom is now written with its fraction
(with_zoom already reads it back); the fit tokens are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Comments that arrive without /AP are drawn from their dictionaries by
appearance::build, which lost three entries:

- /Border [hr vr w [dash]] (Table 166): the dash array made the whole
  array unreadable, so the width fell back to 1 and the border was solid.
  The width is now element 2 whatever follows, and without /BS the
  /Border dash array is drawn. /BS, when present, wins for both (Table
  166: "the Border entry is ignored"), so a /BS without /W means 1.
- Dash arrays are drawn only when finite, non-negative, not all zero
  (§8.4.3.6) and at most 64 long; anything else draws solid.
- Ink applies its /BS dash pattern (Table 168, Table 182), like the
  other stroked shapes.
- A caret with /Sy /P (Table 183) shows a ¶ (Helvetica, WinAnsi 0xB6) to
  its right in its colour, inside /Rect: in the room /RD leaves, or by
  narrowing the caret. /Sy /None draws as before.

The Helvetica font resource, written out three times, is one helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
ISO 32000-2 §8.6.6.5: "DeviceN colour spaces (PDF 1.3) may contain an
arbitrary number of colour components", and scn takes one operand per
component. The content parser (vendored hayro-syntax) held ten operands
per instruction; pushing the eleventh failed, and the failure ended the
whole content stream. A twelve-ink background fill therefore drew
nothing, and nothing after it was drawn either: a blank page, no error.

hayro-syntax patch (26): an instruction keeps up to 256 operands (enough
for the largest DeviceN space the interpreter reads, 255 inks, plus a
pattern name). Past that the oldest are dropped, in order, rather than
the stream ending: operators already read the operands nearest to them,
so this is how stray operands are treated below the cap too.

hayro-interpret patch (55): a Separation or DeviceN tint transform that
can't be read no longer makes the colour space unreadable (its paints
were drawn in DeviceGray from the first tint, white for a full tint);
it keeps the space, and like a transform that fails when evaluated it
paints that colour in the alternate space's initial colour. Those
fallbacks, and a cs/CS colour space that can't be read, were silent:
they are reported once per content stream as
InterpreterWarning::ColorFallback, which pdfcraft-render records as
RenderWarning::ColourFallback and pdfcraft-cli render prints.

Tests, each seen failing with its fix reverted: the report's twelve-ink
page paints black and draws what follows; a four-ink control; 300 stray
operands before rg still draw; a calculator tint transform that pops
too much, and an unknown function type, fall back with one warning and
the rest of the page drawn; hayro-syntax keeps all twelve scn operands
and, past the cap, the latest 256 in order (fails without the reorder).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Main took hayro-syntax patches 26 and 27 (xref streams), so the patch
that keeps up to 256 operands per instruction moves to 28 in
vendor/README.md and in every comment that cites it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A form that leaves its appearances to the viewer had its text and choice
fields drawn for display (#3475), but not its buttons. Now, also for
display only, without changing the file:

- Check boxes and radio buttons with no normal appearance for their
  state show their /MK background and border (Table 192), and when on,
  the /MK /CA caption as a standard-14 ZapfDingbats character
  (12.7.5.2.3), 4 or l by default, in the /DA colour, auto-sized for
  0 Tf.
- Push buttons with none show their /MK background, their /BS border
  (beveled per Table 168) and their centred caption.
- Comb fields with a border colour draw the MaxLen - 1 lines between
  their positions (Table 228), up to 500 cells. This also applies to the
  appearances PdfCraft saves for comb fields.
- A combo box drawn for display shows a drop-down button on screen, with
  its text kept clear of it. Saved appearances and Print as Image leave
  it out (with_printed_field_appearances, printed_display_bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The on-screen drop-down button for combo boxes drawn display-only, the
narrowed text area beside it, and the Print as Image copy without it
(with_printed_field_appearances, printed_display_bytes) are removed.
Open PR #3567 draws the arrow for every combo box in the page view.
Check boxes, radio buttons, push buttons and comb lines are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
An annotation whose Invisible flag (bit 1) is set and whose /Subtype is none
of the standard types of ISO 32000-2 Table 171 is no longer drawn (Table 167)
or printed (Table 167: "do not print it even if the Print flag is set"). A
/Popup annotation's /AP is no longer painted as page content: per §12.5.6.14 a
popup has no appearance stream of its own and is shown by the viewer as a
window for its parent. Printing already left popups out.

Link appearances are painted as before (§12.5.5).

hayro-interpret patch (56), recorded in vendor/README.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A simple font that a document names but doesn't embed (Arial,Bold, ArialMT, ...) is
drawn with a standard-font stand-in, and each glyph was scaled horizontally to its
/Widths advance, narrower or wider. With 600-unit widths, Helvetica-Bold's i and l
(278 units) were widened 2.16 times into solid blocks, and the line read as a heavy
fixed-pitch face. The advance already places the next glyph (ISO 32000-2 §9.2.4;
Widths, Table 109), so a substituted glyph is still narrowed to a narrower advance
but no longer widened. Exact standard-14 fonts are unchanged.

Vendored hayro-interpret patch 57 (StandardKind::outline_glyph), with tests in
crates/render/tests/it/substitute_widths.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
The print filter read /Subtype with a direct-only lookup, so a standard
annotation whose /Subtype is an indirect reference (ISO 32000-2 §7.3.10)
looked like an unknown type and an Invisible flag dropped it from print
(Table 167 applies only to non-standard types, Table 171). The subtype is now
resolved through the document first.

Also removes a stray cell separator in the hayro-interpret row of
vendor/README.md that pushed the test references for patches (44)-(56) out
of the table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Type0Font::new built its FallbackFontQuery from the Type 0 font
dictionary, which has no /FontDescriptor (ISO 32000-2 §9.7.6.1,
Table 119); the descriptor belongs to the descendant CIDFont (§9.7.4.1,
Table 115). Every request for a non-embedded CID font therefore said
not serif, not italic, weight 400, and no resolver could tell a Song or
Mincho font from a Hei or Gothic one, or see /FontWeight 700 (§9.8,
Tables 120, 121).

FallbackFontQuery::with_descriptor takes the name from the font
dictionary and the style from the given descriptor; new() calls it with
the dictionary's own descriptor, so simple fonts are unchanged. The CID
path passes the descendant's descriptor it already reads.

Vendored hayro-interpret patch (58). Tests in
crates/render/tests/it/cid_fallback_query.rs record the request a
font_resolver receives. The CID-fonts parity note now says the Japanese
face also follows the Serif flag and /FontWeight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Two stray `|` separators inside the Test column (before (44) in the
hayro-interpret row, before (22) in the hayro row) made those rows six
cells wide in a five-column table, so the trailing test references fell
outside the rendered table (markdownlint MD056).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
ViewportCommand::BeginResize and StartDrag hand the pointer to the
compositor, which keeps the button's release on Wayland; winit reports
only that the pointer left, and egui doesn't count that as a release. egui
then still thought the primary button held, so the next press on an edge
or the tab strip didn't start a drag until a click inside the window.

window_controls::desktop_grab notes each hand-over, and
release_after_desktop_grab (called first in raw_input_hook) adds the
missed release to the next input, once, unless that input brings one.
Ported from WordCraft 1581c288, confirmed by hand there for repeated edge
resizes on GNOME/Wayland.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
The release a desktop grab kept from egui was appended to the next input,
so when that input carried the press of a second resize or move the
button ended the frame up and the drag stopped. It now goes before the
input's first primary press; only a release ahead of any press counts as
the kept one, since a quick click's release belongs to its own press.
Found by CodeRabbit in review of #3970.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs
crates/engine/src/js.rs: kept main's DocInfo.xfa field and multi-line layout, added the PR's xfa_active gate on doc_scripts.
crates/forms/src/tests.rs: append conflict, kept both tests.
crates/forms/src/tests.rs: append conflict, kept both tests.
crates/forms/src/tests.rs: append conflict, kept both tests.
Conflicts: apps/pdfcraft-cli/src/main.rs (render, check_one, check): kept main's bodies, positional() takes the option spec.
Conflicts: ROADMAP.md and docs/roadmap.md took both sides' status lines, progress-log and change-log rows; M10 table row from #4311, M11 row from #4310.
Conflicts: Cargo.lock: kept ours (the PR's ctutils bump is dropped).
parity/acrobat-features.toml: form.xfa-dynamic evidence list: both sides' entries; notes: both sides' insertions (word-level merge).
parity/acrobat-features.toml: form.fill-text-field evidence is the union of both sides; notes keep both sides' sentences (DoNotScroll clause and the Password sentence).
crates/redact/src/interp.rs: kept both sides: main's per-page DecodeBudget and the PR's
visits counter (MAX_FORM_VISITS, same 50,000 as MAX_PAGE_FORMS; the visit check fires first).
Conflicts: ROADMAP.md: kept both progress-log lines.
crates/automation/src/tools.rs: js_run description: both sides' wording merged.
crates/ui-egui/src/object_ui.rs: main removed the tests module's translation test; kept the PR's two
new delete tests and dropped the translation test.
Conflicts:
- parity/acrobat-features.toml: comment.note: kept main's icon notes text, added the NoRotate sentence and the PR's three evidence entries.
- vendor/README.md: kept main's rows; added the PR's hayro-interpret patch as (53) with its test (53).
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has both sentences. parity/acrobat-features.toml: export.docx evidence has both tests, notes has both statements.
crates/forms/src/tests.rs: append conflict, kept both tests.
parity/acrobat-features.toml: form.fill-text-field evidence is the union; notes keep HEAD's text plus the comb-cells sentence.
Conflicts: crates/automation/src/printing.rs: kept job_duplex (#2748) next to bad; #4161's paper_named/spool_job unchanged.
crates/edit/src/tests.rs: both sides appended tests at the end; kept both.
Cargo.lock: unchanged from base (the PR's ctutils bump did not apply).
Conflicts:
- crates/ui-egui/tests/it/comments.rs: kept both PRs' new tests (adjacent additions).
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has the soft-mask sentence added to ours, M11 row from the earlier merge.
parity/acrobat-features.toml: form.xfa-dynamic evidence: both sides' entries; notes: PR's metrics sentence replaces 'metrics are approximate' on top of main's text (word-level merge).
ROADMAP.md: kept both progress-log lines, newest first by position (HEAD's #4280 line, then this one).
Conflicts:
- ATTRIBUTION.toml: kept main's side; translation catalogs carry no sha256 on main, so the PR's sha256 lines for them are dropped.
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
Conflicts:
- ROADMAP.md: progress log, kept both lines.
crates/js/src/lib.rs: module doc app/util list has execMenuItem and scand; install() doc comment from main.
crates/js/src/tests.rs, crates/engine/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: js notes: execMenuItem wording plus scand wording.
Conflicts:
- crates/ui-egui/src/i18n/mod.rs: kept main's side (main dropped the per-language translation tests; the PR's drawing_scale_labels_are_translated_in_every_catalog is not carried over).
- parity/acrobat-features.toml: measure.distance evidence: kept main's paths (tests/it/), appended the PR's two new entries.
Conflicts: vendor/README.md: hayro-interpret row keeps #4345's patch (55) and #4371's patch (56), with both test lists.
crates/xfa/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: form.xfa-dynamic evidence and notes: both sides' insertions (word-level merge).
crates/engine/src/lib.rs: union of the added:: re-exports (paste_place/paste_rect from #4230, CHAR_SPACING_LIMIT).
crates/ui-egui/src/content_ui.rs: editor keeps #4178's anchored width and horizontal_align and #4266's spacing layouter; spaced_job takes the alignment so the custom layouter keeps it.
crates/ui-egui/tests/it/content.rs: kept both tests.
Conflicts:
- ATTRIBUTION.toml: fi.tsv source: main's text plus the PR's added sentence; the PR's sha256 line dropped (translation catalogs carry none on main).
- ATTRIBUTION.md: main's rows, fi row updated to the same source text.
parity/acrobat-features.toml: form.xfa-dynamic evidence: both sides' entries; notes: both sides' insertions.
Conflicts: vendor/README.md: hayro-interpret row keeps patches (55), (56) and #4374's (57), with their test lists.
crates/xfa/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: form.xfa-dynamic evidence and notes: both sides' insertions.
Conflicts: vendor/README.md: hayro-interpret row keeps patches (55) to (57) and #4380's (58), with their test lists.
Conflicts: docs/roadmap.md: all status lines kept, M10 row from main, M11 row with #3227's two sentences. parity/acrobat-features.toml: save-with-object-streams entry from #3227 with its test paths under tests/it/.
Conflicts: ATTRIBUTION.toml: took main's side (hashes recomputed after the merges). crates/optimize/src/lib.rs, crates/automation/src/lib.rs, tools.rs: Settings and tool schema keep object_streams (#3227) and lzw_to_flate (#4181); doc_optimize description names both options. crates/optimize/tests/optimize.rs: both sides' imports and tests kept.
#3670 raised the form depth limit to 50 and added its own 50,000-visit cap. Main's
per-page DecodeBudget (e2a677164) has the same 50,000-form limit, and the visit cap
fired first, so Apply removed the forms instead of refusing the page as main does.
The visit cap goes; the fan-out test expects the refusal, and the depth-limit
sanitization test builds its chain from MAX_DEPTH.
crates/ui-egui/src/ocr_ui.rs: kept both sides in poll_ocr (poisoned-lock handling plus the automatic-run wait) and in the test module (one mod, both sets of tests); the 4196 test literal gets automatic: None.
ROADMAP.md: kept both progress lines.
crates/optimize/README.md: PR's decoder sentence plus the target-version section from main.
Cargo.lock: auto-merged.
tests/it/control.rs: kept main's side (the PR's unrelated wait-loop hunk dropped, per the review's fix).
Cargo.lock: took main's.
crates/automation/src/a11y.rs: kept both sides (PR's helpers, then main's request_json doc comment).
i18n ca, fi, hi, id, ko, ro, sq .tsv: kept both sides' rows (no duplicate keys).
Session::refresh_with_indexes fills title, author and the other fields from XMP as build_document does; the test applies an edit and an undo and checks they stay.
# Conflicts:
#	crates/ui-egui/tests/it/forms.rs
#3670 raised the form depth limit to 50 and added its own 50,000-visit cap. Main's
per-page DecodeBudget (e2a677164) has the same 50,000-form limit, and the visit cap
fired first, so Apply removed the forms instead of refusing the page as main does.
The visit cap goes; the fan-out test expects the refusal, and the depth-limit
sanitization test builds its chain from MAX_DEPTH.
# Conflicts:
#	crates/ui-egui/tests/it/control.rs
#	crates/ui-egui/tests/it/forms.rs
The reset uses pressed_page (unobstructed, pointer over the page) instead of the raw press origin; the test presses with a dialog open.
# Conflicts:
#	crates/engine/src/js.rs
Editing a large comment whose top-right corner was scrolled away opened a composer that was hidden and could not be cancelled. The card now stays, clamped into the document, while any part of the comment shows.
Recording a save reopens the saved file, which carries no Password values; they are put back in memory only, so the next save still omits them.
A script looping on app.execMenuItem could queue unlimited requests; they now follow the same count and byte limits as other requests.
The fit check required the padded height for every line; it now uses the bound the appearance applies, so a 16 pt field at 12 pt takes one line.
attachment_add used an unbounded std::fs::read; it now uses read_input like the other file-reading tools, and the oversized-input test covers it.
"Attach {name}" and "Delete attachment {key}" were missing from EDIT_LABELS, so the translation-gap report left them out.
Object streams raise the header to 1.5 over a lower catalog /Version; pdf_version now reports the version viewers read.
ShowPanel now queues the same Inspector reveal a user's panel choice does.
main's #4284 stopped hashing translation catalogues in ATTRIBUTION.toml; the
conflict took main's side (no sha256 lines) and kept this branch's source notes.
Integration batch 3: 47 contributor PRs (editing, forms, XFA, rendering)
Integration batch 4: 26 contributor PRs (comments, viewer, pages, printing), after #4390
Integration batch 5: 9 contributor PRs (OCR, attachments, documents, selection), after #4391
# Conflicts:
#	crates/ui-egui/src/i18n/ar.tsv
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/ckb.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/kk.tsv
#	crates/ui-egui/src/i18n/lo.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/te.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
TextBox::fits checked only the height of a wrapped multi-line value.
Long words break by character, so a single character wider than the
box still overflows it, and the appearance reports Overflow::Wide for
a value fits had accepted. The multi-line branch now also requires
every wrapped line to fit the width.
hayro-cmap reads CMap streams with hayro-postscript's token scanner,
whose object::read skipped each `<<` and `>>` by calling itself again.
A CMap holding a long run of `<` or `>` used one stack frame per pair,
overflowed the stack and aborted the process.

Vendor hayro-postscript 0.1.0 with read() looping over the delimiters
instead (from storytold/craft-libs#263), patched in beside hayro-cmap.
Four object::tests cover long `<<` and `>>` runs and the skipped
delimiters, plain and nested.
* Fixed nfpm installation site

* fix bracket
content_draws tells whether some content draws an XObject, directly or
through a form that has no /Resources of its own and so draws through
its caller's. Since #4356 the recursion passed the caller's content
again instead of the nested form's decoded data, so a name drawn only
inside such a form was never found.

Both callers then dropped the entry: unlink_doomed for page-tree
resources (Apply Redactions and the content pass of Remove Hidden
Information) and prune_doomed_xobjects for surviving forms. An image
removed under a mark but still shown elsewhere through such a form went
missing from there as well.

The recursion reads the nested form's data again, as it did before
#4356. Two tests cover the page-tree case and the surviving-form case.
Both fail without the fix.
Clean Up removes named destinations that nothing in the document refers
to. referenced_names counted the /D of GoTo, GoToR and GoToE actions
alike. The /D of a GoToR or GoToE action names a destination in another
document, so a local destination with the same name was kept although
nothing in the document used it.

Only a GoTo's /D now counts as a use. A /Dest key and a GoTo's /D are
read as before.

Test: links with a GoTo, a GoToR and a GoToE action name three
destinations; optimize() keeps the one the GoTo uses and removes the
other two.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
With the view turned 90 or 270 degrees, the page's screen rectangle has
its sides swapped, so xf.rect.width() / xf.pw was off by the page's
height over width: Fill & Sign, form fields and Add text drew typed text
at the wrong size, and Measure snapping, Prepare form, the calculation
order and field table overlays were sized wrong too. They now use
PageXform::zoom (from #3749), which reads the side the width is on.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The cell pixmap was round(scale x step) pixels and repeated, but the cell
was drawn, and the paint transform mapped back, at the unrounded scale.
So the pixmap repeated a fraction of a pixel too far at every step (31 px
for a 30.56 px step at 110 dpi), and cells drifted further from their
place with every step from the pattern's origin.

Vendored hayro patch (25), `tiling_cell_pixels`: the cell is drawn at
round(scale x step) / step, so one step is exactly the pixmap's side, and
the paint transform uses that same scale. The side stays within 1 and the
existing 3000 px cap; a non-finite product keeps the scale and a bounded
side.

Tests: crates/render/tests/it/tiling_cell_placement.rs (fill, turned and
scaled matrix, stroke, all at 110 dpi far from the origin; the helper's
bounds).


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
With only --automation-read-root / --automation-write-root set, a path
went straight to std::fs::canonicalize, so a UNC, WebDAV or device path
(`\host\share\x.pdf`, `\.\pipe\x`) was resolved, contacting the host,
before it was refused. `--root` mode already refuses those by name
(`foreign_share`).

`foreign_share` moves into file_access, and `AutomationRoots::check_path`
applies it to the path made absolute (by name, nothing read) before
following links, with the same refusal as any other path outside the
root. A root on a share still accepts that share. `resolve` is split
into `absolute` (empty path, device names, made absolute) and `resolve`
(links followed), so `out\NUL` keeps its own refusal.

Part of #4003 (the first of its three points).
* M12: XFA check boxes sit where their margins, caption and alignment put them

* M12: a check box's automatic caption room and auto height follow the box as drawn

* M12: a caption reserve of zero sizes the caption to its content, as no reserve does
A Line annotation without an appearance was drawn along /L with its
endings there, ignoring /LL, /LLE, /LLO, /Cap, /CP and /CO. With /LL,
/L gives the ends of the leader lines, not of the line itself.

- The line sits |LL| + LLO from /L, on the left of travel for a
  positive /LL; leader lines run from LLO off /L to LLE past the line.
- /LE endings are drawn at the line itself.
- /Cap true draws /Contents (or the plain text of /RC) in black
  Helvetica 10, turned with the line: Inline breaks the stroke around
  it, Top sets it above, /CO shifts it (with a tick when moved off).
- PdfCraft measurements keep /PCMeasureValue as their caption.
- undrawable_text reports caption text the standard font can't draw.
- Non-finite, huge and negative values, and a zero-length /L, are
  handled leniently; captions are cut to 256 characters.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- render: DocInfo::open_dest resolves /OpenAction (an explicit destination,
  a /GoTo action's /D, or a name or string through /Names /Dests or the
  catalog /Dests) with the existing bounded dest() resolver.
- ui-egui: apply_initial_view goes there with DocView::go_to_dest, so the
  /XYZ point lands at the window's top-left and /FitH top at its top edge.
  With "Always use my default zoom and page layout", only the page.
- organize, ui-egui: /ViewerPreferences /NonFullScreenPageMode (Table 147)
  is read for /PageMode /FullScreen documents and names the panel shown
  behind the full-screen prompt; such a document no longer gets the outline
  panel only because it has an outline.
- organize: set_initial_view keeps an opening action whose page and
  magnification are unchanged, instead of dropping its position or deleting
  a named go-to.
- automation: doc_info reports open_destination.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M4: Document Properties has a Custom tab for custom properties

Document Properties has a new Custom tab between Fonts and Advanced. It
lists the document's custom properties, and Name and Value fields with
Add, Update and Delete edit the list. Clicking a listed property fills
the fields with it. OK writes the changes through SetInfo in the
existing "Change document properties" step, so one undo reverts them
with the other tabs' changes.

Add is off for an empty value, a name already listed, a name that
check_custom_key refuses (its message is the button's tooltip), and a
name the file already uses for an entry the list leaves out (not text,
only padding, or past the 256 listed). A property already in the file
under a refused name or a name that is not UTF-8 is listed, and Add,
Update and Delete stay off for it, so OK cannot fail on it or write a
different key.

Clearing an /Info entry now removes it when /Info is a dictionary
written directly in the trailer. set_info_entry returned without a
change there, so a delete in the Custom tab, a cleared Description field
and a doc_set_info clear left the entry in the file. It now writes a
copy without the entry as a new indirect /Info.

The engine re-exports check_custom_key and adds Document::has_info
(organize::has_info). set_option("dialog", "custom") opens the tab.

* Contributors: add dotCooCoo (Robert Lee)

* M4: Add in the Custom tab stops at the 256 properties the tab lists

Document Properties > Custom lists at most 256 properties, so a property
added past that was saved but left out of the list when the dialog opened
again. Add is now off once the tab's list or the file's own list holds 256;
a new name goes after the file's entries, so a file with more than 256 keeps
Add off after a Delete as well.
The Bookmarks panel opened every top-level bookmark whatever its /Count
said, so a bookmark with a negative /Count showed its children. Every
level now starts as its /Count says (positive open; negative or missing
closed). The reader's toggles and a search still override it.

Moving a bookmark into a different parent (Indent) now opens that
parent, as adding one already did; the forced top-level open used to
hide that it stayed closed.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): header, footer and watermark previews step through the pages

The marks dialogs preview the current page, or the first page the range
selects when it leaves the current one out, with no way to look at
another page. Arrows beside the page label step the preview to the
previous or next page the range selects. The page stepped to resets
each time the dialog opens.

* fix(ui): the marks preview forgets a stepped-to page the range leaves out
Found while testing crates/xfdf/src/lib.rs:274-276 (csv) used by annot_from_xml at :924-928

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
* refactor(xfa): replace model wildcard imports with explicit names (Sonar S2208)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(xfa): complete explicit model imports

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(xfa): drop unused model imports

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Import Corner, which main's parse.rs now uses

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
ISO 32000-2 9.3.6: for a Type 3 font, modes 3 and 7 shall not be
rendered, the other modes render the glyph descriptions, and modes
4 to 7 add nothing to the clipping path. Vendored hayro-interpret
patch 42 drew every mode but 3 filled, so 7 Tr painted the glyphs
in the fill colour (black by default) wherever a following fill
did not cover them.

Patch 64: show_glyph keeps mode 7 for Type 3 glyphs and reports
them as clipping text (GlyphDrawMode::Clip, painting nothing, kept
by extraction), and clip_glyph's Type 3 arm is a deliberate no-op.
Modes 0-2 and 4-6 still render filled.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The special colourant name All refers to every colourant of the output
device at once, and on an additive device its subtractive tint is
complemented before it is applied (ISO 32000-2 §8.6.6.4). The vendored
colour code treated All as an ordinary spot colourant and drew it through
its tint transform, so a /DeviceCMYK transform giving cyan drew 0.5 scn
as light cyan instead of a 50% gray.

Vendored hayro-interpret patch 66: Separation::convert_f32 writes the gray
1 − t for each tint of an All space without evaluating the tint transform.
Fills, strokes, images and shadings in that space all convert through it,
and so does the RGB print raster. /None still paints nothing, other names
still use their alternate, and a DeviceN space naming All keeps its
alternate (the clause defines the name for Separation spaces).

Tests: crates/render/tests/it/separation_all.rs (synthetic).


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M14 #4253: remember the annotation tool pin across restarts

* M14 #4253: clarify roadmap timezone and Markdown formatting

* Keep main's roadmap status lines; only the progress-log entry is new

* Take main's forms.rs: main already gives each form test its own folder

* Roadmap: drop the form-test folder note (main already has its own)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
A combo box with the Edit flag (bit 19) opened only its list, so a value
that isn't one of its options could not be typed on the page, although
form_fill already took one. ISO 32000-2 12.7.5.4 and Table 233: with Edit
set, "the combo box shall include an editable text box as well as a
drop-down list".

- An editable combo box now opens a single-line text box over the field
  as well as its list. Enter, Tab or a click away commits the text as
  Edit::SetFieldValue with a Choice, the path form_fill and the list use,
  so undo, field scripts and the appearance follow; a pick from the list
  still sets an option.
- The editor starts on the shown text of the picked option, and typing a
  shown text picks that option (the engine maps it to its export value).
- A combo box without Edit is unchanged: a drop-down list only.
- Tests: kittest typing into an editable combo (Enter, list pick, Tab,
  save, undo) and a non-editable one refusing typing; automation
  form_fill takes free text only with Edit, drawn before and after save.
- Parity: form.fill-choice notes and evidence.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
The page view had no single reading of the pointer. About twenty page modules each
read egui's raw state (hover_pos, latest_pos, interact_pos, press_origin) and tested it
against an unclipped page rectangle. That state knows nothing of what is drawn over the
pages, and hover_pos is None on the frame a finger or a Windows pen lifts: egui-winit
sends PointerGone with every touch release, and winit reports Windows pen input as
touch. So a tap placed no stamp, followed no link and ticked no check box. A touch drag
of a comment never committed, and the next drag anywhere on the page moved the comment.
The Stamp crosshair showed on the command palette, on the quick bar and on the chrome
over a page scrolled under it, a link's tip floated over the Unlock prompt, and a
double-click on a dialog's backdrop selected the word under it.

canvas::document_area now reads one PagePointer per frame from the pages' content
widget (step 1 of the page pointer router in the cursor-handling review):
- pos: interact_pos, which keeps the position through a touch release, only while egui
  has the pages under the pointer with nothing over them (contains_pointer, and the top
  layer there is the pages', as the closed #2626 did) or while the pages hold a press.
- origin: where the held press began, also on the frame that brings both the press and
  its release. This replaces the text layer's raw-event fallback (#2100).
- edges: the frame's primary presses and releases on the pages, in order.

It reaches the page modules through comments::PageCx and as an argument: the stamp
blocks, text layer, page press and link and comment hover in canvas.rs; comments and the
signature drag preview; forms and Prepare a form; Fill & Sign and the Sign area; crop,
marquee zoom, snapshot, camera and measure; redact and the Link tool; added content,
Edit text, images and objects. page_covered, a hand-kept list of what covers the pages
that missed the Unlock prompt, is gone. The pages' hover tip is click-through, as a
tooltip is, so a quick move onto it does not take the pointer off the link under it.

Tests: tests/it/support/input.rs sends pointer input in the frames the platform delivers
it in (a touch tap and drag, a click in one frame, a double-click). kittest otherwise
runs one frame per event, so the suite never saw a touch release. tests/it/pointer.rs
holds the review's probes and #2626's layer tests. With the fix reverted, the
tap (stamp, link, check box), touch drag, palette, quick bar, chrome, Unlock tip and
dialog double-click tests fail, and the mouse-click and one-frame-click controls pass.
The tip test fails with the tip interactable, and each page_pointer unit test fails
under a mutation of the part it pins. The probes for later steps stay ignored: the
link hand and the I-beam over other tools' targets (step 2), paragraph edges (step 3)
and Escape mid-drag (step 4). touch_tap_ticks_a_check_box is ignored until the
check box focus fix that follows: since #2223 a tap on a check box counts as a click away
from its focus.

tests/it/comments.rs: the multiline markup hover test (#3541) hovered a point below the
window on a page rotated 270 degrees, and its tip showed only because the hover was
tested against the page's whole rectangle. Its harness now zooms to 100 %, so every
rotation shows the whole page, and the hover helper asserts the point is on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plain #[test] in page_pointer.rs reads the source of the page modules (the per-page
chain in canvas::document_area and the modules it calls) and fails on any raw read of
egui's pointer state: hover_pos, interact_pos, latest_pos and press_origin, Context's
pointer_*_pos shortcuts, and the primary_* and any_* button reads. Its message names the
file and line and points to PagePointer. 24 open PRs add such reads to these modules, and
three of them (#2137, #1709, #2235) merge cleanly on top of the snapshot, so they would
quietly bring back the touch and overlay bugs it fixed. CI's existing cargo test
--workspace runs it on Linux and Windows.

To pass, the snapshot also carries the button state (primary_down and any_down, global as
before), and the page-input button reads that were left use it or the frame's press edge:
drag ends in Prepare, redact, the Link tool, added content, Edit text and objects, the
comment grab check, a text field opening on the press, and the press that makes a page
current. Code in these files that is not page input is listed, each with its reason, in
NOT_PAGE_INPUT: the field, paragraph and added-text editors (Areas of their own), the
signature dialog's Scan tab, Organize Pages, the page size hint, comment key handling, and
four view gates in document_area that run before the pages' widget exists (zoom anchor,
drawing edge scroll, glide interruption, full screen's click to turn the page), now small
named functions. Prepare a form's calculation order overlay (calc_order_ui) is a page module
too, and takes the pages' pointer for its hover. The test also fails
on a list entry that reads no pointer state any more, and on a source file that takes
PagePointer without being listed as a page module.

Seen failing: a raw hover_pos() put back into zoom_snap.rs, and main's raw read put back
into the stamp block of document_area, are reported with their lines, as are a stale list
entry and crop.rs left off the module list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A one-line non-negotiable in CLAUDE.md, next to Layering, so agents and contributors
know the rule before the test tells them: page modules read the pointer through
PagePointer (crates/ui-egui/src/page_pointer.rs), never hover_pos, interact_pos,
latest_pos, press_origin or the button reads. It names the test that enforces it,
page_pointer::tests::page_modules_read_the_pointer_only_through_page_pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The form field editors and the Edit text paragraph editor are drawn over the pages,
outside the page pointer, and close on a click away. With a finger, or a pen on
Windows (winit reports it as touch), they closed on taps inside them:
- A tap on a combo box opened its list and closed it in the same frame, so a choice
  could not be picked by touch.
- A tap on a list box row picked it, but the list lost focus, so the arrow keys no
  longer moved the choice.
- A tap on a date field's calendar arrows closed the field.
- With a paragraph open in Edit text, a tap on a Format text control that only
  opens a list, such as Line spacing, applied and closed the paragraph, and the list
  went with it.

Two things were wrong. The combo and list boxes judged the click by hover_pos, and
the paragraph's "outside the document" by latest_pos. Both are None on the frame egui
reports a tap, since egui-winit sends PointerGone with every touch release. They now
read interact_pos, which keeps the tap's position on that frame (egui's own
clicked_elsewhere does the same). And egui 0.36 reports a lost focus for two frames.
The text field editor and the paragraph editor judged their exceptions again on the
second frame, when a tap has no position left and a mouse may have moved on. They now
judge a loss once, on its own frame (edit_text_ui::lost_focus_once, with a flag kept
per editor). A focus taken later in a frame is still seen on the next one, as before.

Tests: tests/it/editor_taps.rs taps with support::input::touch_tap, which sends
the release and PointerGone in one frame as egui-winit does, and pairs each tap with a
mouse click as the control. That click leaves the pointer in the window: kittest's
drop_at sends PointerGone a frame later, which the second-frame judging also turned
into a click away. With the source reverted, the four tap tests fail at their bug
assertion and the controls pass. Reverting one part at a time fails only its own
tests: hover_pos in the combo and list check, egui's lost_focus in the field editor
(the calendar) or in the paragraph editor, and latest_pos for the paragraph's outside.
A tap inside an open text field already worked and has a guard test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A click on a check box or radio button focuses it, so Tab, Space and the arrows reach it
(#2223). The page hands the click to the field, and the field overlay toggles the box in
the same frame unless the click landed elsewhere. That check read hover_pos, which is None
on the frame egui reports a tap: egui-winit sends PointerGone with every touch release,
and winit reports Windows pen input as touch. So a finger or pen tap counted as a click
away. The focus closed and the toggle was dropped: a tap left a check box unticked and
unfocused, and a tap on a radio button selected nothing. The check now uses the overlay's
clicked_outside, which reads interact_pos, as the other field editors do.

Tests: touch_tap_ticks_a_check_box runs again. tests/it/editor_taps.rs taps a focused
check box and the other button of a focused radio group, each with a mouse click as the
control. Each checks that the focus stays and the tap toggles, and for the radio group that
the arrows still move within it. With the fix reverted, the three tap tests fail and the
controls pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page pointer has the pointer only where egui has the pages on top, and egui knows what
is drawn over them from the layers it met in the frame before. Dialogs are drawn after the
pages, so on the frame one opens egui has not met its layer yet. For that frame the pages
kept the pointer, and a press that landed then was theirs: a link's tip showed over the
dialog, and a click placed a stamp or followed a link under it. main's page_covered had
kept the link's tip and click off the page while a dialog was open, from a hand-kept list.

PagePointer::read now also takes whether a dialog is open: PdfCraftApp::modal_open, the
list the page press and the edge scroll already check, the Unlock prompt included. With one
open the pages see no pointer and no press, except a drag they already have, which goes on
to its release. The palette is not a dialog and the page beside it keeps its tools, so it
is left to egui's layers, from the frame after it opens.

Tests: tests/it/pointer.rs hovers a link and opens the Unlock prompt (no tip in that
frame), and clicks with the Stamp tool in the frame the prompt opens (no stamp). A
page_pointer unit test hides the pointer and a click under a dialog, and keeps a drag the
pages have until its release. With the fix reverted, all three fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#4430's merge with main left mod type3_text_clip before mod tiling_cell_placement,
so main's fmt check failed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(ui): text typed into a new text box counts as unsaved work

Closing a tab or quitting while the only unsaved thing was text in a new Add text box skipped the save prompt and lost the text. The box now counts like fill text does: only when it holds non-blank text.

* fix(ui): saving on close keeps text typed into a new text box
Conflicts: optimize_ui.rs imports (this branch's RichText, main's Compatibility; both kept)
and the organize.document-properties parity entry: both evidence lists kept, main's XMP note
kept, and the tab list now names main's Custom tab (#4387) instead of saying there is none.
Conflicts:
- crates/render/src/inspect.rs: this branch's reply-linking methods and main's NoRotate
  rectangles (#3016) both go in the Annotation impl; the annotation reader takes this
  branch's loop with main's no_rotate field.
- crates/automation/src/comments.rs: this branch's HashMap import, main's import list.
- ATTRIBUTION.toml: each catalogue's source note keeps main's secured-document clause and
  this branch's reply-count clause; ATTRIBUTION.md regenerated (cargo xtask assets --write).
- The 33 catalogues: main's new rows, then this branch's reply-count row.

main's #3016 tests build Annotation field by field; they now set parent and grouped.
Integration batch 6: 6 contributor PRs (All tools view, page pointer, comments, redaction, Properties, Home)
* fix(forms): align has_password_flag traversal depth with field enumeration

* fix(forms): extend field_chain depth to 64 and fix test formatting
* fix(redact): remove AcroForm on Hidden::FormFields even when field count is zero

* fix(redact): report AcroForm presence in scan() and format
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found while testing crates/xfa/src/pdf.rs:50-52 (parse_hex32)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/forms/src/af.rs:844-848 (number_text), used by format_value at :1239 and :1256

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2280-2284

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Found while testing crates/js/src/formcalc.rs:2114-2119

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
Header and footer dates had US, ISO and day-first slash forms only. The
dotted forms written in German-speaking and much of Central and Eastern
Europe join them: <<dd.mm.yyyy>>, <<d.m.yyyy>>, <<dd.mm.yy>>, <<d.m.yy>>.
The dialog's date list offers the first three after the existing ones.

Generated with Claude Code

Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Claude <noreply@anthropic.com>
* fix(platform): a saved file keeps its hidden and system attributes on Windows

Every save writes a staging file and renames it over the target, and on
Windows the renamed file keeps the staging file's attributes, not the
replaced file's: a hidden or system file came out of a save visible. The
staging file is now created with the replaced file's FILE_ATTRIBUTE_HIDDEN
and FILE_ATTRIBUTE_SYSTEM (std's OpenOptionsExt::attributes, no unsafe), so
it is hidden from its first moment when the target is, and plain when the
target is plain. A new file gets neither. Read-only targets are still
refused before staging; the archive bit is left to the system.

The test saves plain, hidden, system and hidden+system files through both
staging-name forms and checks the attributes on the staging file before the
flush and on the saved file after; it fails with the fix reverted.

Refs #4593

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(platform): on Windows a saved link takes the link's attributes, not its target's

A save replaces a symbolic link with a regular file, so the attributes it
keeps are the link's own. `metadata` followed the link, and a plain link to
a hidden file came out of a save as a hidden file. Read them with
`symlink_metadata`, as the macOS attribute copy already does. Unix keeps
reading the mode through the link, as before.

The test skips where symbolic links can't be created (no Developer Mode or
administrator rights), as it does on the machine this was written on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Report generators draw each picture as `q re W n cm Do Q` inside a clip the section shares with
its text. `image_transform_scope` only accepted a scope that contained every active clip, which
none does there, so it fell back to transforming the `Do` alone: the picture grew, but its own
clip stayed at the old size and the enlarged picture showed only through its old box.

When no scope contains every clip, the outermost scope holding only the image is used: it holds
all of the image's private clips, which now move with it, while the outer clip, shared with
other content, stays where it is. A clip shared with a caption inside the same scope still stays
fixed (`cropped_image_resizing_moves_private_clips_and_keeps_shared_ones`). Crop uses the same
scope, so cropping such a picture replaces its own clip instead of nesting a second one.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The AccessKit root node had no name, so AT-SPI clients saw a top-level
frame with an empty accessible name. It now takes the window title each
frame and follows it when the title changes.
* fix(xfdf): an empty field in imported XFDF clears the field, as FDF and XML do

* fix(xfdf): a field holding only value-richtext is not cleared on import

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.5: crop an added image by more than half from one side

Each side of an added image's crop was capped at 45 %, so keeping its right
third (70 % off the left) wasn't possible, and the drawing silently cut a
larger crop down. The limit is now per axis: a side may trim up to 95 % as
long as left and right, and bottom and top, keep at least 5 % together.
AddedImage::crop_valid checks input (content_update refuses the rest with
the rule), clamp_crop brings a crop read from a file into range keeping the
sides' proportion, and the Edit panel lets a side go up to what the opposite
leaves.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

* M7.5: accept crops that sum to the limit; keep untouched crop sides exact

`0.05 + 0.9` is just above `0.95` in floating point, so `crop_valid` rejected a crop that
keeps 5 %. Sums now pass within a rounding tolerance, and `clamp_crop` leaves such a crop as
it is. The image panel no longer rounds the stored crop to whole percent or clamps sides the
user doesn't touch: only an edited side is written back.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

* M7.5: an excessive crop from a file keeps its proportion

`clamp_crop` capped each side before fitting the pair, so `[1.5, 0, 0.5, 0]` lost its 3:1
proportion. The pair is now scaled first, from its halves so two huge sides can't overflow;
a pair within the limit keeps each side within it.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>

---------

Signed-off-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Marco Hagspiel <m.hagspiel@lcs-group.com>
Co-authored-by: Claude <noreply@anthropic.com>
* M9: name a laid-out XFA form's signature fields by their SOM path, so other viewers find the signature

* M9: doc comments on the functions the signature naming touches

* M9: say other viewers in the signature naming's comments

* M9: a new layout keeps a signature a generated signature box holds

* M9: the re-layout test checks the signature's picture is kept too

* M9: a signature field named by its SOM path doesn't repeat a name a field already has

* M9: every name a field of the pass takes is known before a signature field is named, in either order

* M9: the signed-box test's doc comment is back on its test

A merge had left it above the next test, which has its own.
* M3: give the kept button release back before the pointer leaves (Wayland)

After a desktop grab the next input on Wayland often brings only
PointerGone. The synthetic release went after it, and a button event
sets egui's pointer position, so the gone pointer came back and hover
stuck at the old place until the pointer moved. The release now goes
before the first PointerGone as well as before the first press; a
release that follows PointerGone is still the kept one and not doubled.
Found by CodeRabbit in review of #3970.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs

* M3: move a release that follows the pointer leaving in front of it

A release the input brings after PointerGone is the one the desktop kept.
Left after PointerGone, it brought the gone pointer back at the release
position; it now moves in front, still not doubled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rPQFoRmQbw8vWhGgGVrRs

---------

Co-authored-by: bmh129 <11478682+bmh129@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Five XFA tests failed at random on a busy machine. The scripts of an
opening or a change share a 3 s wall-clock budget (a click's share 5 s),
each script has 1 s or 3 s, and the engine has 10 s to start; under load
the tests went past one of them, and the scripts they check were skipped
or did not start.

Session::with_xfa_time_scale (and Automation::with_xfa_time_scale)
multiplies each of these limits for a test; the default stays 1. The
script session counts the engine threads it starts on the calling thread
(xfa_session::engines_started).

The hundreds-of-calculations tests run with the limits raised, and the
engine's test now checks that opening starts one engine and a change one
more. A new test sets the factor to 0 and checks that the budget skips
all 210 calculations and says so. Three more tests that failed the same
way under load run with the limits raised.
* i18n(fa): add Persian interface translation catalog

Add crates/ui-egui/src/i18n/fa.tsv covering the current 2,776-key union, register fa (right-to-left, one/other plurals) in i18n and the date languages, and credit the catalog in ATTRIBUTION.

* i18n(fa): fix missing ZWNJ in compound suffixes

Join participles (شده/نشده) and productive suffixes (سازی, گذاری, گیری, بندی, پذیری, کننده, ...) and compound prefixes (هم, غیر, پیش, پس, چند, تک, باز) with U+200C, plus reviewer wording fixes.

* fix(i18n): Persian counts 0 as singular (CLDR fa) and writes weekday names with ZWNJ

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>
Opening a static XFA form gives its fields the values in its datasets
(a form filled in by another application). Each value went through
forms::set_value, which reads the whole field tree to find the field,
writes it, and recalculates, reading the tree again. With n values that
is about 2n readings of n fields: quadratic. A public 136-page form with
about 6,000 fields took 261 s to open, a 95-page one with about 4,000
took 112 s; 8,000 synthetic fields took over ten minutes.

forms::set_values fills a batch as set_value fills each in turn, reading
the form once and recalculating once after the last value (the
calculation order, ISO 32000-2 Table 224 /CO, runs once over the final
values). Fields with the same fully qualified name (12.7.4.2) still share
the value. A name the batch already wrote, or a twin of it, makes the
form be read again first, so each write starts from what the earlier one
left, as one by one. Each value reports its own result; a failed
recalculation fails the batch, as it fails set_value. The engine collects
the changed values and fills them with set_values, finding fields by name
through a map rather than a scan.

The two forms now open in 4 s and 3 s, with the same fields, values,
warnings and document information as before; 8,000 synthetic fields open
in 1.5 s.

Tests: set_values matches set_value one by one (values, twins, the
calculated total, every appearance, saved; refusals for an unknown name,
a read-only field, an unknown option and clearing a no-toggle-off radio
group the batch selected); it reads the form a fixed number of times; the
engine takes 59 data values on a 60-field static form exactly as filling
them one by one does (the read-only one refused); and opening 800 fields
takes under 16 times as long as 100 (about 4x now, about 48x before).


Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M4: Document Properties reads a named opening destination's page, so choosing page 1 replaces it (#4572)

initial_view resolved only explicit opening destinations, so a named one
(ISO 32000-2 §12.3.2.4) read as page 1 at the default magnification, and
set_initial_view kept it as "unchanged" when page 1 was chosen. Names are
now resolved through the /Names /Dests tree and the catalog /Dests with the
existing bounded resolver, and a bare name or string counts as a destination
when page 1 at the default magnification removes /OpenAction.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

* M4: parity notes say page 1 at the default magnification removes /OpenAction

Document Properties replaces a named opening destination (ISO 32000-2
§12.3.2.4) with an explicit one when another page or magnification is
chosen; page 1 at the default magnification removes the catalog's
/OpenAction instead (§7.7.2), so the first page opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M7: a moved text box's drag preview shows the box alone (#4464)

While an added text box or picture was dragged, the pixels that followed
the pointer were cut from the page raster inside the item's box, so
whatever the box covered at its first place (a filled shape, a picture,
other text) was carried along with it.

The drag now renders the item alone too: a copy of the document whose
page keeps only the item's own content stream and no annotations, drawn
on a transparent page (`pdfcraft_edit::isolate_content`,
`Document::added_content_alone`). Those pixels follow the pointer over
the page rendered without the item, so what it covered stays put. The
preview shows once both rasters are in; until then the item stays where
it is, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

* M7: drop the move preview's earlier rasters when a re-render fails (#4464)

When the scale changed after both the page-without-item and item-alone
rasters were in, and a render at the new scale failed, only the renderers
were cleared. The old textures stayed, so the item kept following the
pointer from them although the preview had given up. A failure now clears
both texture slots too, so the item stays where it is while dragged.

The receive step moves into its own method so a test can drive it with a
pool whose renders always fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
A luminosity soft mask is drawn into an RGB buffer and its value read
as the luminance of that buffer. A /DeviceCMYK colour reached the
buffer through the CMYK display profile, where 100% K is a dark grey,
so a mask painted 100% K in a DeviceCMYK group let about an eighth of
the masked paint through (mask value 0.125 instead of 0).

ISO 32000-2 11.5.3 converts a device-space group colour to gray "with
no compensation for gamma or other colour calibration", and 10.4.2.3
gives DeviceCMYK to DeviceGray as
gray = 1 - min(1, 0.3 c + 0.59 m + 0.11 y + k).

hayro-interpret patch (63): when the group colour space of a luminosity
soft mask is /DeviceCMYK itself (not an ICC-based or calibrated space),
the mask's context paints /DeviceCMYK fill and stroke colours as that
gray, and a four-component /BC becomes the same gray. A gray reads back
as itself through any luminance weights that sum to one. CMYK on the
page, CMYK in DeviceGray/DeviceRGB mask groups, and CMYK images,
shadings and tiling patterns inside such a mask keep the profile.

Tests: color::device_cmyk_gray_tests (hayro-interpret) and
it::luminosity_group_space (pdfcraft-render); the three DeviceCMYK
render tests fail with the patch reverted.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M3: an attachment's row opens on a double-click and has a right-click menu (#3466)

In Acrobat, a right-click on an attachment offers Open and Save, and a
double-click opens it. PdfCraft's Attachments panel had only the buttons
at the end of each row.

Each row now senses clicks beneath its buttons (`UiBuilder::sense`, which
egui registers under the row's children, so the buttons keep their own
clicks):
- A double-click opens a PDF in a new tab, as the Open button does, and
  saves any other file, since PdfCraft opens only PDFs.
- A right-click offers the row's own actions with the buttons' labels:
  Open in a new tab (PDFs), Save attachment… and Delete attachment (the
  document's own attachments, when it may be modified).

The name and details labels are no longer selectable, so a click on them
reaches the row.

* M3: a right-click on an attachment row's button opens the row's menu too (#3466)

CodeRabbit on #4513: the menu hung on the row's response only, so a
right-click on its Save, Open or Delete button opened nothing. The menu
now answers the union of the row and its buttons (one menu, the row's id).
Regression test right-clicks a button (fails without the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM

---------

Co-authored-by: Claude <noreply@anthropic.com>
The reading blocks keep style boundaries, so "Before Chevron after." with an
italic case name reached the Office exporters as three blocks, and each writer
emitted one paragraph per block. A block that continues another on the same
line (the test `continuations` already applies to keep such blocks out of
table cells) is now a run in its own size, bold/italic and colour inside the
paragraph of the block it continues, in Word, HTML, RTF and PowerPoint.

- `continued_from` names the block each one continues (nearest end);
  `continuations` is its `is_some`, so table detection is unchanged.
- `paragraphs` groups blocks left to right, joining only a block that starts
  strictly further left, with the same rotation, and never a table cell or the
  slide title.
- Block text is trimmed, so the engine records the source's spaces at its ends
  (`Block::space_around`, from the reading block's editor text); runs keep that
  space, or one where the page shows a gap.
- A paragraph is a heading only if its runs are (a bold lead-in no longer makes
  its line a heading); a smaller footnote marker does not demote a heading.

Style changes that wrap onto the next line still split paragraphs; that needs
the reading-block grouping change and is left for later.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6: with NeedAppearances, check boxes and radio buttons draw their /MK caption (ISO 32000-2 Table 224)

A form that sets /NeedAppearances asks the viewer to construct the
appearances of its widgets (ISO 32000-2 Table 224; deprecated in PDF 2.0).
The display copy already redraws text and choice fields for it, but check
boxes and radio buttons kept the file's own appearance streams, so a box
whose /MK /CA is ZapfDingbats 4 (a check mark) showed the filled square
its /AP carried.

With /NeedAppearances, every check box and radio button that has an /MK
is now drawn in its current state through button_state_shown (merged in
#4368): /MK /BG and /BC, and when on the /MK /CA ZapfDingbats character
(Table 192) in the /DA colour, sized to the box. A widget without /MK
keeps its own appearance, and without /NeedAppearances nothing changes.
A radio button with no /AS now takes its state from /V when /V names its
own on state. Display and print only: the file is saved as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

* chore(forms): wrap button_state_shown's doc comment

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>
* M3: a Page tools menu in the Pages panel (#4415)

The Pages panel's header gets a Page tools menu beside Close: insert a
blank page or pages from a file after the selection, rotate either way,
extract and delete. Each runs the registered command on the selected
thumbnails (the current page when none is) and is greyed out where the
document's security withholds page changes. A menu rather than a row of
buttons, so the thumbnails stay where they were and it fits any width.

* M3: tests drive the Page tools menu's file items too (#4415)

CodeRabbit on #4583: the parity evidence covered rotate, delete, blank
page and permissions through the menu, not Insert pages from a file or
Extract. A test now inserts a picked file's pages and extracts the
selection from the menu; the parity entry cites it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM

* M3: the Page tools menu in every catalog (#4415)

Page tools and five of its items, in each language (AI-assisted, not
separately native-reviewed, like the other recent entries). Insert a
blank page after the selection was in the catalogs already and keeps
its translation there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LymhVaGZCWwB9HNEbLzeMM

* chore(ui): keep thumbnail_size's doc line on thumbnail_size

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* refactor(ui): side panels on craft-ui task panes and the shared rail

The Tools pane and the open side panel are docked craft-ui task panes
(craft_ui::panes::PaneHost, Columns policy, no title bars, not movable),
and the rail is craft_ui::panes::Rail with the page navigation cluster
as its foot. On the right, outermost first: the rail, Tools when
Preferences > Tools panel side is Right, the side panel, the page.
Read mode uses the same panes without Tools; All tools full view still
covers them. Floating, tab groups and drag-in are gone with docking.rs.

- Widths are resizable within the old limits and saved under a new
  side_panels preference (version 1) with the Tools side and open state.
  The old docking value is read once as plain JSON and dropped on save.
- ui.dock becomes ui.panel {panel: tools|side, open, width};
  ui.dock.reset resets the side panels ("Reset side panels").
- F4 shows or hides the side panel (reopening the last one) and
  Shift+F4 the Tools panel, as View menu commands.
- craft-ui pinned to e9630ae7 (craft-libs#309).

* fix(ui): side panels clippy lint and the dragged-width test's grab offset

* chore(deps): pin craft-ui at 7c42807, whose pane resize edges work over the page

* chore(deps): pin craft-ui at craft-libs main ea4c823, with panes::Rail
* Record successful PdfCraft web application check

* E21: bound startup requests and validate font and license inputs

* E21: separate pending control completion and gesture regression phases

* M-egui037: enforce control admission and per-family font notices

* M-egui037: qualify capture fixes and document migration behavior

* egui-037: identify current and historical qualification results
* feat: startup update prompt modal and direct self-updating

Help ▸ Check for updates and the check at start run through craft-update's Updater, which replaces the old GitHub fetcher and update source. The check at start is opt-in for the desktop app (test harnesses never reach the network), an install holds back while documents are unsaved, and a finished update closes through the app's quit path. craft-update stays out of the wasm build.

Ref: storytold/craft-libs#319

* test(updates): the prompt's buttons are DOWNLOAD and LATER

* build: pin craft-update to craft-libs main (db19c37)

* fix(updates): run the Windows update helper before argument parsing

* fix(updates): point the parity evidence at the current update tests
Partial vector redaction previously hid covered areas with clipping
(`clip_out`, `W/W*`) while retaining the complete original drawing instructions
and painting operators in the content stream. This allowed recipients
to recover confidential outlined text or other vector drawings from
supposedly redacted documents.

This change removes vector paths that intersect redaction marks completely
and refuses partial clip retention, ensuring no confidential geometry is
preserved behind clipping. Report counts are updated to record removed paths.
Picks up the update prompt fix that draws its close mark and version pill
without glyphs a font may lack, so the prompt no longer takes the app down
when a bundled face has no ✕ or →.

Part of tracking issue storytold/craft-libs#319

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>
Picks up the startup prompt without the glyphs a font may lack (storytold/craft-libs#343, #344): the close mark is painted and the version pill is ASCII, so apps that panic on a missing glyph no longer take the prompt down.

Part of tracking issue storytold/craft-libs#319
chore(deps): bump 11 safe dependency updates
* test(ui): CJK interface tests skip without craft-fonts

* test(ui): a build that requires craft-fonts never skips the CJK interface tests
feat(ui): menus from craft-menu, native on macOS (#4752)
Some checks failed
ci.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (push) Failing after 0s
freebsd.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (push) Failing after 0s
nix.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (push) Failing after 0s
ci.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (pull_request) Failing after 0s
freebsd.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (pull_request) Failing after 0s
nix.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (pull_request) Failing after 0s
packaging-lint.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (pull_request) Failing after 0s
sonarcloud.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (pull_request) Failing after 0s
packaging-lint.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (push) Failing after 0s
sonarcloud.yml / feat(ui): menus from craft-menu, native on macOS (#4752) (push) Failing after 0s
64e0868b84
* feat(ui): menus from craft-menu, native on macOS

One menu definition (crates/ui-egui/src/menus.rs) built from the command
registry drives the native macOS menu bar (craft-menu's muda backend) and
an in-window egui bar on Windows, Linux and the web, which takes the place
of the "Menu" button at the start of the top bar.

- File, Edit, View, Sign, Window and Help, with separators and submenus.
  Edit gains Cut, Paste, Delete and Select All (they act as their keys
  do) and a Pages submenu; View gains a Tools submenu built from the All
  tools groups; Sign holds Fill & Sign and the certificate commands;
  Window lists the open documents.
- Toggles are check items and the page layouts, side panels and themes
  radio items, with their state from the app.
- File > Open Recent lists the recent files (file.open_recent with an
  index).
- A menu click and its shortcut run the same dispatch, including the
  form-field deferral (#166) and macOS Cmd+W closing the window (#869).
  Dialogs and the palette grey the menus through craft-menu's gate.
- Quit goes through craft-quit, so the unsaved-changes prompt runs.
- craft-ui, craft-quit, craft-menu and craft-keymap pinned to
  storytold/craft-libs 70ade1fb.

Menu state and dispatch follow #2610 by @hajdukste; the menu tree and the
view and panel items follow #3870 by @RivanJarjes.

* test(ui): the menu model, native dispatch and the in-window bar

- Unit tests for the menu definition: every registry menu command is
  listed once with its registry shortcut; check and radio items follow
  the app; a dialog greys document commands and drops their clicks; the
  recent files and open documents are listed once each; a native menu
  click (through a fake craft-menu backend) runs its command once.
- The kittests that opened the old "Menu" button use the menu bar, and
  the ones that look up a label the bar's titles share (Edit, File,
  Sign) skip the bar.
- The in-window bar takes its titles' width, so the mode tabs, search
  and buttons keep their place in the top bar.
- Edit menu clicks that stand for keys (Copy, Cut, Delete, Select All)
  reach this frame's input from `logic`, where test hosts see them too.

* test(ui): the language switch test opens File in the menu bar

Translated titles and rows carry their mnemonic ("ファイル(F)", "開く(O)…").

* feat(ui): the menu bars run Quit themselves, the bar shrinks to its titles, and AppKit's window list stays off

craft-libs main edc41f6 (craft-libs#354): MenuBarUi runs Quit and Minimize, shrink_to_fit sizes the in-window bar, Key::parse reads the registry's arrow and minus glyphs, and mac::install_with(.., window_list(false)) keeps the Window menu to PdfCraft's own document list.

* fix(ui): the menu command filter is an if, for clippy

* test(ui): drop a needless borrow in the edit-tab helper

* test(ui): two tests follow the menu bar

The tests were wrong, not the app. The Preferences language test clicked the old Menu button; it opens Edit in the menu bar and its Preferences row. The prepare Actions test counted the Edit labels: the narrower menu bar leaves room for the Edit tab, so the rows' Edit buttons come third and fourth.
alf merged commit 64e0868b84 into main 2026-10-26 08:47:42 +00:00
alf deleted branch chore/sync-upstream-20261026 2026-10-26 08:47:42 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
iris/pdfcraft!9
No description provided.