* 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>
* 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.
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).
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/.
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.
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.
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.
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.
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.
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
* 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
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.
* 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
* 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
* 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>
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>
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>
* 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.
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.