chore: sync upstream (72 Commits) #20

Merged
alf merged 72 commits from chore/sync-upstream-20261108 into main 2026-11-08 11:58:07 +00:00
Owner

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

  • f7505898 Keep ruled tables when exporting a PDF to Word (#5154)
  • adbf1dc0 M9: signed appearances show their real validation state (#79) (#2808)
  • f841666d Release: refresh contributor credits for each release (#6487) (#6488)
  • 5bb8bbf2 Add dry-run preview to doc_batch without writing outputs (#6486)
  • 5644d56c Merge pull request #6243 from alehel/refactor/page-press-edit-text
  • 9dbede87 Merge pull request #5816 from KenM76/feat/unreadable-object-report
  • 33e3945c Merge main and preserve unreadable diagnostics with cached failures
  • 784ce921 Merge pull request #6497 from helenkwok/web/page-api
  • a94e90da Merge pull request #6471 from ZIDAN44/fix/6440-add-text-cmyk-gray
  • 09a63d6d Merge pull request #6197 from KenM76/feat/rich-copy
  • 21747a4e Merge pull request #5802 from KenM76/perf/failed-object-cache
  • a9ccca5b pdfcraft-web: say ui.open answers with an error on the web
  • 6ec23789 pdfcraft-web: say a timed-out call is not guaranteed to have been skipped
  • 9db177c1 Add text: the ink tests reach the non-number and content_update paths (review)
  • a9e98e0d pdfcraft-web: window.pdfcraft, the page command API from craft-web-api, opt-in with ?control
Upstream-Sync von `upstream/main` (72 Commits). Fast-Forward. - f7505898 Keep ruled tables when exporting a PDF to Word (#5154) - adbf1dc0 M9: signed appearances show their real validation state (#79) (#2808) - f841666d Release: refresh contributor credits for each release (#6487) (#6488) - 5bb8bbf2 Add dry-run preview to doc_batch without writing outputs (#6486) - 5644d56c Merge pull request #6243 from alehel/refactor/page-press-edit-text - 9dbede87 Merge pull request #5816 from KenM76/feat/unreadable-object-report - 33e3945c Merge main and preserve unreadable diagnostics with cached failures - 784ce921 Merge pull request #6497 from helenkwok/web/page-api - a94e90da Merge pull request #6471 from ZIDAN44/fix/6440-add-text-cmyk-gray - 09a63d6d Merge pull request #6197 from KenM76/feat/rich-copy - 21747a4e Merge pull request #5802 from KenM76/perf/failed-object-cache - a9ccca5b pdfcraft-web: say ui.open answers with an error on the web - 6ec23789 pdfcraft-web: say a timed-out call is not guaranteed to have been skipped - 9db177c1 Add text: the ink tests reach the non-number and content_update paths (review) - a9e98e0d pdfcraft-web: window.pdfcraft, the page command API from craft-web-api, opt-in with ?control
alf added 72 commits 2026-11-08 11:58:06 +00:00
Document::try_get kept only objects that loaded. An object whose entry
points at something that doesn't parse falls back to a whole-file
search (scan_for); that failed too, nothing was kept, and the next read
searched the whole file again. A caller that reads such an object
repeatedly, such as the window title of a file whose document
information dictionary is damaged and that asks for its title to be
shown, paid one whole-file search per read, several per frame.

The cache now keeps the load result, a failure as well. It is the same
map with the same bounds, clears and per-reader privacy, so every place
that drops loaded objects (authentication, reconstruction, private scan
readers, eviction) drops remembered failures too. Edits are read from
the overlay before the cache, as before, so a set or freed number still
answers for itself. A failure is kept in nested loads too, as a success
already is; only a poisoned lock is never kept. Failures count towards
the cache's object caps and hold no bytes of their own. Sharing the map
between clones stays sound because entries, data and the stream limit
don't change after open.

Test: an_unreadable_object_is_searched_for_once counts whole-file
searches through a test-only counter (like SECTION_READS): one for
eleven reads and a clone's read. With failures not kept it counts 22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Clones share the object cache and the parsed object streams. A clone that
freed or replaced an object stream then read a member of it put the result
in those shared maps: a failure (which this branch now caches) or, as on
main, the replacement stream and the member parsed from it. Another clone
whose stream is intact then read that instead of the file.

A load that reads an unsaved edit (an edited object stream, or an edited
indirect /Length) is no longer cached, and an edited object stream is
parsed for its own document only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
An object whose load fails reads as null (ISO 32000-2 §7.3.10) and the
file opens, but nothing said so: the repair log records only
document-level repairs, so a file with an unparseable /Info showed no
repair and no reason for its missing title.

pdfcraft-cos Document now records each object whose load fails, with
its generation and the reason, in a set shared by clones and scans like
the object cache: at most 100, keyed by number, first reason kept, cut
to 160 characters, built only for an object not yet listed. It is reset
where the caches are (authenticate, reconstruct); edited objects drop
off a document's list. Document::unreadable_objects() gives neutral
lines ("object 4 0 could not be read: ..."), the engine passes them on,
and automation doc_info adds an unreadable_objects array, kept apart
from repairs so the "has been repaired" notice and the clean-file
checks keep their meaning.

Tests: a cos unit test for listing, dedup, clones, scans and edits; a
cos test for the 100 cap and the no-readable-copy reason; an end-to-end
doc_info test on a synthetic broken file. Each fails with the fix
reverted. The core.repair-log parity entry cites them and says what is
not listed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
An unedited object stream whose indirect /Length is edited in one clone
was parsed with that clone's length and put in the object-stream map
the clones share, so the other clones read its members within the
edited length. A container load that read the clone's edits is now
kept to that clone, like an edited stream.

Test: an_edited_object_stream_length_leaves_other_clones_members_alone
fails without the fix (the other clone reads None) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
The indirect /Length memo kept only the value. A length first resolved
through one clone's edited object stream and reused later in the same
load did not count as an edit read, so an object stream framed by it
could still be parsed with the edited length and put in the
object-stream map and cache the clones share. The memo now keeps
whether resolving the length read edits, and a reuse counts it again.

Test: a_reused_edited_length_leaves_other_clones_members_alone fails
without the fix (the other clone reads None) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Rebuilding the cross-reference index from object headers reset the
unreadable-object list a second time, after adding the object-stream
members, so an object the header scan failed to read dropped off
unreadable_objects() unless something read it again. Those failures
still stand against the full index: the added members leave every
scanned object's entry as it was, and a /Length found only then changes
how much of a stream is read, never whether it reads. The list is now
reset once, before the scan; the object cache is still cleared after
it.

Test: a_rebuilt_file_lists_the_objects_its_scan_could_not_read fails
without the fix (the list is empty) and passes with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Main's ObjectCache (#5958) measures each object once and keeps the total.
It now holds a load result, so failures stay cached: a failure counts no
bytes, an object its held_bytes. The budget check uses the running total,
which replaces this branch's snapshot fold and large-object fast path.
The overlay-read and Poisoned guards are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Tests only: both sides added tests at the same place; both kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
A member that failed to load from the file's object stream was cached as
a failure. Replacing or freeing that stream left the failure in place, so
the member kept reading as null instead of from the edit (CodeRabbit on
#5802). set and free now drop the cached failures of the stream's members.
Cached objects are untouched; a failure holds no bytes, so the cache's
total stands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
The clone-scan check ran after the original had already listed the
object, so it passed whether or not scan findings were shared (CodeRabbit
on #5816). A fresh document is now cloned before anything reads it, only
the clone is scanned, and the original must list the object. Seen failing
with the scan's list unshared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Resolves the conflict with #5548 in crates/cos/src/document/objects.rs,
keeping both sides:

- try_get enters main's FollowScope, then loads and caches the load result
  as this branch does: a failure is kept too, except a Poisoned error or a
  load that read this document's unsaved edits (overlay_reads). The bytes a
  success holds are measured by held_bytes through loaded_bytes.
- scan_for keeps main's `unfound` set and counts with main's
  tests::FILE_SCANS. This branch's OBJECT_SEARCHES counter is removed and
  its test uses FILE_SCANS.

Main's `unfound` now remembers a whole-file search that found nothing,
which was this branch's original case. What the failure cache still adds:
a failure that is not a scan miss. A member of an object stream that can't
be decoded decoded the whole stream again on every read, because only
decoded streams are kept. New test
a_member_of_an_undecodable_object_stream_is_decoded_once counts those
decodes with a test-only OBJSTM_DECODES counter: 1 with the fix, 12 with
failures not cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Brings the branch up to date with main (8717a085), which added the
follow budget, the remembered object-search misses and held_bytes
(#5548) to Document::try_get.

The one conflict was the load at the end of try_get in
crates/cos/src/document/objects.rs. The resolution keeps both sides:
main's FollowScope guard around the load, and this branch's record of
a load failure in the unreadable-object list (a poisoned lock is still
not recorded, since it says nothing about the object). The held_bytes
caching after a successful load is main's, unchanged. Every other file
merged cleanly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
The unreadable list is shared by clones, and each one hides the entries
it has edited. The 100-entry cap counted the whole shared list, so a clone
that edited every listed object recorded none of its own failures and
listed only "more objects could not be read". The cap now counts the
entries the asking document still shows. Each document still lists at
most 100 entries, then the "more" line.

Test: objects_a_clone_edited_do_not_use_up_its_unreadable_allowance. It
was seen failing with each half reverted (left: 1, right: 10; then
left: 110, right: 101).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Copying selected page text now also puts an HTML fragment on the system
clipboard, so a paste into a word processor or mail client keeps
paragraphs, bold, italic, size, colour and font family. The plain text
is unchanged and stays on the clipboard as the alternative.

- export: clip.rs, the clipboard model (ClipText of paragraphs of
  styled spans), its builder with caps (20 000 spans, 2 MiB of text,
  16 MiB of HTML), font_family (subset tag and style suffixes off,
  camel case spaced) and clip_html. Family names are reduced to
  letters, digits, space, '-', '_' and '.', so a font name cannot leave
  the CSS string; text goes through esc, which drops control
  characters.
- engine: Document::rich_copy(page, text, glyphs). Glyphs (view space)
  take the style of the reading block their centre lies in; blocks are
  style-homogeneous. A line wrap inside a block is a space (none after
  a hyphen), a change of block a new paragraph. Refused when the
  document does not allow copying. compare.rs's display unit and
  weight/slant inference are shared, not duplicated.
- automation: text_extract takes format "text" (default) or "html";
  html adds each page's html and styles_incomplete, for the whole page
  or only what rect takes. Other values are InvalidArgs.
- ui-egui: Copy of a run or column selection writes HTML plus the
  plain text in one arboard set (desktop only); the web build and
  Select All of the whole document stay plain text. last_copied_html
  records what was produced, for tests.
- parity: view.copy-with-layout is partial; notes list what is missing
  (RTF, underline/strike-through, tables, Select All, web, glyphs
  outside reading blocks).

Formatting inference and the HTML serialiser are adapted from pdfcer
(MIT, same author) at 540b00ef1a405d1baa67c92f3858535037c7cde8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Keeps both new cos document tests side by side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Crop mode held every press on its page through a hit with no point test,
so its claim won the cursor on every visible page (its Default arrow over
the next page's paragraphs), and its own grip test at the drag's start and
its cursor test used different reaches (10 and 8 px).

- `edit_text_ui::crop_hit` says what of the picture being cropped lies
  under a point: a grip of the box kept, the box itself, or the rest of
  its page (`PicturePart::Crop`, on `Layer::Editor`), and the hand over
  its Done, Cancel and Remove Crop controls, past the page's edge too.
  A drag takes what its press was on (`ImageCx.press`), and the cursor is
  the hit's: one reach for both.
- Delete and Backspace on a selected picture, and crop mode's Enter and
  Escape, move to the app's key handling (`edit_text_ui::picture_keys`),
  which no key reaches under a dialog. Delete works wherever the pointer
  is (the user's ruling: before, it did nothing over a paragraph).
  Escape while a button is down is still the press's: it cancels a crop
  drag, and crop mode stays.

- #5900's `crop_controls_contain` (the right-click check for #5835)
  measures with `crop_frames` and the grips' reach, so the crop's frame
  and reach have one source; its `crop_bounds` was `crop_frames`' frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`object_ui` took a Shift- or ⌘-press before anyone else, whoever owned it,
through its own inventory test at the press point, and declined a plain
press where that test found an object the router didn't (just outside a
picture's edge), so such a press started nothing at all.

- The page view reads the press's modifiers from `PagePointer`
  (`modifiers`: those held at the frame's first primary press). With
  Edit text, a press that adds (Shift, Ctrl or ⌘) is the objects' whole:
  `object_ui::modified_hit` gives `Objects { Toggle(target) }` for the
  object under it, or none (an area selection that adds), `Held` while
  text being typed is open, and `Refused` on a page whose objects can't
  be selected together. Only the press asks: hover and cursors are as
  before.
- `object_ui` is told what the press is to its page (`PagePress`) and acts
  on its own parts; a press nobody holds starts the area selection. The
  inventory test and its decline go, and so does `blocked_page`: the held
  press keeps the editor open until the button is up through its capture.
- A plain press beside a picture, within the 2 px the objects are found
  in but outside the picture, now starts an area selection.
- The guard: `object_ui::input` no longer reads the router.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Edit text group still asked the router for itself and passed answers
from one module to the next: `others_own_pointer`, `text_used`,
`over_source`, `source_blocked` and the pictures' `allow_interaction`
decided who acted, so what the pointer happened to be over at the release,
or an earlier module's answer, could take a click from the module its
press was on.

- Paragraphs (`ParagraphCx`), pictures (`ImageCx`) and added content
  (`ContentCx`) are told the press (`PagePress`) and Edit text's gate (its
  input blocked by a dialog, the palette, another window or tab, as
  before), and act on their own presses; the paragraph outline comes from
  the top hit under the pointer (`hovered`), hidden while the objects
  selected together or added content being moved hold the page. The flags
  go, and the three modules no longer read the router.
- Add text acts on the paragraph its press was on, or the one under the
  pointer; its click on a paragraph refuses a blocked draft and finishes
  the one being typed in the paragraph's own act.
- Clicks act where they were pressed (the user's ruling): a paragraph
  opens though the button came up just past its box, added text is
  selected, and opened for typing, by the item its press was on, and Add
  text's new box starts where it was pressed (a press beside a paragraph
  released inside it starts a box; it placed nothing).
- The picture's grips and rotate knob lose their egui widgets: a drag
  takes what the press was on. A press past a page's edge is the page's
  that has something under it there (the knob of a picture filling its
  page), so the knob still turns the picture there.
- Edit text's right-click goes to what lies on top under it, as every
  tool's does (ruling C): a picture is selected for its menu through
  `edit_text_ui::picture_context`, and a comment over a paragraph or a
  picture gets its own menu. The page view's own picture scan goes.
- #5900's part of that scan (a right-click on the picture being cropped,
  its box or its grips keeps crop mode, over a picture on top of it too)
  is the crop hit's: `CropPart::Cut`, the picture outside the box kept,
  shown or cut away, joins the grips and the box, so a right-click there
  keeps crop mode. Its menu check stays for now (the menu commit takes
  it). Test: `a_right_click_on_the_part_a_crop_cuts_away_keeps_crop_mode`,
  seen failing without `Cut`.
- With the Edit panel open, the tools that don't pick added content up
  still select added text by its own test at the pointer, as before: a
  press-time hit would take the press from the stamps and the other tools
  that place on any press.
- `main`'s press-handler check guards nothing now, and goes; rule (e)
  (`ROUTER_READERS`) keeps the modules from reading the router again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page's menu chose its entries from the selections, in a fixed order:
a selected Prepare field's menu, then a link's, then the picture's, then
a comment's. A selection left from before outranked what was just
right-clicked, so with a field selected, a right-click on the empty page
(or on a comment) opened the field's menu.

The right-click remembers what it went to (`DocView.menu_for`, from the
routed hits), and the menu shows that owner's entries when it has a
menu, or else the page's (the user's ruling). Add text's right-click is
the page's, as before; while a press is held only a link takes its menu.
The picture menu's flag and its geometric test go with it.

#5900's menu check (no image menu over the picture being cropped, #5835)
is the routed owner's too: a right-click that went to the crop (its
picture, box or grips) opens no menu, and `crop_contains_point` and
`crop_controls_contain`, its second point test, go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit text ignores input while a dialog, the palette, another window or
another tab has it. Only a drag of the objects selected together ended
then; a paragraph's move or rewrap, a picture's move, resize, turn or
crop and added content's move waited, and once the input came back with
the button up they landed wherever the pointer was.

While Edit text's input is blocked, `DocView::cancel_edit_text_gestures`
abandons them all, as the objects' already was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The text layer asked the router whether the press's owner kept its drag,
link following asked it which link held the press, and the Link tool
started a new link from a press any link held, since the router kept a
declined drag's capture.

- `PageRouter::drag_started`: a drag the owner declines (text markup;
  with the Link tool, a link) is nobody's once it starts, on the page it
  began on. The Link tool draws a new link only from a drag nobody
  holds.
- `PageInput` carries the press (`PagePress`) and whether its drag is the
  page's text's (`text_drag`): the text layer and link following read
  them, and `PageRouter::owner` goes. Only the page view reads the
  router now.
- Image pick acts on a press nobody holds: a link's click follows the
  link only, and picks no picture under it (the user's ruling).
- The I-beam drops over a link only where a press would go to it, the
  top hit under the pointer (the user's ruling): the tools that select a
  link's text show it there.
- `PageText::glyph_at` is the one test for the text under a point, for
  the text layer and Redact's boxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment's gesture ended only on egui's `drag_stopped()`. When the pages
missed its release (the button let go where they didn't see it), the
gesture stayed live: it kept the page's frames from Fill & Sign and the
shape tools, and the next drag that stopped on the page committed it,
taking the comment there.

It now ends, as the other page gestures do, on `drag_stopped()` or once
the primary button is up, where the pointer is.

Two tests planted a live move with the button up to check that a live
move keeps its page's presses (`main`'s
`active_comment_gesture_keeps_highlighted_link_click` and 4a's
`a_fill_and_sign_click_places_nothing_while_a_mark_is_moved`). That
state now ends at once, so both hold the press through the planted move
instead: the intent stands, and each still fails with the live-move
guard taken out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the Edit text group, the text layer and link following told what
the press is to their page, only the page view reads the router.

- The router's methods and `DocView.router` are `pub(in crate::canvas)`:
  outside `canvas` the compiler refuses a read or a write, aliases
  included, which the guard's text match could miss. `ROUTER_READERS`
  keeps only the page view's dispatcher (`page_tool_input`).
- Rule (f): no function in a page module is named with "grabs" or
  "owns", which decide for themselves whether a press is theirs.
- CLAUDE.md's "Page presses" rule says so: modules never read the
  router; they are told.

Each rule seen failing against a violation planted in `#[cfg(any())]`
code (a `router.keeps_drag()` read in `content_ui`, a `fn fill_grabs` in
`fill_sign`), and a real router read from `content_ui` no longer
compiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It hovered fifty times with a 10 ms sleep between, so a busy runner
could run out of tries before the page's text or paragraph boxes
arrived from their worker threads. It now steps frames with
`support::step_until`, as the other waits for background work do,
until the cursor is the one wanted or 30 seconds pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pages' presses are the primary button's (`PagePointer.edges`, the
router's `began`), so a right-button drag was a press nobody held, and
every tool that started on egui's `drag_started()`, which counts every
button, started on it: the Rectangle tool drew a rectangle, Prepare's
selection rectangle selected the fields in it, the Link tool opened Link
Properties for a new link, the camera moved its quadrilateral to the page,
and the Select tool selected text. On `main` too.

`PagePointer.drag_started` is a primary drag of a press the pages hold,
started this frame. The page modules read it instead of egui's (comments'
shapes, callout and text box, Prepare, the Link tool, the camera, the text
layer, image pick, and the Edit text group, whose owned presses were
primary already); zoom, crop, Sign area and Redact already asked for the
primary button. The guard refuses a bare `.drag_started()` in a page
module (`NOT_PAGE_DRAGS` lists the widgets of their own: Organize's
thumbnails, the signature pad, the Scan tab), and the router's
`drag_started` is `drag_began`, so the rule reads cleanly.

Tests, one per family, each seen failing with the fix reverted:
`a_right_button_drag_draws_no_rectangle`, `…_selects_no_field`,
`…_draws_no_link`, `…_leaves_the_camera_quadrilateral`,
`…_selects_no_text`; the guard rule against a planted `#[cfg(any())]`
call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`fix(ui): a comment's move, resize or reshape ends` ended a comment's move, resize or reshape once the button was
up, and applied it where the pointer then was: when the pages missed
the release (the page scrolled out of range, a tab switch, a tool
switched mid-drag), the comment jumped to wherever the pointer next
showed up over its page, and while the pointer stayed off the pages the
gesture waited for it.

Only a drag that stops on the page applies its gesture now; one that
ends with the button up and no drag stopped there ends with nothing
applied, whether the pointer is on the page or not. The test hovers away
from the move's start, where the jump shows.

Found in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review of the right-clicks 4b routes:

- With Edit text, a right-click on a picture under a link went to the
  link, which Edit text doesn't follow: the page menu opened and the
  picture was unselected. A right-click counts links only for the tools
  that reach them (Select; the Link tool).
- Edit text's paragraphs have no menu, and give way to the picture
  under them (the user's ruling): text over a picture, an OCR'd scan's
  among it, no longer hides the picture's menu.
- With a Prepare field tool, or while a table is set up, Prepare's hit
  leaves fields out, so a right-click on the selected field opened the
  page menu. A right-click reaches fields whatever the field tool.
- In crop mode, a right-click off the crop went to crop mode's whole-page
  hit and unselected every picture. Only one on the picture being
  cropped, its box or its grips is the crop's (#5900's area), which
  opens no menu; elsewhere it goes to what lies there, as without crop
  mode: another picture is selected for its menu.
- Under a held press, the menu was the page's on anything but a link. It
  is the routed owner's when that owner is the one selected (the held
  press leaves the selections alone), the page's otherwise.

`DocView.menu_for` is `pub(in crate::canvas)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`refactor(ui): crop mode is a hit of its picture` moved the picture's Delete to the app's key handling, which
runs before the page view, so its edit applied before added content read
the keys. Added content's selection mirrors the picture's when the
picture is added content (`sync_image_selection`), and once the picture
was gone that index named the next added item on the page: added
content's own Delete, seeing the same key press, deleted it too.

The picture's Delete consumes the key, and drops the mirrored selection
with the picture's.

Found in review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review, each against `main`'s behaviour:

- A double-click on one of the objects selected together only left the
  group: the second press was the group's. It is the object's under it,
  so added text opens for typing and a picture is selected, as before;
  a paragraph opens too (the user's ruling).
- On a page whose objects can't be selected together, a click on the
  empty page left the picture selected: no area selection starts there
  to drop it, and the picture's own test that did is gone. A click nobody
  holds on the page unselects it.
- Crop mode's grips stopped at the page's edge, so those of a picture
  filling its page could be taken from their inner half only. They reach
  past it, cursor, press and right-click: no page took a right-click
  there, which left crop mode and opened the page menu (#5900 keeps crop
  mode there). The box and the picture stop at the page's edge, as a
  clipped artwork's whole frame can reach over the next pages.
- A press past a page's edge was the page's whenever it had any hit
  there: the objects selected together, or the open paragraph's frame,
  took presses on the next page and did nothing with them. Past the edge
  only the selected picture's knob and grips (crop mode's too) take a
  press, and the objects' hit stays on its page.
- A drag from a hole in the selected picture (a clip with a hole) fell
  through, started an area selection and unselected it. The selected
  picture's frame is its own, as before.
- Image pick: a drag or double-click from a link let the picked picture
  go before ruling 5, and does again; the link's click still picks none.

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

- Rule (f) missed `pub(in …)` functions, the visibility the guard's commit brought
  in (`declares` strips any `pub(…)`), and flagged names that merely
  end in its words (`dropdowns`): it now matches whole words of the name.
- Inside `canvas` the narrowed visibility doesn't hold, and a router
  taken whole (`let r = &view.router;`) got past the checks by method
  name; such a use now counts as the router's (`takes_the_router`).
- Docs: the router's module doc and `PagePress::Free` say a declined
  drag is nobody's once it starts; Prepare's note says a highlight's drag
  is Prepare's; Add text's paragraph note names `ParagraphCx::adding`;
  a test's doc claimed crop-mode keys it doesn't press.
- One source each for the objects' menu test (`object_ui::menu_open`)
  and Edit text's gesture cancel (`cancel_pointer_gestures` calls
  `cancel_edit_text_gestures`); the page-menu test asserts an entry
  only the page's menu has.

Each guard addition seen failing against a violation planted in
`#[cfg(any())]` code in `canvas/area.rs`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. With Edit text, a right-click on added text or on an
open paragraph's frame went to that, which has no menu: the picture under
it was unselected and the page's menu opened. `main` opens the picture's.
Those give way to a picture under them, as paragraphs do, and to nothing
else: a comment under added text stays unselected, as before.

A comment in `draw_pages` said a selected Prepare field keeps its menu
off the pages. Since the page menu follows the right-click, it stays
selected and the menu is the page's.

Tests, each seen failing with the fix reverted:
`a_right_click_on_added_text_over_a_picture_selects_the_picture`,
`a_right_click_on_an_open_paragraphs_frame_over_a_picture_selects_the_picture`.
`a_right_click_with_edit_text_on_added_text_over_a_comment_selects_no_comment`
is a pin, seen failing with added text dropped from the hits outright.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. Delete, Backspace and crop's Enter and Escape moved to
the app's keys, which run before anything is drawn and don't look at what
is on screen. So with Edit text and a picture selected:
- Delete in the page grid removed the hidden picture and kept the pages;
- Delete with the pointer on the Pages panel did the same;
- with the picture's page scrolled away or in single-page view on another
  page, Delete removed a picture the reader couldn't see, and Enter
  applied its crop.

`main` read these keys in the picture's own page pass, which none of
those cases reach. `picture_keys` runs there again, from
`page_tool_input`, under Edit text's gate (so no dialog gets them), and
before the group acts. Delete still works wherever on the pages the
pointer is.

Tests, each seen failing with the fix reverted:
`delete_in_the_page_grid_deletes_the_page_and_not_the_hidden_picture`,
`a_selected_pictures_keys_wait_for_its_page_to_be_on_screen`,
`delete_with_the_pointer_on_the_pages_panel_deletes_a_page`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. The selected picture's frame is its own on `main`: a
drag from it moves it, though another picture lies over it there. The
routed press was the top picture's, and the drag only started for the
selected one's, so nothing moved. `PicturePart::OverSelected` says so: a
click there selects the picture on top, as before, and a drag moves the
selected one.

Test, seen failing with the part planted away:
`a_drag_from_the_selected_picture_under_another_moves_the_selected_one`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. On `main` the selected picture's grips and rotate knob
were drag-only widgets of their own, so their drag began at the press.
Their widgets went, and the page's response is click-and-drag, which calls
it a drag only past egui's click distance: a move of a few pixels resized
nothing. They take their drag from the press again. A click that doesn't
move edits nothing, as before; a drag of the picture itself still waits,
since a click selects.

Tests: `a_short_drag_from_a_picture_grip_resizes_it` (seen failing with
the fix reverted), `a_click_on_a_picture_grip_changes_nothing` (a pin,
seen failing with the release planted to edit always).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review, each against `main`:

- With Add text holding text it can't draw, a drag from a paragraph moved
  the paragraph; only its click was refused. Neither acts now (#125's
  draft stays where it is being typed).
- A right-click reached a Prepare field whatever the field tool and while
  a table is set up (a review ruling), but also on a document that can't
  be changed, where Prepare stays open after a tab switch: it selected the
  field and opened a menu of entries the engine would refuse. It selects
  nothing there.

Tests, each seen failing with its fix reverted:
`add_text_moves_no_paragraph_while_it_holds_a_draft_it_cant_draw`,
`with_prepare_on_a_locked_document_a_right_click_on_a_field_selects_nothing`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. `fix(ui): a right-button drag on the pages starts nothing
a left one does` covered drag starts. A right-button drag still moved the
end of a selection already made, as `update_dragged_selection` tested a
drag by any button (`main` replaced the selection instead). And its end
finished a selection whichever button it was, so with a markup tool, Replace
Text or Redact a selection left from another tool became a comment. Both
read the primary button now.

Tests, each seen failing with its fix reverted (Redact's on its own):
`a_right_button_drag_leaves_a_text_selection_alone`,
`a_right_button_drag_makes_no_mark_of_a_text_selection`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review. No change in behaviour.

- The guard's `PAGE_HIT_CALLS` refused nothing once `main`'s press-handler
  check went; it only failed when it was stale, so it goes.
- The guard's notes said `canvas/image_pick.rs` uses none of the marks (it
  takes `PagePointer` now), that page modules read only
  `PagePointer::drag_started` (some ask for the primary button themselves),
  and left out the point tests that tell a tool a press began on its page.
- `content_ui`'s and the comments' notes said added text is its own with
  every tool, and that added content decides for itself.
- `object_ui`'s `!double_clicked()` can't fail (the page view re-routes
  that press), and Fill & Sign's preview recomputed `hovered`.
- `Capture` is private, and `top` is `canvas`'s.
- Test notes gave as history bugs that only this branch ever had.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found in review: the PR lists these and nothing pinned them.

- With Prepare open, a drag from a highlight draws the selection rectangle.
- A double-click on a paragraph selected with others opens it.
- In crop mode, a right-click on the next page goes to what lies there,
  though a clipped artwork's whole frame reaches over it.
- A crop grip takes a press within its reach, 9 px off.
- A comment's menu opens over a picture and over a paragraph.
- A right-click with the button held opens the selected comment's menu.

Each seen failing against a planted violation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for Copy with formatting.

- ui-egui: the HTML copy goes through one arboard Clipboard, created on
  first use and kept in the app (os_clipboard::with_kept, under the same
  process-wide lock), instead of a fresh one per copy. On X11 the copied
  data is served only while its Clipboard lives, so a fresh one dropped
  at once could leave nothing to paste without a clipboard manager. A
  failed create or set falls back to egui's plain-text copy.
- ui-egui: html_clipboard_override stands in for the system clipboard in
  tests. With the system clipboard on, Copy sets HTML and plain text in
  one set and emits no CopyText; when the set is refused it emits
  CopyText with the plain text. The HTML is built only when a clipboard
  will take it (system clipboard on, not the web build). The
  last_copied_html field is gone.
- export: clipboard HTML text is escaped in one pass and also drops C1
  controls, a bare carriage return and the bidirectional overrides and
  isolates. Font sizes are written within 1 to 1000 pt.
- engine: the glyph-to-block step budget is a parameter, tested; the copy
  says styles_incomplete only when matching actually ran out of steps.
- automation: text_extract format "html" has one 32 MiB HTML budget
  across all pages; a page past it gets none, and the result carries
  html_truncated. The tool description says so.
- parity: the new tests are evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
The test pushed until the builder refused; with the cap removed it never
stopped. It now pushes at most two chunks past the cap and checks the cap
stopped it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
CodeRabbit's findings on #6243, each checked against the code:

- A double-click on the objects selected together took its target from
  the hits at the release, so a release just off the paragraph pressed
  opened nothing. The router keeps what lay on top at the press with the
  objects aside (`under_objects`), as every owner acts where its press was.
- A drag first seen on another page left the picked picture picked: that
  page is told `Elsewhere`, and the picture's test no longer ran there.
- Delete with a button held removed the item, and the press held then named
  the next one. Delete waits for the button to come up, for pictures and
  added content.
- A page just off the screen is still run for what lies near it, so a
  picture's keys acted there unseen. They ask whether the page is on screen.
- Add text's drags end while its input is blocked, as Edit text's do.
- With the page gaps hidden, a Shift-click on the row two pages share
  selected on the first page and the second cleared it. A press another
  page holds leaves the selection alone.

Tests, each seen failing with its fix reverted:
`a_double_click_on_a_grouped_paragraph_released_just_off_it_opens_it`,
`a_drag_first_seen_on_the_next_page_lets_the_picked_picture_go`,
`delete_waits_for_the_button_to_come_up` (each half),
`a_selected_pictures_keys_wait_for_its_page_to_be_on_screen` (its
continuous case), `a_paragraph_drag_ends_when_the_window_loses_focus` (Add
text), `a_shift_click_where_two_pages_touch_keeps_what_it_selected`,
`a_press_counts_the_modifiers_of_its_own_event`.
`add_text_keeps_a_changed_paragraph_and_a_draft_when_it_moves_on` pins that a
click on the page applies the open paragraph before a new box starts, so
neither edit can take the other's place; seen failing with the draft's edit
dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Spaces HTML would collapse keep their width across runs, at a
  paragraph's start and after a break.
- font_family returns the families to try: spaced, then as spelt
  ('Sim Sun','SimSun'); a hyphen splits only before a style word, so
  MS-Gothic stays whole.
- clip_html reports its cut; empty paragraphs count against the cap;
  the span cap is checked before a paragraph opens; ClipBuilder's
  Default starts a paragraph; a negative size clamps to 1pt.
- RichCopy::truncated: a capped copy is marked, the clipboard then puts
  plain text only, and text_extract sets html_truncated. No HTML for any
  page after the shared budget runs out.
- A style change at a line end is a wrap, not a paragraph; the block
  hint is skipped when a smaller block overlaps it; no editor reports
  styles_incomplete.
- A column selection copies plain text only.

Each new test fails with its fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
- A line change into a block of another style is a wrap only when the next
  line starts within half a line below the last one; a line far above it
  (the next column) starts a paragraph. The size compare treats NaN as
  "not the same size".
- A smaller block must overlap a block's interior, not just touch its edge,
  to stop the block being used as a hint.
- font_family drops only the part after the last hyphen, and only when it
  names a style (Segoe-UI-Bold gives Segoe UI).
- text_extract's description and html_truncated doc now cover a page's own
  copy caps and say that rect HTML is in line order.
- Parity: the new tests are evidence; column selection HTML is listed as
  missing (plain text only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Conflicts with the generation-aware references (#1143): stream lengths are
kept by full reference, with whether resolving them read unsaved edits; a
repaired slot counts as an edit read like any other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Conflicts with the generation-aware references (#1143): the rebuilt xref
index takes its new repair-notes field; the parity entry keeps both sets of
evidence and notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
# Conflicts:
#	crates/ui-egui/src/canvas.rs
Conflict in crates/engine/src/lib.rs only: keep both module lines
(rich_copy, scan_clean).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
# Conflicts:
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/canvas/page_input.rs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
# Conflicts:
#	CLAUDE.md
page_add_text and content_update took only RGB, so text added to a CMYK
print file painted 0 0 0 rg, rich black on press. They now also take gray
(0 to 1) or cmyk ([c, m, y, k]), painted with g or k and kept in /PCAdded as
/Gray or /CMYK beside /Color, which holds the matching look. An ink applies
only while the colour still shows its look: picking another colour, also in
the Format panel, drops it for good. RGB text is written byte for byte as
before, and a stored ink that doesn't match its colour is ignored.
A stored /CMYK with a non-numeric entry was read from its numbers alone, so
[0 /Bad 0 0 1] became 100 K; a /Gray that isn't a number with a /CMYK beside
it fell through to the CMYK. Both now give no ink, and the text paints in
its /Color.

g and k are written to the thousandth, as rg is, so an ink set or read with
finer digits painted other than what the item, its stored /Gray or /CMYK and
content_list held. The ink is now kept as painted, with its look, when set
and when read back. RGB text is unchanged.
Reading an item's /CMYK resolved the value, which copied a direct array,
then collected every entry before checking there were four. A file with a
huge numeric /CMYK paid for that on each content listing and item lookup.
Now a direct value is read where it is, a reference is followed without a
copy, and the array's length is checked before any entry is read. /Gray is
read the same way. A long /CMYK, direct or by reference, and a /Gray that is
a long array still read as no ink.
?control attaches the UI control channel (the one --control serves) and installs window.pdfcraft: request, execute (ui.command), commands (ui.commands), docInspect (ui.state), uiInspect (ui.inspect) and screenshot. The channel keeps the accessibility tree of every frame, so it stays off unless the page asks. Opening, saving, the file name and the unsaved flag stay with the host messages.
The hand-edited ink test's "CMYK entry not a number" case had five entries,
so the length check refused it before any entry was read; it now has four.
content_update gets its own refusals: two inks at once, an ink out of
range beside another style change, and gray or cmyk on an image, with the
item list unchanged after each.
perf(cos): an object that can't be read is searched for once, not on every read
M3: copy keeps formatting as HTML (Copy with formatting)
Add text: gray or CMYK colour, painted with g or k (#6440)
pdfcraft-web: window.pdfcraft, the page command API from craft-web-api, opt-in with ?control
feat(cos): list the objects that could not be read in doc_info
refactor(ui): tell Edit text, the text layer and links whether a press is theirs (router PR 4b)
* Add dry-run preview to doc_batch without writing outputs

Users cannot preview a batch before it writes files, so they cannot check the planned steps and output names for a folder first.

Add a dry_run option to doc_batch that reuses discovery, returns each file's ordered steps and planned output name, and skips processing and publishing.

Test: doc_batch_dry_run_lists_outputs_without_writing failed before the change and passes after.

* Address review for pull request 6486

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
About > Contributors still showed an old snapshot, because nothing
regenerated contributors/contributors.json before a release. Opt in to
the shared refresh from craft-common-actions: a read-only credits job
regenerates the snapshot for the release commit, and the shared release
workflow installs it before every source build. The snapshot is not
committed back.
* M9: redraw named-layer signature status layers

- /N drawing one Form that holds n1 and n3: n1, n3 and n4 redrawn for the state
- copies made once per original and state; originals never edited
- Lucide icons; captions in Helvetica, embedded Inter, or craft-fonts outlines

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: draw signature states in the display copy, exports and print

- marks follow the bytes shown; a changed verdict redraws after a metadata edit
- background opens pick up the trust and captions current at commit

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: status captions in the interface language

- the three captions in all 22 catalogues
- Signatures panel colours from sign::status_rgb

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: signature badge in the widget's corner

- a quarter of the shorter side, 48pt cap; outline only under 32pt
- dropped for a drawn valid state; bottom-right where the appearance draws one
- sign_list reports verdict_shown; parity and roadmap

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: keep Arabic signature captions in logical order

Pass raw RTL catalogue text to the signature appearance shaper so UAX #9 is applied exactly once. This keeps the rendered word order and exported /ActualText consistent.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M2: read /ActualText over painted paths and pictures

- A replacement that marks no glyph reads where it paints, cut to what is visible and along the way it is drawn.
- A span the page content leaves open ends before the annotations.
- OCR and scan field detection still count such a page as having no text of its own.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M8: redaction strips /ActualText around paint it clips

- A path or shading written again under a clip counts as changed, so a partly redacted word drawn as outlines leaves no name in the file.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: recognise only layer forms that draw n1 and n3

- Settle on the one form holding both first, then read its content once per call.
- Skip an /N that is its own layer form; copies keep the layer's /OC.
- A verdict counts as drawn when any visible widget shows it.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: keep the signature badge and redraw wherever a state changes

- The badge stays for every state; its mark sits bottom-right as the widget lies on the page.
- An edit that changes a verdict redraws every page; captions apply before startup files open.
- Print, the CLI's render and the repair log follow the display copy.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: restore the attribution usage the rebase dropped

- Usage lines for the three icons, Inter and the craft-fonts input; ATTRIBUTION.md rebuilt from them.
- Roadmap and localization notes for this round.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: pin signature field names on a nested form

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: bound the signature field walk

- kids share their parent's name and type instead of each copying them
- a widget stays listed under every field that claims it, as one shared copy
- a field written in place in a shared /Kids array is walked once
- caps of 20,000 fields, 100,000 entries and 4096-byte names; no states drawn past them

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: keep a document signed when the field walk stops early

- list_cached adds a signed row of unknown validity for the fields the walk didn't read
- Save As and actions then append, and Optimize, Sanitize and full rewrites are refused, as for any signed file

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M8: removing hidden content no longer strips the names of shadings

- With no regions to clip, a shading is left alone instead of wrapped and marked clipped
- clean_replacements skips scopes already marked, so deep nesting stays linear

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M2: read a picture's /ActualText over the whole picture

- draw_image's transform maps pixels, not a unit square, so the text sat on one pixel in the corner; the test now draws a 2x2 picture
- Painted grows in place instead of being copied through an Option

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: cap the bytes of names the signature field walk keeps

- A kid's name repeats its parent's, so one long name over 20,000 kids came to 80 MB; the walk now stops at 4 MiB of names and counts the rest as unread
- Cover the widget written into a shared /Kids array and the field cap among kids, which no test caught
- sig_fields is private again, and the cap docs say what happens past each cap

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: skip status-layer work that changes nothing; fix tests that could not fail

- show_status_layers and has_status_layers return before reading the form when nothing is signed, so an unsigned form no longer walks every field on each edit
- revalidate and draw_captions leave documents with no drawn state alone, and no longer copy the signature list to update it
- Scan form detection reads the page's text once; comment and badge tips share one hover rule
- Tests: drop a table row that repeated another, stop taking an expectation from the code under test, stop comparing an Arc with itself, give bare asserts messages

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: test the signature field name cap at the engine boundary

- A 4,096-byte name is read and blocks a full rewrite; at 4,097 bytes the list holds the unread-fields row and a full rewrite is still refused

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: test signature states beside fields the viewer draws

- One display copy holds both a field's drawn appearance and the signature's state.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: draw Thai and Devanagari status captions in their own script

- script_of names Thai and Devanagari, so a craft-fonts face meant for either draws the caption as outlines; without one it stays English.
- Localization, parity and the craft-fonts usage line list the new scripts.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M2: paint left out by an exclusion mixes its /ActualText replacement

- A replacement placed by outlines or pictures, some of them excluded, fails filtered extraction as one with excluded glyphs does.
- Glyphs still place a replacement that has them.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: a long-named field that can't hold a signature leaves the walk whole

- A leaf of another type passed over for its name is no signature field and has none below it, so an unsigned form stays unsigned.
- A signature field, or any field with kids, still counts as unread.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: status captions use document faces only

- Each script takes the face PdfCraft writes it into documents with, so Chinese stays English although the pinned craft-fonts' interface-only Chinese face is meant for Hani.
- The craft-fonts usage line names Devanagari: the pinned revision has a face for it, and Hindi captions are drawn in it.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: refuse a new signature field the field walk can't read back

- Signing adds a field only when the walk reads the whole form with it: past a cap the signature would be listed as unread fields, and its name could repeat an unread field's.
- A field the walk already reads is signed as before.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: refuse a new signature field with a blank name

- The walk lists no field without a name, so signing now also requires the walk to list the new field: one named only with spaces or NULs would be a signature nothing shows.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: an unnamed signed field or an unreadable form counts as unread

- A signed field with no usable /T, or an /AcroForm or /Fields of the wrong type, cuts the field walk short: the document keeps its unread-fields row and stays signed.
- Signing refuses a blank new field name before it adds the field, so the refusal still says why.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: no display copy when no signature state is drawn

- display_bytes_for makes a copy only when it added an appearance or drew a state, as the app does.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: the verdict mark stays on the page

- A widget past the crop box takes its mark in the part on the page, or none when too little of it is there.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: typed values stay drawn while a signature redraws every page

- A form edit that changes a signature's state puts its values back in unbaked after the pages are invalidated.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: the action, Save As and shading tests check their effects

- The signed action and Save As tests reopen the output for the title and the comment; the shading test checks its clip.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: values typed earlier stay drawn while a signature redraws every page

- A form edit that changes a signature's state keeps every value still waiting on its page, not only its own.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: comment and content edits keep typed values drawn too

- An edit to one page's comments or content that changes a signature's state moves no page, so the values still waiting on their pages stay pending, as after a form edit.
- The pending-value test runs a field deletion and a comment after a fill, under a certification that allows the fill; its messages say what it checks.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: the language-change test skips without catalogs

- Like the other language tests, it returns early when the build has no craft-i18n catalogs.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

* M9: signed fields behind kids without a name count

- A kid with no /T but kids of its own is walked as a field node, so a signed field under it is listed by its full name.
- An unnamed kid claiming a signature its field doesn't have cuts the walk short, so the document stays signed and keeps its unread-fields row.

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>

---------

Signed-off-by: Chiranjeevi U <244287281+chiranjeevi-max@users.noreply.github.com>
Keep ruled tables when exporting a PDF to Word (#5154)
Some checks failed
freebsd.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
ci.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
freebsd.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
house-rules.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
nix.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
packaging-lint.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
sonarcloud.yml / Keep ruled tables when exporting a PDF to Word (#5154) (pull_request) Failing after 0s
ci.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
packaging-lint.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
sonarcloud.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
house-rules.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
nix.yml / Keep ruled tables when exporting a PDF to Word (#5154) (push) Failing after 0s
f750589812
Fixes #5149
alf merged commit f750589812 into main 2026-11-08 11:58:07 +00:00
alf deleted branch chore/sync-upstream-20261108 2026-11-08 11:58:07 +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!20
No description provided.