Commit Graph
123 Commits
Author SHA1 Message Date
496e37d72b M5: text boxes and typed text take an oval or a circle for their border (#4543)
* Fill & Sign: a Border control for typed text, and a border colour of its own

Rebased onto main as one commit: main's Fill & Sign text colour (#2444) runs through the
same places as the border (the open box, typed(), the page input, the toolbar and the
preferences), so every step of the branch met it again. Text colour and border now travel
together as TypedLook (typed_with), which keeps typed() and the new function under Clippy's
argument limit; a border without its own colour takes the text's. Make Current Properties
Default on typed text also keeps its colour for new text now.

The branch's commits, in order:
- Typewriter text: a border change grows its box on every side, so the text keeps its place
- Fill & Sign: a Border control for typed text and dates
- Parity: the typewriter border's evidence
- i18n: the Border label in every catalog, recorded in ATTRIBUTION.toml
- Text boxes, typewriter text and callouts: a border colour of their own
- Fill & Sign: the toolbar follows the selected text; a Border colour button
- fill_sign_add and comment_edit take `border_color`
- Parity: the border colour and the toolbar following the selection
- i18n: Border colour and Same as text in every catalog, recorded in ATTRIBUTION.toml
- Typed text's border: /Border's width is read, a non-finite border colour refused, a saved Same as text restored
- Fill & Sign: the toolbar changes only the selected text; Make Current Properties Default keeps new text's border
- Review: Make Properties Default gives a text box's or callout's tool its border colour
- i18n: the new labels in the Persian catalog, recorded in ATTRIBUTION.toml
- Review: a test that dragging the Font size control resizes the selected text to where the drag ends
- Rebase: the typed text's border in main's annot and comments modules (Style's border colour, exports), and craft_ui's ComboBox

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

* M5: text boxes and typed text take an oval or a circle for their border

Rebased onto #4146's new head as one commit (see there: main's Fill & Sign text colour runs
through the same places). The border shape goes into TypedLook's border with the width and
colour; a circle around typed text still takes the snug box.

The branch's commits, in order:
- Text boxes and typed text take an oval or a circle for their border
- A circle goes snug around its text
- Review: fill_sign_add takes an oval or a circle only with a border
- Review: a circle made on a quarter-turned page is sized from the text as shown
- Review: Border ▸ None takes an oval or a circle away with the line
- Review: the Border colour button shows the selected text box's own colour
- i18n: Border shape in the Persian catalog, recorded in ATTRIBUTION.toml
- Review: Border ▸ None on a circled text is one undo step
- Rebase: the border shape in main's annot, engine and comments modules; craft_ui's ComboBox; Fill & Sign's border controls in a function of their own (main's complexity limit)
- Review: Thickness 0 in Comment Properties takes an oval or a circle away, as Border ▸ None does

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

* Open the Font size popup first in the drag test: main moved the size into a popup

* Border shape per selected comment, and split the oval/circle border out of draw_free_text (clippy cognitive complexity)

* Re-run CI (the push of the main merge did not start it)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 17:32:01 +01:00
0839c1e761 Fill & Sign: a Border control for typed text, and a border colour of its own (#4146)
* Fill & Sign: a Border control for typed text, and a border colour of its own

Rebased onto main as one commit: main's Fill & Sign text colour (#2444) runs through the
same places as the border (the open box, typed(), the page input, the toolbar and the
preferences), so every step of the branch met it again. Text colour and border now travel
together as TypedLook (typed_with), which keeps typed() and the new function under Clippy's
argument limit; a border without its own colour takes the text's. Make Current Properties
Default on typed text also keeps its colour for new text now.

The branch's commits, in order:
- Typewriter text: a border change grows its box on every side, so the text keeps its place
- Fill & Sign: a Border control for typed text and dates
- Parity: the typewriter border's evidence
- i18n: the Border label in every catalog, recorded in ATTRIBUTION.toml
- Text boxes, typewriter text and callouts: a border colour of their own
- Fill & Sign: the toolbar follows the selected text; a Border colour button
- fill_sign_add and comment_edit take `border_color`
- Parity: the border colour and the toolbar following the selection
- i18n: Border colour and Same as text in every catalog, recorded in ATTRIBUTION.toml
- Typed text's border: /Border's width is read, a non-finite border colour refused, a saved Same as text restored
- Fill & Sign: the toolbar changes only the selected text; Make Current Properties Default keeps new text's border
- Review: Make Properties Default gives a text box's or callout's tool its border colour
- i18n: the new labels in the Persian catalog, recorded in ATTRIBUTION.toml
- Review: a test that dragging the Font size control resizes the selected text to where the drag ends
- Rebase: the typed text's border in main's annot and comments modules (Style's border colour, exports), and craft_ui's ComboBox

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

* Open the Font size popup first in the drag test: main moved the size into a popup

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-11-05 17:08:08 +01:00
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
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
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
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
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
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
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
alehelandClaude Opus 5.5 6a1783599d fix(comments): Delete asks the engine whether it would delete
The menus greyed Delete out from the listing's reply links while the
engine refused on the PDF's own /IRT and /Parent links, so the two could
disagree. pdfcraft_annot::deletable is now the one check: delete_annotation
runs it, and Document::annotation_deletable gives the Comments panel and
the page's context menu the same answer before they offer Delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 13:09:59 +01:00
alehelandClaude Opus 5.5 28a31aebc0 fix(comments): previews lift a stamp whatever its replies' locks; Delete greys out over a locked reply
The locked-reply check in delete_annotation also reached stamp_preview
and signature_image, which only take the annotation off a copy to draw
the page beneath it: an unlocked stamp with a locked reply lost its drag
preview. They now use lift_annotation, which checks no locks.

The Comments panel's Delete (comment and reply menus) and the page's
context menu are greyed out while a reply below is locked
(comments::locked_below), rather than refusing after the click.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:33:14 +01:00
alehelandClaude Opus 5.5 72edd945e5 fix(comments): a locked reply keeps the comment or reply it answers
Deleting a comment or a reply took every reply to it, locked or not: only
the target's own Locked flag was checked. A locked reply can't be deleted
on its own, so delete_annotation now refuses while anything that would go
with the target is locked (pop-ups follow their comment).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:14:42 +01:00
Dylan M. Taylor 81c63c037a Group annotation, form and document operation parameters 2026-10-27 23:25:39 +00:00
Dylan M. Taylor 7d1ed99f51 Refactor annot for strict Clippy checks 2026-10-27 23:12:50 +00:00
aiongg 2e8d361123 refactor(annot): split appearance.rs into appearance/
New module: symbols.
2026-10-26 22:18:50 +00:00
aiongg 5ce4d24412 refactor(annot): split lib.rs into src/
New modules: types, lookup, create, modify, draw, info.
2026-10-26 22:18:50 +00:00
aiongg e0c319d273 Integration: a comment listed through a reference chain keeps its replies after an edit (#4061) 2026-10-24 23:18:59 +00:00
aiongg d821f91001 Integration: fix(comments): thread replies by their /IRT reference, count comments like Acrobat (#4061) 2026-10-24 22:48:11 +00:00
aiongg f94b34adc9 Integration: fix(redact,cos): cleared redaction marks leave nothing behind (#4226) 2026-10-24 22:48:11 +00:00
alehel ad8c2f46dd Merge origin/main into fix/comment-threads-by-irt
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.
2026-10-24 20:42:11 +02:00
mtlcl410andClaude Opus 5.5 4f436d3dd4 M5: line comments draw their leader lines and caption (ISO 32000-2 Table 178) (#4354)
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>
2026-10-24 10:30:16 -04:00
alehel c16956f539 Merge origin/main into fix/comment-threads-by-irt
main's #4284 stopped hashing translation catalogues in ATTRIBUTION.toml; the
conflict took main's side (no sha256 lines) and kept this branch's source notes.
2026-10-24 11:31:20 +02:00
aiongg a7778f92f1 Merge PR #4362: M5: comments without an appearance draw their /Border dashes, Ink dashes and the caret's ¶ (ISO 32000-2 Tables 166, 168, 183) 2026-10-24 06:23:19 +00:00
aiongg 581faa876a Merge PR #4349: M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184) 2026-10-24 06:23:15 +00:00
aiongg ee989ab5ca Merge PR #3016: M5: sticky notes stay upright where they are placed on rotated pages (NoRotate, #2583)
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).
2026-10-24 06:22:25 +00:00
a047c6ba09 chore: resolve SonarCloud maintainability issues (#4356)
* chore: resolve SonarCloud maintainability issues

* fix(packaging): use verify_sha256 helper function

* style: cargo fmt

* fix: restore wildcard imports and fix method-reference type errors

Revert the glob-import expansions (they missed SomIndices and left unused
imports), revert char::is_ascii_digit where the iterator yields char by
value, and pass the content slice to prune_doomed_xobjects as &[Vec<u8>].

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

* Keep XObject decodability check without binding content in interp

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dylan Taylor <dylan@craftmaker.dev>
2026-10-23 23:28:18 -04:00
mtlcl410andClaude Opus 5.5 001a900ac7 M5: comments without an appearance draw their /Border dashes, Ink dashes and the caret's ¶ (ISO 32000-2 Tables 166, 168, 183)
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
2026-10-24 10:41:46 +08:00
mtlcl410andClaude Opus 5.5 2f97fd2d59 M5: the placeholder for an unknown stamp name stays grey whatever its /C
An AP-less stamp whose /Name has no drawing got its placeholder frame in
the annotation's /C colour (/C [1 0 0] drew a red frame), while the doc
comment and parity note promise a neutral grey one. The frame now always
strokes PLACEHOLDER_GREY; the fourteen Table 184 names keep /C.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
2026-10-24 09:27:38 +08:00
mtlcl410andClaude Opus 5.5 3db28cc4e3 M5: stamps without an appearance draw the stamp they name (ISO 32000-2 Table 184)
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
2026-10-24 09:15:12 +08: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 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
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 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
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
Guillem Hermida 558fa0c995 Merge remote-tracking branch 'origin/main' into fix/2583-note-icon-no-rotate
# Conflicts:
#	ROADMAP.md
#	crates/annot/src/lib.rs
#	crates/automation/src/comments.rs
#	crates/engine/src/lib.rs
#	crates/engine/src/tests.rs
#	crates/render/src/inspect.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/comments_panel.rs
#	crates/ui-egui/tests/it/comments.rs
#	parity/acrobat-features.toml
#	vendor/README.md
2026-10-23 10:19:47 +02:00
Dylan M. Taylor 93b25f67a6 Merge main into fix/coderabbit-c5-forms-misc
Keep the Ops iterator from this branch and carry main's long-number skip
(MAX_RETRIED_TOKEN) into its operand error path.
2026-10-22 22:07:19 -04:00
aiongg 6394779b81 fix(annot): check a whole link batch before creating any link object 2026-10-22 20:51:55 +00:00
aiongg 02f0f7396f fix(annot): count web-address text after its UTF-8 conversion
An address of invalid UTF-8 grows up to three times when converted, so
the listing's 16 MiB cap counted fewer bytes than it kept. The cap now
counts the converted text; the raw length is still checked first, so an
address already over the cap is never converted.
2026-10-22 20:04:04 +00:00
aiongg c2e96de0b2 fix(annot): extend each page's annotation list once when adding many links
add_many added links one at a time, and each addition walked the page tree
and copied and rewrote the page's whole /Annots array, so a batch of links
cost quadratic time. The pages are found once and each page's new links are
appended in one update.
2026-10-22 18:02:33 +00:00
aiongg 05d09de1e3 fix(annot): read a shared page tree Kids array once
The page walk pushed a node's /Kids array for every new node, so many page
tree nodes sharing one indirect /Kids array re-read it for each node:
quadratic work while the seen check skipped every entry. A shared array
object is read under the first node that lists it, and the walk stops
after four million /Kids entries.
2026-10-22 18:02:33 +00:00