Commit Graph
70 Commits
Author SHA1 Message Date
57c8de09ab M6: preserve XFDF field text around XML comments (#1647)
Join the text nodes in each field value so XML comments and processing instructions do not truncate imported text. Cover data extraction and form import through PDF save/reopen.

Co-authored-by: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:47:41 -04:00
95a972a3e7 M2: preserve known unfiltered inline-image sample boundaries (#1562)
Use checked sample lengths and row padding before the EI delimiter heuristic.
Cover valid binary terminator-like samples, masks, Indexed, aliases, invalid
layouts and saved classic/packed PDFs.

Co-authored-by: MMexicann <MMexicann@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:20:10 -04:00
a790a046a5 M7: keep line breaks when editing a multi-line paragraph (#1443) (#1486)
Adding a word to a multi-line paragraph merged or reflowed its lines.
Two causes:

- Chrome/Skia subset fonts leave glyphs of the /DW width out of /W.
  has_glyph treated those CIDs as missing, so words containing them
  measured short, run calibration inflated, unchanged rows rewrapped
  and new letters fell back to Helvetica. A composite subset with an
  explicit /DW now counts a ToUnicode-mapped CID absent from /W as
  present.
- Text drawn turned on the page (landscape pages drawn a quarter turn
  round) was joined into lines and rows along the page's x axis, so the
  whole paragraph became one line. Lines, rows and blocks are now found
  along each text's own baseline (identical values for upright text), a
  turned paragraph is rewritten in its own text space, and that rewrite
  keeps explicit line breaks. Formatting part of turned text is refused
  instead of being misplaced.


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

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 03:10:46 -04:00
02a4d81a22 Keep selected form values and page content through later edits (#1319, #1305, #1324, #1306) (#1452)
* Keep selected form values and page content through later edits (#1319, #1305, #1324, #1306)

An XFA radio item whose text contains a space was written back as an appearance-state name, so the next layout selected nothing. Replace Pages filled the gap between a split selection. Marking a figure decorative replaced every content stream on the page. Control clicks and drags accepted coordinates that overflow to infinity.

* Move the decorative-figure regression out of pdfcraft-edit

edit and a11y are both layer 4, so a dev-dependency from edit to a11y fails the layer check. The same regression now runs in the engine, which already depends on both.

* Drop pdfcraft-a11y from the edit crate's lockfile entry

* Run a tool after a background open of the file it asked for

A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.

* Wait for background opens in the command tests

Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.

* Compare batch copies to the PDF that was written

A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:05:21 -04:00
2fa110e185 M1: repair exhausted xref hints without object-number overflow (#1337) (#1359)
* M7: share Windows local-link fixture portability fixes (#1265)

Use the identical test-only prerequisite already proposed in #1263: Unix-only mutable vectors, ordinary Windows drive paths and escaped PDF literal strings. This is shared CI support for the separate xref overflow fix; production link handling is unchanged.

Generated with Codex.

* M1: return an error for exhausted xref object numbers (#1337)

Check the next-object addition and return CosError at the xref offset rather than overflowing or wrapping to object zero. Classic-table and stream regressions reproduced the original panic; ordinary, zero-offset and repaired-range controls preserve catalog access and allocation.

Validation: 2732 passed, 0 failed, 13 ignored; strict all-target Clippy, fmt, 31 WASM crates, layering, assets, deny and parity passed. The rebuilt CLI opened and rendered a clean synthetic page.

Generated with Codex.

* M1: place overflow notes beside stable milestone entries (#1337)

Keep the tested code unchanged while avoiding progress-note conflicts with newly landed main updates. Formatting and diff checks passed.

Generated with Codex.

* Merge main: adapt exhausted-xref repair to the compact xref index

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-15 03:02:16 -04:00
db34952a94 M6: expose dropdown and list-box field alignment (#1208) (#1263)
Reuse the existing alignment picker for choice properties, keeping existing /Q and choice values until changed. Cover cancel, undo/redo, save/reopen and retained options/flags through the real UI.

Generated with Codex.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:01:17 -04:00
6b185cdc5b Add permission-aware selected-text Copy to Edit and context menus (#1244)
* M3: add selected-text Copy to page and Edit menus (#1225)

Register selected page-text Copy with the existing document permission gate and expose it in Edit and the ordinary page context menu. Route floating Copy through the same gate while preserving focused-input and Organize clipboard ownership.

Four regressions fail behaviorally on the exact base; five candidate regressions pass. All eight cargo xtask ci stages pass: 2575 tests passed, 12 ignored, and 31 WASM crate checks. Native synthetic checks verify Unicode clipboard output, disabled no-selection/restricted actions, and permissions-password unlock using presented Xvfb/Vulkan software-rendered frames. Physical display and hardware GPU coverage are not claimed; the optional cargo-deny audit was unavailable.

Generated with Codex for FredApps.

* Tests: scope GoToR case mutation to Unix for Windows Clippy

Keep the common directory/non-PDF cases immutable. Shadow them in the Unix-only block so FIFO coverage can append its case without an unused-mut warning on Windows. No assertion or platform coverage is removed.

This repairs the exact hosted Windows Clippy failure inherited from upstream. Source review and whitespace checks passed; no local build, test or formatter was run during the pause. Automatically triggered hosted CI will validate this head separately.

Generated with Codex for FredApps.

* Tests: scope document link target mutation to Unix for Windows Clippy

Preserve directory coverage on all native platforms and device coverage on Unix. Static diff, whitespace and conflict checks passed; no local build, test or formatter was run during the pause.

Generated with Codex for FredApps.

* Tests: escape GoToR fixture file paths as PDF literal strings

Windows absolute paths were interpreted as PDF escape sequences, introducing control characters and failing the hosted local GoToR root test. Escape backslashes and parentheses in the fixture helper without changing product behavior or assertions. Static diff and whitespace checks passed; hosted CI will validate the repair. No local builds, tests or formatters ran.

Generated with Codex for FredApps.

* Tests: use ordinary absolute paths in the GoToR fixture

Build the absolute target from the original sandbox base rather than its canonicalized Windows verbatim path. Assert absolute-path form and canonical identity with the sandbox target. Preserve production UNC/device/verbatim rejection and existing containment and destination assertions. Static diff and whitespace checks passed; no local build, test or formatter ran.

Generated with Codex for FredApps.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 03:00:45 -04:00
71ceafd467 M1: keep unknown trailer entries on a full save (#1338) (#1431)
* M1: keep unknown trailer entries on a full save (#1338)

A full rewrite built a new trailer from a few known keys, so accepted custom metadata disappeared without a repair note. Copy every other entry and write objects that only those entries reference.

* Run a tool after a background open of the file it asked for

A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.

* Wait for background opens in the command tests

Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.

* Compare batch copies to the PDF that was written

A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.

* Drop the unused batch fixture left by the merge

Main already keeps the written PDF in original, so the older source binding is unread and fails -D warnings.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:59:58 -04:00
c10e7b8f0c Preserve literal spaces in page-label prefixes (#2355)
* M0: update rustls to 0.23.46

Co-authored-by: Codex <codex@openai.com>

* M2: preserve literal page-label prefix spaces

Keep label prefixes separate from document metadata trimming and cover literal spaces through public inspection.

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:57:34 -04:00
89f0fa2bd5 M14: Italian catalog: complete the missing entries (#2320)
* M14: Italian catalog: complete the missing entries

it.tsv lacked 265 keys that the other catalogs carry. The 201 the code
looks up are added in one dated section at the end of the catalog: the
sentences the accessibility checker, preflight, signature validation and
page-range parser word (their WORDINGS), the certificate rows and calendar
weekday and month labels (their tl_ctx contexts), digital-ID, docking and
print messages, and five plurals (one|other). Italian now has 2,528
entries.

The new rows follow the wording the catalog already uses: history labels
say "Modifica ..." like "Change {key}", and the Creator and Producer keys
that label fills in say "Applicazione" and "Produttore PDF", as the
Document Properties dialog names those fields. The Optimize PDF summary
says "eliminato" like its Discard options, the checker and preflight
sentences say "ha rilevato" and "Viene usato", the trust messages say
"nel tuo elenco di certificati attendibili", the password and certificate
rows say "non corrispondono" and "rilasciato" like the existing "The
passwords don't match." and "Issued by" rows, and the print hint quotes
the "Disattivato" option as the dialog shows it.

The other 64 are left out because no non-test code looks them up:
- 10 tooltip texts with a fixed macOS shortcut ("Zoom in (⌘+)"); the code
  fills the shortcut in through "{key}" rows Italian already has;
- 9 older wordings the code no longer uses: the five Combine folder
  messages, "Save (M4)", " on page {label}", the owner-only security
  message and the old "Text recognition isn't installed" message (the
  code now says "its model files are missing");
- 45 generic labels and plurals only the Lithuanian catalog has. The code
  says "{n} file|{n} files" as two plain strings, "1 file" and
  "{n} files", which Italian has; "{n} comment|..." and "{n} result|..."
  are used nowhere.

The entries are AI-assisted translations written from the meaning of the
English (clean-room; no proprietary localisation consulted), and keep
their placeholders and trailing ellipsis. The catalog's SHA-256 in
ATTRIBUTION.toml is updated by hand, as the other catalog changes do.
localization-parity.md, the ROADMAP progress log, the M14 rows of
docs/roadmap.md and docs/localization.md record the new count, and
parity/acrobat-features.toml now says Italian translates the engine's
sentences too.

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

* Refresh catalog hashes after merging main

---------

Co-authored-by: kcobelli-pixel <257471167+kcobelli-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:54:32 -04:00
c1146d0d8d feat(ui): improve back and forward page navigation (#2080) (#2151)
Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:47:33 -04:00
1af7a38bd0 Fix multi-select choice index ordering (#2144)
* M6: sort multi-select choice indices

Write ascending /I option indices without reordering /V selections.
Add forms and engine regressions for reverse selection and save/reopen.

Co-authored-by: Codex <codex@openai.com>

* M0: update rustls to 0.23.46

Update only the resolved rustls version and registry checksum.
Keep the existing choice-index feature changes and advisory policy.

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:47:08 -04:00
3a4de17707 M3: preserve coordinate clicks across slow frames (#2100)
Pair coordinate press and release after hover warmup so frame delays do not turn clicks into holds. Preserve text multi-click selection for coalesced pointer events.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:44:14 -04:00
Lucas Sampaio DiasandBrandon Thomas aa050612c5 M3: preserve Ctrl+Alt+Q input during digital ID creation (#1980) (#2055)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:41:32 -04:00
d3571c4019 M2: keep the raw crop minimum in physical points (#1982)
Apply the minimum after page-unit scaling without inflating small raw crops. Retain the existing unit policy and nearly-zero fallback, with regression coverage for origins, rotations and reciprocal rounding.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:36:12 -04:00
8bd200f643 M3: retain native open replies behind dialogs and pickers (#1953)
Check admission before each prepared result so a modal or queued picker keeps its document recipient. Wake retained replies once after picker consumption or worker abandonment.

Preserve merged #1914 continuation/startup behavior and the three worker regressions from upstream 3f044b62 (jcl-bit and its recorded coauthors). Keep the inherited printing/dependency changes intact; add only the remaining ownership barrier, wakes and focused regressions.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:33:01 -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
9c875656de fix(render, ui): form-heavy documents draw and fill fast (spot-colour images, widget placement, Fields panel) (#1691)
* fix(render): convert spot-colour images once per pixel value (vendored hayro-interpret patch 28)

An image whose pixels hold at most 16 bits of samples (an 8-bit Separation or
DeviceN image, a 1-, 2- or 4-bit gray or indexed one) is now converted to RGB
through a table of every possible pixel value instead of pixel by pixel.

A Separation or DeviceN tint transform is a PDF function, often a PostScript
calculator program, and the image decoder ran it once per pixel. Fillable
character sheets draw their background art as an 8-bit DeviceN image: one with
a 3304 x 4105 background took 3.8 s and 430 MB to draw each page (Poppler: 0.27 s),
when opening it and again after every field was filled. It now draws in 0.11 s
and 100 MB, pixel for pixel the same.

The table decodes each sample through the same `decode_sample` as the per-pixel
path, keeps `fix_image_length`'s rules for short and overlong data (the pixels
past the data are converted as before), and is used only when the image has
more pixels than the table has entries.

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

* fix(render): place widgets without /P in one pass over the pages

Opening a document, the inspector finds the page of a form field whose widget
has no /P by searching every page's /Annots for it, once per field: O(fields x
annotations). A form of 8,000 such fields took 1.17 s to open instead of
0.27 s. The pages' /Annots are now read once, on the first such field, into a
map from annotation to page.

A widget that several pages list is now placed on the first of them, as
pdfcraft-forms places it; the search took whichever page a HashMap visited
first.

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

* fix(ui): the Fields panel keeps up with large forms

Every frame the Fields panel searched the form for each row by name, once to
sort the rows into tab order and again to find what a click selects: O(fields²)
a frame. It also laid out, drew and built the tooltip of every row, scrolled
into view or not. With 8,000 fields the app used 13.4 s of CPU for 20 pointer
moves with the panel open (0.2 s with it closed).

The rows are now worked out by `field_rows`, which pairs each row with the
first form field of its name through one map a frame, as the engine pairs
them, and a row out of view is allocated and keeps its clicks, keyboard focus
and accessibility label but isn't drawn and builds no tooltip. The same 20
moves now take 0.86 s.

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

* docs: roadmap entries for faster form-heavy documents

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

* fix(render): convert each lookup-table entry on its own (vendored hayro-interpret patch 28)

Patch 28 converted the whole table of possible pixel values in one batch.
Separation and DeviceN `convert_f32` concatenate each tint result, and a Type 4
program that returns the wrong number of components is only warned about, so a
malformed result for a value the image never uses shifted every later entry. A
32 x 32 image whose only sample (255) is red drew black because tint 0 returned
one component (PR #1691 review).

`convert_each` now converts the table one entry at a time, so a malformed entry
changes only itself. The largest table (65,536 entries, a two-ink DeviceN image
over DeviceCMYK) takes about 2 ms longer.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:20:31 -04:00
9bbd765ea4 Keep page labels and focused navigation consistent across reloads (#1667)
* M3: await remote-link refusal in the UI regression

Wait for the asynchronous notification before checking the existing no-tab and refusal-message assertions.

Co-authored-by: Codex <codex@openai.com>

* M3: keep page label input consistent across reloads

Co-authored-by: Codex <codex@openai.com>

---------

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:19:13 -04:00
3d7336fc41 M5: add comment and reply submission shortcuts (#1457)
Submit highlight comment text and replies with Ctrl+Enter, retain Command+Enter
for floating composers, and let Shift+Enter insert reply newlines. Keep ordinary
Enter-to-post for replies and restrict submission to the focused editor.

Add UI regressions and parity evidence. Normalize existing lint and formatting
issues and pin the reload test's English labels so the required checks pass on
this Windows machine.

Co-authored-by: Armin Reisgys <Armin.Reisgys@de.bosch.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Luchsen <210128845+Luchsen@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-15 02:18:03 -04:00
Márk Bartalos b89cbd0044 M4: keep the last saved revision across undo (#1310, #1311, #1312) (#1435)
* M4: keep the last saved revision across undo (#1310, #1311, #1312)

Undo treated a return to the opened file as unsaved work, and after a save it followed the restored snapshot's older base. Revert and the next save now stay on the last saved file, so that revision remains in the PDF.

* Run a tool after a background open of the file it asked for

A tool chosen with no document opened the picked file on the worker, but only ran when that open finished inside the picker callback. Remember the command on the pending open, and let the command test wait for the worker.

* Wait for background opens in the command tests

Opening a picked file or a recent file finishes on the worker, so a fixed handful of frames can assert before the tab exists.

* Compare batch copies to the PDF that was written

A new text PDF stamps the current second, so a later pdf() call is a different file. The numbered copy and the source must match the bytes that were saved.

* Drop the unused batch fixture left by the merge

Main already keeps the written PDF in original, so the older source binding is unread and fails -D warnings.
2026-10-15 02:17:06 -04:00
Jules Kreuer 039706c437 Clear image leftovers (#1151)
* M8: Remove image redaction leftovers

* M8: Move roadmap note
2026-10-15 02:16:35 -04:00
dbef872ed7 M2: preserve precision when writing page transforms (#946)
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
2026-10-15 02:16:13 -04:00
skylar taylor-barrick b5467d93a0 fix(ui): preserve native open actions and password prompts
Carry selected tool actions through native file preparation, password retries and conversion, and defer startup intake until pending native opens finish.

Await observable completion in native opening, password, recovery and linked-PDF tests while retaining their content, identity, dirty-state, cancellation and destination checks. Verify the expected validation error and preserve the original document after refused consent targets.

M3; refs #1914.
2026-10-13 12:18:41 -06:00
e88dd728cb M13: scan each directory once in CLI checks (#1501)
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:41:41 -04: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
13a921ae4a M2: keep byte boundaries in packed TIFF-predictor rows (#1499)
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:27:26 -04:00
4cb803038d M3: confirm before opening absolute GoToR paths (#1364) (#1489)
* M3: confirm before opening absolute GoToR paths (#1364)

* fix(i18n): translate GoToR confirmation dialog labels

---------

Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 02:16:21 -04:00
Yousef MojahidandBrandon Thomas 0dd5fdd5bd M7: keep Arabic ActualText ligatures in their word (#1473)
Normalize single-glyph RTL replacements before assembling editable lines. Preserve authoritative multi-glyph spans and cover lam-alef variants, marks, CID codes, replacement, copy/search and save/reopen.

Assisted-by: Codex

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 01:52:22 -04:00
Márk BartalosandBrandon Thomas fff985f30d M5: keep stamps upright on rotated pages (#1398) (#1460)
A stamp placed on a page with /Rotate was drawn in user space, so the
label or picture turned with the page. Counterrotate the appearance so
it reads upright as the page is displayed.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 01:42:57 -04:00
Márk BartalosandBrandon Thomas 9ed3abe490 Keep undrawn comments and accurate search hits (#1317, #1327, #1302, #1315, #1340) (#1442)
Flattening deleted a cloudy comment it could not draw, and a refused text-box restyle or resize left the new properties in the file. Arrow defaults were stored on plain lines. A custom match ending on a separator included the next glyph, and the vertical-text redaction error claimed an overlap it had not checked.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 01:09:36 -04:00
Earl CoandBrandon Thomas 890b83c4c4 Add reviewable text-only PDF auto-tagging (#1440)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-13 01:02:05 -04:00
Márk Bartalos c6128c039a M7: keep a neighbouring line when replacing text (#1316) (#1427)
Glyph boxes of tightly spaced lines overlap, so replacement treated the next line as a duplicate and removed it. A second drawing is removed only when it also shares the edited line's baseline.
2026-10-13 00:01:50 -04:00
FredAppsandCodex fcc1d9ead0 M2: adapt Lab Bradford white points in LMS (#1417)
Follow up #1237 by deriving Bradford scaling ratios from the source and
D65 white points in LMS. Preserve CalRGB and the merged Indexed/Range
handling. Add independent neutral/chromatic, identity, asymmetric Range,
and degenerate-cone render regressions.

Co-authored-by: Codex <noreply@openai.com>
2026-10-12 23:59:51 -04:00
057e453a16 Retire inherited resources (#1348)
* M8: Retire inherited redaction resources

* Redaction: pin inherited resources only on redacted pages, shared by reference

localize_resources rewrote every page in the document on every apply and copied a direct
inherited /Resources dictionary into each one, so a large document grew by N x the dict.
Now only redacted pages that inherit resources are pinned; a direct inherited dictionary is
made one indirect object and shared by reference. The parent's /Resources is retired when no
other page still inherits it (the leak fix), and kept (through the shared reference) when an
unredacted page still draws with it. A /Parent chain that can't be followed or disagrees with
the page tree retires nothing. Tests: an unredacted page is left untouched and the rewrite is
bounded; redacting every inheriting page retires the parent's resources. ROADMAP entry moved
into the revision-history table.

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-12 23:49:57 -04:00
Márk BartalosandBrandon Thomas 87b448b664 M1: write a valid xref when an incremental save has no objects (#1339) (#1424)
An unchanged classic-table file used to append xref immediately followed by trailer, which qpdf treats as a damaged file. A zero-length subsection keeps trailer-only updates without rewriting any object entry.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 08:57:37 -04:00
4bfd3c2729 M14: add Lithuanian interface and date localization (#1401)
* M14: add Lithuanian interface localization

Register Lithuanian UI and date languages with three integer plural forms
and a 2,572-entry contributor-original catalog. Cover dynamic dialog labels,
contextual calendar and certificate wording, commands, tools, and every key
in the other bundled catalogs. Add real-shell calendar, inspector and
password-dialog regressions; preserve PDF values and document state.

Update localization documentation and asset attribution. Native-speaker/VLKK
terminology review remains outstanding.

Validation: cargo xtask ci passed; cargo-deny was skipped because it is not
installed. Five rendered dialogs and contributor manual testing checked UI.

Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com>

* M14: preserve multilingual localization documentation

Restore the existing language descriptions instead of replacing general
localization guidance with Lithuanian-specific coverage. List every
registered language and control-channel code; keep Lithuanian details
alongside the existing catalog descriptions.

Include the previously omitted Kazakh, Polish, Slovenian, Greek and Turkish
catalogs in the shipped-language table without changing the historical
coverage snapshot or existing assessments. Keep Lithuanian roadmap notes
concise and additive.

Validation: all 22 registered languages appear in documentation; every
existing descriptive paragraph is retained. cargo xtask ci passed, with
cargo-deny skipped because it is not installed.

Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com>

* M14: record completed Lithuanian native-speaker review

The contributor confirms they are a native Lithuanian speaker and have
reviewed the translation. Remove obsolete pending-review labels from the
asset manifest and documentation; regenerate ATTRIBUTION.md. Remove the
redundant generated-message disclaimer from the Lithuanian description,
keeping the existing general localization documentation unchanged.

Validation: cargo xtask ci passed; cargo-deny was skipped because it is
not installed. Catalog content and its SHA-256 are unchanged.

Co-Authored-By: OpenAI gpt-6.1-sol <noreply@openai.com>

* Lithuanian: cover main's Copy Image labels, list lt last, check its glyphs

Adds the six Copy Image rows from #1363, moves lt to the end of LANGUAGES
and DATE_LANGUAGES (after tr), adds lt.tsv to the primary-font coverage
test, writes the JavaScript preferences path with '>' as the other
catalogs do (Inter has no ▸), and refreshes counts and hashes.

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

---------

Co-authored-by: OpenAI gpt-6.1-sol <noreply@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:54:15 -04:00
2839152af1 M12: preview text structure proposals for auto-tagging (#1383)
* M12: preview text structure proposals for auto-tagging

* Auto-tag preview: no unregistered command, allowed on copy-restricted files like a11y.check

The preview is read-only with no UI command yet, like render_preview, so it no
longer names the Planned a11y.autotag catalog id (tool_table_is_well_formed).

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

* M12: preview text structure proposals for auto-tagging

* Keep read-only auto-tag preview separate from unavailable command

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 08:20:13 -04:00
186c1dc767 M7: preserve Arabic text order in copy, search and editing (#1369)
* M7: preserve Arabic text order when copying and editing

Record per-line ActualText for shaped added text and preserve its logical order in Edit text. Cover mixed scripts, marks, copy/search, retyping and save/reopen; retain the drawing golden.

Assisted-by: Codex

* Edit text: keep only right-to-left ActualText out of line joining and bidi recovery

Word and Chrome wrap Latin ligatures in /ActualText spans mid-word; splitting
lines at every span broke such words into pieces in Edit Text. Only spans
whose ActualText has strong right-to-left characters keep their own line and
typing order now. Regression: a Latin fi ligature span stays on its line.

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-12 07:11:23 -04:00
d2d1b16b21 M5: adjust Fill and Sign text size (#1285)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:13:48 -04:00
Jules Kreuer 47d612817a M8: Clean indirect and form tag references (#1351) 2026-10-12 05:43:12 -04:00
51982db121 M3: match Greek final sigma in case-insensitive search (#1349)
* M3: use the macOS Finder label in the tab-menu regression

The UI already uses Reveal in Finder on macOS, but the regression test
queried Reveal in File Explorer on every platform. Select the platform's
label while preserving the disabled-action and inactive-tab assertions.

Co-Authored-By: Codex <noreply@openai.com>

* M3: match Greek final sigma in case-insensitive search

Fold capital, normal and final sigma to the same character so Greek words
match in either case. Cover exact search, whole words, glyph ranges and
bounded matches without changing the existing Turkish-I behavior.

Record the search fix and the macOS test portability follow-up in the
roadmap. Workspace tests, all-target Clippy, formatting, wasm, layering
and a synthetic CLI before/after comparison pass on macOS.

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: nevicia2010-hub <259836477+nevicia2010-hub@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
2026-10-12 05:42:27 -04:00
fcf6e39d46 M14: tune Windows low-DPI JetBrains Mono hinting (#1264)
* M14: tune Windows low-DPI mono hinting

* Theme: ignore the raster-sharpness probe test

`windows_hinting_improves_low_dpi_mono_raster_without_changing_layout_or_coverage`
asserts exact egui rasterisation deltas that move with egui upgrades; it is now
`#[ignore]` (run with --ignored when retuning). The tweak and its policy test
stay.

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-12 03:25:25 -04:00
8cacb44728 M3: follow Windows' Animation effects setting; add an Animation effects preference (#1235)
* M3: follow Windows' Animation effects setting; Smooth scrolling preference

With Settings > Accessibility > Visual effects > Animation effects turned
off, the wheel still eased: egui spreads a coarse wheel step over about a
quarter of a second, for scrolling and for Ctrl-wheel zooming, and has no
switch for it.

- crates/ui-egui/src/motion.rs reads each frame's wheel input the way egui
  does, minus the easing, and hands it to the scroll areas and the views
  (zoom_delta). A test sums what egui eases and compares it with what is
  read at once, and fails if egui stops easing.
- With the system asking for no animations, egui's own animations take no
  time and the progress card's bar steps without its sweep.
- The desktop app asks WinRT's UISettings.AnimationsEnabled at launch and
  every two seconds of use, so a change applies without a restart.
- Preferences has Smooth scrolling: Use system setting (default), On, Off.
  On or Off decides the wheel's scrolling and zooming, and scrolling to a
  place, whatever the system says. Saved, settable as smooth_scrolling on
  the control channel, translated in all 20 catalogs.

How far a notch scrolls or zooms is unchanged. macOS, Linux and the
browser aren't asked for their setting yet; Off works everywhere.

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

* Credit Jeremy Yao by name in About (contributors/people.toml)

My own entry: real name and display name "Jeremy Yao".

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

* M3: rename Smooth scrolling to Animation effects; it decides every animation

The preference also governs Ctrl-wheel zooming, so "Smooth scrolling"
described only part of it. As "Animation effects" (Use system setting, On,
Off) it now decides every animation in the window, the way the Windows
setting of the same name does:

- the wheel's scrolling and zooming, and scrolling to a place (as before);
- egui's own animations, the progress card's sweep, and the chrome's hover
  and press washes (new on main, with durations of their own), which used
  to follow the system alone or not at all.

Saved and set on the control channel as animation_effects (system, on,
off); the unreleased smooth_scrolling name is gone. The three labels are
retranslated in all 20 catalogs.

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

* Tests: the app's two real-GPU tests take turns (FreeBSD SIGSEGV)

FreeBSD CI died with SIGSEGV in the pdfcraft binary's tests on this branch
(d4b928f1). The log shows a_gpu_error_while_a_window_is_set_up... (started
first, still running) and pipeline_creation_error... (started shortly
before) both in flight when the process died: two test threads inside the
graphics libraries at once. Nothing else still running was native code.

Both now take one lock for the whole test, as the UI tests already do for
the same reason (crates/ui-egui/tests/view.rs). Each takes about two
seconds with a device, which is why they can overlap.

The FreeBSD job has been failing at the same step with the same exit code
on main (84a75eae, 4a1e205f, 3d2bdfe7: every completed run since #1042
added the second real-GPU test) and on unrelated PRs. Their logs weren't
available to confirm it is the same crash.

Not reproduced locally (no FreeBSD machine here); the next FreeBSD run is
the check.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:46:40 -04:00
c1d98946a9 M7: open local GoToR PDF links natively (#1265)
* M7: open local GoToR PDF links natively

* GoToR links: read only regular .pdf files under a size cap; add missing translations

A GoToR link in an untrusted PDF could name /dev/zero (unbounded read), a FIFO
(UI thread hangs) or a directory. Both the UI path and the `link_open` tool now
go through `pdfcraft_engine::read_remote_document`, which requires a `.pdf`
extension and a regular file (metadata checked before opening) of at most
1 GiB, and stops reading if the file grows past the cap. Regression tests cover
directories, a /dev/zero symlink and a FIFO (unix) in the engine, the tool and
the UI.

Adds the four link notifications to all 20 catalogs and "The existing action
will be preserved." to el, es, pl, te and zh-hant.

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-12 02:34:19 -04:00
2aea7dcfeb M10: print from the web build through the browser's print dialog (gap 4) (#1243)
* Gates: fix two clippy failures on main

- ocr_ui: "Also recognize pages that already have text" used egui's own
  checkbox, which clippy.toml disallows (#1184 landed beside #1173);
  use widgets::checkbox like every other box.
- camera_edges: Rust 1.97's manual_range_contains; the rewrite keeps the
  old answer for a NaN angle (false) with an is_finite check.

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

* M10: print from the web build through the browser's print dialog (gap 4)

The web build had no printer: spool::submit refuses on wasm, so Print
could only download the print-ready PDF. Now its Print dialog offers
"Browser's print dialog" (the default there), which reaches every
printer the operating system knows.

- web_print: PdfCraft draws the print-ready PDF's sheets itself
  (engine Exporter), one sheet a frame since wasm has no threads:
  300 dpi up to 100 sheets, 200 up to 300, 150 up to 1,000, at most
  64 MP a sheet and 1 GiB of PNGs a job. The PNGs become blob URLs
  and print from a hidden srcdoc iframe, one sheet per page, margin 0.
- Chromium prints every page at the default @page size, so a sheet of
  the paper's size turned sideways is rotated 90 degrees CCW onto it
  (as drivers do); other sizes get named @page rules.
- Copies, two-sided and colour are greyed out (the browser's dialog
  sets them). If print() is refused (e.g. a sandboxed frame), the
  print-ready PDF downloads instead, with the reason.
- pdfcraft-print stays free of web APIs; six new strings in all 20
  catalogs (ATTRIBUTION hashes refreshed); docs and parity updated,
  gap 4 closed.

Verified in headless Chromium against a trunk build: three sheets at
300 dpi, the landscape sheet turned, no blank trailing page, and the
download fallback. Firefox and Safari not yet tried.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 02:31:36 -04:00
13637b8440 M7: transform private image crops across nested states and streams (#1269)
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:38:32 -04:00
Earl CoandBrandon Thomas 05a862995d Accessibility: show figure findings on the page (#1280)
* Highlight locatable accessibility figure findings

* M12: locate annotation and form accessibility findings

* M12: keep one contributor profile entry

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:29:25 -04:00
Raj PatelandClaude Opus 5.5 1970935587 M14: edits let go of the previous working file before writing the next (gap 13) (#1279)
* M0: keep the camera edge test NaN-safe under clippy's manual_range_contains

Clippy 1.97 flags `d < 0.35 || d > PI - 0.35` in the camera edge hit test. Its suggested
`!(0.35..=PI - 0.35).contains(&d)` would count a NaN direction as a hit, which the two
comparisons did not, so the NaN check is kept explicitly. Behaviour is unchanged.

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

* M14: edits let go of the previous working file before writing the next (gap 13)

Every edit writes a new working file: the original plus one appended revision. The previous
one stayed alive in the working bytes, the display bytes and the renderer while the next was
written, so after a few edits a 1 GiB file peaked at three copies.

Each refresh path (full, comments/form, content, undo, redo, after save) now releases them
first: the working and display bytes fall back to the original (already held by the editor)
and the renderer is replaced by an empty one until the refresh rebuilds it. A refresh that
still fails after rollback draws the original instead of nothing. Save still writes from the
editor, never from these.

Measured on a synthetic 1 GiB file (release build, Linux): five edits peak at 2.1 GiB instead
of 3.1 GiB, in 8.5 s instead of 16 s. Saving still peaks at three copies; reading from disk on
demand stays blocked on the renderer (hayro parses one contiguous buffer, gap 2). gaps.md,
the parity notes and the roadmap record both.

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 01:28:01 -04:00
DWAA1660andBrandon Thomas c52072b678 Add editable page-range selection to Organize Pages (#1092) (#1174)
* #1092: Add editable Organize page-range selection

Generated with Codex.

* #1092: Leave overlapping main CI cleanup to PR #1167

Generated with Codex.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:25:38 -04:00