Commit Graph
204 Commits
Author SHA1 Message Date
42bc4f117d M5: copy and paste comments between documents, select several, finish connected lines from the menu (#3484)
Comments copy and paste, within and between documents. ⌘C with comments selected (and no text
selected) copies them, each with its replies, as XFDF: into the app's clipboard (every tab and
window) and as text onto the system clipboard. ⌘V, Edit ▸ Paste comments or the page's context
menu pastes them onto the current page at their own positions, as new comments under names of
their own, so nothing is replaced and pasting twice gives two sets; the copies are selected.
XFDF with comments from the system clipboard pastes too, so comments travel between processes.

pdfcraft_xfdf::export_xfdf_comments writes a selection with its replies; import_comments takes
a target page and a name prefix and imports copies (the usual import still replaces same names).
Engine: Edit::PasteComments { xfdf, page } (one undo step, comment permission) and
Session::export_comments. Tools: comments_copy (read-only) and comments_paste.

Several comments select at once: ⌘-click (Ctrl on Windows and Linux) or ⇧-click adds one or
takes it out, on the page and in the Comments panel; the one clicked last has the handles.
Delete removes them all as one step, a drag moves them together, the context menu copies them.

Connected lines, polygons and clouds finish from the right-click menu's Complete (with Undo last
point and Cancel drawing), as other editors do, besides Enter and a double-click.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 16:41:45 +01:00
c3c36f77cf M5: a comment's text, border or line, and fill each at an opacity of its own (#4542)
* A comment's text, border or line, and fill each at an opacity of its own

Comment Properties showed one opacity for a whole comment, so a text box's
see-through background took its text and border with it, and a rectangle
marking an area on a plan couldn't have an opaque line over a faint fill.
Now a text box, typewriter text or callout has a Text, Border and Fill
opacity, a rectangle, oval, polygon or cloud a Line and Fill opacity (a part
not drawn is greyed); everything else keeps one Opacity.

The line's stays /CA, the fill's is /ca (PDF 2.0's opacity for what is
filled) and the text's /PCTextOpacity, each only where it differs from the
one it would follow, and the appearance draws them with /GS0 (strokes at the
line's, fills at the fill's) and /GS1 for the text, so every viewer shows
them. The quick bar's Opacity and StyleAnnotation's set the whole comment's.

Engine: Style::{fill_opacity, text_opacity}, Edit::PartOpacity,
annot::{set_part_opacity, appearance::part_opacity}, Props::{fill_opacity,
text_opacity}. Make Properties Default and the settings carry the parts.
Headless: comment_add / comment_edit fill_opacity, text_opacity; comment_list
reports opacity, fill_opacity, text_opacity. Four labels in every catalog
(AI-assisted translations from English, noted in ATTRIBUTION.toml).

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

* Review: the text's opacity slider shows what the text is drawn at; comment_list skips replies' properties; ui-parity's change record

A text box without fill and border whose text kept an opacity of its own showed the border's
in its Text opacity slider; it now shows the text's, and a change sets the whole comment's.
The opacity sliders are named by their labels. comment_list reads a comment's properties only
for comments, not for every reply.

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

* Rebase: comment_list tells replies by is_reply, as main now threads them

main threads replies by their /IRT reference (#4061); the opacity fields skip a reply the
same way.

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

* Review: a text-only text box made default gives the tool its text's opacity

With neither a border nor a fill, a text box's text is the whole comment:
Comment Properties shows its drawn opacity (the text's own, when set), and a
change there sets the whole comment's. Make Properties Default, and the
context menu's Make Current Properties Default, stored /CA instead, so a
text box whose text was at 50 % gave the tool 100 %. The default is now the
text's drawn opacity in that case.

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

* Rebase: the part opacities in main's annot layout (Style's fields, the text box drawing's parts, set_part_opacity exported)

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

* Rebase: main's new comment test builds a Style with the part opacities

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 14:39:36 +01:00
mtlcl410 58a3c8f0a8 Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/annot/src/appearance.rs
2026-11-05 06:28:37 +08:00
mtlcl410 35290bd0da Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/annot/src/appearance.rs
2026-11-05 06:26:16 +08:00
FredApps 52d68d7844 annot: share threed's MAX_MODEL_BYTES with model3d
main's 3D-embed change (#5808) added crates/annot/src/threed.rs with its own
pub MAX_MODEL_BYTES (same 256 MiB value), so both were re-exported from the
crate root and pdfcraft-annot no longer compiled after merging main. model3d
now uses threed's constant.
2026-11-04 19:52:51 +01:00
FredApps 697a02e2ef Merge storytold/main into feat/model3d-render
# Conflicts:
#	crates/automation/src/lib.rs
#	crates/automation/tests/it/main.rs
2026-11-04 19:52:51 +01:00
mtlcl410 97eb841f1d Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/engine/src/lib.rs
2026-11-05 02:37:48 +08:00
mtlcl410 4f58d57f8e Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-05 02:35:13 +08:00
mtlcl410 e7683c4619 Merge remote-tracking branch 'upstream/main' into HEAD 2026-11-05 02:35:11 +08:00
mtlcl410 b96b7e6f77 Merge remote-tracking branch 'upstream/main' into HEAD
# Conflicts:
#	crates/ui-egui/src/tools/comments/geometry.rs
#	crates/ui-egui/src/tools/comments/input.rs
#	crates/ui-egui/src/tools/comments/paint.rs
2026-11-05 00:21:08 +08:00
KenM76andClaude Opus 5.5 fb05b733f9 M12: model_render, and a 3D reference's stream under /3D
A 3D reference dictionary holds the shared stream under /3D (ISO
32000-2 §13.6.3), not /3DD; it is followed once, and the caller refuses
anything that is not a stream, so a chain or cycle of references ends.
The tool is renamed model_render, since MCP tool names here are lowercase
letters and underscores only, and to match model_add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 22:54:28 -05:00
KenM76andClaude Opus 5.5 ae1b28cf1d M12: model3d_render, a 3D annotation's PRC model drawn to a PNG
pdfcraft-annot reads a page's 3D annotations and the model each shows:
the /3DD stream, or the stream a /3DRef names (at most four deep, so a
cycle ends), decoded within 256 MB and typed PRC or U3D by /Subtype.
pdfcraft-engine exposes both on Document.

The automation tool model3d_render picks a page's Nth 3D model, parses
PRC with pdfcraft-prc, assembles and frames it with pdfcraft-prc-render
from a chosen direction (default an isometric-style view, Z up), and
returns the image as a PNG. U3D gives an error until a decoder lands.
parity misc.3d-view moves to partial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-03 21:44:55 -05:00
mtlcl410andClaude Opus 5.5 875c04dfaf Merge storytold/pdfcraft main into feat/dynamic-stamps
main moved the interface translations to craft-i18n (#6055) and removed
crates/ui-egui/src/i18n/*.tsv; this branch's catalog rows go with them
and are left for craft-i18n. Untranslated labels show in English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-04 09:54:00 +08:00
KenM76andClaude Opus 5.5 ace92f3f2f Merge origin/main
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
2026-11-03 11:09:49 -05:00
mtlcl410andClaude Opus 5.5 390704d856 Merge storytold/pdfcraft main into feat/comment-popups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-04 00:02:32 +08:00
mtlcl410andClaude Opus 5.5 68c56768fd Merge storytold/pdfcraft main into feat/comment-groups
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-03 23:53:10 +08:00
AdrianTM 0d3f57e120 M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850) (#5954)
* M5: keep Fill & Sign editing on Enter and preserve line breaks (#5850)

* M5: preserve typewriter whitespace through readers and edits (#5850)

* M5: retain direct-subtype comment listing for Fill & Sign fix (#5850)
2026-11-03 10:24:45 -05:00
KenM76andClaude Opus 5.5 4c087336aa M12: 3D models: declare PRC's Adobe extension before 2.0; refuse a poster with no area
Before PDF 2.0, PRC in a 3D stream is an Adobe extension to PDF 1.7, so a
PRC model added to a file below 2.0 now declares /Extensions /ADBE at
BaseVersion 1.7, ExtensionLevel 1, keeping other extensions and a later
ADBE level, and leaving an /ADBE that is not a dictionary (an array of
them) alone (ISO 32000-2 §7.12). U3D and 2.0 files are unchanged.

A PDF poster whose page box has no area was drawn as nothing behind a
size forced to 1; it is now refused with EditError::Source.

Both found by CodeRabbit. Tests a_prc_model_declares_the_adobe_extension_before_pdf_2
and a_3d_poster_page_without_area_is_refused fail with their fix reverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 18:48:12 -05:00
KenM76andClaude Opus 5.5 9da253ebee fix(annot): a 3D poster fitted to its rectangle with rounding is taken
A poster box fitted to the annotation rectangle in floating point can
end a few ulps outside it (a 7x3 picture centred in 0 100 3.6 110.8
spans -2.2e-16 to 3.6000000000000005), and the exact containment check
refused it. The check now allows a slack of 1e-9 of the rectangle's
largest coordinate, clamps the box to the rectangle, and only then
requires it to have width and height, so a box wholly within the slack
outside the rectangle is still refused.

Tests: a_3d_poster_fitted_to_its_rectangle_with_rounding_is_taken
fails without the slack (Invalid); the new case in
a_3d_poster_outside_its_rectangle_or_without_a_size_is_refused fails
when the size check runs before the clamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 17:24:42 -05:00
KenM76andClaude Opus 5.5 16ffab9b03 M12: add a 3D model (U3D or PRC) to a page with a poster (#1297)
A new `model_add` automation tool and `Edit::AddModel3d` embed a U3D or
PRC file as a 3D annotation (ISO 32000-2 13.6.2) in a chosen rectangle:

- The model goes into a Flate-compressed 3D stream (13.6.3) with /Type /3D
  and /Subtype U3D or PRC, read from the file's header (`U3D` and a zero
  byte, or `PRC`); anything else is refused before the document changes.
  Models over 256 MiB (MAX_MODEL_BYTES) are refused. model_add checks the
  file's size (from the file system) and its header before reading the
  rest, so a large file that is not a model is never loaded; the path goes
  through the read root, and `read_model` joins the helpers the
  file-access test knows take a resolved path.
- /3DD references the stream, so the annotation has its own instance.
- /3DA states /A: click (XA, the standard's default and the tool's),
  page_open (PO) or page_visible (PV).
- /AP /N is the poster readers print and show before activation: a chosen
  image or PDF page fitted inside the box keeping its shape and centred (the
  custom-stamp picture path: `mark_source`, `fit_picture`, and the stamp's
  placement matrix now shared as `appearance::picture_place`), or, with no
  picture, a light frame labelled "3D model". It is drawn in user space and
  not counter-rotated, so on a page with /Rotate it turns with the page, as
  the model does.
- /Contents carries the name (the file stem by default) as the alternate
  description (12.5.2).
- A document older than the model needs gets /Version in its catalog
  (7.7.2): 1.6 for U3D, 1.7 for PRC, as the optimizer does for JBIG2.
  Adobe's extension level for PRC in 1.7 is not written.
- The 3D rectangle has its own MAX_MODEL_SIDE (14400 pt).

The edit is undoable ("Add 3D model"), needs the comments permission and
rebuilds only the comments view. A new parity entry, misc.3d-add, is
partial: no poster rendered from the model, no views (/VA, /3DV), no
replacing the poster, no UI command, no /Extensions. misc.3d-view is
unchanged. ROADMAP.md progress log line.

Adapted from pdfcer (MIT, same author), commit 1ca5ece, rewritten to this
repository's types and rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-02 16:09:08 -05:00
mtlcl410andClaude Opus 5.5 4a89a456d0 Merge storytold/pdfcraft main into feat/dynamic-stamps
Conflicts:
- crates/engine/src/tests.rs: both sides added tests at the end of the
  file. Kept this branch's three stamp-line tests and main's page-deletion
  save and image-transform tests; the closing braces after the markers are
  shared, so the branch's last test gets its own.
- crates/ui-egui/src/i18n/{ca,fi,hi,id,ko,nb,ro,sq,th,vi}.tsv: both sides
  added rows at the same place. Kept both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 16:24:08 +08:00
mtlcl410andClaude Opus 5.5 68c245c2a1 Merge storytold/pdfcraft main into feat/comment-groups
Conflicts:
- crates/engine/src/comments.rs (comment_kind): main added the
  Freehand Highlight arm, this branch the Replace Text arm; both kept.
- crates/automation/tests/it/automation.rs and
  crates/ui-egui/tests/it/comments.rs: both sides appended tests at the
  end of the file; both kept, each test closed on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 16:20:25 +08:00
mtlcl410andClaude Opus 5.5 9ffb00f8fd Merge storytold/pdfcraft main into feat/comment-popups
Conflicts, each where both sides added at the same place; both kept:
- crates/annot/src/tests.rs, crates/automation/tests/it/automation.rs and
  crates/ui-egui/tests/it/comments.rs: this branch's pop-up tests and main's
  new tests.
- crates/ui-egui/src/i18n/{ca,fi,ro,vi}.tsv: this branch's pop-up lines and
  main's image-paste and Home lines. No key is added twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 12:40:36 +08:00
mtlcl410andClaude Opus 5.5 5b196f9079 Merge storytold/pdfcraft main into fix/freetext-redact-appearance
Conflicts:
- crates/annot/src/modify.rs: main (faebcfd26) made the same change to
  border_width_of (appearance::border_width) and added ink_rect_for_width
  after it; kept main's body and new function, and this branch's doc comment.
- crates/annot/src/tests.rs: both sides added tests at the same place; kept
  this branch's text-box tests and main's freehand-highlight test, each with
  its own closing braces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 12:19:17 +08:00
mtlcl410andClaude Opus 5.5 512eee1316 Merge storytold/pdfcraft main into feat/callout-reshape-1152
Conflicts:
- crates/annot/src/modify.rs: the create imports; kept this branch's
  callout helpers and main's ink_bounds.
- crates/annot/src/tests.rs: both sides added tests at the same place
  (the callout tests here, main's freehand highlight test); kept both.
- crates/automation/tests/it/automation.rs, crates/ui-egui/tests/it/comments.rs,
  crates/ui-egui/src/tools/comments/tests.rs: both sides added tests or
  helpers at the same place; kept both, each with its own closing brace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 12:18:38 +08:00
mtlcl410andClaude Opus 5.5 8f75c414e9 M5: comments are sized with the border width they are drawn with
border_width_of, which fits a text box's new text and sizes callouts,
polygons and line endings, read only /BS /W (and turned a negative width
into 0), while the appearance draws with appearance::border_width: /BS /W,
else /Border's third number, 1 for an unusable one. A text box with
/Border [0 0 4] and no /BS was drawn with a 4-pt border but fitted as if
it had a 1-pt one, 6 pt shorter. border_width_of now is that rule.

Review on #4365 (CodeRabbit, crates/annot/src/modify.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 09:11:53 +08:00
mtlcl410andClaude Opus 5.5 95ba736d66 M5: a callout knee with a coordinate that isn't a number takes the bottom side whatever the other
callout_side's doc says a coordinate that isn't a number takes the bottom
side, but every comparison with NaN is false, so the other coordinate
alone decided: [NaN, y above the box] gave Top, [x left of it, NaN] Left.
A NaN coordinate is now checked before any comparison.

Review on #4094 (CodeRabbit, crates/annot/src/create.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-02 09:07:57 +08:00
archamon faebcfd264 fix(annot,ui): stabilize ink width bounds, search translated tool labels, and fix hotbar settings label 2026-11-01 20:58:16 +00:00
archamon 53c79ca9b0 feat(ui,annot): customizable hotbar strip, freehand highlight, and moveable navigation rail
- Add Freehand-Highlight tool with 100pt line width support, Lucide highlighter icon, and PDF Multiply blending.
- Add customizable floating and docked hotbar with command picker, size slider, full-side strip chrome integration, and settings gear icon anchored at the bar end.
- Add left/right docking options for the navigation rail with toggle button and preferences setting.
2026-11-01 20:23:12 +00:00
mtlcl410andClaude Opus 5.5 3602d9f2d9 M5: a group member outlives its deleted primary without /IRT; /IRT walks tested past their bounds
Deleting a group's primary (delete_annotation, lift_annotation) or
clearing a redaction mark with a group member (redact::clear_marks) now
removes /IRT from each subordinate that stays and keeps its /RT /Group,
as Acrobat desktop does. The kept member no longer refers to the deleted
annotation, so a full save leaves the deleted one's content out. It is
part of the same edit: one undo restores the group, /IRT included.
lift_annotation's copy-only uses (the page beneath a dragged stamp or
signature) only render the copy, so the change there is harmless.

Each /IRT walk this PR relies on gets a test past its bound and a loop:
group_primaries on an unsettled loop (render), comment_summary under a
100-deep chain of subordinates and a self-listed loop (engine), and
deletion over a 200-deep thread and reply and group loops (annot).

Review on #4358.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 22:09:33 +08:00
mtlcl410andClaude Opus 5.5 9d38c80b8a M5: a callout's knee on the box's outline meets the side it is on (#1152)
callout_side gave every position a side, but a knee exactly on the left,
right or top edge, or on a corner, took the bottom side, and the leader
line was drawn across the box. A knee on the outline (or within 0.01 pt
inside it, as the page hands positions back as f32) now meets the side
whose edge it lies on, a corner the left or right side as beside the box.
Inside the box, or with a coordinate that isn't a number, it meets the
bottom side. The rule is written out on callout_side and every tie is
tested.

Tests also pin the /CL invariant (ISO 32000-2 Table 177: four or six
numbers): four- and six-number callouts keep their count through the
arrow point, knee, box move, box resize, whole move and save/reopen, and
ten malformed /CL values are refused, left as they were by a resize and
shifted in place by a move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 19:52:38 +08:00
mtlcl410andClaude Opus 5.5 2c02ca3a56 M5: a text box's /DA without a usable size, with a non-finite operand or naming no listed font draws at the defaults (ISO 32000-2 12.7.4.3)
A /DA string comes from the file. Reviewing how the PR reads it:

- An operand that is not a finite number was taken as one: Rust parses
  "NaN" and "inf", so "NaN NaN NaN rg" reached the appearance stream as
  "NaN NaN NaN rg" (clamping keeps NaN), and "/Helv inf Tf" drew at the
  400 pt cap. parse_da now ignores such an operand with its operator.
- A Tf without its size ("/TiRo Tf") lost its font: da_font_name read
  the name two tokens before Tf only. The name is now read whether or
  not a size sits between it and Tf; the size falls back to 12 pt.
- A zero or negative size keeps the default 12 pt (zero is auto-size
  only for form fields, §12.7.4.3; a text box has none), now stated in
  parse_da's comment.

Tests (each fails with its fix reverted): a text_style unit test over
missing, zero, negative, NaN, infinite, huge and non-numeric sizes,
missing Tf, non-finite and out-of-range colours, garbage and non-UTF-8
bytes; and an appearance test that draws boxes from such /DA strings,
including a font in /DR and one no /DR lists, and checks the stream's
font, size and colour and that no NaN or inf reaches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 19:05:11 +08:00
mtlcl410andClaude Opus 5.5 c2251a4e16 M5: a pop-up /Rect too large for f32 reads as no pop-up; reversed corners are tested (#2137 review)
A file's huge number became infinite as f32 and reached the window's
place. Both readers (annot summaries and the render inspector) now take
such a rectangle as no pop-up, as one that isn't four numbers; opening it
gives it a new place. Reversed corners were already put in order; tests
now cover both, read from a file and saved through set_popup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 16:38:13 +08:00
mtlcl410andClaude Opus 5.5 f508fdf053 M5: pop-up rectangles read alike, edits in one frame kept, pop-up drafts follow their comment (#2137 review)
- The render path reads a pop-up as none unless its /Rect is four numbers,
  as pdfcraft_annot::summaries does; set_popup gives a comment's own pop-up
  without one the place a new pop-up gets, so opening it shows it again.
- The composer's edit and a pop-up's, landing in one frame, are queued
  together in one batch instead of the pop-up's replacing the composer's.
- A pop-up's unsaved text, reply draft, drag and resize follow their comment
  when a deletion moves it, are dropped on undo/redo, and are dropped after
  any other edit that leaves their (page, index) naming another comment
  (pages moved, inserted or deleted).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 10:22:55 +08:00
mtlcl410andClaude Opus 5.5 65eb3257c5 M5: a pop-up is its comment's when /Annots lists the comment through a reference to it (#2137)
Review on #2137: when /Annots holds 5 0 R and object 5 is only a
reference to the comment 6 0 R, the pop-up's /Parent names 6 0 R, but
the ownership check compared it with 5 0 R. The comment's pop-up was
then missing from both the on-open listing (render inspect) and the
summaries after an edit (annot). Both now compare the references they
resolve to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 09:16:03 +08:00
mtlcl410andClaude Opus 5.5 a7fd7f27ae M4: a pop-up counts as a comment's only when it is a pop-up of that comment (#2137)
Review on #2137. set_popup accepted a pop-up whose /Parent was present but
not a reference, and the comment list (pdfcraft_annot::summaries and the
inspector) reported whatever /Popup named: another annotation's rectangle
and open state. One check, own_popup, now decides for both: /Subtype
/Popup, and /Parent absent or a reference to the comment (ISO 32000-2:2020
12.5.6.14, Table 186: "Optional; shall be an indirect reference").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-11-01 09:16:03 +08:00
mtlcl410andClaude Opus 5.5 7a916ae703 M6: pop-ups: a /Popup that isn't the comment's own is replaced, edits follow comment permission, selected markup shows its connector
Review follow-ups on #2137.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q
2026-11-01 09:16:03 +08:00
mtlcl410andClaude Fable 5.1 d1e1357775 M5: comment pop-up windows on the page, with connector lines
A double-click on a comment opens its pop-up window as in Acrobat:
author, date, the text edited in place, the replies; the title bar
drags it, x closes it, each an undo step saved in /Popup (/Open, /Rect)
and a note's own /Open. A connector line in the comment's colour joins
the comment to its open window on roll-over or selection. comment_popup
does the same headlessly; comment_list reports the pop-up. Three undo
labels in 22 catalogues (Vietnamese included).

Ported onto main's split modules: set_popup lives in annot's modify.rs
and PopupInfo in info.rs; the pop-up windows are their own module,
ui-egui tools/comments/popups.rs, listed with the page modules that
read the pointer and set the cursor themselves; the hover tip filter is
in comments::describe. PageCx's pointer and redaction look, the
tests/it evidence paths and the Annotation test fixtures follow main
(formerly fcd33e131, c68e5d902, 0a7fb530b, 8af2c338e, 345c1573d), and
labels are logical text now that egui shapes bidi itself.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-11-01 09:16:02 +08:00
mtlcl410andClaude Opus 5.5 8845066ab7 M5: Delete deletes the group member clicked, as Acrobat does
Acrobat desktop deletes only the member of a group (/RT /Group) that was
clicked: deleting the primary keeps its subordinates, on its page or another,
and deleting a subordinate keeps the rest of its group. ISO 32000-2 §12.5.6.2
lists deletion among the operations that act on the entire group; this
follows Acrobat. A subordinate left without its primary is a comment of its
own.

Replace Text stays one comment: its strikeout and caret go together from
either, in both orientations (the caret as primary, and the strikeout as
primary, as earlier PdfCraft builds wrote it), as Acrobat deletes the pair.

On the page, a click on a subordinate still selects its group, and Delete
(the key or the menu) acts on the member clicked. The group-primary lookup
that deletion used is gone with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 22:34:46 +08:00
mtlcl410andClaude Opus 5.5 665b361d5c M5: a group is one page's, as Acrobat reads it
A subordinate (/RT /Group) whose /IRT primary is on another page is now a
comment of its own, listed with its own author and text, rather than part
of that group. ISO 32000-2 §12.5.6.2 doesn't limit a group to a page, but
Acrobat (desktop and iPad) and PDF-XChange all keep such a subordinate when
the primary is deleted, and Acrobat desktop lists it on its own; tested on
a synthetic two-page file. pdfcraft_render::link_replies and
pdfcraft_annot::summaries both apply the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 17:04:57 +08:00
mtlcl410andClaude Opus 5.5 22d0e4cc4e M6: a lined custom stamp keeps its layout on quarter-turned pages (#1741)
On a page turned 90 or 270 degrees the stamp's rectangle has its sides
swapped in user space, and the appearance is counter-rotated so viewers fit
its turned bounding box to /Rect (ISO 32000-2 12.5.5). The lined layout was
computed in user-space axes, so that fit scaled the two displayed axes
differently: the picture came out squashed and the band stretched.

The layout is now made in the displayed box (the rectangle with its sides
swapped) and the stamp records the page's turn in /PCPictureRotate, so the
appearance carries the counter-rotation itself and a later restyle rebuilds
it the same way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 13:52:29 +08:00
mtlcl410andClaude Opus 5.5 528357a38e M6: custom stamps can carry a line filled in when they are placed (#1741)
Create Custom Stamp offers an optional line under the picture, a
template with {name}, {date} and {time} filled in when the stamp is
placed, in the built-in dynamic stamps' format, and kept with the stamp
library. The picture keeps its shape above the line and the stamp widens
to fit the line. stamp_custom takes the same line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:52:29 +08:00
mtlcl410andClaude Opus 5.5 492011348e M5: comments in an /RT /Group are one comment, with the primary's attributes, ported onto main
A single squashed port of the branch's four commits onto current main, which
split annot's lib.rs (info.rs, modify.rs), render's inspect.rs (model.rs,
annotations.rs, tests.rs), engine's lib.rs (comments.rs, edit/run.rs) and
ui-egui's comments.rs (tools/comments/*). The behaviour and tests are the
branch's (ISO 32000-2 §12.5.6.2):

- a group subordinate is part of its primary's comment: listed, selected,
  exported and deleted with it, with the primary's attributes;
- Replace Text writes the caret as the primary and the strikeout as its
  subordinate; earlier groups (strikeout primary, text on the caret) still
  list their text, through a map;
- deleting a group refuses a locked member, and a subordinate whose primary
  is past the depth limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 08:29:58 +08:00
mtlcl410andClaude Opus 5.5 24ac313428 M5: a callout given its arrow point alone keeps its knee (#1152)
comment_edit with one point on a callout whose /CL has six numbers built
the new line from that point and the end only, so the knee was dropped
without a word. An agent moving just the arrow lost the knee.

place_text_box now keeps the old knee when only the arrow point is given;
two points still set both, and a four-number /CL still gains a knee only
when one is sent. The points schema says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 08:02:40 +08:00
mtlcl410andClaude Opus 5.5 adc0b91b1e M5: callouts reshape by their arrow point, knee and text box, ported onto main (#1152)
A single squashed port of the branch's seven commits onto current main, which
split the files this touched into modules: annot (create.rs, modify.rs,
info.rs), render inspect (model.rs, annotations.rs), engine comments.rs and
ui-egui tools/comments/*. The behaviour and tests are the branch's:

- a callout's arrow point and knee have handles and move on their own; its
  line follows the box as Acrobat's does; its text box resizes;
- comment_list gives a callout's points and text box, comment_edit reshapes it;
- /CL is read all or nothing, and a box sent back as f32 still moves or keeps
  its line.

On main, select_input's knee cursor moved into point_cursor to stay under the
cognitive-complexity limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-31 08:01:02 +08:00
mtlcl410andClaude Opus 5.5 ff886283f8 M5: text boxes and redaction marks without an appearance draw their style and outline, ported onto main
A single squashed port of the branch's five commits onto current main, which
split annot's lib.rs into modules (create.rs, modify.rs, info.rs, draw.rs) and
appearance::build into per-subtype drawing functions. The behaviour and tests
are the branch's (ISO 32000-2 Tables 166, 177, 195):

- text boxes without /AP draw their default 1-pt border, their /DA face when it
  is a standard text face, and /DS font, size and colour (text_style.rs);
- unapplied redaction marks are outlined in an array /OC colour, with or
  without /QuadPoints, and keep /C otherwise;
- a text box's colour or size change keeps the face it is drawn in
  (restyle_text), and edited text boxes re-fit in their drawn face and size.

On main, the branch's set_style test calls use StyleChange, and the parity
entry keeps main's evidence paths (render raster/tests.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
2026-10-30 23:56:30 +08:00
FredApps b21af3b969 Integrate 21 clean-merging PdfCraft parity PRs
Preserve each original PR head and its authorship as a merge parent.
Includes #4304 #4335 #4384 #4404 #4179 #5300 #4719 #4541 #2815 #4620 #3789 #3779 #2960 #5208 #4596 #3636 #4494 #4013 #5134 #2133 #2635.
Conflicting candidates skipped. Combined CI pending; see docs/integration-20261030.md.
2026-10-30 10:35:42 +01:00
Oliver HagmannandClaude Opus 5.5 f0cd96e90b Review: isolate_annotations says that an index off the page's list is an error
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:54:15 +01:00
Oliver HagmannandClaude Opus 5.5 b68362bdc2 Rebase: isolate_annotations is exported from main's lookup module
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:50:58 +01:00
Oliver HagmannandClaude Opus 5.5 67b5b2dcf6 A moved comment is drawn where it goes while it is dragged
Moving a rectangle, an oval or any other comment showed only its blue
frame going, the shape staying where it was until release. The comment is
now rendered alone once as the drag starts (Document::comment_layer: a
read-only copy with only that comment, no page content) and laid over the
page where it goes; the page beneath keeps showing it where it was until
the move is applied, as a CAD program's move does. Rendering the page
without it would cost a whole page's render at every drag on a heavy plan.
Stamps and Fill & Sign marks keep their own layers. annot: isolate_annotations
for several comments of a page, so a group can be drawn together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 05:50:58 +01:00