Conflicts:
- crates/render/src/inspect.rs: this branch's reply-linking methods and main's NoRotate
rectangles (#3016) both go in the Annotation impl; the annotation reader takes this
branch's loop with main's no_rotate field.
- crates/automation/src/comments.rs: this branch's HashMap import, main's import list.
- ATTRIBUTION.toml: each catalogue's source note keeps main's secured-document clause and
this branch's reply-count clause; ATTRIBUTION.md regenerated (cargo xtask assets --write).
- The 33 catalogues: main's new rows, then this branch's reply-count row.
main's #3016 tests build Annotation field by field; they now set parent and grouped.
A Line annotation without an appearance was drawn along /L with its
endings there, ignoring /LL, /LLE, /LLO, /Cap, /CP and /CO. With /LL,
/L gives the ends of the leader lines, not of the line itself.
- The line sits |LL| + LLO from /L, on the left of travel for a
positive /LL; leader lines run from LLO off /L to LLE past the line.
- /LE endings are drawn at the line itself.
- /Cap true draws /Contents (or the plain text of /RC) in black
Helvetica 10, turned with the line: Inline breaks the stroke around
it, Top sets it above, /CO shifts it (with a tick when moved off).
- PdfCraft measurements keep /PCMeasureValue as their caption.
- undrawable_text reports caption text the standard font can't draw.
- Non-finite, huge and negative values, and a zero-length /L, are
handled leniently; captions are cut to 256 characters.
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Session::refresh_with_indexes fills title, author and the other fields from XMP as build_document does; the test applies an edit and an undo and checks they stay.
crates/engine/src/lib.rs: union of the added:: re-exports (paste_place/paste_rect from #4230, CHAR_SPACING_LIMIT).
crates/ui-egui/src/content_ui.rs: editor keeps #4178's anchored width and horizontal_align and #4266's spacing layouter; spaced_job takes the alignment so the custom layouter keeps it.
crates/ui-egui/tests/it/content.rs: kept both tests.
crates/js/src/lib.rs: module doc app/util list has execMenuItem and scand; install() doc comment from main.
crates/js/src/tests.rs, crates/engine/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: js notes: execMenuItem wording plus scand wording.
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
crates/forms/src/tests.rs: append conflict, kept both tests.
parity/acrobat-features.toml: form.fill-text-field evidence is the union; notes keep HEAD's text plus the comb-cells sentence.
Conflicts:
- parity/acrobat-features.toml: comment.note: kept main's icon notes text, added the NoRotate sentence and the PR's three evidence entries.
- vendor/README.md: kept main's rows; added the PR's hayro-interpret patch as (53) with its test (53).
parity/acrobat-features.toml: form.fill-text-field evidence is the union of both sides; notes keep both sides' sentences (DoNotScroll clause and the Password sentence).
The on-screen drop-down button for combo boxes drawn display-only, the
narrowed text area beside it, and the Print as Image copy without it
(with_printed_field_appearances, printed_display_bytes) are removed.
Open PR #3567 draws the arrow for every combo box in the page view.
Check boxes, radio buttons, push buttons and comb lines are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A form that leaves its appearances to the viewer had its text and choice
fields drawn for display (#3475), but not its buttons. Now, also for
display only, without changing the file:
- Check boxes and radio buttons with no normal appearance for their
state show their /MK background and border (Table 192), and when on,
the /MK /CA caption as a standard-14 ZapfDingbats character
(12.7.5.2.3), 4 or l by default, in the /DA colour, auto-sized for
0 Tf.
- Push buttons with none show their /MK background, their /BS border
(beveled per Table 168) and their centred caption.
- Comb fields with a border colour draw the MaxLen - 1 lines between
their positions (Table 228), up to 500 cells. This also applies to the
appearances PdfCraft saves for comb fields.
- A combo box drawn for display shows a drop-down button on screen, with
its text kept clear of it. Saved appearances and Print as Image leave
it out (with_printed_field_appearances, printed_display_bytes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Comments that arrive without /AP are drawn from their dictionaries by
appearance::build, which lost three entries:
- /Border [hr vr w [dash]] (Table 166): the dash array made the whole
array unreadable, so the width fell back to 1 and the border was solid.
The width is now element 2 whatever follows, and without /BS the
/Border dash array is drawn. /BS, when present, wins for both (Table
166: "the Border entry is ignored"), so a /BS without /W means 1.
- Dash arrays are drawn only when finite, non-negative, not all zero
(§8.4.3.6) and at most 64 long; anything else draws solid.
- Ink applies its /BS dash pattern (Table 168, Table 182), like the
other stroked shapes.
- A caret with /Sy /P (Table 183) shows a ¶ (Helvetica, WinAnsi 0xB6) to
its right in its colour, inside /Rect: in the room /RD leaves, or by
narrowing the caret. /Sy /None draws as before.
The Helvetica font resource, written out three times, is one helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
A rubber stamp from another program with no normal appearance was not
drawn at all. The display copy now gives it one:
- the fourteen standard names of Table 184 draw that stamp with
PdfCraft's stamp drawing (eight from the palette, six with labels of
their own), fitted to /Rect, in /C or the stamp's colour;
- no /Name draws Draft, the table's default, unless /IT says the stamp
is an image or a snapshot;
- any other name (names match byte for byte, §7.3.5) draws a thin grey
frame with both diagonals, so the stamp stays visible and selectable.
The drawing is display-only, like the other comments without an
appearance: Save writes the file as it was, and build() still declines
these stamps, so restyling and flattening never write it into the file.
A stamp with its own /AP /N is never touched. stamp() now takes the
label and the sign-here flag instead of a StampKind.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
Review: copying an added JPEG 2000 image failed even within its own
document, because the exporter can't write JPEG 2000 yet. Add Image stores
such a picture as the file it was given, so that file is the copy: it
pastes into the same document as the same object and into another as one
embedded image. The notes now also say that a CMYK picture stored
losslessly turns to RGB in another document.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.
pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.
The content_copy tool drives it headlessly, across pages and documents.
The desktop keys and menus come in follow-up PRs.
Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
The signing saves wrote with default options, so a redaction mark added
and cleared in the session (or a comment added and deleted) reached the
file through a signature, a timestamped signature, embedded LTV evidence
or a document timestamp, with its QuadPoints, overlay text and authors.
A signed revision can't be changed afterwards, so nothing could take it
out again.
`sign`, `sign_with_timestamp` and `dss::embed` now save with
`drop_unlinked`: everything they add is reachable (the signature through
the field's `/V`, the store from the catalog), and the ByteRange
placeholder is found in the written bytes.
`timestamp_document` can't: its `/DocTimeStamp` dictionary has no field
pointing to it, so the option would drop it and every document timestamp
would fail with "ByteRange placeholder not found". It frees the leftovers
first with the new `Document::free_unlinked_additions`, which frees
exactly what the option leaves out, and then adds the dictionary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
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
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
* 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>