Commit Graph
81 Commits
Author SHA1 Message Date
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
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
aiongg da288517e6 fix(organize): resume the field-rename suffix search where it stopped
Renaming a colliding field tried Name-1, Name-2, ... from 1 every time, so
many fields of one name cost a quadratic number of attempts during import,
combine and their conflict previews. Taken names only grow, so the search
now resumes after the last suffix handed out for that name; the result is
still the first free variant.
2026-10-22 21:16:20 +00:00
aiongg d39b55dd6d fix(organize): register imported form fields in linear time
Each imported top-level field was checked against every field already in
/AcroForm /Fields before it was appended, so a page with many distinct
widget fields cost time quadratic in their number. Membership is now kept
in a set beside the list.

Adds a test-only work counter to the crate, so tests can bound the work a
hostile document causes without timing it.
2026-10-22 21:16:20 +00:00
aiongg 9c9bbe2ad6 fix(organize): page content is joined within one decode budget
Pruning and page_as_form joined every /Contents entry with a fresh
1 GiB allowance each, so a page naming one compressed stream many
times became gigabytes. Both share one helper that decodes against a
per-page DecodeBudget and fails when it runs out.
2026-10-22 16:57:54 +00:00
aiongg 2c13fd9373 Merge r2 track t16: mixed: automation-mcp, crate:misc, crypt-security, export-convert, optimize, preflight-pdfa, save-io
# Conflicts:
#	ATTRIBUTION.toml
#	contributors/people.toml
#	crates/automation/src/comments.rs
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
#	parity/acrobat-features.toml
2026-10-22 02:23:58 +00:00
aiongg dcf2802f96 Merge r2 track t15: mixed: accessibility-tags, dialogs-prefs, javascript, organize-pages
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/engine/src/tests.rs
2026-10-22 02:23:22 +00:00
aiongg 8bb2eef740 Merge r2 track t13: cos-parse, vendored-crate fixes
# Conflicts:
#	ATTRIBUTION.md
#	crates/annot/src/tests.rs
#	crates/forms/src/tests.rs
#	crates/organize/src/tests.rs
2026-10-22 02:21:53 +00:00
aiongg be19528dca Merge PR #3735: M4: resolve indirect initial-view boolean preferences
Conflicts:
- ROADMAP.md, docs/roadmap.md: the PR's progress-log entry, revision row and status line added on top
2026-10-22 01:49:09 +00:00
aiongg 9299ecbce5 Merge PR #3608: Page-tree walks in annot, forms and organize skip repeated children 2026-10-22 01:47:42 +00:00
aiongg 0f0d7dcb67 Merge PR #1830: M11: remove PDF/A or PDF/X identification (optimize.remove-standards-id)
ATTRIBUTION.toml: sha256 lines taken from main, recomputed after all merges
2026-10-22 01:47:19 +00:00
aiongg f424cf0627 Merge PR #3311: fix(organize): prune named destinations without recursive calls
docs/roadmap.md: revision history keeps both sides' rows, newest first.
2026-10-22 01:45:57 +00:00
Adrian 946a77cf9b M4: a new bookmark goes to where the reader is (#3865)
A bookmark made in the app always went to the top of the page. It now goes to
the top-left of the selected text, or with nothing selected to the top of the
window, as /XYZ left top null: the reader's zoom is kept, and with only the top
pinned so is the horizontal position.

- render: PageInfo::xyz_for_view, the inverse of dest_fraction. It pins only
  the coordinates given; on a page turned a quarter the displayed y decides the
  user left and the displayed x the user top.
- organize: add_bookmark_at and OutlineError::BadPosition (a coordinate that is
  not finite is refused before anything changes).
- engine: Edit::AddBookmark takes left and top (None, None is the page top, as
  before).
- automation: bookmark_add takes top and left in points from the top-left of the
  displayed page, as bookmark_list reports them, bounded and refused when not a
  usable number.
- ui: DocView::bookmark_position. It follows the window's vertical when the view
  is turned a quarter (View > Rotate View), picks the selection's corner that
  shows at the top-left, and in a spread of unequal pages uses the page that
  crosses the window's top edge.
- parity: organize.bookmark-add evidence and what is still missing.
2026-10-21 12:12:03 -04:00
Marco Arnulfo 4e94aa1cbb fix(cos): find objects a broken cross-reference table leaves out (fixes #1628) 2026-10-21 10:18:13 +02:00
Unhumanje cbcdca77f0 fix(organize): imports find the objects they added when new numbers fill gaps
import_pages_with and insert_file_pages took the objects numbered above the
destination's highest number for the ones the import added. When no number
is left above the highest (an exhausted /Size, or object 4294967295),
Document::add fills free numbers below it: the copies skipped resource
sharing, and a refused import left its objects in the destination. With
object 4294967295 the `n + 1` overflowed, a panic in debug builds and in
release a wrap to 0 that counted every object as added.

Record the object numbers before the import and take the ones present
afterwards but not before.
2026-10-21 08:31:49 +02:00
Mex 3d32ffd27c M4: resolve indirect initial-view boolean preferences
Read all six boolean ViewerPreferences through the document resolver. Synthetic regressions cover direct/indirect dictionaries, true/false, malformed values and save/reopen.
2026-10-20 21:07:13 +02:00
oracola91 586246d69a organize: check the page's own MediaBox values in the walk test
The root's MediaBox and the page's own both have four numbers, so the
length check passed even if the page lost its own value. Compare the
values instead.
2026-10-20 11:23:29 +03:00
oracola91 15f9203243 Page-tree walks in annot, forms and organize skip repeated children
annot::page_refs, the forms page_refs and organize's walk queued every
/Kids entry before the seen check, and organize cloned the inherited
attributes, Resources included, for each queued entry. A page tree whose
/Kids lists the same child many times, or whose nodes share one /Kids
array, held all of those entries at once before they were dropped as
already seen.

Each walk is now a small iterator with one cursor per open /Kids array,
the way pdfcraft_model::pages has walked since #1926. organize carries
inherited attributes as the nodes that set them and copies them only for
the pages it returns. Page order, the pages found, the inherited values
and the million-node cap are unchanged.
2026-10-20 10:30:20 +03:00
PerchAnton 3326882463 M4: use fixed-size slices in named destination regressions 2026-10-19 23:54:13 +03:00
PerchAnton cc22faca2f M4: keep large named destination trees usable on page deletion 2026-10-19 23:47:18 +03:00
PerchAnton 031532b8a5 M4: bound named-destination pruning during page deletion (#3309) 2026-10-18 21:01:43 +03:00
Mawfyy 932bf9c6bf M4: Ctrl-drag copies pages in the Organize grid (#2997) (#3092)
* M4: Ctrl-drag copies pages in the Organize grid (#2997)

Dragging thumbnails in the Organize page grid with Ctrl held (Option on
macOS) drops copies at the gap instead of moving the pages: a "+" badge
by the pointer, the copies land at the gap selected, and one undo step
takes them out. The insertion point joins the engine edit
(Edit::DuplicatePages { at }) so the copy is one undo step rather than a
duplicate plus a move.

page_duplicate takes a 1-based "at" (the first copy becomes page "at"),
so agents can drive the same thing, and the UI control channel's
ui.drag modifiers now reach the input state -- which gestures that read
it (this copy-drag, and Alt-drag column select) need.

Closes #2997.

* fix(ui): take the grid's copy modifier from the runtime OS (#3092 review)

Option-drag in the Organize grid moved the pages instead of copying them in a Mac
browser: `cfg!(target_os = "macos")` is false in the wasm build, and eframe sets the
context's OS from the browser. `copy_modifier` now takes the context and asks
`commands::mac_shortcuts`, as the shortcut labels do.

The drag helpers in `tests/editing.rs` and `tests/control.rs` picked the modifier the
same compile-time way, so they would have sent Ctrl on a Mac host; both read the
context's OS now. The new regression test
`option_copies_and_ctrl_moves_when_the_context_reports_macos` drives a Mac context —
Ctrl moves, Option copies — and fails without this change.
2026-10-18 11:13:22 -06:00
aiongg 40d7b0bc47 Merge upstream main a12fe8f41 into next 2026-10-18 14:56:01 +00:00
a12fe8f411 M4: custom document properties through the engine and the tools (#2076)
* M4: custom document properties through the engine and the tools (#organize.custom-properties)

/Info entries beyond the ones the spec names are custom properties:
organize lists them (custom_info) and set_info accepts any such name
after Acrobat's checks (no standard name, no empty name, no spaces,
delimiters, / or #, at most 127 bytes); inspection lists them too
(DocInfo::custom); doc_info reports them under custom and doc_set_info
sets or clears them. The Custom tab in Document Properties and the XMP
mirror stay planned.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* M4: custom document properties list the same before and after an edit

Review follow-up. The open-time reader (render/inspect.rs) now uses the
engine's rule (pdfcraft_organize::custom_info, trimmed, empty values
dropped): text strings only, NUL/whitespace trimmed, empty values left out,
and the 256 cap counting listed entries. Before, it also listed name
values (`/Dept /RnD`) and capped before filtering, so the list could change
after the first unrelated edit. An automation test opens a file with a name
value, blank, NUL-padded, empty and numeric entries and checks doc_info's
custom list is the same before and after setting the Title.

Drops the ROADMAP.md log line, which conflicted with main.

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

* M4: custom properties drop NUL/whitespace padding in one pass, before the cap

Review follow-up. custom_info now trims NULs and whitespace in any mix
(" \0Lab\0 " is "Lab"), leaves out values that are only padding, and does
both before taking 256, so blank entries can't crowd real ones out. It is
the one rule: the engine uses it as it is, and the open-time reader trims
the same way. Tests: 300 blank entries before a real one, and mixed
padding, in organize and through doc_info.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 10:49:30 -04:00
Mex 19d138e951 M4: accumulate repeated page rotation indices
Address maintainer feedback on the existing PR. Preserve upstream work and document tested scope and validation limits.
2026-10-18 11:51:29 +02:00
Mex 5af8e88fca Merge branch 'main' into fix/organize-resolve-indirect-rotation 2026-10-18 11:12:44 +02:00
EnricoandClaude Opus 5.5 eeadc79d15 M4: reverse page order (organize.reverse-pages) (#1720)
Organize pages ▸ More ▸ Reverse pages (and the command palette) reverses
the selected pages among their own positions, or the whole document when
fewer than two pages are selected. It is one undoable step, needs the
assemble permission like the other page edits, and is available headless
as the page_reverse automation tool (pages optional, default all).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 03:48:47 -04:00
Enrico SitaandClaude Opus 5.5 26051871d0 M11: remove PDF/A or PDF/X identification (optimize.remove-standards-id)
A file edited after it was made PDF/A or PDF/X can keep claiming a
standard it no longer meets. organize::remove_standard_id removes the
claim: the pdfaid: XMP properties for PDF/A, or GTS_PDFXVersion and
GTS_PDFXConformance in the document information and as pdfxid: XMP
properties for PDF/X. Output intents and other claims (PDF/UA) stay.

The XMP is edited with the packet editor Document Properties already
uses (sync_info and the new remove_properties share its read and write),
so the rest of the packet stays byte for byte and unsafe packets are left
alone. It is one undoable edit, in Standards ▸ PDF/A as a button worded
like its undo label, and the standards_remove_id tool (pdfa or pdfx).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:45:31 +02:00
Mex 87e69d2c72 M4: merge current upstream into indirect rotation fix 2026-10-15 21:48:28 +02:00
cccd55fab2 M1: edit finds the pages a damaged file's readers find (#1664)
* M1: edit finds the pages a damaged file's readers find (fixes #1656)

* M1: rebuild a lost page tree only when the reader shows the same pages, and refuse edits to signed rebuilt files (#1656)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 04:01:45 -04:00
AlexandBrandon Thomas c4d8c5bb55 feat(automation): bookmarks and links say where on the page they go (#2344)
bookmark_list and link_list add a destination: its type (XYZ, Fit, FitH,
FitV, FitR) and its position in the same top-left points as every rect.
A link through a named destination now gives its page (to_page, named)
instead of only the name.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:56:48 -04:00
Marco Arnulfo d09eabd664 fix(organize): keep page labels on their pages when pages are deleted (fixes #2091) (#2093) 2026-10-15 02:43:38 -04:00
c0f76fc73a M4: set page transitions and display times (/Trans, /Dur) through the engine and the tools (#2083)
* M4: set page transitions and display times (/Trans, /Dur) through the engine and the tools

organize::set_transition writes or removes a page's /Trans dictionary
(only the entries the style reads and that differ from the defaults) and
/Dur, with every value checked before any page changes; Edit::SetTransition
is one undo step ("Set page transitions", translated in every catalogue);
page_transition_set is the tool. The counterpart of reading them (#2074);
playing them and the Organize dialog stay planned.

Refs #379.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz

* Refresh catalog hashes after merging main

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:42:28 -04:00
Marco Arnulfo 03f232be5a M4: write each page label dictionary as an indirect object (#1763) (#1959) 2026-10-15 02:33:35 -04:00
9f0ed4ba86 feat(organize): Insert pages from file brings the file's bookmarks (#1947)
Inserting an annex into a guide dropped the annex's bookmarks: the pages
came in without them, while Combine Files keeps every file's bookmarks
under one named after the file.

Insert pages from file now does the same: the inserted file's bookmarks,
destinations mapped to the copied pages (positions kept, named ones
resolved), go under one new bookmark titled with the file's name at its
first inserted page, collapsed. That bookmark is placed among the
document's top-level bookmarks before the first one pointing after the
inserted pages. A file without bookmarks adds none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:31:53 -04:00
mtlcl410andClaude Fable 5.1 af62daeaf8 fix(organize): bound how deep an import follows the object graph (#1800) (#1921)
The page Copier recursed once per reference and once per nested array or
dictionary with no bound, so a source whose page reached a chain of thousands
of shallow indirect objects overflowed the stack in Insert Pages, Combine,
Extract, Split and Duplicate; the memo map only cut cycles. The copy now keeps
a depth of at most 256, refuses with an OrganizeError beyond it, and
import_pages_with frees the objects a refused copy added, so the destination
is as it was.


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

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-15 02:29:36 -04:00
Tanish MankuandClaude Opus 5 f988cf87ed feat(oracle): measure rendering against Poppler, and turn the pixel test on
Gap 12 of docs/gaps.md: until rendering is measured, every claim about its quality is a judgement.
Text extraction had an oracle and written output had qpdf; rendering had nothing.

`cargo xtask render-oracle` renders page 1 of every corpus file with our CLI and with Poppler's
pdftoppm and reports how far apart they are. The metric is the point. A raw pixel comparison is
useless here -- two correct renderers disagree on every glyph edge, so it calls a perfect page
badly wrong and says nothing about whether a shading is missing. `pixels::compare` counts a pixel
as differing only when it differs from every pixel in the other image's 3x3 neighbourhood by more
than a tolerance, in both directions, taking the worse. A one-pixel shift is forgiven; absent or
miscoloured content is not.

First reading, 250 files at 72 dpi, tolerance 8: 229 compared, median 0.89%, mean 2.16%, p90
5.35%, 25 within 0.1%. It reports rather than gates -- the corpus is not on CI, and a first
reading of a borrowed renderer is a baseline to improve on, not a bar to clear. The worst results
cluster on JBIG2 `bitmap-*` files and on ZapfDingbats, which aims the renderer work at something
specific for the first time. 15 files could not be compared because the two renderers round the
page box to different pixel sizes; they are reported as unmeasurable rather than scored.

Verified against the knob's bounds end to end: tolerance 255 scores every file 0.00%, tolerance 0
takes the median to 0.95% and p90 to 26.36%.

Also turns on `extract_type3_shared_xobjects_matches_poppler_pixels`, which had never run anywhere.
Poppler is now installed on the Linux runner, and the test is version-robust by construction --
both sides of its comparison go through the same pdftoppm, so it is about our two documents rather
than about Poppler's output. Verified in all three states: runs and passes with pdftoppm present,
skips without it, fails without it when PDFCRAFT_ORACLES_REQUIRED is set.

`run_capture` keeps its exact behaviour, including leaving the buffer empty on non-UTF-8 output;
the byte-oriented capture an image needs is a second entry point on the same timeout logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 14:28:19 -07:00
Tanish MankuandClaude Opus 5 0754740fd9 feat(oracle): make a missing external oracle loud instead of a silent pass
Every test that wanted an external oracle probed for it by hand and chose for itself what to do
when it was missing. The common choice -- print a line and return -- reports as a pass, so a suite
with no oracles installed looked exactly like one in which every oracle agreed.

That was not hypothetical. No CI runner installed qpdf, so `qpdf_agrees` had never executed on a
runner, while README.md said saved output was "independently checked with qpdf". For R2-R4 that
test is the only check of owner-password authentication (Algorithm 7) that does not go through our
own code.

`pdfcraft-oracle` holds the probes and one rule: a missing oracle is a declared skip or a failure,
never a silent pass. `PDFCRAFT_ORACLES_REQUIRED` picks which, on the same convention as
`CRAFT_FONTS_REQUIRED`. CI installs qpdf and sets it, so a skip notice there is now a red build.
The crate has no panicking path of its own; `or_skip!` expands its assert into the caller, which
is test code.

Verified by running the qpdf tests against qpdf 12.4.2 for the first time -- they pass, so the
README claim is now both enforced and true. Both gate directions were watched: absent and
unrequired skips with a notice, absent and required fails with the install command in the message.

Only qpdf is installed in CI, because it is the only tool the test job uses. The Poppler pixel
comparison in crates/organize stays ignored until a change turns it on and verifies it there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-14 13:56:00 -07:00
MMexicannandCodex 2a218f3420 M4: resolve indirect page rotations before editing
Read the effective rotation through its PDF integer object so inherited and
page-local indirect values retain their orientation when rotated.

Co-Authored-By: Codex <noreply@openai.com>
2026-10-13 14:13:38 +00:00
5ebb53a4a4 M4: preserve shared Type 3 glyph resources during extraction (#1500)
Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:34:12 -04:00
AlexandClaude Opus 5.5 9324afaa4f fix(organize): Document Properties also change the XMP title and author (#1658)
`set_info` changed only `/Info`. Files that carry XMP (LibreOffice and Word
exports, PDF/A) kept the old dc:title and dc:creator, so viewers that read
XMP first kept showing the old title, and PDF/A preflight reported the two
as different (6.6.3).

The matching XMP property (Title dc:title, Author dc:creator, Subject
dc:description, Keywords pdf:Keywords, Creator xmp:CreatorTool, Producer
pdf:Producer) is now replaced where it stands, added in a description of
its own, or removed with an empty value. The packet is edited as text and
the rest of it stays byte for byte; files without XMP get none, and a
packet that isn't UTF-8 is left as it was.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:18:35 -04:00
Marco Arnulfo da9cefa33c M4: copied pages keep the resources they list only up the page tree (part of #1628) (#1635) 2026-10-13 00:16:19 -04:00
MarekSpodBiedry 9215b6f3b4 Name the failing option and page count when a chained edit page is out of range (#1550) (#1575) 2026-10-13 00:10:05 -04:00
Jason Brashear a9745dd22d fix(organize): normalize rotations before adding so large quarter turns cannot overflow (#1512)
* Contributors: credit webdevtodayjason as Jason Brashear

* fix(organize): normalize rotations before adding so large quarter turns cannot overflow
2026-10-13 00:05:50 -04:00
012b34a2a5 M4: rename colliding form field names when combining (organize.field-name-conflicts) (#1155)
* M4: rename colliding form field names when pages are imported or combined

Insert Pages from File, Combine Files and the grouped combine registered
incoming form fields under names the destination already used, silently
blurring them into one field (fields of one fully-qualified name are one
field, so their values mix). Importing now walks both sides' fully
qualified names (parent.T.child) — including names that hang only off
page widgets, which /AcroForm never lists — and renames a colliding
field's own /T to the first free variant (Text1-1, Text1-2, …), so its
children keep their place in the tree and the second of two colliding
incoming fields is renamed too.

A field_conflicts policy (rename by default; merge keeps the older
behaviour) and reporting — ImportOutcome::renamed_fields, the combine
outcomes, and a field_name_conflicts(dst, src) pre-scan — let callers
choose and prepare; engine, tool and Combine-tab wiring follow.

* M4: engine and tool wiring for renamed field names

Insert Pages from File carries a field_conflicts policy (rename by
default) and reports what it renamed: a repair-log note names each
old → new pair and warns that scripts referring to the old names were
not rewritten, and the undo step's label counts the renames, so nothing
is slipped in silently. Undo restores the original names, as ever.

The session's combine paths gain reporting variants —
combine_unlocked_reporting, combine_grouped_reporting — returning the
bytes and the renames, and doc_combine takes field_conflicts
("rename" | "merge") and lists renamed_fields [[old, new], …] in its
result when fields were renamed, so agents can find what their scripts
must call the field now.

* M4: the Combine tab warns before combining that field names collide

The Warnings column previews, per file, the form fields whose names
collide with earlier files' and will be renamed (Text1 → Text1-1, …),
computed once per list by a new in-order pre-scan (organize
combine_field_conflicts over the whole list, engine wrapper over the
tab's byte sources; a file that cannot be opened previews as nothing,
as its row already says why). The message is plural-aware and
translated in all sixteen catalogs.

* M4: parity for renamed field names on combine and insert

organize.field-name-conflicts ships: rename by fully-qualified name
(or merge, on request), reported everywhere it happens — repair log
and undo label on insert, doc_combine's result, the Combine tab's
warning column. The notes say what is still missing: the tab's preview
ignores narrowed page ranges, the UI offers no merge choice,
Create ▸ Multiple files renames silently, and scripts and XFA bindings
that reference old names are never rewritten.

* M4: review fixes for the field-name conflict work

Four findings from the PR's code review:

- Clippy: the -D warnings gate failed on the new engine tests (a
  format! with no interpolation, two drop(doc) no-ops on a
  reference); the branch could not pass its own CI.

- The Combine tab's preview warned that a file split into parts
  collides with itself: the preview made one source per row, while
  the combine groups parts of one file and imports it once. The
  preview now dedupes by group (each group's first entry stands for
  the file, as the combine does), and every part shows the file's
  preview.

- Collision detection compared /T bytes, so one name written in two
  encodings (PDFDocEncoding, UTF-16BE) did not collide — the exact
  silent blur this work exists to prevent — and renaming a UTF-16BE
  name rewrote it as from_utf8_lossy mojibake. Names are now compared,
  suffixed and written as decoded text (PdfString::to_text /
  PdfString::text), which is also how the forms crate reads them.

- The engine's combine_unlocked/combine_grouped duplicated their
  _reporting twins line for line; they now delegate with the Rename
  policy, as organize's own wrappers already did.

* Field-name conflicts: only Combine and Insert from File rename

import_pages (Duplicate, Replace) and extract_pages (Extract, Split) go back to Merge, so
copies of a document's own fields keep their names and a source with two same-named field
roots isn't split apart. Pasting pages into the document they were copied from also merges;
pasting into another document renames as Insert from File does. Regression tests for
duplicate, replace, extract/split (organize), duplicate (engine) and paste (ui-egui); the
PR's rename tests call import_pages_with(Rename) explicitly.

Merge fix-up: main made edit labels a translatable Label, so the rename suffix is now two
wordings in EDIT_LABELS ("Insert pages from {name} — 1 field renamed" / "— fields
renamed"), translated in all 20 catalogs. The Combine warning's @plural row is added to
el, kk, sl and tr. Catalog hashes refreshed.

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

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 21:18:21 -04:00
287fce8f9c M2: bound the total size of page-label prefixes (#1146)
Both page-label readers decode each range's prefix into a string of its
own: render's inspection for its range table, Organize for the ranges it
returns. Ranges may share one prefix object, so a small file could ask
for a copy per range: within the 200,000-entry tree limit, about 200 MiB
of copies of one 1,024-byte prefix. Decoded prefixes now take at most
4 MiB in all (MAX_PREFIX_TOTAL_BYTES, beside MAX_LABEL_TOTAL_BYTES). Past
that the tree is refused before any label is made, as the other tree
limits refuse it.

Tests: 5,000 ranges sharing a 1,000-byte prefix are refused in both
readers (relabelling such a tree changes nothing), and exactly 4 MiB of
prefixes is still read.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-11 18:38:48 -04:00