Commit Graph
134 Commits
Author SHA1 Message Date
NetterSilver ebcc564f1e Keep the outline intact when move_bookmark gets an invalid target (#4901) 2026-11-09 10:09:46 +08:00
NetterSilver 6e2729ae21 Reject signs in XMP numeric character references (#4755) 2026-11-09 10:08:34 +08:00
Ethunxxx d67b82c591 M4: bookmarks run their actions as links do (organize.bookmark-actions) (#5410) 2026-11-09 10:05:18 +08:00
jonathanwxh-cell cf07ece17d fix(organize): remove uninherited crop boxes and guard disjoint or overflowing boxes (#5282) 2026-11-09 10:04:32 +08:00
Max Mittelstädt 475ba82fea Track XMP attribute quotes only inside tags (#5271) 2026-11-09 10:03:44 +08:00
Ethunxxx 4ca5fccf9e M4: Initial View magnification offers 25% and 100% (#5254) (#5265) 2026-11-09 10:03:30 +08:00
bflatastic 2992146abb Merge #6312: M1: a document that encrypts only its embedded files opens without a password (core.attachment-only-encryption)
# Conflicts:
#	crates/render/src/inspect/tests.rs
#	vendor/README.md
2026-11-09 08:12:28 +07:00
oracola91 d76bbd28f7 Page copies bring only the form fields on the pages copied (#6461) 2026-11-09 08:52:13 +08:00
BFlat e0f90d5f2d Edit a PDF: select all, copy, cut and paste page objects within and between documents (#6639)
Refs #5989. Ctrl/Cmd+A selects every object on the page; copied objects paste as page content that draws the same, within a document or into another, with organize's bounded deep copy across documents. Headless: object_copy, object_paste, object_move all.
2026-11-09 02:32:50 +08:00
Grzegorz MartensandClaude Opus 5.5 ae677644e0 M2.6: Extract and Split prune nothing past the form depth limit
Pruning reads forms 8 deep. Past that a form's /XObject dictionary was kept whole, but the forms
it draws weren't read, nor what they draw through resources of their own. When those resources
are shared with content that was read, they were cut to the names that content draws, and the
images only the deep forms draw were dropped from the part. This was so on `main` for a form read
whole, and in this change's reading of a damaged form too.

A walk the depth limit stops now leaves the file unpruned, as an overlarge Type 3 resource graph
already does: `nothing_is_pruned_past_the_depth_limit` (a form 9 deep, whole or damaged partway,
drawing a form that draws through resources the second page shares). That also settles a form
first reached past the limit and drawn again higher up, whose second reading `forms_seen` skips.
Down to the limit pruning goes on, under a page whole or damaged partway and with the form 8 deep
whole or damaged: `pruning_goes_on_down_to_the_depth_limit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 18:49:17 +01:00
Grzegorz MartensandClaude Opus 5.5 c3ca063373 M2.6: a test pins that pruning reads a long chain of forms only to the depth limit
A chain of 100,000 forms, whole or each damaged partway, is read only down to the depth limit, so the walk's recursion stays bounded: a_long_chain_of_forms_is_read_only_to_the_depth_limit. Nothing pinned it: with the depth step dropped in either of the two calls that go one form deeper (reading a form whole, and reading one that is damaged), or the depth check in the walk of damaged content made to apply only with the read-once guard, the walk follows the chain to its end and the stack overflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 084fa0927e M2.6: say why pruning reads a form's /Subtype only when direct
The sweep flags the read in Pruner::form, moved there by this change. A /Subtype given by reference reads as no form wherever the workspace tells forms from images; resolving it is a change across all of them, and the comment says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 75bf5ed264 M2.6: a test pins that pruning reads the forms of damaged content at the depth limit
Forms are read 8 deep. A damaged form that deep has every form it
names read as drawn, so one drawing through resources another page
shares keeps what it draws: a_damaged_form_at_the_depth_limit_has_its_forms_read.
Mutation testing found nothing pinned that boundary.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 8bf7412220 M2.6: pruning reads every form that damaged content may draw
Earlier in this change, a page or form damaged partway, like one that
doesn't decode, kept its own /XObject dictionary whole and was read no
further. So a form it draws was never read: when the form's resources
are shared with another page, the names the form draws there went, and
extracted or split pages lost those images. On main the part before the
damage was read, so forms drawn there kept theirs, and forms drawn past
it didn't.

Now every form the resources of such content name is read as drawn, as
any of them may be drawn before the damage or past it. Each set of
resources has its forms read once.

Test: extract_keeps_images_forms_drawn_around_damage_draw_through_shared_resources,
with a page and a form each damaged before and past the form they
draw, and each not decoding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 69e26167e4 M2.6: a page's content is read with its /DecodeParms given by reference
`decode_with_damage` and `decode_contents_with_damage` read a stream's
`/DecodeParms` only when direct, as `Stream::decoded` does, so content behind
a predictor whose parameters, or a number in them, are an indirect object
decoded with the predictor's tag bytes left in, and was taken as whole. Edit
then listed and wrote back the wrong bytes, and pruning dropped an image such
a form draws. ISO 32000-2 lets those values be indirect.

- `decode_with_damage` takes the document and reads the parameters through
  it, and so does `decode_contents_with_damage`, and `decode_contents` with
  it: printing, accessibility's reading of a page and a page copied as a form
  read them too. Code that calls `decode` on a stream itself doesn't yet.
- It reads through `Document::decoding_view`: `with_direct_parms`, but a copy
  of the stream holds only its `/Filter` and the parameters, its data shared,
  so a stream a page's `/Contents` lists again and again isn't copied whole
  for each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
Grzegorz MartensandClaude Opus 5.5 50d0fcc403 M2.6: pruning keeps what a page or form damaged partway may draw
Extracting, splitting and combining pages drops the `/XObject` entries no
page draws. A page or form damaged partway was read up to the damage as the
whole of it, so an image named only after the damage was dropped, though
the stream still names it and other readers may still read that far.

- `content_bytes` says whether a page's stream is damaged, and pruning keeps
  everything such a page could reach, as it does for content that doesn't
  decode. A form is read the same way.
- A page made a form XObject (stamps, watermarks, backgrounds from a PDF)
  still draws what decoded before the damage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmsdeUCdTaD72jkDw2pJKY
2026-11-07 12:37:35 +01:00
mtlcl410andClaude Opus 5.5 76ae58c8be M1: attachments held encrypted aren't copied by Combine; engine test and parity for EFOpen
Combine refuses to copy an embedded file stream that its source holds
encrypted (opened without the password its embedded files need), rather
than write that data into another document as the file. An engine test
opens and saves such a document through the session, and the parity
entry core.attachment-only-encryption records what this covers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
2026-11-05 23:00:44 +08:00
KenM76andClaude Opus 5.5 43939cc13f M4: bookmark tree round-trips what real files hold; review fixes (#6018)
- organize: a /FitR destination missing one of its four numbers is copied
  with its page and no view, instead of failing the whole copy (the
  rectangle can't be kept, and import refuses a partial one).
- organize: `visit` keeps the visited node's path in step with its stack
  instead of rebuilding it per node (O(depth) per node before).
- automation: an empty title is accepted when present, so a bookmark a
  file left untitled exports and imports back; a missing title is still
  refused.
- automation: "no bookmark at" names the path 1-based, as it was given.
- automation: `bookmark_copy` and `bookmark_import` report `truncated`;
  an exported file carries `"truncated": true` when the export left
  bookmarks out, and importing it says so.
- automation: `bookmark_export` is marked destructive (it overwrites
  `out`) and idempotent for MCP.

Tests: a_fit_r_missing_a_value_copies_with_its_page_and_no_view (organize),
odd_bookmarks_round_trip_and_a_cut_tree_says_so (automation). Each fix
reverted alone fails one of them; the visit change is behaviour-neutral
and covered by the existing outline tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-04 04:39:20 -05:00
KenM76andClaude Opus 5.5 801976a574 feat(organize): export, import and copy bookmark trees (#6018)
bookmark_export returns an outline, or one bookmark's subtree, as JSON:
titles, nesting, open state, colour, bold/italic and destinations as page
plus view in PDF user space (ISO 32000-2 12.3.2.2), optionally written to
a file. bookmark_import takes it back, inline or from that file, under any
bookmark at any position or in place of the outline, as one undoable edit;
bookmark_copy does the same from another open document by page number.

A bookmark whose page the target lacks keeps its place without a
destination and is reported in `unmapped`. Every node is checked before
anything is written (title, page, colour, view type and arity, 33 levels,
100,000 bookmarks), and the error names the node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
2026-11-04 03:37:55 -05:00
Cursor Agent d58d121170 Merge remote-tracking branch 'upstream/main' into prep/4596-merge-main
Co-authored-by: jonathanwxh-cell <jonathanwxh-cell@users.noreply.github.com>
2026-10-29 04:49:23 +00:00
.CooCoo 222a124650 M5: Import comments takes another PDF and copies its comments onto the same pages (#4595)
* M5: Import comments takes another PDF and copies its comments onto the same pages

Import comments read only XFDF, FDF, XML, XDP, CSV and text data files.
It now also takes a PDF: each comment of that PDF is copied onto the
page with the same number, at the same place, with its appearance
stream, its pop-up, its replies and its layer. Comments on pages past
the document's last page are left out, together with replies to them.
Links, form fields and redaction marks are not copied. A comment named
like one already on its page replaces it, as an XFDF or FDF import
does. A PDF without comments is refused with "nothing in the file
matches this document", and a PDF that needs a password is refused.

- organize: import_comments copies the comments with the page copier
  that Insert Pages uses, and registers the layers they use.
- engine: Edit::ImportData reads a file that starts with %PDF- as a PDF.
  Any other file goes to the data importer first, and is read as a PDF
  when that fails, %PDF- occurs in its first 1,024 bytes and the PDF
  reader opens it.
- ui: Comments > Import comments lists .pdf files; form.import_data
  lists the same files as before.
- automation: the doc_import_data description names the PDF case.
- parity: comment.import-from-pdf is shipped.

* Import comments: an annotation that is no comment keeps its name

A comment imported from a PDF replaced any annotation of the same /NM
on its page that was not locked, a redaction mark or a widget, so a
link, screen or 3D annotation of that name was overwritten. Only an
annotation whose subtype is one of the copied comment subtypes is
replaced now; for any other, the copy is added beside it without the
name. The subtype is read through an indirect reference.

* Import comments: index each page's annotations instead of scanning them per comment

* Import comments: a layer that BaseState OFF hides stays hidden

* Import comments: a PDF with binary bytes before its header imports on an XFA form, and two comments with one name both land

On an XFA form, a decode failure no longer stops the import when a PDF
header is in the first 1,024 bytes; the PDF's comments are copied. Two
incoming comments on one page with the same /NM no longer replace the
same comment: the first replaces it, and the second is added without
the name.

* Import comments: an inline annotation on the page is replaced by name

An annotation written inline in a page's /Annots is made an indirect
object before names are matched, so an incoming comment with its /NM
replaces it instead of being added beside it.

* Import comments: two new comments with one name keep it once

When no comment on the page has the name, the first incoming comment
keeps its /NM and each later one with the same name is added without it.
2026-10-28 17:53:03 -04:00
Claude 03fdf97cde Merge remote-tracking branch 'upstream/main' into feat/bookmarks-from-text-styles
# Conflicts:
#	crates/ui-egui/src/commands.rs
#	crates/ui-egui/src/dialogs.rs

Main split the command and dialog dispatch for its stricter clippy
rules; New bookmarks from text styles goes into the command list after
New bookmarks from structure, and its dialog into the dialog dispatch
next to the other dialogs with their own buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
2026-10-28 06:07:14 +00:00
Dylan M. Taylor f40a57878b Refactor organize for strict Clippy checks 2026-10-27 23:12:51 +00:00
Claude 65d8385616 Merge remote-tracking branch 'upstream/main' into feat/bookmarks-from-text-styles
# Conflicts:
#	crates/automation/src/lib.rs
#	crates/engine/src/lib.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/panels.rs

Main's module layout (#4889) moved what this branch changed: the tools to
automation's dispatch.rs (the levels parser to json.rs), the edit to
engine's edit.rs, edit/labels.rs and edit/run.rs, Document::text_styles
to document.rs, the dialog state to state.rs and app.rs, and the menu
item to panels/right.rs and panels/bookmarks.rs. The dialog module joins
the others in dialogs/. Main's craft-menu bar (#4752) lists its commands
by hand, so bookmark.from_text_styles is added after
bookmark.from_structure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GbuSKAS2k5248LGFEapMUR
2026-10-27 20:46:00 +00:00
Cursor Agentandjonathanwxh-cell 14efab25da Merge upstream/main into prep/4596-merge-main
Keep the print module split and reapply the /Contents space join in
imposition::decoded_contents, matching the renderer and the organize pruner.

Co-authored-by: jonathanwxh-cell <jonathanwxh-cell@users.noreply.github.com>
2026-10-27 03:18:57 +00:00
aiongg 58c5799e3f refactor(organize): split import.rs into import/
New modules: import/copy.rs, import/fields.rs, import/combine.rs, import/carry.rs.
2026-10-26 22:18:50 +00:00
mtlcl410andClaude Opus 5.5 e391053658 M4: Document Properties reads a named opening destination's page, so choosing page 1 replaces it (ISO 32000-2 12.3.2.4) (#4574)
* M4: Document Properties reads a named opening destination's page, so choosing page 1 replaces it (#4572)

initial_view resolved only explicit opening destinations, so a named one
(ISO 32000-2 §12.3.2.4) read as page 1 at the default magnification, and
set_initial_view kept it as "unchanged" when page 1 was chosen. Names are
now resolved through the /Names /Dests tree and the catalog /Dests with the
existing bounded resolver, and a bare name or string counts as a destination
when page 1 at the default magnification removes /OpenAction.

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

* M4: parity notes say page 1 at the default magnification removes /OpenAction

Document Properties replaces a named opening destination (ISO 32000-2
§12.3.2.4) with an explicit one when another page or magnification is
chosen; page 1 at the default magnification removes the catalog's
/OpenAction instead (§7.7.2), so the first page opens.

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-25 10:02:40 -04:00
jonathanwxh-cell 522653d6b1 fix(organize,print): join /Contents array pieces with a space (#3502) 2026-10-25 12:03:40 +08:00
Lcoding15 4ce68f517b M4: New bookmarks from text styles, for headings that aren't tagged (#1693)
Bookmark options > New bookmarks from text styles... lists the styles the
document's text is set in (font without its subset tag, the size shown,
bold, italic, how many lines, a sample), the largest first, and takes one
style per bookmark level. Every line in a level's style becomes a
bookmark of that level under a new first "Untitled" bookmark, as from
structure; a heading set on several rows is one bookmark, and each goes
to the top of its heading.

The detection lives in engine::text_style_bookmarks on top of
edit::text_lines, reading each page top to bottom then left to right, so
two headings on one row stay two. text_styles and
bookmark_from_text_styles reach it headlessly, and the command
bookmark.from_text_styles opens the dialog. OutlineEntry gets an
optional top for the /XYZ destination; structure bookmarks keep theirs.
2026-10-25 01:03:08 +02:00
mtlcl410andClaude Opus 5.5 5f64994b4d M3: a closed top-level bookmark opens closed (ISO 32000-2 Table 151 /Count) (#4389)
The Bookmarks panel opened every top-level bookmark whatever its /Count
said, so a bookmark with a negative /Count showed its children. Every
level now starts as its /Count says (positive open; negative or missing
closed). The reader's toggles and a search still override it.

Moving a bookmark into a different parent (Indent) now opens that
parent, as adding one already did; the forced top-level open used to
hide that it stayed closed.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:30:30 -04:00
.CooCoo 2ac1df2f1f M4: Document Properties has a Custom tab for custom properties (#4387)
* M4: Document Properties has a Custom tab for custom properties

Document Properties has a new Custom tab between Fonts and Advanced. It
lists the document's custom properties, and Name and Value fields with
Add, Update and Delete edit the list. Clicking a listed property fills
the fields with it. OK writes the changes through SetInfo in the
existing "Change document properties" step, so one undo reverts them
with the other tabs' changes.

Add is off for an empty value, a name already listed, a name that
check_custom_key refuses (its message is the button's tooltip), and a
name the file already uses for an entry the list leaves out (not text,
only padding, or past the 256 listed). A property already in the file
under a refused name or a name that is not UTF-8 is listed, and Add,
Update and Delete stay off for it, so OK cannot fail on it or write a
different key.

Clearing an /Info entry now removes it when /Info is a dictionary
written directly in the trailer. set_info_entry returned without a
change there, so a delete in the Custom tab, a cleared Description field
and a doc_set_info clear left the entry in the file. It now writes a
copy without the entry as a new indirect /Info.

The engine re-exports check_custom_key and adds Document::has_info
(organize::has_info). set_option("dialog", "custom") opens the tab.

* Contributors: add dotCooCoo (Robert Lee)

* M4: Add in the Custom tab stops at the 256 properties the tab lists

Document Properties > Custom lists at most 256 properties, so a property
added past that was saved but left out of the list when the dialog opened
again. Add is now off once the tab's list or the file's own list holds 256;
a new name goes after the file's entries, so a file with more than 256 keeps
Add off after a Delete as well.
2026-10-24 10:30:25 -04:00
mtlcl410andClaude Opus 5.5 fc91ab9250 M3: a document opens at its /OpenAction destination, position and named destinations included (ISO 32000-2 Table 149, 12.3.2.4) (#4376)
- render: DocInfo::open_dest resolves /OpenAction (an explicit destination,
  a /GoTo action's /D, or a name or string through /Names /Dests or the
  catalog /Dests) with the existing bounded dest() resolver.
- ui-egui: apply_initial_view goes there with DocView::go_to_dest, so the
  /XYZ point lands at the window's top-left and /FitH top at its top edge.
  With "Always use my default zoom and page layout", only the page.
- organize, ui-egui: /ViewerPreferences /NonFullScreenPageMode (Table 147)
  is read for /PageMode /FullScreen documents and names the panel shown
  behind the full-screen prompt; such a document no longer gets the outline
  panel only because it has an outline.
- organize: set_initial_view keeps an opening action whose page and
  magnification are unchanged, instead of dropping its position or deleting
  a named go-to.
- automation: doc_info reports open_destination.


Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:30:21 -04:00
aiongg b232ed217c Merge PR #2588: M3: Document Properties shows a title and author stated only in XMP (#2373) 2026-10-24 07:00:29 +00:00
aiongg f053da3d93 Merge PR #1960: Attachments: attach a file and delete one (panel, edit, tools); duplicate names numbered as text
i18n ca, fi, hi, id, ko, ro, sq .tsv: kept both sides' rows (no duplicate keys).
2026-10-24 07:00:24 +00:00
aiongg a954a96cb5 Merge PR #4041: fix: Issue #4020, Extract keeps the bookmarks of the pages it copies 2026-10-24 06:59:38 +00:00
aiongg 70d92a052b Integration: #2289 keep Contents, BM, Lang and AF on the split widget 2026-10-24 06:21:39 +00:00
aiongg afba441f2a Merge PR #2289: organize: split a combined field/widget so every copied page owns its widget 2026-10-24 06:21:17 +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
midasmatterhorn 1f41c0ccaa organize: preserve widget actions and inherited field state 2026-10-23 21:31:13 +03:00
midasmatterhorn 81e054231c organize: format field widget splitting changes 2026-10-23 19:36:12 +03: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
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
Osvaldo DuranandClaude Opus 5.5 4ebf287e61 Merge upstream main 0dc77fe4 into pr/attachments
crates/organize/src/import.rs: upstream's name_tree_entries (seen-node set, each node read once) kept whole; this PR's only change to it, pub(crate) for attachments.rs, applied on top.
crates/organize/src/attachments.rs: its name_tree_entries call passes the new seen set.
crates/automation/tests/attachment_key.rs, crates/ui-egui/tests/attach_picker_target.rs: moved into tests/it/ and registered in main.rs, following upstream's one-binary layout (organize keeps its single test file, as upstream's rule says).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 03:15:10 -04:00
magdale76andClaude Opus 5.5 76f28a41e5 M3: an empty XMP alternative or list reads as no value, not as its markup (#2373)
`get_property` returned the raw child markup when a property had no
`rdf:li` items, so `<dc:title><rdf:Alt/></dc:title>` with no /Info title
showed "<rdf:Alt/>" in Document Properties. Character data that still
holds a `<` is markup, never a value: an empty Alt/Seq/Bag, or an item
holding a nested structure, now reads as none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:33:29 +02:00
magdale76andClaude Opus 5.5 6996aac44a M3: Document Properties shows a title and author stated only in XMP (#2373)
Description read Title, Author, Subject and Keywords only from /Info. A file
that states them only in its XMP packet (/Info is deprecated in PDF 2.0, and
some writers leave it out) showed empty fields, while Acrobat shows them.
organize::info now falls back to the packet's dc:title, dc:creator,
dc:description, pdf:Keywords, xmp:CreatorTool and pdf:Producer: a language
alternative gives its x-default item, and several authors are joined with
"; ". The engine fills the inspection's missing fields the same way, so the
read-only Description rows and doc_info agree with the editable fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 23:33:26 +02:00
aiongg a52e39a5e2 Merge #4114 (fix/coderabbit-b-shared): shared DecodeBudget
The tests each branch appends to the audit, edit and organize test files are
kept side by side; prune.rs merges cleanly, with the form memo on top of the
budgeted page content.
2026-10-22 21:17:55 +00:00
aiongg cb1ddc899b fix(organize): find XMP attributes in one pass over the packet
Each candidate attribute match searched the whole packet before it for the
last < and >, so a long text node repeating the attribute name cost time
quadratic in the packet size (up to the 16 MiB decode cap). The scan now
keeps the last < and > as it moves forward.
2026-10-22 21:16:21 +00:00
aiongg 1f6ce5ecc7 fix(organize): analyse each form once when pruning unused XObjects
The pruner decoded and walked a form XObject again on every Do that drew
it, so a form drawing itself a few times per level was analysed
exponentially often within the depth limit. Forms are now remembered with
the resources owner they were drawn against, and a repeat records nothing
new.
2026-10-22 21:16:20 +00:00
aiongg cab1dc3eb5 fix(organize): read each name-tree node once when importing
Looking up a named destination and listing attachments in a source's name
trees followed every /Kids entry with only a depth limit, so a node listing
the same child twice per level made the walk (and the attachment list)
exponential in the depth. Both walks now skip a node already read.
2026-10-22 21:16:20 +00:00
aiongg 59dbdee428 fix(organize): copy annotation actions with one shared reference map
Fixing up copied annotations cloned the whole source-to-destination map for
every non-GoTo action and dropped the copies it made, so many annotations
cost quadratic memory and time and an object shared by their actions was
copied once per action. The copier now lends its map to the action copy and
takes it back with the new entries.
2026-10-22 21:16:20 +00:00