Deleting a field took it off its pages and out of the field tree but left
its entry in the form's /CO array. That reference kept the field
dictionary, its value and its widgets reachable, so a full save after
applying redactions over the field still wrote the redacted value.
* M6: Prepare a form ▸ Text field table: rows and columns of text fields as one edit
Drag an area with the Text field table tool and set it up in a movable
window: rows, columns, row and column spacing, the first row number,
column widths (draggable dividers, arrow keys once focused) and a name
pattern per column, with the table previewed on the page. Add fields adds
every cell as one undoable edit ("Add text field table").
- forms: `table` lays the table out in view space and names its cells. It
refuses the whole table for a cell under MIN_FIELD_SIDE (now one
constant) or a name with a period, used twice or already taken, and
free_base picks the first free TableK for default names.
- engine: `field_table::edit` maps the cells onto the page, /Rotate and
/UserUnit included, and checks their size again there.
- automation: `form_add_field_table`, with an end-to-end test.
- ui-egui: the window belongs to one page of one document and closes when
that stops being the one prepared; Enter commits a number instead of
adding the table; dividers between many columns no longer panic in
f64::clamp. Strings in all 22 catalogs, and the Spanish engine-wording
tests cover the table's mistakes.
- parity: form.multiple-copies is partial; copying an existing field into
a grid is still missing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: text field table: refuse corners past the page's number range, run it by its command
Review follow-up on #3479.
- engine: a corner finite as an f64 but past f32's range came back from
the page's f32 mapping infinite. With one such corner a side was
infinite rather than NaN, so the cell passed and add_field refused it
only once the edit was applied ("the field is too small"). The edit now
refuses any corner that isn't finite on the page with the table's own
reason, before anything is added.
- automation: form.add.text_table runs form_add_field_table through
command_run, as comment.stamp runs stamp_custom.
- ui-egui: doc comments for the window's three remaining helpers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6: text field table: close on document edits, lay out once per change, keep Escape for fields
Review follow-up on #3479 (benandez).
- The window belongs to the document as it was when it opened: it closes
when the edit generation moves (a page rotated, moved or deleted, an edit
undone), so a table set up for one page never lands on another or off it.
- The table is laid out and checked once per change, not several times a
frame: the window and the page preview share one `Laid` (cells and the
edit), and the preview draws only the cells in view, naming only those
with room for a name.
- Escape in a field of the window leaves the field and keeps the setup; a
second Escape closes the window.
- A table placed with a click is moved in from the page's edges, so all of
it is on the page.
- `free_base` takes one pass over the names; `{seq}` is documented as
counting from the first row number; `form_add_field_table` takes its
names from the batch instead of laying the table out again; the
unreachable `Dialog::FieldTable` arm says why it is there.
Each new test fails with its fix reverted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: text field table: drawn with its panel closed, the window opens Prepare a form instead of closing
With the tool picked and the Prepare a form panel closed, the form is
still prepared and a table can be drawn. Drawing sends the tool back to
Select, though, so nothing was prepared on the next frame, and the table
window, which lasts only while the form is prepared, closed itself at
once: the table couldn't be set up. Opening the window now opens the
Prepare a form panel it belongs to. Closing the panel while the window
is open still closes the window.
Found by CodeRabbit, which saw the preview stop drawing; the window
closing first was the larger effect. prepare.rs
a_table_drawn_with_the_panel_closed_opens_it_again fails without the fix
("the window stays open": (None, false)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* Drop the second generation binding the main merge left in document_area
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: calculation order: read, check, fix and set the order fields calculate in
A form's calculations run in its calculation order (/CO), which a field
joins when it gets a calculation, so a chain defined in the wrong order
shows stale values. calculation_order reads the order against the fields,
with what each formula reads (field lists, notation names, literal getField
names) and what is wrong: a field reading one that calculates later, a loop
(strongly connected components, found without recursion), a field /CO
leaves out, and /CO entries that calculate nothing. Fix order is a stable
topological sort that moves only what must move and leaves loops alone.
set_calculation_order writes /CO afresh; a name two fields share (a
malformed form) stands for the next of them in the current order.
Edit::SetCalculationOrder sets it in one undo step and recalculates, and
form_calculation_order reads, sets, moves one field or fixes the order
headlessly. An edited formula now keeps its field's place in /CO, a field
/CO lists twice calculates once, and recalculation runs each field by
object, so two fields sharing a name both calculate. The /CO code of the
Calculate tab, field scripts and recalculation goes through one place.
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>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
* M6: renaming a field renames it in the calculations that read it
A rename silently broke every formula that used the field: a sum dropped
it, notation read it as 0 and a script kept a stale value. set_props now
rewrites the name in each field's Calculate script, in the same edit:
AFSimple_Calculate field lists, simplified field notation (the BVCALC
comment and its translation) and getField calls naming it in one string
literal. The rest of each script is kept as written; names built at run
time, non-UTF-8 script streams and a name another field still has are
left alone, and a self-check keeps a script whose meaning would change.
Notation is read by one lexer shared by notation_js, eval_notation and
the rename. form_set_props returns formulas_updated; Field Properties
says how many calculations now use the new name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BfMGHQNgdq5yf5tKZHGR3z
* M6: form_set_props leaves the renamed field out of formulas_updated by object
The renamed field was matched by the name given, but set_props stores it trimmed and under
its parent's prefix. When the same call changed the field's own formula, formulas_updated
listed the field as one the rename had rewritten.
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>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Button appearances always drew the caption in regular Helvetica, sized
from the button's height, whatever the field's /DA asked for: DA Form
31's 'Attachments Menu' (9 pt bold) came out at 4 pt. The caption now
takes the /DA's font from the form's /DR, its size (an automatic size
still fits the button) and its color, as text fields already do.
Renaming a field changes only its own /T, so MASTER.a renamed to b
becomes MASTER.b. set_props compared the bare new partial name with the
other fields' fully qualified names: renaming MASTER.a to b succeeded
while MASTER.b existed, leaving two fields with one fully qualified
name, which are one field with a shared value (ISO 32000-2 §12.7.4.2);
and renaming MASTER.a to c was refused because of an unrelated
top-level field c.
The check now compares the new full name, leaving the field itself out
so a kid keeping its own partial name still succeeds, and the error
names that full name. Top-level renames are checked as before.
* M6: make list boxes directly selectable and scroll to selection (#1524)
- In-place selection: List boxes are rendered directly on the page and
selectable in place. Clicking an option row selects it immediately;
Ctrl/Cmd-clicking toggles items in multi-select lists, and Shift-clicking
selects ranges, without opening a popup menu.
- Appearance and scroll (/TI): When options are selected, /TI (top index)
is automatically adjusted so the selected items are scrolled into view
within the widget's visible rows, and option rows and selection highlights
are drawn into the appearance stream.
- Unit and UI tests: Added tests in pdfcraft-forms verifying /TI scrolling
and appearance generation for multi-select lists, and UI tests in
pdfcraft-ui-egui covering in-place click and Ctrl-click toggling.
Also ensured test harness in reload.rs explicitly sets language to en.
Fixes#1524
* List boxes: keep the engine's page appearance, consume arrow keys
Drop the overpaint of every fillable list box with an egui mock (it ignored the
PDF's fonts, colours and /MK /R; the engine redraws the appearance with the new
/TI), and consume ArrowUp/ArrowDown in the list overlay so they don't also
scroll the page.
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>
add_field gave text and date fields a fixed /Helv 12 Tf, which is clipped in
a box shorter than 12 pt, and a script can't see the clipping. They now get
/Helv 0 Tf (Auto), as Acrobat's new text fields do: 12 pt where there is
room, smaller where there isn't. An explicit size set afterwards still wins;
choice fields keep 12 pt.
Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
set_props with a complete look read the widget's /MK without resolving
it, so an indirect one started from an empty dictionary: a check box
lost its Cross mark, its 90-degree rotation and any unknown entry, and
the loss survived a save. The /MK is now resolved first, as the
check_style branch already does; a direct /MK behaves as before.
Adds tests::a_whole_look_keeps_the_rest_of_an_indirect_mk, which fails
before this change.
* M14: text the fonts can't draw is kept and reported, never written as ? silently
Form values and captions, text boxes, callouts, Fill & Sign text, measurement labels and stamp by-lines keep text the standard fonts can't draw, draw ? for it, mark the annotation (PCUndrawn) and report the edit (interface notice, tools' warnings); forms set /NeedAppearances (not in PDF/A). Flattening, redaction overlay text, typed and visible signatures and create-from-text refuse instead. Reports escape such characters as [U+XXXX].
* Catalogs: the two new can't-draw messages in Czech, Slovene, Kazakh, Turkish and Greek; refresh hashes
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6: bulk-edit shared form field properties (#95)
Add opt-in shared properties, partial appearance patches and atomic CLI/MCP
batches. Complete current-page, rectangle and Fields-panel selection, with
20 synthetic regressions for preservation, rollback and dialog lifecycle.
Co-Authored-By: Codex <noreply@openai.com>
* Bulk field properties: translated messages, a catalog check, and in-place /DA patches
- The bulk dialog's flag label, errors and notices are in all 13 full catalogs; the missing-field error is a template (fmt + tl!) instead of format!. New test bulk_field_properties_labels_are_in_every_full_catalog.
- patch_da rewrites the last Tf and fill colour in place (appending only when there is none), so /DA doesn't grow per edit; sizes clamp to 300 like parse_da.
- A size- or font-only edit leaves widgets untouched; an indirect /MK or /BS is updated where it lives instead of inlined into each widget.
- Draft::new says why: a widgetless field gets its own message, not 'could not be found'.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: pin the session clock in the bulk-fields harness (save_bytes stamps /ModDate)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Tevinhead <173904641+Tevinhead@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>
* D5: README for cos and engine, and an honest cos status line
Thirteen crates have no README, although the session protocol tells every session to read the README of the crate it is touching. These are the two biggest: cos is where every hostile file lands first, and engine is the whole API surface a frontend gets.
Each states its layer and the rules that bind it, sketches the API against the real signatures, and ends with what is deliberately not done. The cos README spells out the clone-is-a-snapshot property and why both writers return bytes rather than taking a sink; the engine README spells out the four-step editing model that makes what you see what Save writes.
Also corrects the module doc on cos/src/lib.rs, which still said encryption and object-stream output for full saves were not done. Both shipped: security.rs handles R2-R6 with crypt filters and /EFF, and SaveOptions::object_streams defaults to true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Preserve alternate appearances and indirect push-button values
Keep rollover, down and custom appearance entries when normal appearances are regenerated, without modifying shared dictionaries. Resolve indirect button caption and layout values. Cover full and incremental saved output, and wait for raster readiness in the existing Linux scroll test.
Co-authored-by: Codex <codex@openai.com>
* M14: let neutral Windows locales fall through to display language
Co-authored-by: Codex <codex@openai.com>
* pdfcraft-cli --help lists `mcp` only when the build has it
Follow-up to #540: a build without the `mcp` feature (`--no-default-features`) still listed
`pdfcraft-cli mcp` in its usage, and `pdfcraft-cli mcp` then said "unknown command".
USAGE is now put together with `concat!`, and its `mcp` lines come from a macro that is empty
without the feature. In such a build `pdfcraft-cli mcp` says the MCP server was left out instead
of calling it an unknown command.
- tests/help.rs: usage_lists_mcp_only_when_built_with_it (runs in both feature sets) and
mcp_without_the_feature_says_it_was_left_out (without the feature)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* engine README: the execution plan is local-only, so don't link a path that doesn't exist
---------
Co-authored-by: Tanish Manku <tmanku.dev@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
Co-authored-by: Codex <codex@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* M6: rotate form field widgets with /MK /R
Filling or restyling a rotated field redrew its text upright. Appearances now follow /MK /R, and Field Properties and form_set_props can set the rotation.
* Forms: a rotation refused as too small is checked before any other property is written; no indexing by widget number
* hu, uk: Rotation:
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Japanese forms often name a check box's on state in Shift-JIS (「はい」 is
/#82#CD#82#A2). State names were read with from_utf8_lossy, so ticking such a
box wrote a different name to /AS and /V, no appearance matched, and the box
was drawn with no mark (or not at all).
Names now map to text losslessly (forms::name_text / name_bytes: UTF-8 as
itself, other bytes and `#` as #XX) wherever states and values are read and
written: /V, /DV and /AS, PdfCraft's own state appearances, and FDF import and
export, so such values also survive an FDF or XFDF round trip.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* Forms: push buttons run their show/hide-a-field action
A push button whose Mouse Up action is a Hide action (ISO 32000-2
§12.6.4.11) did nothing when clicked: the action wasn't read, so the
button counted as having no action at all.
Buttons now read it: /T as a field name, a reference to a field or one
of its widgets (named by its fully qualified name, following /Parent
with a cycle guard), or an array of these; /H false shows instead of
hiding. Clicking hides or shows the named fields and their kids through
the same edit that a script setting field.display uses, so it is one
undo step.
A hidden widget (Hidden or NoView flag) no longer takes clicks, tab
stops, hover frames or field highlights. That also covers fields that a
script hides.
Tests: forms push_buttons_read_hide_actions (names, references, arrays,
/H, a /Parent cycle) and ui-egui hide_actions_hide_and_show_fields.
parity: edit.action-show-hide-field is shipped.
* Forms: push buttons with an action show the pointing hand
Hovering a push button showed the "not allowed" cursor, although
clicking it runs its action. Buttons that aren't read-only and have an
action now show the pointing hand; read-only buttons and buttons with
no action keep the "not allowed" cursor.
Test: hide_actions_hide_and_show_fields checks the cursor (it fails
without this change).
* Clippy: needless borrow in the hide-action test
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>
The app, crates, binaries, bundle id, packaging, icons, screenshots and docs are now
PdfCraft / pdfcraft (repo storytold/pdfcraft). On first launch the desktop app moves the
PrintCraft settings and crash-recovery folders to the new name, and both apps read settings
saved under the old storage key, so upgrades keep recent files, preferences and unsaved work.
The signing test fixtures keep their PrintCraft subject names (they are binary).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Field Properties > Options > Check Box Style (Button Style for radio
buttons): Check, Circle, Cross, Diamond, Square or Star, as Acrobat
offers. The choice is stored in each widget's /MK /CA with Acrobat's
ZapfDingbats codes (4 l 8 u n H), so other viewers draw the same mark,
and PrintCraft draws it as paths, needing no symbol font. Styles set
by other apps are read back too; the defaults stay a check for check
boxes and a circle for radio buttons.
- forms: CheckStyle, check_style(), FieldProps::check_style; set_props
writes /MK /CA (resolving an indirect /MK so its colours survive) and
redraws; text and choice fields refuse a style.
- engine: Document::field_check_style.
- automation: form_set_props takes check_style, form_fields reports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two crashes that a PDF can trigger, each with a regression test that failed before this change:
- Deeply nested arguments in a field's format script (`[[[[…` or `new Array(new Array(…`) and
deeply nested simplified field notation (`((((…` or `----…1`) recursed until the stack
overflowed, which aborts the whole app. Nesting is now capped at 64 levels; deeper input is
rejected like any other unparseable script.
- A date with a huge year (`1/1/2147483647`) overflowed `weekday`, and a `DateTime` with month 0
indexed before the month names. The weekday math is now done in i64, years beyond i32 don't
parse, and month and day names are looked up with `get`.
Also removes the crate's remaining `expect`/`unreachable!` calls and denies the panic lints.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>