2556 Commits

Author SHA1 Message Date
aiongg a7801dc84e fix(ui): text typed into a new text box counts as unsaved work (#4441)
* fix(ui): text typed into a new text box counts as unsaved work

Closing a tab or quitting while the only unsaved thing was text in a new Add text box skipped the save prompt and lost the text. The box now counts like fill text does: only when it holds non-blank text.

* fix(ui): saving on close keeps text typed into a new text box
2026-10-24 14:25:44 -04:00
Brandon Thomas d445410807 Order the render integration-test modules (cargo fmt) (#4486)
#4430's merge with main left mod type3_text_clip before mod tiling_cell_placement,
so main's fmt check failed.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:57:58 -04:00
mtlcl410 cc5fd54868 M6: an editable combo box takes a typed value (ISO 32000-2 Table 233 Edit) (#4451)
A combo box with the Edit flag (bit 19) opened only its list, so a value
that isn't one of its options could not be typed on the page, although
form_fill already took one. ISO 32000-2 12.7.5.4 and Table 233: with Edit
set, "the combo box shall include an editable text box as well as a
drop-down list".

- An editable combo box now opens a single-line text box over the field
  as well as its list. Enter, Tab or a click away commits the text as
  Edit::SetFieldValue with a Choice, the path form_fill and the list use,
  so undo, field scripts and the appearance follow; a pick from the list
  still sets an option.
- The editor starts on the shown text of the picked option, and typing a
  shown text picks that option (the engine maps it to its export value).
- A combo box without Edit is unchanged: a drop-down list only.
- Tests: kittest typing into an editable combo (Enter, list pick, Tab,
  save, undo) and a non-editable one refusing typing; automation
  form_fill takes free text only with Edit, drawn before and after save.
- Parity: form.fill-choice notes and evidence.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:44:38 -04:00
Perchenko Anton 94a12715ce Remember the annotation tool pin across restarts (#4253) (#4276)
* M14 #4253: remember the annotation tool pin across restarts

* M14 #4253: clarify roadmap timezone and Markdown formatting

* Keep main's roadmap status lines; only the progress-log entry is new

* Take main's forms.rs: main already gives each form test its own folder

* Roadmap: drop the form-test folder note (main already has its own)

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:44:33 -04:00
mtlcl410 0dd871f772 M2: a Separation /All colour paints every colourant, gray 1 − tint on screen (ISO 32000-2 8.6.6.4) (#4439)
The special colourant name All refers to every colourant of the output
device at once, and on an additive device its subtractive tint is
complemented before it is applied (ISO 32000-2 §8.6.6.4). The vendored
colour code treated All as an ordinary spot colourant and drew it through
its tint transform, so a /DeviceCMYK transform giving cyan drew 0.5 scn
as light cyan instead of a 50% gray.

Vendored hayro-interpret patch 66: Separation::convert_f32 writes the gray
1 − t for each tint of an All space without evaluating the tint transform.
Fills, strokes, images and shadings in that space all convert through it,
and so does the RGB print raster. /None still paints nothing, other names
still use their alternate, and a DeviceN space naming All keeps its
alternate (the clause defines the name for Separation spaces).

Tests: crates/render/tests/it/separation_all.rs (synthetic).


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:36:24 -04:00
mtlcl410 6718f1f34e M2: Type 3 text in rendering mode 7 is not rendered and adds no clip (ISO 32000-2 9.3.6) (#4430)
ISO 32000-2 9.3.6: for a Type 3 font, modes 3 and 7 shall not be
rendered, the other modes render the glyph descriptions, and modes
4 to 7 add nothing to the clipping path. Vendored hayro-interpret
patch 42 drew every mode but 3 filled, so 7 Tr painted the glyphs
in the fill colour (black by default) wherever a following fill
did not cover them.

Patch 64: show_glyph keeps mode 7 for Type 3 glyphs and reports
them as clipping text (GlyphDrawMode::Clip, painting nothing, kept
by extraction), and clip_glyph's Type 3 arm is a deliberate no-op.
Modes 0-2 and 4-6 still render filled.


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:34:36 -04:00
Dylan M. Taylor 9aef0dfb78 refactor(xfa): explicit model imports (Sonar S2208) (#4403)
* refactor(xfa): replace model wildcard imports with explicit names (Sonar S2208)

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

* fix(xfa): complete explicit model imports

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

* fix(xfa): drop unused model imports

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

* Import Corner, which main's parse.rs now uses

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-24 10:31:05 -04:00
NetterSilver 82f0b6f8a0 Reject non-finite numbers in XFDF coordinate lists on import (#4449)
Found while testing crates/xfdf/src/lib.rs:274-276 (csv) used by annot_from_xml at :924-928

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-10-24 10:30:38 -04:00
aiongg 5fa865f82a fix(ui): header, footer and watermark previews step through the pages (#4448)
* fix(ui): header, footer and watermark previews step through the pages

The marks dialogs preview the current page, or the first page the range
selects when it leaves the current one out, with no way to look at
another page. Arrows beside the page label step the preview to the
previous or next page the range selects. The page stepped to resets
each time the dialog opens.

* fix(ui): the marks preview forgets a stepped-to page the range leaves out
2026-10-24 10:30:34 -04:00
mtlcl410 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
mtlcl410 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
mtlcl410 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
Eric Kaden 96cf936b31 M12: XFA check boxes sit where their margins, caption and alignment put them (#4300)
* M12: XFA check boxes sit where their margins, caption and alignment put them

* M12: a check box's automatic caption room and auto height follow the box as drawn

* M12: a caption reserve of zero sizes the caption to its content, as no reserve does
2026-10-24 10:30:11 -04:00
Gabriel Luís da Silva be0ec70756 fix(automation): roots-only mode refuses other shares and devices by name (#4289)
With only --automation-read-root / --automation-write-root set, a path
went straight to std::fs::canonicalize, so a UNC, WebDAV or device path
(`\host\share\x.pdf`, `\.\pipe\x`) was resolved, contacting the host,
before it was refused. `--root` mode already refuses those by name
(`foreign_share`).

`foreign_share` moves into file_access, and `AutomationRoots::check_path`
applies it to the path made absolute (by name, nothing read) before
following links, with the same refusal as any other path outside the
root. A root on a share still accepts that share. `resolve` is split
into `absolute` (empty path, device names, made absolute) and `resolve`
(links followed), so `out\NUL` keeps its own refusal.

Part of #4003 (the first of its three points).
2026-10-24 10:30:06 -04:00
mtlcl410 a08ec799f5 M2: tiling pattern cells sit at their XStep/YStep multiples at every scale (ISO 32000-2 8.7.3.1) (#4421)
The cell pixmap was round(scale x step) pixels and repeated, but the cell
was drawn, and the paint transform mapped back, at the unrounded scale.
So the pixmap repeated a fraction of a pixel too far at every step (31 px
for a 30.56 px step at 110 dpi), and cells drifted further from their
place with every step from the pattern's origin.

Vendored hayro patch (25), `tiling_cell_pixels`: the cell is drawn at
round(scale x step) / step, so one step is exactly the pixmap's side, and
the paint transform uses that same scale. The side stays within 1 and the
existing 3000 px cap; a non-finite product keeps the scale and a bounded
side.

Tests: crates/render/tests/it/tiling_cell_placement.rs (fill, turned and
scaled matrix, stroke, all at 110 dpi far from the origin; the helper's
bounds).


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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:30:02 -04:00
Daniele Lombardi 4047e9aaa5 fix(ui): seven tools take the page zoom from the side the page's width is on (#4425)
With the view turned 90 or 270 degrees, the page's screen rectangle has
its sides swapped, so xf.rect.width() / xf.pw was off by the page's
height over width: Fill & Sign, form fields and Add text drew typed text
at the wrong size, and Measure snapping, Prepare form, the calculation
order and field table overlays were sized wrong too. They now use
PageXform::zoom (from #3749), which reads the side the width is on.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 10:29:58 -04:00
FredApps 47d2a8f16e refactor(ui): separate field-table divider input from painting (#4419)
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
2026-10-24 10:29:52 -04:00
Dylan M. Taylor 39d69c3165 refactor(engine): extract quad_score and page_has_form_xobject (Sonar S3776) (#4408)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 10:29:47 -04:00
Dylan M. Taylor 486949424c refactor(a11y): extract text_spans and xobject_bbox helpers (Sonar S3776) (#4407)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 10:29:43 -04:00
.CooCoo 02b6fcc5ef fix(optimize): a GoToR or GoToE /D no longer keeps a local named destination (#4445)
Clean Up removes named destinations that nothing in the document refers
to. referenced_names counted the /D of GoTo, GoToR and GoToE actions
alike. The /D of a GoToR or GoToE action names a destination in another
document, so a local destination with the same name was kept although
nothing in the document used it.

Only a GoTo's /D now counts as a use. A /Dest key and a GoTo's /D are
read as before.

Test: links with a GoTo, a GoToR and a GoToE action name three
destinations; optimize() keeps the one the GoTo uses and removes the
other two.
2026-10-24 10:29:33 -04:00
oracola91 bc4f8e2602 Redaction keeps images still drawn through a form without /Resources (#4413)
content_draws tells whether some content draws an XObject, directly or
through a form that has no /Resources of its own and so draws through
its caller's. Since #4356 the recursion passed the caller's content
again instead of the nested form's decoded data, so a name drawn only
inside such a form was never found.

Both callers then dropped the entry: unlink_doomed for page-tree
resources (Apply Redactions and the content pass of Remove Hidden
Information) and prune_doomed_xobjects for surviving forms. An image
removed under a mark but still shown elsewhere through such a form went
missing from there as well.

The recursion reads the nested form's data again, as it did before
#4356. Two tests cover the page-tree case and the surviving-form case.
Both fail without the fix.
2026-10-24 10:29:28 -04:00
aiongg b6b0bbab08 fix(forms): a multiline field rejects a glyph wider than the field (#4447)
TextBox::fits checked only the height of a wrapped multi-line value.
Long words break by character, so a single character wider than the
box still overflows it, and the appearance reports Overflow::Wide for
a value fits had accepted. The multi-line branch now also requires
every wrapped line to fit the width.
2026-10-24 09:37:35 -04:00
aiongg 2ae1e915c9 fix(ui): comment composer visibility uses the shown rectangle 2026-10-24 12:13:22 +00:00
aiongg f9a179fee0 Merge integration batch B's review fixes 2026-10-24 08:26:10 +00:00
aiongg afa14c3b98 Merge integration batch A's review fixes 2026-10-24 08:26:10 +00:00
aiongg 70404ac310 Integration: format the review-fix tests (#4339) 2026-10-24 08:24:09 +00:00
aiongg 9595f5940c Integration: a script-opened panel reveals a background Inspector tab (#3580)
ShowPanel now queues the same Inspector reveal a user's panel choice does.
2026-10-24 08:24:01 +00:00
aiongg 82eedb6607 Integration: a retained-version optimize reports the later of header and catalog version (#3227)
Object streams raise the header to 1.5 over a lower catalog /Version; pdf_version now reports the version viewers read.
2026-10-24 08:21:14 +00:00
aiongg 29889fc1e0 Integration: list the attachment edit labels for translation (#1960)
"Attach {name}" and "Delete attachment {key}" were missing from EDIT_LABELS, so the translation-gap report left them out.
2026-10-24 08:20:44 +00:00
aiongg 1fb1877923 Integration: attachment_add reads its file through the size-capped reader (#1960)
attachment_add used an unbounded std::fs::read; it now uses read_input like the other file-reading tools, and the oversized-input test covers it.
2026-10-24 08:20:39 +00:00
aiongg be826a3bcd Integration: a short multi-line field accepts the line it draws (#4339)
The fit check required the padded height for every line; it now uses the bound the appearance applies, so a 16 pt field at 12 pt takes one line.
2026-10-24 08:17:01 +00:00
aiongg 06959df890 Integration: execMenuItem requests go through the request cap (#3580)
A script looping on app.execMenuItem could queue unlimited requests; they now follow the same count and byte limits as other requests.
2026-10-24 08:15:46 +00:00
aiongg b04257d5eb Integration: a typed Password-field value stays in the open document after Save (#3943)
Recording a save reopens the saved file, which carries no Password values; they are put back in memory only, so the next save still omits them.
2026-10-24 08:15:01 +00:00
aiongg 8021aee1f8 Integration: the edit composer stays visible while part of its comment is on screen (#4095)
Editing a large comment whose top-right corner was scrolled away opened a composer that was hidden and could not be cancelled. The card now stays, clamped into the document, while any part of the comment shows.
2026-10-24 08:14:45 +00:00
aiongg 2208dc16c8 Integration: cargo fmt 2026-10-24 07:32:52 +00:00
aiongg 7685882aa1 Merge integration batch C (track r3-t07) 2026-10-24 07:25:53 +00:00
aiongg 8ce5a3ec96 Merge integration batch A at #2648's updated head 2026-10-24 07:24:24 +00:00
aiongg 315454d3a9 Merge PR #2648 at its updated head
# Conflicts:
#	crates/engine/src/js.rs
2026-10-24 07:21:13 +00:00
aiongg 35f3fe027e Integration: #3694 a press beneath a menu or dialog does not end Select All
The reset uses pressed_page (unobstructed, pointer over the page) instead of the raw press origin; the test presses with a dialog open.
2026-10-24 07:20:42 +00:00
aiongg 95ec6348c3 Merge upstream main into integration batch A
# Conflicts:
#	crates/ui-egui/tests/it/control.rs
#	crates/ui-egui/tests/it/forms.rs
2026-10-24 07:12:10 +00:00
aiongg 7733b619cf Integration: #3670's fan-out test allows for a loaded machine 2026-10-24 07:11:16 +00:00
aiongg a9fe3d4e7f Integration: drop the doc comment of the removed visit cap (#3670 × main) 2026-10-24 07:11:16 +00:00
aiongg 2a91f1d717 Integration: the page's decode budget owns the form count in redaction (#3670 × main)
#3670 raised the form depth limit to 50 and added its own 50,000-visit cap. Main's
per-page DecodeBudget (e2a677164) has the same 50,000-form limit, and the visit cap
fired first, so Apply removed the forms instead of refusing the page as main does.
The visit cap goes; the fan-out test expects the refusal, and the depth-limit
sanitization test builds its chain from MAX_DEPTH.
2026-10-24 07:11:16 +00:00
aiongg 2ffcd0f1d1 Merge PR #3694: Select All takes the whole document's text while scrolling continuously (fixes #2639) 2026-10-24 07:10:11 +00:00
aiongg deffc376cf Integration: #2588 keep XMP-only document information across full refreshes
Session::refresh_with_indexes fills title, author and the other fields from XMP as build_document does; the test applies an edit and an undo and checks they stay.
2026-10-24 07:10:11 +00:00
aiongg 91b6f7f172 Integration: #3670's fan-out test allows for a loaded machine 2026-10-24 07:06:18 +00: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 cededfaccb Integration: #1960 put the attachment_action doc line back above attachment_action 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