Commit Graph
3518 Commits
Author SHA1 Message Date
midasmatterhorn 81e054231c organize: format field widget splitting changes 2026-10-23 19:36:12 +03:00
RiceDee caf7bdd8e1 fix(ui): typing in one tab's text box never reaches another tab's
The paragraph box, and the added-text box, each keep one Undo history under
one egui id, shared by every tab. A box cleared it when it opened on new
text, but a box in another tab on the same text (two copies of a document)
kept it: typing taken back with Undo in one tab came back with Redo in the
other, from the key or from the menu. The document that last used the
history is now kept beside it, and the history is cleared whenever another
document's box takes it over.

Tests: the paragraph and added-text cases, each with two copies of a
document open; the paragraph case tries the menu's Redo too.
2026-10-24 03:33:26 +11:00
RiceDee f1992f5aa1 fix(ui): typing in one tab's text box never reaches another tab's
The paragraph box, and the added-text box, each keep one Undo history under
one egui id, shared by every tab. A box cleared it when it opened on new
text, but a box in another tab on the same text (two copies of a document)
kept it: typing taken back with Undo in one tab came back with Redo in the
other. The document that last used the history is now kept beside it, and
the history is cleared whenever another document's box takes it over.

Tests: the paragraph and added-text cases, each with two copies of a
document open.
2026-10-24 03:24:58 +11:00
Combiler e85c654ead Merge branch 'main' into main 2026-10-23 16:06:49 +00:00
alehelandClaude Opus 5.5 195a2c9bab fix(annot,redact): inline replies and pop-ups go with what they point at
`/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>
2026-10-23 17:54:53 +02:00
alehelandClaude Opus 5.5 23f487b7f3 fix(cos,engine): saving leaves out what was added and removed again
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>
2026-10-23 17:54:52 +02:00
alehelandClaude Opus 5.5 d9bc4b17a5 fix(redact): clearing marks leaves the page as it was and takes replies with them
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>
2026-10-23 17:54:52 +02:00
RiceDee 98a5dde398 fix(ui): formatting keeps per-row alignments, the selection, and Redo
- A row aligned on its own lost its alignment when the paragraph was then
  given a colour, size or font: the rewrite left the rows' alignments out
  once they had been applied. They now go with every rewrite.
- egui drops a box's selection while the box is unfocused, so after one
  alignment or format a second choice went to the whole paragraph or box.
  The selection is given back when formatting returns the focus.
- Redo from the menu or toolbar applied the box's typing first, a new edit
  that dropped the document's Redo. It now redoes in the box, as the key does.
2026-10-24 02:34:48 +11:00
RiceDee ea6420cde5 M7: a lone row aligns across the column under it, not the page's busiest
alignment_frame found the column by the left edge most of the page's other
rows share. On a page of two columns, a title over the column with fewer rows
took the other column's edge, failed to fit in it, and was aligned in its own
box. Only rows reaching across the title now count.

Tests: the two-column case; a two-colour title (the styled-region path) is
framed too; a justified paragraph sent with its rows split by line breaks, as
the editor sends it, still fills every row but the last.
2026-10-24 02:34:48 +11:00
RiceDee 56b5d4f52e test(ui): the narrow-box tests check the text reaches the box's right edge 2026-10-24 02:34:48 +11:00
RiceDee 489a76acba fix(ui): an unchanged added-text box closes without an edit
Finishing a box trimmed its text before comparing it with the item, so
an unchanged box whose text was saved with trailing spaces finished as
an edit. Before an Undo or Redo step that edit was applied, clearing
the document's Redo. A box matching its item is now no edit.
2026-10-24 02:34:48 +11:00
RiceDee d162efacaa test(ui): the Redo test checks Undo restores the exact paragraph text 2026-10-24 02:34:48 +11:00
RiceDee 7f0b451e2b fix(ui): a narrow added-text box keeps the edge its lines share
The editor of a box narrower than its 60-point minimum was anchored by
the box's own alignment. A box whose lines are all right-aligned (or
centred) on their own while the box stays Left wasn't anchored, so its
text was placed against the wider editor's edge, past the box. The
lines are now compared with each other and the editor keeps the edge
they share.
2026-10-24 02:34:48 +11:00
RiceDee eba7e47159 M7: text_edit with aligns alone keeps the paragraph's rows
Without new text, text_edit rewrote a paragraph from its text joined
with spaces, so its rows were rewrapped into one and only the first
entry of `aligns`, which counts rows, had any effect. With `aligns`
given, the paragraph's own rows (its editor text) are kept, as in the
app.
2026-10-24 02:34:48 +11:00
RiceDee a44b850dac fix(ui): a narrow added-text box keeps its text at its own edge
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.
2026-10-24 02:34:48 +11:00
RiceDee 5c61312d81 fix(ui): Redo after Undo in a page text box puts the typing back
With typing undone in a paragraph or added text box, its text matches
the page again, so Undo and Redo went to the document. Redo then closed
the box and redid a document change (or nothing), instead of putting the
typing back as the box's own Redo did before. And Shift+Cmd+Z is also
Cmd+Z to the Undo shortcut, so even a Redo the box kept was taken as a
document Undo.

Redo stays with the box while its own history has typing to put back
(the test egui makes when it handles the key), and Shift+Cmd+Z is never
a document Undo.
2026-10-24 02:34:48 +11:00
RiceDee 1f29575d34 M7: align a one-row paragraph across the column it sits in
Centring one highlighted line of a paragraph moves it out of the
paragraph's rows, so the next alignment of it saw a one-row paragraph
and aligned it in its own box: Left left it where it was, and since a
single row grows to the page's margin, each Centre moved it further
right. An alignment for a one-row paragraph is now laid out across the
column of text it sits in (the left edge the page's other text uses, to
the furthest right those rows reach), or its own box when no column
holds it.
2026-10-24 02:34:48 +11:00
RiceDee 5c1c3462eb M7: align the selected lines of a paragraph or text box on their own
Format text's alignment always re-aligned the whole paragraph or added
text box, and clicking it closed an added text box, losing the
selection, so one line couldn't be aligned. With text selected,
alignment now applies to the lines (text between line breaks) the
selection touches; with only the caret, to the whole box as before.

- Added text stores each line's alignment in its /PCAdded parameters
  (/Aligns, null where a line follows /Align). A justified line's last
  row is not stretched.
- An edited paragraph sends its rows' alignments with each rewrite
  while it is open (BlockStyle::aligns), in both the single-style and
  the styled-region rewrite.
- Both editors draw each line by its own alignment, and the alignment
  buttons show the selected line's. An added text box stays open while
  Format text is used, its style applied at once; clicks are judged by
  where they were pressed, so a touch or pen tap on the panel doesn't
  close either editor.
- Automation: `aligns` on page_add_text, content_update and text_edit.
2026-10-24 02:34:48 +11:00
RiceDee dcf33ed1c8 fix(ui): Ctrl+Z in an open paragraph undoes the document, not stale text
Ctrl+Z is left to a focused text field, so in the paragraph editor it
went to the box's own Undo history. That history still held the
paragraph's text from before the last formatting change, so Ctrl+Z put
it back into a box laid out for the page after the change, a layout
the page never had, and the document kept the change. The box shared
its id across paragraphs too, so it could put back another paragraph's
text.

The history of the paragraph editor and the added-text box now starts
at what the page has. Undo and Redo keep the keys only while the box
holds typing the page doesn't have; then they reach the document. An
open box is applied (as clicking away applies it) and closed before an
Undo or Redo step, so it never shows a state the document has left;
text it can't apply keeps it open and stops the step.
2026-10-24 02:34:34 +11:00
RiceDee 54e2895ae6 fix(ui): the added-text editor keeps the box's alignment
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).
2026-10-24 02:34:34 +11:00
PhilipandClaude Sonnet 5.5 5051a16b54 M10: review of #1609: drop an automatic run's result once the document has changed
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>
2026-10-23 16:32:11 +01:00
Marco Arnulfo cd6f031b82 M7: added text takes character spacing from Format text (fixes #4245) 2026-10-23 17:31:02 +02:00
Osvaldo Duran MelendezandClaude Opus 5.5 dd396c2096 Merge upstream main 57e531cc into pr/attachments
Only ATTRIBUTION.toml met this branch: main's copy, hashes recomputed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:31:00 -04:00
alehelandClaude Opus 5.5 7c3dfe0a2a test(render): a reply finds its comment through a reference chain and a shared /Annots array
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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 931b9a599a fix(comments): a status whose comment can't be found is listed nowhere
`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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 b70d88f235 fix(comments): test /IRT to links and widgets; say whose a reply's status is
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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 f35ec52fda fix(automation): comment_list reports a reply's own status and checkmark
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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 ecb3f6527e fix(automation): comment_list reads a status or checkmark from the annotation it was set on
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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 a629835fec fix(automation): refuse a comment id that several comments share
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>
2026-10-23 17:30:08 +02:00
alehelandClaude Opus 5.5 21814f60f5 fix(comments): show stray and nested replies, leave Replace Text out of the count
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>
2026-10-23 17:30:08 +02:00
Claude c2fd321ce3 fix(comments): show the reply count under the Comments header
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
2026-10-23 17:30:08 +02:00
Claude be600950c9 fix(comments): thread comment replies by /IRT reference, count comments like Acrobat
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
2026-10-23 17:30:07 +02:00
alehelandClaude Opus 5.5 45dccb22d0 M3: All tools search finds menu commands; Escape stays with the palette
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>
2026-10-23 17:26:53 +02:00
alehel 81158a6e11 Merge remote-tracking branch 'origin/main' into fix/background-hidden-by-page-backdrop
# Conflicts:
#	ATTRIBUTION.toml
2026-10-23 17:23:07 +02:00
Philip 9ee24d7ca9 Merge remote-tracking branch 'origin/main' into feat/edit-scanned-page-offers-ocr
# Conflicts:
#	parity/acrobat-features.toml
2026-10-23 16:10:49 +01:00
nlevaandOpus 5.5 medium ultracode d8aff8bfc7 M7: a batch of content edits refreshes the added content and page marks
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
2026-10-23 17:09:29 +02:00
alehel 322ee2c806 Merge remote-tracking branch 'origin/main' into feat/all-tools-expand-view
# Conflicts:
#	ATTRIBUTION.toml
2026-10-23 16:53:01 +02:00
57e531ccca G14: Romanian: translate the 64 catalog rows still in English (#4016)
* G14: Romanian: translate the 65 catalog rows still in English

The Romanian catalog landed in main through the integration branch
(bf3a199f, PR #2505); the keys main gained since were filled with English.
This translates them: calculation order, the zoom box, rich-text formatting,
print options, Pages thumbnail size, Reshape comment, the CMS signed-attribute
and short-RSA-key warnings, and the page-numbering Start and page Subset
labels. Romanian translations main already has (the text field table) are
kept as they are. ATTRIBUTION.toml carries the new SHA-256.

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

* G14: Romanian: quote „data” in the CMS warning; Subset reads Selecție

Review of #4016: the content type "data" is quoted so it isn't read as "the
date", and the page Subset label and the booklet subset label both read
"Selecție" instead of "Filtru" / the anglicism "Subset broșură".
ATTRIBUTION.toml carries the new SHA-256.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:43:59 -04:00
f46268fbe4 M9: visible signatures read upright on rotated pages (#4067)
* M9: visible signatures read upright on rotated pages

A visible digital signature placed on a page with /Rotate was drawn in
user space, so its graphic and details turned with the page: on scans
stored landscape with /Rotate 270 the signature came out sideways.
Stamps (#1398) and Fill & Sign marks (#2392) were already corrected;
signatures were not.

A new signature widget on a turned page gets /MK /R equal to the page
rotation (inherited rotation included, via pdfcraft_model::pages), and
its appearance is laid out in the box as displayed (sides swapped for a
quarter turn) with a /Matrix from pdfcraft_model::view_matrix_for that
maps it exactly onto /Rect. Signing an existing field follows that
field's own /MK /R, so a field without one is drawn as before. Pages
that are not turned produce the same bytes as before.

Tests: sign crate (layout equals the upright box's, /MK /R, /Matrix,
inherited /Rotate, existing fields with and without /MK /R, malformed
/MK /R) and an engine render test on 0/90/180/270 pages with the
quadrant image. Both fail with the fix reverted.

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

* Keep one evidence/notes pair in the sign.digital parity entry

---------

Co-authored-by: vjeko2404 <vjekoslav@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:43:54 -04:00
Eric Kaden 44b7e0a40f Merge remote-tracking branch 'upstream/main' into fix/exec-menu-attachments
# Conflicts:
#	crates/js/src/lib.rs
#	crates/js/src/tests.rs
#	parity/acrobat-features.toml
2026-10-23 10:43:34 -04:00
aalbertiarchitecteandClaude Sonnet 5.5 9206254142 Open Save As and export dialogs in the document's own folder (#4012)
* Save As and exports open in the document's own folder

Save As, saving a document that needs a location, the Office and
comment/form data exports, Optimize and Sign now start the file dialog in
the folder of the open PDF instead of the system's default. A new
document and an email attachment's temporary copy keep the old behaviour.

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

* Match the full Outlook temporary-copy layout before skipping the document folder

Anchor is_mail_temp_copy to the cache layout (one random folder, then the file) and
to the Packages\Microsoft.OutlookForWindows_<id> folder, so ordinary paths that merely
contain those names keep the document's own folder. Regression cases added.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:43:20 -04:00
Jayesh Prajapati 70704ff260 M2: text extraction finishes a column before the next one across a paragraph gap (#3658) (#4214)
* M2: text extraction finishes a column before the next one across a paragraph gap (#3658)

The block order took the top-most unread block next, so after the first
paragraph of a left column it jumped to the top of the right column and
read the rest of the left column last. Now the nearest block below the
one just read, overlapping it horizontally and at most 2.5 heights of
its last line below it, comes next, unless an unread block sits above it
in its own span (a column above a full-width heading or footnote),
beside it on its left, or shares its first line (the first lines overlap
by at least 30% of the taller one: a table or form row, which still
reads across, also with labels and values in different sizes or on
shifted baselines; a line that only touches it or a small footnote
marker is not a row). Footers, footnotes and captions further away keep
their old place. Extraction, Find and copy share this order.

Known limit: when a paragraph in the next column starts on the same line
as the continuation (text on a baseline grid), it counts as a row and
the top-most rule applies, so the order of #3658 comes back there.
Telling prose from a row in that case needs more evidence.

Fixes #3658

* M14: update ctutils to 0.4.5 for RUSTSEC-2026-0385

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)
2026-10-23 10:43:10 -04:00
Ahmad Mu 44e8eeb289 Cancel stale Organize page drags after document changes (#4039)
* M4: Cancel stale Organize page drags after document changes

* M4: Record Organize drag cancellation coverage
2026-10-23 10:43:05 -04:00
mtlcl410andClaude Opus 5.5 e6b9792539 M4: bookmarks show and set their title colour and style (ISO 32000-2 Tables 151, 152) (#4171)
* M4: the Bookmarks panel shows each bookmark's colour and style (#3193)

ISO 32000-2 §12.3.3: an outline item's /C is "An array of three numbers
in the range 0.0 to 1.0, representing the components in the DeviceRGB
colour space of the colour that shall be used for the outline entry's
text" (Table 151), and its /F flags say italic (bit 1) and bold (bit 2)
(Table 152). Both were ignored: every title was drawn in the theme's
text colour and face.

The inspector (OutlineItem) and the bookmark list (organize::Bookmark,
used once a document is edited) now read them: each component is kept
within 0-1, and black (the default) or a /C that isn't three numbers
reads as no colour. The panel draws bold titles in the semibold face,
italic ones slanted, and the colour as given, lightened towards the text
colour only as far as it needs to read on the panel (3:1), so dark blue
stays visible in the dark theme. bookmark_list reports color, bold and
italic where they differ from the defaults.

Mutants, each failing a test: italic or bold bit ignored (both readers,
the panel), black kept as a colour, no clamping, a four-number /C
accepted, colour ignored or truncated in the panel, the contrast floor
removed, always jumping to the text colour. Checks for non-finite
numbers were equivalent (clamping already covers any size), so they
are gone.

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

* M4: set a bookmark's style and colour from its menu or bookmark_set_style (#3193)

#3193 asks for a way to mark bookmarks by colour, for example chapters
that are rarely used. ISO 32000-2 §12.3.3 gives every outline item a
title colour (/C, Table 151, DeviceRGB, default black) and style flags
(/F, Table 152: bit 1 italic, bit 2 bold, default 0); the previous
commit shows them, and this one lets the user change them.

- organize: set_bookmark_color writes /C, removing it for black (the
  default) and refusing a component outside 0-1; set_bookmark_style
  sets or clears the bold and italic bits, keeps any bit the table
  doesn't define, and removes /F when no bit is left.
- engine: SetBookmarkColor and SetBookmarkStyle edits, undoable, under
  the same assemble permission as the other bookmark edits. Their undo
  labels are the menus' own names, already translated in every
  catalogue.
- ui-egui: the bookmark menu gains Style (Bold and Italic check boxes)
  and Colour (the comment swatch grid, with Other colour...). Only
  existing UI strings are used.
- automation: bookmark_set_style takes any of color ([r, g, b], 0-1),
  bold and italic, and changes only what it is given.

Mutants, each failing a test: no range check (library and tool), black
kept as /C, a bit not set, a bit not cleared, an untouched bit cleared,
/F 0 written, the bits swapped (library and menu), the tool accepting
nothing to change or ignoring bold/italic, the menu's colour ignored.

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

* M4: a colour and a style set in one bookmark_set_style call are one undo step (#3193)

Review on #4171 (CodeRabbit). Given both, the tool applied two edits,
so undo took back only the style and left the colour, and a failing
second edit left the first applied. They now go in one Edit::Batch
named "Style", all or nothing. "Colour" and "Style" join EDIT_LABELS,
and "Style" BATCH_LABELS, so the history can translate them.

The test sets colour and style together and undoes once; with the two
edits applied separately it fails.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:59 -04:00
pat26-dotcomandClaude Opus 5.5 7322091d95 Accessibility: set a table's Summary and a header cell's Scope (#4103, #4104) (#4130)
* M12: set a table's Summary and a header cell's Scope (#4103, #4104)

accessibility_set_tag_property takes `summary` (Table tags) and `scope`
(TH tags: Row, Column or Both). Both are Table attributes, written into
the tag's attribute object with /O /Table in /A: an existing one is
edited in place, keeping other attribute objects and their revision
numbers; one reached by reference is copied into the tag's own /A first,
so tags sharing it keep their value; a new one is added when there is
none. Clearing drops the key, an attribute object left with only /O,
and an empty /A. The tag type is checked after the role map.

accessibility_tags lists both. Setting a summary clears the
table-summary finding.

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

* M12: edit the Table attribute object that holds the value (#4103, #4104)

With more than one /O /Table attribute object in a tag's /A, setting a
summary or scope edited the first one even when a later one held the
key, and clearing left that later value in place (both still read back
as the old value). Setting now edits the object holding the key (else
the first Table object, else a new one), and clearing removes the key
from every Table object holding it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:54 -04:00
PiohrunandClaude Opus 5.5 ba01244987 M6: calculations read their notation only from a comment, and a regex after a condition as a regex (#3993)
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex

Two findings from CodeRabbit's review of #3550, both reproduced first:

- parse_calculate and calculation_sites found BVCALC and EVCALC anywhere
  in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`:
  renaming `a` rewrote the string, and the form calculated `a` instead of
  running the script. The lexer now keeps the text of each comment, and
  the notation is read from the first comment that holds BVCALC and the
  EVCALC after it (notation_range), by both the Calculate tab and a
  rename. A line comment counts, and a comment that never ends runs to
  the end of the script, as before.
- A `/` after any `)` divided, so after the condition of `if`, `while`,
  `for` or `with` a regular expression was read as code: renaming `a`
  rewrote `if (flag) /getField("a")/.test(x)`, and in
  `if (flag) /"/.test(x)` the quote started a string that hid the
  script's real getField calls. The lexer now notes which `(` opened
  such a condition (not a method of that name, `o.if(x)`), and a `/`
  after its `)` starts a regular expression.

af::tests::notation_is_read_only_from_a_comment,
af::tests::a_regex_after_a_condition_is_not_read_as_code and
tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script
fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated.

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

* M6: form.props-calculate notes that a regex right after a brace is still read as code

From CodeRabbit's review of this PR. The lexer still divides after `}`,
which ends a block as often as an object literal, so in
`if (x) { } /"/.test(s); event.value = getField("a").value;` the quote
starts a string and the getField is hidden from a rename and from the
calculation order. Checked with a scratch test before writing it down.

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

* M6: a line comment ends at any line terminator, so code after `\r` is still read

From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`,
so with `\r` line endings it hid the rest of the script: a rename missed
the getField calls after it (since #3550), and a string after it that
spells BVCALC ... EVCALC read as the notation. A `/` taken for the start
of a regular expression likewise looked past `\r` for its end, and could
swallow the next line's code.

Both now stop at any of JavaScript's line terminators (`\n`, `\r`,
U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are
skipped rather than read as the start of a word.

af::tests::a_line_comment_ends_at_any_line_terminator fails without each
of the three changes: with `\r`, the string read as notation `a`; with
the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the
getField after it; without skipping U+2028, the getField after it was
not a call.

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

* M6: a word, number or regex's flags end at U+2028 and U+2029

From CodeRabbit's review of this PR. The scans for an identifier, a
number and a regular expression's flags took every byte past ASCII as
part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")`
the getField was part of one word and not a call: a rename missed it, and
the calculation order didn't read it.

`word` now takes a position and is false where line_ends_at is true, so
the three scans share one rule. The arm that skips U+2028 and U+2029
between tokens stays: without it, the punctuation arm would step one byte
into the character.

af::tests::a_token_ends_at_a_line_separator fails without the fix, for a
word, a number and a regex's flags alike; it and
a_line_comment_ends_at_any_line_terminator fail without the skipping arm.

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

* M6: the `while` that ends a `do` loop counts as a condition, said and tested

A `/` after the `)` of a do-while starts the next statement, so it starts a
regular expression there too. The comment on BEFORE_CONDITION says so, and
a_regex_after_a_condition_is_not_read_as_code covers it (from review).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:48 -04:00
Andrew GentryandClaude Opus 5.5 7169160df9 fix(ui): keep colour popups open when "+" opens the custom colour picker (#4066)
* fix(ui): keep colour popups open when "+" opens the custom colour picker

The colour popups used egui's default menu close behaviour (CloseOnClick),
which closes on any click, including the "+" swatch inside the popup. The
picker's open state was remembered, so it appeared only on the next open.

Close these popups only on clicks outside them: the quick bar's colour and
fill popups, the comment context menu's Colour submenu and Format text's
colour popup. Picking a swatch or pressing Apply still closes them.

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

* contributors: credit agent-reed by GitHub profile name

* test(ui): pin the fill popup and context-menu Colour submenu keeping their picker open

The quick bar's Fill colour popup and the comment context menu's Colour
submenu each get a kittest test: "+" shows the picker without closing the
popup, and choosing a colour still closes it. Both fail with the fix reverted.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:43 -04:00
Piyush Goel 30bafb4992 ui: add vertical spacing around panel header separators to prevent clipping (#4073) 2026-10-23 10:42:38 -04:00
mtlcl410andClaude Opus 5.5 ac41758cf3 M3: a Launch link that names a PDF opens it in PdfCraft (ISO 32000-2 §12.6.4.6) (#4185)
* M3: a Launch link that names a PDF opens it in PdfCraft (#4168)

Documents that cross-reference each other often use Launch actions to
open another PDF next to them (#4168: 268 links such as
..\se\se_ST.pdf). PdfCraft follows no Launch action, so clicking them
did nothing, while the same link written as a GoToR opens.

ISO 32000-2 §12.6.4.6: a Launch action "launches an application or
opens or prints a document". Starting programs is not something a
document should be able to do, but opening a PDF is what a remote
Go-To already does. So a Launch action whose /F names a .pdf file, and
that has no /Win launch parameters (application, operation,
arguments), is read as the remote Go-To to that file's first page
(LinkTarget::RemoteGoTo with launch: true). It then goes through the
same path as one: PDF only, --root confinement, confirmation for
absolute paths, opened as a document and never handed to the OS.
Windows writers separate such paths with \, where §7.11.2.1 uses /, so
a path without any / has its \ read as /. doc_info's link target says
"action": "Launch" or "GoToR".

Tests: launch_actions_that_name_a_pdf_open_it_like_a_remote_link
(relative with \, a file specification, an executable, /Win, a bare
.pdf, a mixed path, a URL file system) and
a_launch_link_to_a_pdf_opens_it_like_a_remote_link (link_open opens
the PDF in the root; a program link opens nothing). Mutants, each
failing: the /Win check dropped, \ always or never read as /, any
extension, a bare ".pdf", case-sensitive ".pdf", Launch not read.

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

* M3: a Launch link with a stray /D still opens its PDF (#4168)

Launch (ISO 32000-2 Table 207) has no /D, but launch_pdf went through
remote_go_to, which rejects the action when /D isn't a valid GoToR
destination. A Launch naming c.pdf with /D 0 stayed Other(Launch).
The /F parsing is now shared (action_file) and Launch ignores /D.

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>
2026-10-23 10:42:32 -04:00
mtlcl410andClaude Opus 5.5 20a395c0ae M5: sticky notes draw the icon they name (ISO 32000-2 Table 175) (#4182)
A note's /Name picks its icon, and ISO 32000-2 §12.5.6.4, Table 175 asks
every viewer to provide icons for seven standard names: Comment, Key,
Note, Help, NewParagraph, Paragraph, Insert ("Additional names may be
supported as well"). PdfCraft drew two shapes: Comment, and a page for
everything else. So choosing Key, Help, Insert, NewParagraph or
Paragraph in Properties changed nothing visible, and notes in received
files that use these names, or further ones such as Check or Star, all
looked like the same page. Recolouring such a note redrew it as a page.

note_icon now draws a shape of its own for each standard name and for
seven further names in common use (Check, Circle, Cross, CrossHairs,
Star, RightArrow, UpArrow). They are simple geometric drawings written
for PdfCraft, filled in the note's colour with a dark outline like the
existing two. NoteIcon gains the further names (NoteIcon::ALL); the
Properties chooser and comment_add offer all fourteen. Another name
still draws as Note, the table's default.

Tests: every_note_icon_draws_its_own_shape (all fourteen differ; an
unknown name and a missing one draw Note) and
a_star_note_keeps_its_star_when_recoloured. Mutants, each failing them:
from_name limited to the standard seven, Star or Key drawn as Note, an
unknown name drawn as Comment. One survivor reshaped Circle while
keeping it distinct, which the distinctness test rightly allows.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:26 -04:00