479 Commits

Author SHA1 Message Date
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
.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
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
aiongg ce684cd1d3 Merge track r3-t06 2026-10-24 06:50:31 +00:00
aiongg 6a206a0a2e Merge track r3-t05 2026-10-24 06:48:48 +00:00
aiongg 692f34fce2 Merge track r3-t04 2026-10-24 06:40:21 +00:00
aiongg 9705e7a6c1 Merge track r3-t03 2026-10-24 06:39:33 +00:00
aiongg 1d270206be Merge PR #3227: M11: target PDF version and object streams option in PDF Optimizer
Conflicts: docs/roadmap.md: all status lines kept, M10 row from main, M11 row with #3227's two sentences. parity/acrobat-features.toml: save-with-object-streams entry from #3227 with its test paths under tests/it/.
2026-10-24 06:25:55 +00:00
aiongg eac3ad6e0f Merge PR #4068: i18n(fi): translate the 64 labels left in English after #2210
Conflicts:
- ATTRIBUTION.toml: fi.tsv source: main's text plus the PR's added sentence; the PR's sha256 line dropped (translation catalogs carry none on main).
- ATTRIBUTION.md: main's rows, fi row updated to the same source text.
2026-10-24 06:24:19 +00:00
aiongg 80f9555cf8 Merge PR #3786: M5: the arrow keys move the selected comment 2026-10-24 06:23:32 +00:00
aiongg f71a305915 Merge PR #4230: M7: paste added text boxes and images in the engine and through content_copy (#2636)
ROADMAP.md, docs/roadmap.md: kept both sides' status and progress-log lines; did not restore the older M14 status line that the branch had already trimmed.
2026-10-24 06:23:24 +00:00
aiongg 9c47dd36cf Merge PR #4331: M10: apply soft-mask Decode intervals to image exports
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has the soft-mask sentence added to ours, M11 row from the earlier merge.
2026-10-24 06:22:48 +00:00
aiongg c097653162 Merge PR #4329: M10: preserve table text from covered export slots
Conflicts: ROADMAP.md, docs/roadmap.md: both sides' status, progress and change-log lines; M10 row has both sentences. parity/acrobat-features.toml: export.docx evidence has both tests, notes has both statements.
2026-10-24 06:22:30 +00:00
aiongg 8b1f21f77a Merge PR #4311: M10: write DOCX cell widths from their grid columns
Conflicts: ROADMAP.md and docs/roadmap.md took both sides' status lines, progress-log and change-log rows; M10 table row from #4311, M11 row from #4310.
2026-10-24 06:21:54 +00:00
aiongg 4167ac6d5a Merge PR #4310: M11: honor page UserUnit when measuring image resolution 2026-10-24 06:21:23 +00:00
aiongg 4fedfe7e8c Merge PR #2648: fix(xfa): skip PDF fallback scripts while XFA layout is active
crates/engine/src/js.rs: kept main's DocInfo.xfa field and multi-line layout, added the PR's xfa_active gate on doc_scripts.
2026-10-24 06:21:20 +00:00
aiongg fa3a0221d9 Merge PR #3000: fix(edit): batch styled paragraph fallback glyphs 2026-10-24 06:21:12 +00:00
bmh129 0b63a73c04 M3: progress docs for repeated window resizes and moves on Wayland
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwtfrnom6Eg1Jt7xZ7VA8
2026-10-24 00:46:28 -05:00
nleva 302ad68502 M7: paste added text boxes and images in the engine and through content_copy (#2636)
Edit::PasteContent pastes an added text box or image copied in the same
document or another, as it was (a text box's style; an image's picture,
turns, flips and crop), as one undo step. A pasted image draws its picture
from the copied object while that is still the picture in the target, else
from an added image there that already holds it (an earlier paste from
another document), else the file is embedded. Document::added_image_file
gives a copied image's picture as a file and honours the copy permission.

pdfcraft_edit::added::paste_rect / paste_place place a paste where the item
was, 18 pt down and to the right past each copy of it on the page (an image
with the same box and transforms counts whichever object holds its
picture), or at a point, kept on the page.

The content_copy tool drives it headlessly, across pages and documents.

The desktop keys and menus come in follow-up PRs.

Co-Authored-By: Opus 5.5 medium ultracode <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK
2026-10-24 01:17:34 +02:00
Kyle Kassen e373dffa20 M8: redact labelled Italian international slash numbers 2026-10-23 16:37:10 -05:00
Kyle Kassen 50b5e90d63 M10: apply soft-mask Decode intervals to image exports 2026-10-23 16:37:10 -05:00
Kyle Kassen 9dc4bd598c M10: preserve table text from covered export slots 2026-10-23 16:37:10 -05:00
Kyle Kassen 30d85bbf15 M10: write DOCX cell widths from their grid columns 2026-10-23 16:37:10 -05:00
Kyle Kassen a8839bc5b1 M10: preserve page artwork when printing annotations 2026-10-23 16:37:10 -05:00
Kyle Kassen bf86689fd7 M11: measure optimizer image resolution in physical page units 2026-10-23 16:37:10 -05:00
aiongg c4a6dd6be3 Assets: don't hash translation catalogs in ATTRIBUTION.toml (#4284)
* Assets: translation catalogs aren't hashed

The 33 UI translation catalogs are project files under the project
licence, and git history records their provenance. Their ATTRIBUTION.toml
entries keep title, author, source and licence but carry no sha256, so a
PR that changes UI text no longer has to edit ATTRIBUTION.toml.

cargo xtask assets accepts a kind = "translation" entry for a .tsv under
crates/ui-egui/src/i18n/ without a hash, rejects one that has a hash, and
still requires a hash for every other entry.

* Assets: a leftover hash on a translation catalog is a note, not an error
2026-10-23 16:41:52 -04:00
aiongg 8d99a5ce1f Translations: a missing string never fails a test; xtask ci prints the gaps (#4302)
Delete the translation coverage tests and the tests of one language's
wording. Keep catalog validation (format, duplicates, placeholders, plural
forms, command ids) and the tests that use a language (registration,
plural rules, RTL order, glyphs, casing, font fallback).

cargo xtask i18n-report runs i18n::tests::translation_gaps_report, which
prints each language's missing count and first few keys and always
passes; xtask ci runs it after the tests.
2026-10-23 15:42:18 -04:00
aiongg 045c8b39a7 CI: nextest, line tables only, one wasm check (#4269)
* CI speed: line tables for workspace crates, no debug info for dependencies

* CI speed: xtask ci runs nextest and doctests, one wasm check, the CLI without default features
2026-10-23 13:34:21 -04:00
aiongg cf0f80604f Allow OFL typefaces, Noto CJK and Source Han included (AGENTS.md §1.1) (#3908)
* Allow OFL typefaces whoever designed them (AGENTS.md §1.1)

Noto CJK and Source Han from their open-source releases are allowed like any
other OFL font. The fonts build no longer skips them, the asset check accepts
an OFL font credited to Adobe, and the docs describe the Chinese faces that
result. The ban on Adobe icons, artwork, UI graphics and the fonts Adobe
products install stays.

* Leave out whole CJK faces other than the Japanese ones for size

A Chinese or Korean face is embedded whole only under 4 MiB, so the 16.4 MB
Noto Sans CJK SC in craft-fonts stays out of the binary and the embedded
total against the pinned craft-fonts is unchanged (25,204,828 bytes).

* docs: replacement boxes for Chinese without craft-fonts only on the web or without an installed Chinese face

* xtask assets: the OFL font exemption needs at least one licence ID

* fonts build: count a CJK script tag with a region (Hant-HK) as CJK, shared with the embedding test

* AGENTS.md: the asset gate exempts OFL fonts from the Adobe-author check

* fonts build: count a Hangul-only face (Hang) as CJK for the size rule

* fonts: the CJK embedding test checks the size rule, sharing its constants with build.rs

* docs: the missing Hans face is a property of the pinned craft-fonts, not of every build
2026-10-23 13:02:56 -04:00
Ahmad Mu 44e8eeb289 Cancel stale Organize page drags after document changes (#4039)
* M4: Cancel stale Organize page drags after document changes

* M4: Record Organize drag cancellation coverage
2026-10-23 10:43:05 -04:00
Piohrun ba01244987 M6: calculations read their notation only from a comment, and a regex after a condition as a regex (#3993)
* M6: calculations read their notation only from a comment, and a regex after a condition as a regex

Two findings from CodeRabbit's review of #3550, both reproduced first:

- parse_calculate and calculation_sites found BVCALC and EVCALC anywhere
  in the script, so `var s = "BVCALC a EVCALC";` read as the notation `a`:
  renaming `a` rewrote the string, and the form calculated `a` instead of
  running the script. The lexer now keeps the text of each comment, and
  the notation is read from the first comment that holds BVCALC and the
  EVCALC after it (notation_range), by both the Calculate tab and a
  rename. A line comment counts, and a comment that never ends runs to
  the end of the script, as before.
- A `/` after any `)` divided, so after the condition of `if`, `while`,
  `for` or `with` a regular expression was read as code: renaming `a`
  rewrote `if (flag) /getField("a")/.test(x)`, and in
  `if (flag) /"/.test(x)` the quote started a string that hid the
  script's real getField calls. The lexer now notes which `(` opened
  such a condition (not a method of that name, `o.if(x)`), and a `/`
  after its `)` starts a regular expression.

af::tests::notation_is_read_only_from_a_comment,
af::tests::a_regex_after_a_condition_is_not_read_as_code and
tests::a_script_that_spells_the_markers_in_a_string_runs_as_a_script
fail without the fix. Parity, ROADMAP.md and docs/roadmap.md updated.

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

* M6: form.props-calculate notes that a regex right after a brace is still read as code

From CodeRabbit's review of this PR. The lexer still divides after `}`,
which ends a block as often as an object literal, so in
`if (x) { } /"/.test(s); event.value = getField("a").value;` the quote
starts a string and the getField is hidden from a rename and from the
calculation order. Checked with a scratch test before writing it down.

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

* M6: a line comment ends at any line terminator, so code after `\r` is still read

From CodeRabbit's review of this PR. A `//` comment ran to the next `\n`,
so with `\r` line endings it hid the rest of the script: a rename missed
the getField calls after it (since #3550), and a string after it that
spells BVCALC ... EVCALC read as the notation. A `/` taken for the start
of a regular expression likewise looked past `\r` for its end, and could
swallow the next line's code.

Both now stop at any of JavaScript's line terminators (`\n`, `\r`,
U+2028, U+2029; line_ends_at), and U+2028 and U+2029 between tokens are
skipped rather than read as the start of a word.

af::tests::a_line_comment_ends_at_any_line_terminator fails without each
of the three changes: with `\r`, the string read as notation `a`; with
the comment fixed but not the regex scan, `i++ / 2;\r` swallowed the
getField after it; without skipping U+2028, the getField after it was
not a call.

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

* M6: a word, number or regex's flags end at U+2028 and U+2029

From CodeRabbit's review of this PR. The scans for an identifier, a
number and a regular expression's flags took every byte past ASCII as
part of a word, U+2028 and U+2029 included, so in `x\u{2028}getField("a")`
the getField was part of one word and not a call: a rename missed it, and
the calculation order didn't read it.

`word` now takes a position and is false where line_ends_at is true, so
the three scans share one rule. The arm that skips U+2028 and U+2029
between tokens stays: without it, the punctuation arm would step one byte
into the character.

af::tests::a_token_ends_at_a_line_separator fails without the fix, for a
word, a number and a regex's flags alike; it and
a_line_comment_ends_at_any_line_terminator fail without the skipping arm.

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

* M6: the `while` that ends a `do` loop counts as a condition, said and tested

A `/` after the `)` of a do-while starts the next statement, so it starts a
regular expression there too. The comment on BEFORE_CONDITION says so, and
a_regex_after_a_condition_is_not_read_as_code covers it (from review).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 10:42:48 -04:00
skylar taylor-barrick adea2c908a M14: recover existing macOS PDF associations after open and save (#3558)
* M14: recover existing macOS PDF associations

* M14: address macOS association recovery review

* M14: reuse upstream save metadata preservation
2026-10-22 15:49:13 -06:00
PerchAnton c5279baf35 M6 PR #2648: retain active-XFA script isolation with current main 2026-10-22 20:21:39 +03:00
PerchAnton d323e86706 M7 PR #3000: retain batched styled fallbacks with current main 2026-10-22 20:11:21 +03:00
Janiz-zzz 667bc697b2 i18n(fi): translate the 64 labels left in English after #2210
Replaces the English placeholders added during the #2210 integration
(calculation order, text formatting, print, thumbnails, layers, comment
reshape and signature checks, plus the two signature wordings from
3e180944) with Finnish, and updates the fi.tsv sha256 and the entry
count in docs/localization.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:38:44 +03:00
Oliver Hagmann 9a76f90f07 M5: the arrow keys move the selected comment
With a comment selected, the arrow keys scrolled the view (↑ ↓) or turned the page (← →), so
a text box, rectangle or line could only be placed with the mouse. Now an arrow key moves the
selected comment a screen pixel, and ⌘/Ctrl or Shift and an arrow ten, as Edit text nudges
objects; up is up on screen whatever the zoom, the page's rotation and the view's. Each press
is one undo step (Edit::MoveAnnotation). The keys are taken before the view's navigation,
which has them back once nothing is selected. Text markup stays with its text, and a locked
comment where it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 18:33:32 +02:00
aiongg 9526363152 Build each crate's integration tests as one binary (#4087)
* Build each crate's integration tests as one binary

Every tests/*.rs file is its own executable that links the whole crate
graph. Move each crate's test files into tests/it/ as modules of one
tests/it/main.rs, for apps/pdfcraft, apps/pdfcraft-cli, automation, cos,
crypt, engine, render, sign and ui-egui. ui-egui's ui_group binary and
the support modules move into tests/it/ too. Crates with a single test
file keep it. The test executables go from 150 to 52, and every crate
runs the same number of tests as before.

ui-egui perf keeps its own binary (own process): its frame-time budgets
would charge each measurement for the other tests running beside it.

What the move needs:
- include_str!/include_bytes! and #[path] paths gain one "../".
- ui-egui: one GPU lock in tests/it/main.rs replaces the five per-file
  ones (content, document_links, editing, fill_sign, view), so one
  process creates one wgpu device at a time. The drop-line test in ui.rs
  and the screenshot blocks behind PDFCRAFT_*_SHOT(S) and
  CRAFT_UI_DOCKING_DIR, which render without holding it, take it too.
- pdfcraft winit_wayland_scroll: the vendored winit source it compiles
  names crate::event, so that shim lives in main.rs.
- parity evidence names the new paths.

* Name the one-binary test paths in docs and comments

- READMEs, docs, ATTRIBUTION and source comments name tests/it/<file>.rs.
- The oracle README's example filters with --test it encryption::qpdf_agrees.
- pdfcraft's GPU lock comment points at the UI tests' shared lock in
  crates/ui-egui/tests/it/main.rs.
2026-10-22 11:28:58 -04:00
jonathanwxh-cell 17f84e4391 Merge main into feat/optimize-compatibility-version
Retain compatibility regression coverage with upstream content-boundary and image-size tests.
2026-10-22 21:34:02 +08:00
aiongg 472717fcd7 Merge upstream main 88dad98dd into next
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	crates/engine/src/dates.rs
#	crates/ui-egui/src/i18n/ar.tsv
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/el.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/kk.tsv
#	crates/ui-egui/src/i18n/lt.tsv
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/te.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/tests/fonts.rs
#	docs/localization-parity.md
#	docs/localization.md
#	docs/roadmap.md
#	parity/acrobat-features.toml
2026-10-22 05:22:09 +00:00
aiongg fb05fda269 Integration: ROADMAP.md, docs/roadmap.md, docs/gaps.md merged line by line
The track merges used a union driver for these files, which kept two
versions of a milestone row or status line wherever two tracks changed the
same one. They are recomputed as base plus each track's changes, with two
edits of one line combined into one line. The status lines that #2805, #3808
and #3532 put above the Stage line join the status list below it, newest
first.
2026-10-22 02:30:24 +00:00
aiongg ad04768906 Merge r2 track t17: choice-pass takes
# Conflicts:
#	ATTRIBUTION.md
#	ATTRIBUTION.toml
#	apps/pdfcraft/src/main.rs
#	crates/engine/src/dates.rs
#	crates/fonts/src/lib.rs
#	crates/ui-egui/src/canvas.rs
#	crates/ui-egui/src/i18n/mod.rs
#	crates/ui-egui/src/system_fonts.rs
#	crates/ui-egui/src/theme.rs
#	crates/ui-egui/tests/content.rs
#	crates/ui-egui/tests/fonts.rs
2026-10-22 02:25:34 +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 c00a8b7616 Merge r2 track t14: signatures
# Conflicts:
#	ATTRIBUTION.toml
#	crates/annot/src/tests.rs
2026-10-22 02:22:02 +00:00
aiongg 3cf35888d7 Merge r2 track t12: i18n, i18n: new language catalogues
# Conflicts:
#	ATTRIBUTION.toml
2026-10-22 02:21:42 +00:00
aiongg 15f558aaa8 Merge r2 track t11: redaction, mixed: combine, compare, crate:edit, measure, ocr
# Conflicts:
#	ATTRIBUTION.toml
#	crates/automation/src/tools.rs
#	crates/edit/src/lib.rs
#	crates/ui-egui/tests/compare.rs
#	crates/ui-egui/tests/ui_group/marks.rs
2026-10-22 02:21:38 +00:00
aiongg 123b214180 Merge r2 track t10: xfa 2026-10-22 02:21:06 +00:00
aiongg 2363b0619b Merge r2 track t09: print
# Conflicts:
#	ATTRIBUTION.toml
#	crates/ui-egui/src/lib.rs
2026-10-22 02:21:06 +00:00
aiongg 69e755c439 Merge r2 track t08: fonts
# Conflicts:
#	crates/automation/src/tools.rs
#	vendor/README.md
2026-10-22 02:20:50 +00:00
aiongg 4d5025c6da Merge r2 track t07: text-extract-select
# Conflicts:
#	crates/edit/src/tests.rs
#	crates/render/src/raster.rs
#	vendor/README.md
2026-10-22 02:20:03 +00:00