* Forms: keep tiles and render unbaked field values during page re-render
In complex interactive forms, switching focus away from an edited field
caused its text to disappear for several seconds while background tiles
re-rendered.
- In DocView, retain existing tiles as stale rather than immediately
purging them on page_changed so the canvas remains sharp without flashing.
- Prioritize visible tiles over backdrops in the render queue.
- Track unbaked form fields and paint an immediate egui widget preview
for committed text, checkbox, and radio inputs until fresh raster
tiles arrive.
- Scope page invalidation on form edits to pages containing the edited
or calculated fields.
- Hoist fields lookup in recalculate_with and apply_changes to eliminate
repeated parsing.
- Run JavaScript scripts via catch_unwind on the current thread instead of
spawning a new thread per script invocation.
- Add contributor entry for Piohrun in contributors/people.toml.
- Add test switching_fields_keeps_committed_text_visible_immediately.
* Forms: address review feedback on tile stale tracking, hoist, and diff invalidation
- Revert crates/js to preserve dedicated 1 GiB stack thread for boa scripts.
- In DocView::page_changed, purge non-current-tag tiles and mark only current-tag tiles stale.
- Restore demand queue order in prepare_render_queue.
- Add unbaked overlay entries only on successful edit apply and only for fields whose values changed; fix font zoom calculation on 90°/270° pages.
- Re-read fields(doc) in recalculate_with only when script returns changes.
- Diff form before and after edit in apply_edit, invalidating pages with changed fields, falling back to document_changed for XFA or page-count changes.
- Add tests covering script recalculation side effects across pages, unbaked overlay cleanup on tile delivery/zoom/rejection, and checkbox recalc chaining.
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* fix(forms): encode appearances with the chosen font
* docs: record form font encoding limits
* docs: log verified form encoding regressions
---------
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>
A PDF whose AcroForm /Fields array is empty or missing listed no
fields, so none were fillable even though every widget sat on a page
with its name and value: Acrobat and the browsers fill those. The page
widgets are now adopted as fields — after the tree-listed ones, through
each widget's topmost unlisted /Parent so a field split across several
widgets stays one — and the leniency is recorded, never silent.
- forms: fields() walks /Fields as before, then adopts the unlisted
page widgets; adopted_page_fields() counts them for the note.
- render: the inspector's field list adopts them the same way, so the
Fields panel, the field-count notice and menu gating follow.
- engine: opening such a file notes "the form's /Fields list omits N
fields; they were read from the page annotations" in the repair log
(Document Properties ▸ Advanced).
Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Four squash merges in a row (#450, #414, #451, #351) each re-added the
same 206-line block from #417 to crates/forms/src/tests.rs, so the crate's
tests no longer compiled (E0428: three test functions defined five times)
and main went red on every platform. Keep the first copy. The same merges
repeated one ROADMAP log line four times; keep one.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures
The xfa crate lays out the field kinds that were left blank or shown as
text fields:
- Choice lists become combo boxes (editable with textEntry) or list
boxes (open="always"; multi-select with open="multiSelect"), with the
shown items and the saved values of <items save="1"> as /Opt pairs,
/V, /I and /DV as saved values, several on separate lines in the
data. Field::saved_value maps shown text to saved values for the
layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
fields the app can sign.
- Image fields show the picture the data (base64) or the template
holds; barcode fields show their value as text, with a warning that
no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
XObjects with a soft mask for transparency; JPEGs stay as they were.
Sizes come from the headers, so layout never decodes; decoding is
capped at 40 million pixels and failures are warnings, not errors.
Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).
Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.
* 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>
* D2, D3: fuzz every tool's arguments, and prove root confinement
AGENTS.md §3 and §4 treat tool arguments as untrusted, but nothing fed hostile arguments to the tool table: xtask fuzz mutates PDFs only. And tools::check_args validates shape alone (object, no unknown keys, required present), not types or ranges, so a string where an integer belongs or i64::MIN where a page number belongs reaches the handler.
Two tests. The first calls every tool with a zoo of wrong types, numeric extremes, empty and 100,000-character strings, NUL and right-to-left-override names, mistyped and deeply nested containers: 19,299 calls over 132 tools, each required to return Ok or a readable ToolError rather than panic. The second is the D3 audit mechanized: every path argument is tried with .. traversal, absolute, drive and UNC paths, and a planted symbolic link, and must be refused; 451 attempts, all refused.
Reading the code says every handler resolves its own paths and that no tool dispatches another internally, so the PhotoCraft hole does not exist here. A test keeps that true as tools are added; a written finding would not.
Both were proved to fail before passing. An out-of-bounds index in page_rotate is reported as: page_rotate panicked on {"degrees":-1,"doc":1}. Disabling resolve() is reported as 82 leaks naming the tool and path, including doc_save writing through ../ and through the symbolic link.
The first draft of the confinement test did not catch a fully disabled resolve(): almost every tool failed on a non-existent document id before it ever looked at a path, and a file-listing diff cannot see a read. It now opens a real document first so handlers reach their path arguments, and asserts each call is refused rather than only diffing the filesystem.
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>
* M13: common MCP tools through the confined automation path
Preserve task tools and compact mode, add complete hints and strict argument-key errors, and catch escaped tool panics.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* M13: session resources and version-aware MCP result hints
Keep per-document resources and legacy response shapes; add document/command resources and private cache hints for modern clients.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@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
* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject
The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge land/414 into land/451; keep main's MCP protocol versions, cap command_batch at 1000 steps
The 2026-07-28 revision and its resultType/ttlMs/cacheScope fields are dropped:
main doesn't advertise them and they couldn't be checked against the spec.
command_batch refuses more than 1000 steps. docs/mcp.md says that unknown
argument keys are now refused with -32602 (a behaviour change for clients that
sent extra keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge land/451 into land/351; escape targets stay in the test's temp dir, and out/image paths are tried
Absolute escape targets are now paths under the test's own temp base instead of
/etc/passwd, C:\Windows\… and a UNC share, so a confinement bug can only touch
files the test made. String 'out' and 'image' properties (and any described as a
file path) are tried too. The backslash traversal runs only on Windows: elsewhere
it is an ordinary file name inside the root.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Yuvraj Singh <singh.yuvraj31@icloud.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: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures
The xfa crate lays out the field kinds that were left blank or shown as
text fields:
- Choice lists become combo boxes (editable with textEntry) or list
boxes (open="always"; multi-select with open="multiSelect"), with the
shown items and the saved values of <items save="1"> as /Opt pairs,
/V, /I and /DV as saved values, several on separate lines in the
data. Field::saved_value maps shown text to saved values for the
layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
fields the app can sign.
- Image fields show the picture the data (base64) or the template
holds; barcode fields show their value as text, with a warning that
no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
XObjects with a soft mask for transparency; JPEGs stay as they were.
Sizes come from the headers, so layout never decodes; decoding is
capped at 40 million pixels and failures are warnings, not errors.
Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).
Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.
* 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>
* M13: common MCP tools through the confined automation path
Preserve task tools and compact mode, add complete hints and strict argument-key errors, and catch escaped tool panics.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* M13: session resources and version-aware MCP result hints
Keep per-document resources and legacy response shapes; add document/command resources and private cache hints for modern clients.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@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
* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject
The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge land/414 into land/451; keep main's MCP protocol versions, cap command_batch at 1000 steps
The 2026-07-28 revision and its resultType/ttlMs/cacheScope fields are dropped:
main doesn't advertise them and they couldn't be checked against the spec.
command_batch refuses more than 1000 steps. docs/mcp.md says that unknown
argument keys are now refused with -32602 (a behaviour change for clients that
sent extra keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Yuvraj Singh <singh.yuvraj31@icloud.com>
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: storybored379 <storybored379@users.noreply.github.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
* XFA: choice lists, signature, password, image and barcode fields; PNG and GIF pictures
The xfa crate lays out the field kinds that were left blank or shown as
text fields:
- Choice lists become combo boxes (editable with textEntry) or list
boxes (open="always"; multi-select with open="multiSelect"), with the
shown items and the saved values of <items save="1"> as /Opt pairs,
/V, /I and /DV as saved values, several on separate lines in the
data. Field::saved_value maps shown text to saved values for the
layout and the script tree alike, so scripts read what is saved.
- Password fields are password text fields; signature fields are /Sig
fields the app can sign.
- Image fields show the picture the data (base64) or the template
holds; barcode fields show their value as text, with a warning that
no bars are drawn.
- PNG and GIF pictures (fields and draws) are decoded to Flate image
XObjects with a soft mask for transparency; JPEGs stay as they were.
Sizes come from the headers, so layout never decodes; decoding is
capped at 40 million pixels and failures are warnings, not errors.
Engine: multi-select list values from scripts and data are one per
line. README and parity notes updated (form.xfa-dynamic).
Hardened after an adversarial review pass: picture sizes refused from
the header past 20 million pixels or 65,535 a side, the GIF decoder
given limits (a frame far larger than its screen is refused before
allocation), template pictures read whole up to 32 MB, pictures
decoded once and remembered in the catalog by content hash so
re-layouts reuse them, data pictures that aren't images reported by
field; choice values matched against saved values first, several
values deduplicated and capped, /I ascending and unique, /DV through
the same mapping, <value> children read, the item cap reported; the
reopen path takes another viewer's choice values (several per line);
CMYK JPEGs inverted only with Adobe APP14.
* 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>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@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
* Merge land/450 into land/414; XFA pictures are found again by a SHA-256 on their XObject
The image cache was a /PCXfaImages map in the catalog, keyed by DefaultHasher
(not stable across Rust releases) and read back from the file. Each image
XObject now records the SHA-256 of its picture's bytes, and a re-layout finds
the XObjects on the pages it replaces by that hash; nothing goes in the catalog.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
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: Flowershangfromthebranches <152056395+Flowershangfromthebranches@users.noreply.github.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.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>
* M7: select and edit grouped Form XObject figures
Co-Authored-By: Codex <noreply@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: Codex <noreply@openai.com>
Co-authored-by: Streetblock <github@streetblock.de>
Co-authored-by: lloyd <lloyd@stackworx.io>
Co-authored-by: Brandon Thomas <echelon@gmail.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>
#516 added pdfcraft-render as a dev-dependency of pdfcraft-forms for one
rendering test. Both crates are layer 3, so `cargo xtask layers` (run in
CI) rejects the sideways edge and main went red. The test now lives in
crates/engine/tests/form_rotation.rs, where both crates are already
dependencies, and forms drops the dev-dependency. The parity evidence
points at the new location.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
* M6.2: form fields in Unicode CID fonts show Japanese instead of "?"
Japanese forms often give their fields a /DA font that /DR defines as a Type0
font with a predefined Unicode CMap, such as a non-embedded HeiseiMin-W3 with
/UniJIS-UCS2-H. Field appearances accepted only simple fonts, so they fell back
to Helvetica and WinAnsi, and every Japanese character was drawn as "?".
Appearances now use such fonts and encode the text in the CMap's form (UCS-2,
UTF-16, UTF-8 or UTF-32), measuring full-width characters as one em so centring,
auto size, comb cells and multiline wrapping fit. Identity-H and embedded CMaps
still fall back to Helvetica.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M6.2: vertical Unicode CMaps fall back to Helvetica, since field appearances are horizontal
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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>
pdf-lib and other writers give radio groups and check boxes an /Opt array
(PDF 1.4, one export value per widget) and name the on states by position
(/0, /1 ...). Filling such a field by its export value ("Pick-up") failed with
"has no option", and form_fields listed the options as 0, 1.
- Field::export_of / state_for / export_for_state map between a widget, its
/Opt export value and its on-state name; fields without /Opt are unchanged
- set_value accepts an export value for radio groups and check boxes; the
"has no option" error lists the export values
- form_fields reports a radio group's options and selected value as export
values
- Test: opt_export_values_select_button_states
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Layers: run set-layer-visibility actions, keeping radio-button groups
The engine can apply a set-layer-visibility action (SetOCGState,
ISO 32000-2 §12.6.4.13) to a document's layers: each ON, OFF or Toggle
in order, naming the layer by its optional content group, so a toggle
sees the state the changes before it left. With /PreserveRB (the
default), a layer turned on turns off the other layers of its
radio-button groups, now read from /OCProperties /D /RBGroups; turning
one off leaves them alone. The renderer is rebuilt once per action, not
once per layer.
Like the Layers panel, this changes the view, not the document.
Tests: engine set_layer_actions_toggle_in_order_and_keep_radio_groups
and set_layer_actions_change_what_renders (pixels before and after),
render radio_button_layer_groups_are_read.
* Links: run set-layer-visibility actions
Clicking a link whose action is SetOCGState showed "SetOCGState actions
run in the JavaScript engine (M6)" and did nothing. Links now read the
action (/State, also when indirect, and /PreserveRB) and clicking runs
it; the Layers panel follows. Hovering shows "Set layer visibility"
(translated in the ja, es, zh-hans and zh-hant catalogs), and doc_info
describes the action in its link list.
Tests: render links_read_set_layer_actions, automation
doc_info_describes_set_layer_links and ui-egui
links_set_layer_visibility.
* Forms: push buttons run set-layer-visibility actions
A push button whose mouse-up action is SetOCGState counted as having no
action, so clicking it did nothing. Buttons now read the action as
links do, and clicking runs it.
Tests: forms push_buttons_read_set_layer_actions and ui-egui
push_buttons_set_layer_visibility.
parity: edit.action-set-ocg-state is partial. The notes say what is
missing: creating the action, bookmarks and other triggers, /Next
chains, JavaScript ocg.state, and the Layers panel keeping radio-button
groups itself.
* French: Set layer visibility
The French catalog is coverage-checked since #291.
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>
* 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>
* Comments: a text box's rectangle follows its edited text
Editing a text box (or typewriter or callout) rewrote /Contents but
kept the old /Rect: the appearance clipped the new text to the old box
and silently dropped lines that no longer fit, while shorter text left
an oversized box. set_contents now re-measures the text with the same
Helvetica widths as at creation and re-fits the box: the wrap width and
top edge stay, the height fits the wrapped lines. A callout's leader
line keeps its place and re-attaches to the moved box (that geometry
moved from set_rect into a shared apply_text_box). A locked box keeps
its text editable, so it re-fits too.
Covered by annot tests (grow and shrink with nothing clipped, callout
leader re-attach, locked box) and a comment_edit round trip through the
automation tools; parity entries cite them.
Co-Authored-By: GLM <noreply@z.ai>
* Polish edit text and images tool
* feat(edit-text): select source font style
* fix(edit-text): preserve detected PDF font styles
* feat(edit-text): support Japanese replacements
* fix(forms): escape non-ASCII format script strings
* Forms: unwrap_or instead of map_or with identity (clippy map_or_identity)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: GLM <noreply@z.ai>
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>