M6: Acrobat JavaScript: sandboxed boa engine with the forms object model (printcraft-js); custom keystroke/validate/calculate/format scripts, button scripts, document JavaScripts, JavaScript console (⌘J), Preferences ▸ Enable JavaScript; js_run, js_document_scripts, js_set_document_script, form_set_script, js_enabled tools
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a7ffee01b5
commit
06f8d73ec7
|
|
@ -4,13 +4,15 @@
|
|||
|
||||
Every asset PrintCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
|
||||
|
||||
## In this repository (239)
|
||||
## In this repository (241)
|
||||
|
||||
| Asset | Title | Author | Licence | Source | Used for |
|
||||
|---|---|---|---|---|---|
|
||||
| `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/square-terminal.svg` | Lucide icon "square-terminal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-terminal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/file-code.svg` | Lucide icon "file-code" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-code.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-justify.svg` | Lucide icon "align-justify" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-justify.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/align-right.svg` | Lucide icon "align-right" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-right.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
| `assets/icons/columns-3.svg` | Lucide icon "columns-3" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/columns-3.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
|
||||
|
|
|
|||
|
|
@ -41,6 +41,28 @@ kind = "icon"
|
|||
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
||||
sha256 = "bc70fba645343be0760d1a87f9e0975a10ead2b8d95883314f4cea007376e79f"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/square-terminal.svg"
|
||||
title = "Lucide icon \"square-terminal\""
|
||||
author = "Lucide Contributors (portions Cole Bemis, Feather)"
|
||||
source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/square-terminal.svg)"
|
||||
licence = "ISC"
|
||||
licence_file = "assets/icons/LICENSE-lucide.txt"
|
||||
kind = "icon"
|
||||
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
||||
sha256 = "023f88e71f9e3efad5904bf35d59657aece6b7e48bf77051e04dd8a3192b8278"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/file-code.svg"
|
||||
title = "Lucide icon \"file-code\""
|
||||
author = "Lucide Contributors (portions Cole Bemis, Feather)"
|
||||
source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/file-code.svg)"
|
||||
licence = "ISC"
|
||||
licence_file = "assets/icons/LICENSE-lucide.txt"
|
||||
kind = "icon"
|
||||
usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
|
||||
sha256 = "a52ebec6ffe56b062b3a7bc11a844be424555694736da11a4654481fb5668493"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/align-justify.svg"
|
||||
title = "Lucide icon \"align-justify\""
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ dependencies = [
|
|||
"accesskit",
|
||||
"accesskit_consumer 0.36.0",
|
||||
"atspi-common",
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"serde",
|
||||
"zvariant",
|
||||
]
|
||||
|
|
@ -148,6 +148,15 @@ dependencies = [
|
|||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aligned-vec"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
|
||||
dependencies = [
|
||||
"equator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
|
|
@ -571,6 +580,150 @@ dependencies = [
|
|||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_ast"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9221b0a090572b4a118b46c5874c0781fe4bc9546b85f0a615b8bfb609ca4ed7"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"boa_interner",
|
||||
"boa_macros",
|
||||
"boa_string",
|
||||
"indexmap",
|
||||
"num-bigint",
|
||||
"rustc-hash 2.1.3",
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_engine"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6aaf7fdc4299df68adb67e1a5440a51d34630a1f5271b9698fd74bc97b60d9"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"arrayvec",
|
||||
"async-channel",
|
||||
"bitflags 2.13.2",
|
||||
"boa_ast",
|
||||
"boa_gc",
|
||||
"boa_interner",
|
||||
"boa_macros",
|
||||
"boa_parser",
|
||||
"boa_string",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"cow-utils",
|
||||
"dashmap",
|
||||
"dynify",
|
||||
"fast-float2",
|
||||
"futures-concurrency",
|
||||
"futures-lite",
|
||||
"getrandom 0.4.3",
|
||||
"hashbrown 0.17.1",
|
||||
"icu_normalizer",
|
||||
"indexmap",
|
||||
"intrusive-collections",
|
||||
"itertools",
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"num_enum",
|
||||
"oneshot",
|
||||
"pastey",
|
||||
"portable-atomic",
|
||||
"rand 0.10.3",
|
||||
"regress",
|
||||
"rustc-hash 2.1.3",
|
||||
"ryu-js",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"small_btree",
|
||||
"static_assertions",
|
||||
"tag_ptr",
|
||||
"tap",
|
||||
"thin-vec",
|
||||
"thiserror 2.0.21",
|
||||
"time",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_gc"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af82119558ab1002d1978c8459b53a3ebb506177d29a3aca37c07c1777b1d966"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"boa_macros",
|
||||
"boa_string",
|
||||
"hashbrown 0.17.1",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_interner"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317c631e791f5cb7d0017e43c0a758b0eaffd40c34c4c360e2a6be7752b78cd1"
|
||||
dependencies = [
|
||||
"boa_gc",
|
||||
"boa_macros",
|
||||
"hashbrown 0.17.1",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"phf 0.14.0",
|
||||
"rustc-hash 2.1.3",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_macros"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b882fd440dc0f4b7441367cf89567bb077920356750c9a76a16c002d8ec2fa5c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cow-utils",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.6",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_parser"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "641e81b3c8cbb7a2140547fb3ea534e9edf210b4e8c920b5fa864a919e11fbcf"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"boa_ast",
|
||||
"boa_interner",
|
||||
"boa_macros",
|
||||
"fast-float2",
|
||||
"icu_properties",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"regress",
|
||||
"rustc-hash 2.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boa_string"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30bf86c2bcff5bf2547acbfc516c5d3108ca527cb131511d749bf6fbef762c0e"
|
||||
dependencies = [
|
||||
"fast-float2",
|
||||
"itoa",
|
||||
"pastey",
|
||||
"rustc-hash 2.1.3",
|
||||
"ryu-js",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "8.0.4"
|
||||
|
|
@ -886,6 +1039,12 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||
|
||||
[[package]]
|
||||
name = "cpubits"
|
||||
version = "0.1.1"
|
||||
|
|
@ -995,6 +1154,20 @@ version = "1.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.2"
|
||||
|
|
@ -1012,6 +1185,12 @@ dependencies = [
|
|||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "des"
|
||||
version = "0.9.0"
|
||||
|
|
@ -1098,6 +1277,26 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dynify"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81acb15628a3e22358bf73de5e7e62360b8a777dbcb5fc9ac7dfa9ae73723747"
|
||||
dependencies = [
|
||||
"dynify-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynify-macros"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec431cd708430d5029356535259c5d645d60edd3d39c54e5eea9782d46caa7d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ecb"
|
||||
version = "0.2.1"
|
||||
|
|
@ -1423,6 +1622,26 @@ version = "0.36.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
|
||||
|
||||
[[package]]
|
||||
name = "equator"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
|
||||
dependencies = [
|
||||
"equator-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equator-macro"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -1474,6 +1693,12 @@ dependencies = [
|
|||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-float2"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
|
|
@ -1523,6 +1748,12 @@ version = "0.1.14"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
|
||||
|
||||
[[package]]
|
||||
name = "flatbuffers"
|
||||
version = "24.12.23"
|
||||
|
|
@ -1623,6 +1854,19 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-concurrency"
|
||||
version = "7.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"pin-project",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
|
|
@ -1904,6 +2148,12 @@ dependencies = [
|
|||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
|
|
@ -1921,6 +2171,8 @@ version = "0.17.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
|
|
@ -1961,7 +2213,7 @@ dependencies = [
|
|||
"hayro-syntax",
|
||||
"kurbo 0.13.1",
|
||||
"moxcms",
|
||||
"phf",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash 2.1.3",
|
||||
"siphasher",
|
||||
"skrifa 0.42.1",
|
||||
|
|
@ -2008,6 +2260,12 @@ dependencies = [
|
|||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.3"
|
||||
|
|
@ -2102,6 +2360,8 @@ dependencies = [
|
|||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
|
|
@ -2212,6 +2472,12 @@ dependencies = [
|
|||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intrusive-collections"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4275b20e6057cd7733fd8df8a5a31701e4fe44497dad0f3fa0e1c4fb971506be"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
|
|
@ -2617,6 +2883,32 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
@ -2659,6 +2951,15 @@ dependencies = [
|
|||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
|
|
@ -2991,6 +3292,12 @@ version = "1.21.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oneshot"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe21416a02c693fb9f980befcb230ecc70b0b3d1cc4abf88b9675c4c1457f0c"
|
||||
|
||||
[[package]]
|
||||
name = "orbclient"
|
||||
version = "0.3.55"
|
||||
|
|
@ -3076,6 +3383,12 @@ dependencies = [
|
|||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pastey"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.13.0"
|
||||
|
|
@ -3120,11 +3433,21 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"phf_macros 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
|
||||
dependencies = [
|
||||
"phf_macros 0.14.0",
|
||||
"phf_shared 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.13.1"
|
||||
|
|
@ -3132,7 +3455,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
"phf_shared 0.13.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared 0.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -3141,8 +3474,21 @@ version = "0.13.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"phf_generator 0.13.1",
|
||||
"phf_shared 0.13.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
|
||||
dependencies = [
|
||||
"phf_generator 0.14.0",
|
||||
"phf_shared 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
|
|
@ -3157,6 +3503,15 @@ dependencies = [
|
|||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pic-scale"
|
||||
version = "0.7.12"
|
||||
|
|
@ -3327,6 +3682,12 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
|
|
@ -3497,6 +3858,7 @@ dependencies = [
|
|||
"printcraft-edit",
|
||||
"printcraft-fonts",
|
||||
"printcraft-forms",
|
||||
"printcraft-js",
|
||||
"printcraft-model",
|
||||
"printcraft-ocr",
|
||||
"printcraft-optimize",
|
||||
|
|
@ -3541,6 +3903,13 @@ dependencies = [
|
|||
name = "printcraft-geom"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "printcraft-js"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"boa_engine",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "printcraft-model"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3951,6 +4320,15 @@ version = "0.8.11"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "regress"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32eef8b209c3c1c15dbad02c1f30f9539f00dc7253e0cbcdaae442a50a09d7c1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "renderdoc-sys"
|
||||
version = "1.1.0"
|
||||
|
|
@ -4229,6 +4607,12 @@ dependencies = [
|
|||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu-js"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04d056b875a9d2e6cb9a61d127afee9ac5999b9f87bcb32079d1318e505be714"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
|
@ -4492,6 +4876,15 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "small_btree"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba60d2df92ba73864714808ca68c059734853e6ab722b40e1cf543ebb3a057a"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.2"
|
||||
|
|
@ -4624,6 +5017,27 @@ dependencies = [
|
|||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
|
@ -4673,6 +5087,18 @@ dependencies = [
|
|||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tag_ptr"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05"
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
|
|
@ -4695,6 +5121,12 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4568d7e143ec86d2021c338bae2afa88699e84b8e0af523626654fe6f03a1748"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
|
@ -4749,6 +5181,39 @@ dependencies = [
|
|||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
|
|
@ -5000,6 +5465,12 @@ dependencies = [
|
|||
"xmlwriter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
|
@ -5919,6 +6390,12 @@ dependencies = [
|
|||
"primefield",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.4"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ printcraft-sign = { path = "crates/sign", version = "0.1.0" }
|
|||
printcraft-optimize = { path = "crates/optimize", version = "0.1.0" }
|
||||
printcraft-a11y = { path = "crates/a11y", version = "0.1.0" }
|
||||
printcraft-ocr = { path = "crates/ocr", version = "0.1.0" }
|
||||
printcraft-js = { path = "crates/js", version = "0.1.0" }
|
||||
printcraft-edit = { path = "crates/edit", version = "0.1.0" }
|
||||
printcraft-create = { path = "crates/create", version = "0.1.0" }
|
||||
printcraft-forms = { path = "crates/forms", version = "0.1.0" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<!-- @license lucide-static v1.49.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-file-code"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||
<path d="M10 12.5 8 15l2 2.5" />
|
||||
<path d="m14 12.5 2 2.5-2 2.5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 524 B |
|
|
@ -0,0 +1,17 @@
|
|||
<!-- @license lucide-static v1.49.0 - ISC -->
|
||||
<svg
|
||||
class="lucide lucide-square-terminal"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m7 11 2-2-2-2" />
|
||||
<path d="M11 13h4" />
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 407 B |
|
|
@ -200,3 +200,56 @@ impl Automation {
|
|||
Ok(json!({ "available": ocr::available(), "search_dirs": dirs, "languages": langs }))
|
||||
}
|
||||
}
|
||||
|
||||
fn request_json(r: &printcraft_engine::js::Request) -> Value {
|
||||
use printcraft_engine::js::Request as R;
|
||||
match r {
|
||||
R::Reset(n) => json!({ "reset": n }),
|
||||
R::Print => json!({ "print": true }),
|
||||
R::GoToPage(p) => json!({ "page": p + 1 }),
|
||||
R::LaunchUrl(u) => json!({ "url": u }),
|
||||
R::Submit(u) => json!({ "submit": u }),
|
||||
R::Focus(f) => json!({ "focus": f }),
|
||||
R::Beep => json!({ "beep": true }),
|
||||
}
|
||||
}
|
||||
|
||||
impl Automation {
|
||||
pub(crate) fn js_run(&mut self, a: &Args) -> Result<Value> {
|
||||
let id = self.doc(a)?.id;
|
||||
let o = self.session.run_javascript(id, a.str("script")?, a.opt_str("field")?).map_err(failed)?;
|
||||
let reqs: Vec<Value> = o.requests.iter().map(request_json).collect();
|
||||
Ok(json!({ "alerts": o.alerts, "console": o.console, "requests": reqs, "error": o.error, "result": o.result }))
|
||||
}
|
||||
|
||||
pub(crate) fn js_document_scripts(&self, a: &Args) -> Result<Value> {
|
||||
let list: Vec<Value> = self.doc(a)?.document_scripts().into_iter().map(|(n, s)| json!({ "name": n, "script": s })).collect();
|
||||
Ok(json!({ "scripts": list }))
|
||||
}
|
||||
|
||||
pub(crate) fn js_set_document_script(&mut self, a: &Args) -> Result<Value> {
|
||||
let edit = printcraft_engine::Edit::SetDocumentScript { name: a.str("name")?.into(), script: a.opt_str("script")?.map(str::to_string) };
|
||||
let id = self.doc(a)?.id;
|
||||
self.session.apply(id, edit).map_err(failed)?;
|
||||
self.js_document_scripts(a)
|
||||
}
|
||||
|
||||
pub(crate) fn form_set_script(&mut self, a: &Args) -> Result<Value> {
|
||||
let edit = printcraft_engine::Edit::SetFieldScript {
|
||||
name: a.str("field")?.into(),
|
||||
event: a.str("event")?.into(),
|
||||
script: a.opt_str("script")?.map(str::to_string),
|
||||
};
|
||||
let id = self.doc(a)?.id;
|
||||
self.session.apply(id, edit).map_err(failed)?;
|
||||
let out = self.session.take_js_output(id);
|
||||
Ok(json!({ "field": a.str("field")?, "event": a.str("event")?, "console": out.console, "errors": out.errors }))
|
||||
}
|
||||
|
||||
pub(crate) fn js_enabled(&mut self, a: &Args) -> Result<Value> {
|
||||
if let Some(on) = a.opt_bool("enabled")? {
|
||||
self.session.set_javascript(on);
|
||||
}
|
||||
Ok(json!({ "enabled": self.session.javascript() }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,6 +503,11 @@ impl Automation {
|
|||
"sign_list" => self.sign_list(&a)?,
|
||||
"accessibility_check" => self.a11y_check(&a)?,
|
||||
"ocr_recognize" => self.ocr_recognize(&a)?,
|
||||
"js_run" => self.js_run(&a)?,
|
||||
"js_document_scripts" => self.js_document_scripts(&a)?,
|
||||
"js_set_document_script" => self.js_set_document_script(&a)?,
|
||||
"js_enabled" => self.js_enabled(&a)?,
|
||||
"form_set_script" => self.form_set_script(&a)?,
|
||||
"ocr_status" => self.ocr_status()?,
|
||||
"ocr_recognize_files" => self.ocr_recognize_files(&a)?,
|
||||
"accessibility_report" => self.a11y_report(&a)?,
|
||||
|
|
|
|||
|
|
@ -737,6 +737,27 @@ pub fn tools() -> Vec<ToolDef> {
|
|||
t("accessibility_set_alt", "Set alternate text", "Set a figure's alternate text (alt; empty or omitted clears it), or mark it decorative (decorative: true: its content becomes an artifact and the figure leaves the tags). figure is a number from accessibility_figures. Returns the figures. Undoable.")
|
||||
.cmd("a11y.alt_text")
|
||||
.with(schema(json!({ "doc": doc(), "figure": { "type": "integer", "minimum": 1 }, "alt": { "type": "string" }, "decorative": { "type": "boolean" } }), &["doc", "figure"])),
|
||||
t("js_run", "Run JavaScript", "Run Acrobat JavaScript in the document, as the JavaScript console does (or as push button `field`'s Mouse Up script when field is given). The form object model is available: this/getField, event, app, util, console, display, color, and the document-level scripts. Field changes and resetForm are applied as one undoable step; returns the script's alerts, console output, requests (print, page, url, submit) and error.")
|
||||
.cmd("tools.js_console")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "script": { "type": "string" }, "field": { "type": "string", "description": "Run as this button's Mouse Up event." } }),
|
||||
&["doc", "script"],
|
||||
)),
|
||||
t("js_document_scripts", "Document JavaScripts", "List the document-level JavaScripts (name and source), which define functions field scripts use.")
|
||||
.ro()
|
||||
.cmd("tools.document_js")
|
||||
.with(schema(json!({ "doc": doc() }), &["doc"])),
|
||||
t("js_set_document_script", "Edit a document JavaScript", "Add or replace the document-level JavaScript `name` with `script`, or delete it (script omitted). Undoable.")
|
||||
.cmd("tools.document_js")
|
||||
.with(schema(json!({ "doc": doc(), "name": { "type": "string", "minLength": 1 }, "script": { "type": "string" } }), &["doc", "name"])),
|
||||
t("form_set_script", "Set a field's JavaScript", "Field Properties ▸ Run custom script: set field's JavaScript for event keystroke, format, validate or calculate (the field's actions; calculated fields join the calculation order) or mouse_up (a button's action). Omit script to remove it. The scripts use Acrobat's object model (event.value, event.rc, getField, util.printf, …) and run when the field changes. Undoable.")
|
||||
.cmd("form.prepare")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "field": { "type": "string" }, "event": { "type": "string", "enum": ["keystroke", "format", "validate", "calculate", "mouse_up"] }, "script": { "type": "string" } }),
|
||||
&["doc", "field", "event"],
|
||||
)),
|
||||
t("js_enabled", "JavaScript on or off", "Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript: set it with `enabled`, or read it. With JavaScript off, field scripts other than Acrobat's AF calls don't run.")
|
||||
.with(schema(json!({ "enabled": { "type": "boolean" } }), &[])),
|
||||
t("ocr_recognize", "Recognize text (OCR)", "Scan & OCR ▸ Recognize text: render pages, read the words in them and add them as invisible text over the page image, so scanned pages become searchable and selectable (a searchable image; the image is not changed). Pages that already have text are skipped unless skip_text_pages is false. Returns each page's recognised text, word count or why it was skipped. Needs the OCR models (ocr_status). Undoable as one step.")
|
||||
.cmd("ocr.recognize")
|
||||
.with(schema(
|
||||
|
|
|
|||
|
|
@ -1329,3 +1329,50 @@ fn ocr_recognize_files_writes_searchable_copies() {
|
|||
let found = ok(&mut a, "text_find", json!({ "doc": out, "query": "recognition" }));
|
||||
assert!(found.to_string().contains("\"page\""), "{found}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn javascript_through_tools() {
|
||||
let dir = workdir("js");
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
|
||||
for (name, y) in [("Price", 20), ("Qty", 60), ("Total", 100)] {
|
||||
ok(&mut a, "form_add_field", json!({ "doc": doc, "page": 1, "type": "text", "rect": [20, y, 180, y + 22], "name": name }));
|
||||
}
|
||||
ok(
|
||||
&mut a,
|
||||
"js_set_document_script",
|
||||
json!({ "doc": doc, "name": "lib", "script": "function money(v) { return util.printf('EUR %,2.2f', v); }" }),
|
||||
);
|
||||
assert_eq!(ok(&mut a, "js_document_scripts", json!({ "doc": doc }))["scripts"][0]["name"], "lib");
|
||||
ok(
|
||||
&mut a,
|
||||
"form_set_script",
|
||||
json!({ "doc": doc, "field": "Total", "event": "calculate", "script": "event.value = getField('Price').value * getField('Qty').value;" }),
|
||||
);
|
||||
ok(&mut a, "form_set_script", json!({ "doc": doc, "field": "Total", "event": "format", "script": "event.value = money(event.value);" }));
|
||||
ok(
|
||||
&mut a,
|
||||
"form_set_script",
|
||||
json!({ "doc": doc, "field": "Qty", "event": "validate", "script": "if (event.value < 1) { app.alert('Order at least one'); event.rc = false; }" }),
|
||||
);
|
||||
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "Price": "1250", "Qty": "2" } }));
|
||||
assert!(page_text(&mut a, doc)[0].contains("EUR 2.500,00"), "{:?}", page_text(&mut a, doc));
|
||||
let err = a.call("form_fill", &json!({ "doc": doc, "values": { "Qty": "0" } })).unwrap_err();
|
||||
assert!(err.to_string().contains("Order at least one"), "{err}");
|
||||
|
||||
let r = ok(
|
||||
&mut a,
|
||||
"js_run",
|
||||
json!({ "doc": doc, "script": "console.println(getField('Total').value); getField('Qty').value = 3; this.pageNum = 1; app.alert('ok');" }),
|
||||
);
|
||||
assert_eq!(r["console"][0], "2500");
|
||||
assert_eq!(r["alerts"][0], "ok");
|
||||
assert_eq!(r["requests"][0]["page"], 2);
|
||||
let f = ok(&mut a, "form_fields", json!({ "doc": doc }));
|
||||
let total = f["fields"].as_array().unwrap().iter().find(|x| x["name"] == "Total").unwrap().clone();
|
||||
assert_eq!(total["value"], "3750");
|
||||
let r = ok(&mut a, "js_run", json!({ "doc": doc, "script": "nope()" }));
|
||||
assert!(r["error"].as_str().unwrap().contains("nope"));
|
||||
assert_eq!(ok(&mut a, "js_enabled", json!({ "enabled": false }))["enabled"], false);
|
||||
assert!(a.call("js_run", &json!({ "doc": doc, "script": "1" })).is_err());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ printcraft-sign = { workspace = true }
|
|||
printcraft-optimize = { workspace = true }
|
||||
printcraft-a11y = { workspace = true }
|
||||
printcraft-ocr = { workspace = true }
|
||||
printcraft-js = { workspace = true }
|
||||
printcraft-fonts = { workspace = true }
|
||||
printcraft-create = { workspace = true }
|
||||
printcraft-model = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -248,6 +248,9 @@ pub const COMMANDS: &[CommandSpec] = &[
|
|||
c("optimize.reduce", "Reduce file size…", FILE, None, Document, "file-down"),
|
||||
c("optimize.advanced", "Optimize PDF…", FILE, None, Document, "settings-2"),
|
||||
c("export.text", "Export to text…", FILE, None, Document, "type"),
|
||||
c("app.preferences", "Preferences…", None, None, Nothing, "settings"),
|
||||
c("tools.js_console", "JavaScript console…", None, Some(Shortcut::cmd("J")), Document, "square-terminal"),
|
||||
c("tools.document_js", "Document JavaScripts…", None, None, Modification, "file-code"),
|
||||
c("ocr.recognize", "Recognize text…", None, None, Modification, "scan-text"),
|
||||
c("ocr.recognize_batch", "Recognize text in multiple files…", None, None, Nothing, "files"),
|
||||
c("a11y.check", "Check for accessibility…", None, None, Document, "accessibility"),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,222 @@
|
|||
//! Acrobat JavaScript in documents: field scripts run during form edits ([`JsRunner`], wired
|
||||
//! into `forms` through its [`printcraft_forms::Scripts`] hook), and button actions and console
|
||||
//! input run on request ([`Session::run_javascript`]). What scripts ask of the viewer (alerts,
|
||||
//! printing, navigation, links) is kept for the front end in [`JsOutput`].
|
||||
//!
|
||||
//! Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript is [`Session::set_javascript`]; with it
|
||||
//! off, scripts don't run and AF calls keep working natively.
|
||||
|
||||
use printcraft_forms::{Field, FieldChange, FieldEvent, FieldKind, ScriptResult, Scripts};
|
||||
pub use printcraft_js::{Limits, Outcome, Request};
|
||||
|
||||
use crate::{DocId, Edit, EditError, Session};
|
||||
|
||||
/// What scripts produced for the viewer since it last looked.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JsOutput {
|
||||
pub alerts: Vec<String>,
|
||||
pub console: Vec<String>,
|
||||
pub requests: Vec<Request>,
|
||||
/// Script errors (as Acrobat's console shows them).
|
||||
pub errors: Vec<String>,
|
||||
}
|
||||
|
||||
impl JsOutput {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.alerts.is_empty() && self.console.is_empty() && self.requests.is_empty() && self.errors.is_empty()
|
||||
}
|
||||
|
||||
fn absorb(&mut self, o: &Outcome) {
|
||||
self.alerts.extend(o.alerts.iter().cloned());
|
||||
self.console.extend(o.console.iter().cloned());
|
||||
self.requests.extend(o.requests.iter().cloned());
|
||||
self.errors.extend(o.error.iter().cloned());
|
||||
}
|
||||
|
||||
pub(crate) fn append(&mut self, other: JsOutput) {
|
||||
self.alerts.extend(other.alerts);
|
||||
self.console.extend(other.console);
|
||||
self.requests.extend(other.requests);
|
||||
self.errors.extend(other.errors);
|
||||
}
|
||||
}
|
||||
|
||||
fn kind(k: FieldKind) -> printcraft_js::FieldType {
|
||||
use printcraft_js::FieldType as T;
|
||||
match k {
|
||||
FieldKind::Text => T::Text,
|
||||
FieldKind::CheckBox => T::CheckBox,
|
||||
FieldKind::Radio => T::RadioButton,
|
||||
FieldKind::PushButton => T::Button,
|
||||
FieldKind::Combo => T::ComboBox,
|
||||
FieldKind::List => T::ListBox,
|
||||
FieldKind::Signature => T::Signature,
|
||||
}
|
||||
}
|
||||
|
||||
/// `field.display` from the first widget's annotation flags.
|
||||
fn display(doc: &printcraft_cos::Document, f: &Field) -> i32 {
|
||||
let flags = f.widgets.first().and_then(|w| doc.get(w.obj).as_dict().and_then(|d| d.int(b"F"))).unwrap_or(4);
|
||||
if flags & 2 != 0 {
|
||||
printcraft_js::DISPLAY_HIDDEN
|
||||
} else if flags & 32 != 0 {
|
||||
printcraft_js::DISPLAY_NO_VIEW
|
||||
} else if flags & 4 == 0 {
|
||||
printcraft_js::DISPLAY_NO_PRINT
|
||||
} else {
|
||||
printcraft_js::DISPLAY_VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/// A form field as scripts see it.
|
||||
pub fn field_state(doc: &printcraft_cos::Document, f: &Field) -> printcraft_js::FieldState {
|
||||
let mut s = printcraft_js::FieldState::new(f.name.clone(), kind(f.kind), f.value.clone());
|
||||
s.default = f.default.clone();
|
||||
s.readonly = f.read_only();
|
||||
s.required = f.has(printcraft_forms::flags::REQUIRED);
|
||||
s.display = display(doc, f);
|
||||
s.char_limit = f.max_len;
|
||||
s.options = match f.kind {
|
||||
FieldKind::CheckBox | FieldKind::Radio => f.widgets.iter().filter_map(|w| w.on_state.clone()).map(|o| (o.clone(), o)).collect(),
|
||||
_ => f.options.clone(),
|
||||
};
|
||||
s
|
||||
}
|
||||
|
||||
/// The changes a script made, against the fields as they were.
|
||||
fn changes(before: &[printcraft_js::FieldState], after: &[printcraft_js::FieldState]) -> Vec<FieldChange> {
|
||||
after
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
let b = before.iter().find(|b| b.name == a.name)?;
|
||||
let c = FieldChange {
|
||||
name: a.name.clone(),
|
||||
value: (a.value != b.value).then(|| a.value.clone()),
|
||||
read_only: (a.readonly != b.readonly).then_some(a.readonly),
|
||||
required: (a.required != b.required).then_some(a.required),
|
||||
display: (a.display != b.display).then_some(a.display),
|
||||
};
|
||||
(c.value.is_some() || c.read_only.is_some() || c.required.is_some() || c.display.is_some()).then_some(c)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Runs field scripts with the document's object model and keeps what they print or ask for.
|
||||
pub struct JsRunner {
|
||||
pub doc: printcraft_js::DocInfo,
|
||||
pub doc_scripts: Vec<String>,
|
||||
pub output: JsOutput,
|
||||
/// The fields in the document as the forms code last read them (for `display`).
|
||||
cos: printcraft_cos::Document,
|
||||
}
|
||||
|
||||
impl JsRunner {
|
||||
pub fn new(cos: &printcraft_cos::Document, file_name: &str) -> JsRunner {
|
||||
let doc =
|
||||
printcraft_js::DocInfo { file_name: file_name.to_string(), num_pages: printcraft_model::pages(cos).len(), page: 0, info: info(cos) };
|
||||
JsRunner { doc, doc_scripts: printcraft_forms::document_scripts(cos), output: JsOutput::default(), cos: cos.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// The Info dictionary as `this.info` keys (lower-case).
|
||||
fn info(cos: &printcraft_cos::Document) -> Vec<(String, String)> {
|
||||
let Some(d) = cos.trailer().get(b"Info").map(|i| cos.resolve(i)).and_then(|i| i.as_dict().cloned()) else { return Vec::new() };
|
||||
d.iter()
|
||||
.filter_map(|(k, v)| match &*cos.resolve(v) {
|
||||
printcraft_cos::Object::String(s) => Some((String::from_utf8_lossy(k).to_lowercase(), s.to_text())),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Scripts for JsRunner {
|
||||
fn run(&mut self, event: FieldEvent, script: &str, target: &Field, value: &str, fields: &[Field]) -> ScriptResult {
|
||||
let states: Vec<_> = fields.iter().map(|f| field_state(&self.cos, f)).collect();
|
||||
let ev = printcraft_js::Event::field(event.name(), &target.name, value);
|
||||
let o = printcraft_js::run(script, &ev, &self.doc, &states, &self.doc_scripts, Limits::default());
|
||||
self.output.absorb(&o);
|
||||
if o.error.is_some() {
|
||||
// As in Acrobat, a failing script leaves the value alone (the error goes to the console).
|
||||
return ScriptResult { rc: true, value: value.to_string(), changes: Vec::new(), message: None };
|
||||
}
|
||||
let after: Vec<_> = states.iter().map(|s| o.changed.iter().find(|c| c.name == s.name).unwrap_or(s).clone()).collect();
|
||||
ScriptResult {
|
||||
rc: o.rc,
|
||||
value: o.value.clone(),
|
||||
changes: changes(&states, &after),
|
||||
message: (!o.rc).then(|| o.alerts.last().cloned()).flatten(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A document-level JavaScript: its name and source.
|
||||
pub fn document_scripts(cos: &printcraft_cos::Document) -> Vec<(String, String)> {
|
||||
printcraft_forms::document_scripts_named(cos)
|
||||
}
|
||||
|
||||
impl crate::Document {
|
||||
/// Document JavaScripts (name, source), in name order.
|
||||
pub fn document_scripts(&self) -> Vec<(String, String)> {
|
||||
self.editor.as_ref().map(|e| document_scripts(&e.cos)).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript (on by default).
|
||||
pub fn set_javascript(&mut self, on: bool) {
|
||||
self.js_off = !on;
|
||||
}
|
||||
|
||||
pub fn javascript(&self) -> bool {
|
||||
!self.js_off
|
||||
}
|
||||
|
||||
/// What scripts printed or asked for since the last call (alerts, console, print, …).
|
||||
pub fn take_js_output(&mut self, id: DocId) -> JsOutput {
|
||||
self.docs.iter_mut().find(|d| d.id == id).map(|d| std::mem::take(&mut d.js_output)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Run `script` in document `id`, for a button (`target`: the field, event "Mouse Up") or
|
||||
/// the console (no target). Field changes and `resetForm` are applied as one undoable step;
|
||||
/// everything else the script asked for is returned.
|
||||
pub fn run_javascript(&mut self, id: DocId, script: &str, target: Option<&str>) -> Result<Outcome, EditError> {
|
||||
if self.js_off {
|
||||
return Err(EditError::Invalid("JavaScript is turned off (Preferences ▸ JavaScript)".into()));
|
||||
}
|
||||
let doc = self.get(id).ok_or(EditError::NoDocument)?;
|
||||
let cos = doc.editor.as_ref().map(|e| e.cos.clone()).ok_or_else(|| EditError::ReadOnly(doc.read_only_reason.clone().unwrap_or_default()))?;
|
||||
let runner = JsRunner::new(&cos, &doc.name);
|
||||
let fields = printcraft_forms::fields(&cos);
|
||||
let states: Vec<_> = fields.iter().map(|f| field_state(&cos, f)).collect();
|
||||
let event = match target {
|
||||
Some(t) => printcraft_js::Event { will_commit: false, ..printcraft_js::Event::field("Mouse Up", t, "") },
|
||||
None => printcraft_js::Event::doc("Console"),
|
||||
};
|
||||
let mut o = printcraft_js::run(script, &event, &runner.doc, &states, &runner.doc_scripts, Limits::default());
|
||||
let after: Vec<_> = states.iter().map(|s| o.changed.iter().find(|c| c.name == s.name).unwrap_or(s).clone()).collect();
|
||||
let changes = changes(&states, &after);
|
||||
let resets: Vec<Vec<String>> = o
|
||||
.requests
|
||||
.iter()
|
||||
.filter_map(|r| match r {
|
||||
Request::Reset(n) => Some(n.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
o.requests.retain(|r| !matches!(r, Request::Reset(_)));
|
||||
let mut edits = Vec::new();
|
||||
if !changes.is_empty() {
|
||||
edits.push(Edit::ApplyScriptChanges { changes });
|
||||
}
|
||||
for names in resets {
|
||||
let all: Vec<String> = fields.iter().map(|f| f.name.clone()).collect();
|
||||
let names = (!names.is_empty())
|
||||
.then(|| all.iter().filter(|n| names.iter().any(|x| *n == x || n.starts_with(&format!("{x}.")))).cloned().collect());
|
||||
edits.push(Edit::ResetForm { names });
|
||||
}
|
||||
if !edits.is_empty() {
|
||||
self.apply(id, Edit::Batch { label: "Run JavaScript".into(), edits })?;
|
||||
}
|
||||
Ok(o)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
pub mod catalog;
|
||||
pub mod commands;
|
||||
pub mod export;
|
||||
pub mod js;
|
||||
pub mod links;
|
||||
pub mod ocr;
|
||||
|
||||
|
|
@ -115,6 +116,15 @@ enum Scope {
|
|||
Form,
|
||||
}
|
||||
|
||||
/// Edits during which field scripts run.
|
||||
fn uses_scripts(edit: &Edit) -> bool {
|
||||
match edit {
|
||||
Edit::SetFieldValue { .. } | Edit::ApplyScriptChanges { .. } | Edit::SetFieldScript { .. } => true,
|
||||
Edit::Batch { edits, .. } => edits.iter().any(uses_scripts),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn scope_of(edit: &Edit) -> Scope {
|
||||
match edit {
|
||||
// A file attachment also changes the Attachments list.
|
||||
|
|
@ -133,7 +143,7 @@ fn scope_of(edit: &Edit) -> Scope {
|
|||
| Edit::ResizeAnnotation { .. }
|
||||
| Edit::StyleAnnotation { .. }
|
||||
| Edit::SetAnnotationInfo { .. } => Scope::Comments,
|
||||
Edit::SetFieldValue { .. } | Edit::ResetForm { .. } | Edit::SetFieldImage { .. } => Scope::Form,
|
||||
Edit::SetFieldValue { .. } | Edit::ResetForm { .. } | Edit::SetFieldImage { .. } | Edit::ApplyScriptChanges { .. } => Scope::Form,
|
||||
Edit::Batch { edits, .. } => {
|
||||
let mut scopes = edits.iter().map(scope_of);
|
||||
let first = scopes.next().unwrap_or(Scope::Full);
|
||||
|
|
@ -187,6 +197,8 @@ pub struct Document {
|
|||
sig_cache: Arc<printcraft_sign::DigestCache>,
|
||||
editor: Option<Editor>,
|
||||
config: RenderConfig,
|
||||
/// What field scripts printed or asked for (see [`Session::take_js_output`]).
|
||||
js_output: js::JsOutput,
|
||||
}
|
||||
|
||||
impl Document {
|
||||
|
|
@ -776,6 +788,24 @@ pub enum Edit {
|
|||
MarkDecorative {
|
||||
figure: u32,
|
||||
},
|
||||
/// Document JavaScripts: add, replace (`script`) or remove (`None`) the document-level
|
||||
/// script `name`.
|
||||
SetDocumentScript {
|
||||
name: String,
|
||||
script: Option<String>,
|
||||
},
|
||||
/// Field Properties ▸ a custom script: set or remove field `name`'s JavaScript for `event`
|
||||
/// (keystroke, format, validate, calculate or mouse_up).
|
||||
SetFieldScript {
|
||||
name: String,
|
||||
event: String,
|
||||
script: Option<String>,
|
||||
},
|
||||
/// What a script (button or console) changed in form fields: values, read-only, required
|
||||
/// and visibility.
|
||||
ApplyScriptChanges {
|
||||
changes: Vec<printcraft_forms::FieldChange>,
|
||||
},
|
||||
/// Scan & OCR ▸ Recognize text: put recognised words on `page` as invisible text over the
|
||||
/// image (from [`ocr::OcrJob::run`]).
|
||||
AddOcrText {
|
||||
|
|
@ -971,6 +1001,10 @@ impl Edit {
|
|||
Edit::SetAltText { .. } => "Set alternate text".into(),
|
||||
Edit::MarkDecorative { .. } => "Mark figure as decorative".into(),
|
||||
Edit::AddOcrText { .. } => "Recognize text".into(),
|
||||
Edit::ApplyScriptChanges { .. } => "Run JavaScript".into(),
|
||||
Edit::SetFieldScript { name, .. } => format!("Edit script of {name}"),
|
||||
Edit::SetDocumentScript { script: None, .. } => "Delete document JavaScript".into(),
|
||||
Edit::SetDocumentScript { .. } => "Edit document JavaScript".into(),
|
||||
Edit::EditTextLine { .. } | Edit::EditTextBlock { .. } => "Edit text".into(),
|
||||
Edit::EditPageImage { change, .. } => match change {
|
||||
ImageEdit::Move(_) => "Move image".into(),
|
||||
|
|
@ -1095,7 +1129,7 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(),
|
|||
Err(EditError::NotPermitted("comments"))
|
||||
}
|
||||
}
|
||||
Edit::SetFieldValue { .. } | Edit::ResetForm { .. } | Edit::SetFieldImage { .. } => {
|
||||
Edit::SetFieldValue { .. } | Edit::ResetForm { .. } | Edit::SetFieldImage { .. } | Edit::ApplyScriptChanges { .. } => {
|
||||
if p.fill_forms() {
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -1139,6 +1173,8 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(),
|
|||
| Edit::SetAltText { .. }
|
||||
| Edit::MarkDecorative { .. }
|
||||
| Edit::AddOcrText { .. }
|
||||
| Edit::SetDocumentScript { .. }
|
||||
| Edit::SetFieldScript { .. }
|
||||
| Edit::EditTextLine { .. }
|
||||
| Edit::EditTextBlock { .. }
|
||||
| Edit::EditPageImage { .. }
|
||||
|
|
@ -1155,6 +1191,8 @@ fn check_permission(edit: &Edit, p: &printcraft_cos::Permissions) -> Result<(),
|
|||
|
||||
/// Dates and unique ids stamped onto what an edit creates.
|
||||
struct EditCtx {
|
||||
/// Field scripts run (`None`: JavaScript is off) and what they produced.
|
||||
js: Option<js::JsRunner>,
|
||||
date: Option<String>,
|
||||
/// Today in local time, for date tokens.
|
||||
today: (i64, u32, u32),
|
||||
|
|
@ -1173,7 +1211,7 @@ impl EditCtx {
|
|||
// Real clock: mix in sub-second time so ids from two sessions don't collide.
|
||||
seed ^= std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.subsec_nanos() as u64).unwrap_or(0) << 32;
|
||||
}
|
||||
Self { date: now.map(printcraft_cos::pdf_date), today: (1970, 1, 1), seed, count: 0 }
|
||||
Self { js: None, date: now.map(printcraft_cos::pdf_date), today: (1970, 1, 1), seed, count: 0 }
|
||||
}
|
||||
|
||||
/// 32 bytes of entropy for new encryption keys and salts. `RandomState` is seeded by the
|
||||
|
|
@ -1291,7 +1329,14 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -
|
|||
Edit::SetAnnotationInfo { page, index, author, subject, icon } => {
|
||||
printcraft_annot::set_info(doc, *page, *index, author.as_deref(), subject.as_deref(), *icon, &cx.meta())?;
|
||||
}
|
||||
Edit::SetFieldValue { name, value } => printcraft_forms::set_value(doc, name, value)?,
|
||||
Edit::SetFieldValue { name, value } => match cx.js.as_mut() {
|
||||
Some(js) => printcraft_forms::set_value_with(doc, name, value, js)?,
|
||||
None => printcraft_forms::set_value(doc, name, value)?,
|
||||
},
|
||||
Edit::ApplyScriptChanges { changes } => match cx.js.as_mut() {
|
||||
Some(js) => printcraft_forms::apply_script_changes(doc, changes, js)?,
|
||||
None => printcraft_forms::apply_script_changes(doc, changes, &mut printcraft_forms::NoScripts)?,
|
||||
},
|
||||
Edit::SetFieldImage { name, image } => {
|
||||
let (img, _) = printcraft_create::image_xobject(doc, name, image)?;
|
||||
let px = match &*doc.get(img) {
|
||||
|
|
@ -1325,6 +1370,14 @@ fn run_edit(doc: &mut printcraft_cos::Document, edit: &Edit, cx: &mut EditCtx) -
|
|||
Edit::EditTextLine { page, line, text } => {
|
||||
printcraft_edit::replace_line(doc, *page, *line, text)?;
|
||||
}
|
||||
Edit::SetFieldScript { name, event, script } => {
|
||||
printcraft_forms::set_field_script(doc, name, event, script.as_deref())?;
|
||||
match cx.js.as_mut() {
|
||||
Some(js) => printcraft_forms::recalculate_with(doc, js)?,
|
||||
None => printcraft_forms::recalculate(doc)?,
|
||||
};
|
||||
}
|
||||
Edit::SetDocumentScript { name, script } => printcraft_forms::set_document_script(doc, name, script.as_deref())?,
|
||||
Edit::AddOcrText { page, words } => {
|
||||
printcraft_edit::stamp(doc, *page, "OCR", printcraft_ocr::text_layer(words))?;
|
||||
}
|
||||
|
|
@ -1583,6 +1636,8 @@ pub enum EditError {
|
|||
SignedRewrite(String),
|
||||
#[error("this document is signed: rewriting it would invalidate its signatures (save it incrementally instead)")]
|
||||
Signed,
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
impl From<printcraft_sign::SignError> for EditError {
|
||||
|
|
@ -1599,6 +1654,8 @@ pub struct Session {
|
|||
clock: Option<fn() -> i64>,
|
||||
/// Certificates trusted for signing (Acrobat: Trusted Certificates).
|
||||
trust: Arc<TrustStore>,
|
||||
/// Preferences ▸ JavaScript ▸ Enable Acrobat JavaScript, inverted (on by default).
|
||||
js_off: bool,
|
||||
}
|
||||
|
||||
/// Validate the signature fields of `cos` (written as `bytes`).
|
||||
|
|
@ -1720,6 +1777,7 @@ impl Session {
|
|||
sig_cache,
|
||||
editor,
|
||||
config,
|
||||
js_output: Default::default(),
|
||||
});
|
||||
Ok(id)
|
||||
}
|
||||
|
|
@ -1732,7 +1790,9 @@ impl Session {
|
|||
pub fn apply(&mut self, id: DocId, edit: Edit) -> Result<(), EditError> {
|
||||
let now = self.now();
|
||||
let today = self.today();
|
||||
let js_off = self.js_off;
|
||||
let doc = self.doc_mut(id)?;
|
||||
let name = doc.name.clone();
|
||||
let mut cx = EditCtx::new(now, doc.generation ^ (id.0 << 48));
|
||||
cx.today = today;
|
||||
let reason = doc.read_only_reason.clone().unwrap_or_default();
|
||||
|
|
@ -1742,6 +1802,9 @@ impl Session {
|
|||
check_permission(&edit, &p)?;
|
||||
}
|
||||
let mut next = editor.cos.clone();
|
||||
if !js_off && uses_scripts(&edit) {
|
||||
cx.js = Some(js::JsRunner::new(&next, &name));
|
||||
}
|
||||
run_edit(&mut next, &edit, &mut cx)?;
|
||||
// Signed documents are only ever saved incrementally: an edit that needs a full rewrite
|
||||
// (applying redactions, changing security, sanitizing) would invalidate the signatures.
|
||||
|
|
@ -1773,6 +1836,9 @@ impl Session {
|
|||
}
|
||||
doc.dirty = true;
|
||||
doc.generation += 1;
|
||||
if let Some(js) = cx.js {
|
||||
doc.js_output.append(js.output);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1235,3 +1235,67 @@ fn recognize_text_makes_a_scanned_page_searchable() {
|
|||
let again = s.recognize_text(id, &[], ocr::OcrSettings::default()).unwrap();
|
||||
assert!(again[0].skipped.is_some());
|
||||
}
|
||||
|
||||
/// A form whose scripts are custom JavaScript: total = price × qty (calculate, through a
|
||||
/// document-level function), shown with a custom format; qty is validated; a button script.
|
||||
fn scripted_form() -> Vec<u8> {
|
||||
let text = |name: &str, y: u32, aa: &str| {
|
||||
format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /Rect [20 {y} 180 {}] /P 3 0 R /F 4 {aa} >>", y + 20)
|
||||
};
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /Names << /JavaScript << /Names [(helpers) 10 0 R] >> >> /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 7 0 R] /CO [6 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 8 0 R >> >> >> >>".to_string(),
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 300] >>".to_string(),
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 7 0 R] >>".to_string(),
|
||||
text("price", 250, "/V (2.5)"),
|
||||
text("qty", 220, "/AA << /V << /S /JavaScript /JS (if (event.value > 100) { app.alert('At most 100'); event.rc = false; }) >> >>"),
|
||||
text("total", 190, "/AA << /C << /S /JavaScript /JS (event.value = times\\(getField('price').value, getField('qty').value\\);) >> /F << /S /JavaScript /JS (event.value = 'USD ' + util.printf('%.2f', event.value);) >> >>"),
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (go) /Rect [20 150 80 170] /P 3 0 R /F 4 >>".to_string(),
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".to_string(),
|
||||
"<< >>".to_string(),
|
||||
"<< /S /JavaScript /JS (function times\\(a, b\\) { console.println('times'); return a * b; }) >>".to_string(),
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_javascript_validates_calculates_and_formats() {
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
let id = s.open("order.pdf", None, Arc::new(scripted_form()), None).unwrap();
|
||||
let value = |s: &Session, n: &str| s.get(id).unwrap().form.iter().find(|f| f.name == n).unwrap().value.clone();
|
||||
s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("4".into()) }).unwrap();
|
||||
assert_eq!(value(&s, "total"), ["10"], "calculated by the script");
|
||||
assert!(page_texts(&s, id)[0].contains("USD 10.00"), "{:?}", page_texts(&s, id));
|
||||
let out = s.take_js_output(id);
|
||||
assert!(out.console.contains(&"times".to_string()), "{out:?}");
|
||||
|
||||
let err = s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("500".into()) }).unwrap_err();
|
||||
assert_eq!(err.to_string(), "At most 100");
|
||||
assert_eq!(value(&s, "qty"), ["4"]);
|
||||
|
||||
// A button script changes fields in one undoable step, and asks for a page.
|
||||
let o =
|
||||
s.run_javascript(id, "getField('price').value = 3; getField('total').readonly = true; app.alert('Done'); this.print();", Some("go")).unwrap();
|
||||
assert_eq!(o.alerts, ["Done"]);
|
||||
assert_eq!(o.requests, [js::Request::Print]);
|
||||
assert_eq!(value(&s, "total"), ["12"], "recalculated after the change");
|
||||
assert!(s.get(id).unwrap().form.iter().find(|f| f.name == "total").unwrap().read_only());
|
||||
assert_eq!(s.get(id).unwrap().can_undo(), Some("Run JavaScript"));
|
||||
|
||||
// JavaScript off: scripts don't run.
|
||||
s.set_javascript(false);
|
||||
s.apply(id, Edit::SetFieldValue { name: "qty".into(), value: FieldValue::Text("500".into()) }).unwrap();
|
||||
assert_eq!(value(&s, "total"), ["12"]);
|
||||
assert!(s.run_javascript(id, "1", None).is_err());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,17 @@ pub struct Actions {
|
|||
pub calculate: Calculate,
|
||||
/// Events whose script isn't one of the AF calls (they need the JavaScript engine).
|
||||
pub unsupported: Vec<&'static str>,
|
||||
/// Those scripts' JavaScript, run through [`crate::Scripts`].
|
||||
pub scripts: Scripts,
|
||||
}
|
||||
|
||||
/// Field scripts that aren't AF calls, by event.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Scripts {
|
||||
pub format: Option<String>,
|
||||
pub keystroke: Option<String>,
|
||||
pub validate: Option<String>,
|
||||
pub calculate: Option<String>,
|
||||
}
|
||||
|
||||
impl Actions {
|
||||
|
|
|
|||
|
|
@ -172,15 +172,24 @@ fn frame(doc: &Document, wd: &Dict, w: f64, h: f64) -> (String, f64) {
|
|||
|
||||
/// The appearance of a text or choice field's widget showing `values`.
|
||||
pub fn field_appearance(doc: &Document, f: &Field, w: &Widget, values: &[String]) -> Stream {
|
||||
field_appearance_as(doc, f, w, values, true)
|
||||
}
|
||||
|
||||
/// [`field_appearance`], where `format` false means `values` are already what to show (a
|
||||
/// custom Format script ran).
|
||||
pub fn field_appearance_as(doc: &Document, f: &Field, w: &Widget, values: &[String], format: bool) -> Stream {
|
||||
// The Format event: what is shown, not what is stored.
|
||||
let formatted: Vec<String>;
|
||||
let values =
|
||||
if matches!(f.kind, crate::FieldKind::Text | crate::FieldKind::Combo) && values.len() == 1 && f.actions.format != crate::af::Format::None {
|
||||
formatted = vec![crate::af::format_value(&f.actions.format, &values[0])];
|
||||
&formatted[..]
|
||||
} else {
|
||||
values
|
||||
};
|
||||
let values = if format
|
||||
&& matches!(f.kind, crate::FieldKind::Text | crate::FieldKind::Combo)
|
||||
&& values.len() == 1
|
||||
&& f.actions.format != crate::af::Format::None
|
||||
{
|
||||
formatted = vec![crate::af::format_value(&f.actions.format, &values[0])];
|
||||
&formatted[..]
|
||||
} else {
|
||||
values
|
||||
};
|
||||
let wobj = doc.get(w.obj);
|
||||
let wd = wobj.as_dict().cloned().unwrap_or_default();
|
||||
let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0));
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ fn rgb(c: [f64; 3]) -> Object {
|
|||
}
|
||||
|
||||
/// The AcroForm dictionary's reference, creating the form (with `/Helv` in `/DR`) if needed.
|
||||
fn ensure_form(doc: &mut Document) -> Result<ObjRef, FormError> {
|
||||
pub(crate) fn ensure_form(doc: &mut Document) -> Result<ObjRef, FormError> {
|
||||
let root = doc.root().ok_or(FormError::NoForm)?;
|
||||
let existing = doc.get(root).as_dict().and_then(|d| d.get(b"AcroForm").cloned());
|
||||
let r = match existing {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,14 @@ use printcraft_cos::{Dict, Document, ObjRef, Object, PdfString};
|
|||
pub mod af;
|
||||
pub mod appearance;
|
||||
mod author;
|
||||
mod scripting;
|
||||
pub use author::{
|
||||
BorderStyle, FieldFont, FieldProps, Look, NewField, add_field, delete_field, duplicate_field, look, redraw_field, set_button_icon, set_props,
|
||||
};
|
||||
pub use scripting::{
|
||||
FieldChange, FieldEvent, NoScripts, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named, set_document_script,
|
||||
set_field_script,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
@ -236,7 +241,10 @@ fn actions_of(doc: &Document, d: &Dict, widgets: &[ObjRef]) -> af::Actions {
|
|||
if let Some(f) = js(b"F") {
|
||||
match af::parse_format(&f) {
|
||||
Some(fm) => out.format = fm,
|
||||
None if !f.trim().is_empty() => out.unsupported.push("format"),
|
||||
None if !f.trim().is_empty() => {
|
||||
out.unsupported.push("format");
|
||||
out.scripts.format = Some(f);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -247,21 +255,30 @@ fn actions_of(doc: &Document, d: &Dict, widgets: &[ObjRef]) -> af::Actions {
|
|||
match af::parse_format(&k) {
|
||||
Some(fm @ af::Format::Mask(_)) => out.format = fm,
|
||||
Some(_) => {}
|
||||
None if !k.trim().is_empty() => out.unsupported.push("keystroke"),
|
||||
None if !k.trim().is_empty() => {
|
||||
out.unsupported.push("keystroke");
|
||||
out.scripts.keystroke = Some(k);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if let Some(v) = js(b"V") {
|
||||
match af::parse_validate(&v) {
|
||||
Some(vv) => out.validate = vv,
|
||||
None if !v.trim().is_empty() => out.unsupported.push("validate"),
|
||||
None if !v.trim().is_empty() => {
|
||||
out.unsupported.push("validate");
|
||||
out.scripts.validate = Some(v);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if let Some(c) = js(b"C") {
|
||||
match af::parse_calculate(&c) {
|
||||
Some(cc) => out.calculate = cc,
|
||||
None if !c.trim().is_empty() => out.unsupported.push("calculate"),
|
||||
None if !c.trim().is_empty() => {
|
||||
out.unsupported.push("calculate");
|
||||
out.scripts.calculate = Some(c);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -581,6 +598,11 @@ fn walk(
|
|||
|
||||
/// Fill the field `name`.
|
||||
pub fn set_value(doc: &mut Document, name: &str, value: &FieldValue) -> Result<(), FormError> {
|
||||
set_value_with(doc, name, value, &mut NoScripts)
|
||||
}
|
||||
|
||||
/// [`set_value`], running the fields' JavaScript through `scripts`.
|
||||
pub fn set_value_with(doc: &mut Document, name: &str, value: &FieldValue, scripts: &mut dyn Scripts) -> Result<(), FormError> {
|
||||
let all = fields(doc);
|
||||
if all.is_empty() {
|
||||
return Err(FormError::NoForm);
|
||||
|
|
@ -589,16 +611,22 @@ pub fn set_value(doc: &mut Document, name: &str, value: &FieldValue) -> Result<(
|
|||
if f.read_only() {
|
||||
return Err(FormError::ReadOnly(name.into()));
|
||||
}
|
||||
write_value(doc, f, value)?;
|
||||
recalculate(doc)?;
|
||||
write_value(doc, f, value, scripts)?;
|
||||
recalculate_with(doc, scripts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run every field's Calculate script in the form's calculation order (`/CO`, then the other
|
||||
/// calculated fields), as Acrobat does after any value changes. Returns how many changed.
|
||||
pub fn recalculate(doc: &mut Document) -> Result<usize, FormError> {
|
||||
recalculate_with(doc, &mut NoScripts)
|
||||
}
|
||||
|
||||
/// [`recalculate`], running custom Calculate (and Format) scripts through `scripts`.
|
||||
pub fn recalculate_with(doc: &mut Document, scripts: &mut dyn Scripts) -> Result<usize, FormError> {
|
||||
let all = fields(doc);
|
||||
if !all.iter().any(|f| f.actions.calculate != af::Calculate::None) {
|
||||
let calculated = |f: &Field| f.actions.calculate != af::Calculate::None || f.actions.scripts.calculate.is_some();
|
||||
if !all.iter().any(calculated) {
|
||||
return Ok(0);
|
||||
}
|
||||
let co: Vec<ObjRef> = acroform(doc)
|
||||
|
|
@ -609,7 +637,7 @@ pub fn recalculate(doc: &mut Document) -> Result<usize, FormError> {
|
|||
.collect();
|
||||
let mut order: Vec<String> = co.iter().filter_map(|r| all.iter().find(|f| f.obj == *r)).map(|f| f.name.clone()).collect();
|
||||
for f in &all {
|
||||
if f.actions.calculate != af::Calculate::None && !order.contains(&f.name) {
|
||||
if calculated(f) && !order.contains(&f.name) {
|
||||
order.push(f.name.clone());
|
||||
}
|
||||
}
|
||||
|
|
@ -621,11 +649,25 @@ pub fn recalculate(doc: &mut Document) -> Result<usize, FormError> {
|
|||
let prefix = format!("{n}.");
|
||||
now.iter().filter(|x| x.name == n || x.name.starts_with(&prefix)).flat_map(|x| x.value.first().cloned()).collect()
|
||||
};
|
||||
let Some(v) = af::calculate(&f.actions.calculate, &lookup) else { continue };
|
||||
if f.value.first().map(String::as_str).unwrap_or("") != v && f.kind == FieldKind::Text {
|
||||
let current = f.value.first().cloned().unwrap_or_default();
|
||||
let v = if let Some(js) = &f.actions.scripts.calculate {
|
||||
let f = f.clone();
|
||||
let r = scripts.run(FieldEvent::Calculate, js, &f, ¤t, &now);
|
||||
scripting::apply_changes(doc, &r.changes, &f.name)?;
|
||||
if !r.rc {
|
||||
continue;
|
||||
}
|
||||
r.value
|
||||
} else {
|
||||
let Some(v) = af::calculate(&f.actions.calculate, &lookup) else { continue };
|
||||
v
|
||||
};
|
||||
let now = fields(doc);
|
||||
let Some(f) = now.iter().find(|f| f.name == name) else { continue };
|
||||
if current != v && matches!(f.kind, FieldKind::Text | FieldKind::Combo) {
|
||||
let f = f.clone();
|
||||
doc.update_dict(f.obj, |d| d.set(b"V".to_vec(), PdfString::text(&v)))?;
|
||||
redraw(doc, &f, std::slice::from_ref(&v))?;
|
||||
redraw(doc, &f, std::slice::from_ref(&v), scripts)?;
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -636,7 +678,7 @@ fn invalid<T>(m: impl Into<String>) -> Result<T, FormError> {
|
|||
Err(FormError::Invalid(m.into()))
|
||||
}
|
||||
|
||||
fn write_value(doc: &mut Document, f: &Field, value: &FieldValue) -> Result<(), FormError> {
|
||||
fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scripts: &mut dyn Scripts) -> Result<(), FormError> {
|
||||
match (f.kind, value) {
|
||||
(FieldKind::Text, FieldValue::Text(t)) => {
|
||||
if let Some(max) = f.max_len
|
||||
|
|
@ -647,6 +689,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue) -> Result<(),
|
|||
// Keystroke (on commit) and Validate, as Acrobat runs them before accepting a value.
|
||||
let t = &af::keystroke(&f.actions.format, &f.name, t).map_err(FormError::Invalid)?;
|
||||
af::validate(&f.actions.validate, t).map_err(FormError::Invalid)?;
|
||||
let t = &scripting::accept(doc, f, t, scripts)?;
|
||||
doc.update_dict(f.obj, |d| {
|
||||
if t.is_empty() {
|
||||
d.remove(b"V");
|
||||
|
|
@ -655,7 +698,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue) -> Result<(),
|
|||
}
|
||||
d.remove(b"RV");
|
||||
})?;
|
||||
redraw(doc, f, std::slice::from_ref(t))
|
||||
redraw(doc, f, std::slice::from_ref(t), scripts)
|
||||
}
|
||||
(FieldKind::CheckBox, v) => {
|
||||
let state = f.widgets.iter().find_map(|w| w.on_state.clone()).unwrap_or_else(|| "Yes".into());
|
||||
|
|
@ -735,7 +778,7 @@ fn write_value(doc: &mut Document, f: &Field, value: &FieldValue) -> Result<(),
|
|||
d.set(b"I".to_vec(), Object::Array(indices));
|
||||
}
|
||||
})?;
|
||||
redraw(doc, f, &exports)
|
||||
redraw(doc, f, &exports, scripts)
|
||||
}
|
||||
(FieldKind::PushButton, _) => invalid(format!("{:?} is a button; it has no value", f.name)),
|
||||
(FieldKind::Signature, _) => invalid(format!("{:?} is a signature field; sign it with Fill & Sign or a digital ID", f.name)),
|
||||
|
|
@ -765,9 +808,16 @@ fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), For
|
|||
}
|
||||
|
||||
/// Regenerate the normal appearance of every widget of a text or choice field.
|
||||
fn redraw(doc: &mut Document, f: &Field, values: &[String]) -> Result<(), FormError> {
|
||||
fn redraw(doc: &mut Document, f: &Field, values: &[String], scripts: &mut dyn Scripts) -> Result<(), FormError> {
|
||||
let shown = match values {
|
||||
[one] if matches!(f.kind, FieldKind::Text | FieldKind::Combo) => scripting::formatted(doc, f, one, scripts),
|
||||
_ => None,
|
||||
};
|
||||
for w in &f.widgets {
|
||||
let stream = appearance::field_appearance(doc, f, w, values);
|
||||
let stream = match &shown {
|
||||
Some(s) => appearance::field_appearance_as(doc, f, w, std::slice::from_ref(s), false),
|
||||
None => appearance::field_appearance(doc, f, w, values),
|
||||
};
|
||||
let ap = doc.add(Object::Stream(stream));
|
||||
let mut apd = Dict::new();
|
||||
apd.set(b"N".to_vec(), Object::Ref(ap));
|
||||
|
|
@ -805,7 +855,7 @@ pub fn reset(doc: &mut Document, names: Option<&[String]>) -> Result<usize, Form
|
|||
// A reset is not blocked by NoToggleToOff: go through the writer directly.
|
||||
let mut tmp = f.clone();
|
||||
tmp.flags &= !flags::NO_TOGGLE_TO_OFF;
|
||||
write_value(doc, &tmp, &v)?;
|
||||
write_value(doc, &tmp, &v, &mut NoScripts)?;
|
||||
changed += 1;
|
||||
}
|
||||
recalculate(doc)?;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,310 @@
|
|||
//! Field scripts beyond the AF calls: the hook through which the engine runs JavaScript.
|
||||
//!
|
||||
//! `forms` does not depend on a JavaScript engine (architecture §3: `forms → js` is not an
|
||||
//! allowed edge). It calls a [`Scripts`] implementation at the points Acrobat runs field
|
||||
//! events — Keystroke (on commit) and Validate before a value is accepted, Calculate in the
|
||||
//! calculation order, Format when the appearance is drawn — and applies what the script asks
|
||||
//! for: the event's value, `rc`, and changes to other fields.
|
||||
|
||||
use printcraft_cos::{Document, Object, PdfString};
|
||||
|
||||
use crate::{Field, FieldKind, FieldValue, FormError, fields, flags};
|
||||
|
||||
/// The field events forms run scripts for.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FieldEvent {
|
||||
Keystroke,
|
||||
Validate,
|
||||
Calculate,
|
||||
Format,
|
||||
}
|
||||
|
||||
impl FieldEvent {
|
||||
/// `event.name`.
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
FieldEvent::Keystroke => "Keystroke",
|
||||
FieldEvent::Validate => "Validate",
|
||||
FieldEvent::Calculate => "Calculate",
|
||||
FieldEvent::Format => "Format",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A change a script made to a field (`None`: unchanged).
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct FieldChange {
|
||||
pub name: String,
|
||||
pub value: Option<Vec<String>>,
|
||||
pub read_only: Option<bool>,
|
||||
pub required: Option<bool>,
|
||||
/// `display.visible` 0, `hidden` 1, `noPrint` 2, `noView` 3.
|
||||
pub display: Option<i32>,
|
||||
}
|
||||
|
||||
/// What a script did.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ScriptResult {
|
||||
/// `event.rc`.
|
||||
pub rc: bool,
|
||||
/// `event.value` afterwards.
|
||||
pub value: String,
|
||||
pub changes: Vec<FieldChange>,
|
||||
/// Why it rejected the value (its alert), for the error shown.
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Runs field scripts (JavaScript).
|
||||
pub trait Scripts {
|
||||
/// Run `script` for `event` on `target`, whose value is `value`; `fields` is the whole form.
|
||||
fn run(&mut self, event: FieldEvent, script: &str, target: &Field, value: &str, fields: &[Field]) -> ScriptResult;
|
||||
}
|
||||
|
||||
/// No script engine (or JavaScript turned off): scripts change nothing.
|
||||
pub struct NoScripts;
|
||||
|
||||
impl Scripts for NoScripts {
|
||||
fn run(&mut self, _: FieldEvent, _: &str, _: &Field, value: &str, _: &[Field]) -> ScriptResult {
|
||||
ScriptResult { rc: true, value: value.to_string(), changes: Vec::new(), message: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the Keystroke (commit) and Validate scripts for a new text value. Returns the value to
|
||||
/// store, or why it is rejected.
|
||||
pub(crate) fn accept(doc: &mut Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Result<String, FormError> {
|
||||
let mut v = value.to_string();
|
||||
for (event, script, default) in [
|
||||
(FieldEvent::Keystroke, &f.actions.scripts.keystroke, "The value entered does not match the format of the field"),
|
||||
(FieldEvent::Validate, &f.actions.scripts.validate, "The value entered is not valid for the field"),
|
||||
] {
|
||||
let Some(js) = script else { continue };
|
||||
let all = fields(doc);
|
||||
let r = scripts.run(event, js, f, &v, &all);
|
||||
apply_changes(doc, &r.changes, &f.name)?;
|
||||
if !r.rc {
|
||||
return Err(FormError::Invalid(r.message.unwrap_or_else(|| format!("{default} [ {} ]", f.name))));
|
||||
}
|
||||
v = r.value;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// The text a custom Format script shows for `value`, if the field has one.
|
||||
pub(crate) fn formatted(doc: &Document, f: &Field, value: &str, scripts: &mut dyn Scripts) -> Option<String> {
|
||||
let js = f.actions.scripts.format.as_ref()?;
|
||||
let r = scripts.run(FieldEvent::Format, js, f, value, &fields(doc));
|
||||
Some(r.value)
|
||||
}
|
||||
|
||||
/// Apply script changes to other fields (values written without re-running their scripts;
|
||||
/// read-only, required and visibility as flags). `except` is the event's own field, whose value
|
||||
/// the event sets.
|
||||
pub(crate) fn apply_changes(doc: &mut Document, changes: &[FieldChange], except: &str) -> Result<(), FormError> {
|
||||
for c in changes {
|
||||
let all = fields(doc);
|
||||
let Some(f) = all.iter().find(|f| f.name == c.name) else { continue };
|
||||
if let Some(v) = &c.value
|
||||
&& c.name != except
|
||||
&& *v != f.value
|
||||
{
|
||||
let value = match f.kind {
|
||||
FieldKind::Text => FieldValue::Text(v.first().cloned().unwrap_or_default()),
|
||||
FieldKind::CheckBox => FieldValue::Check(!v.is_empty()),
|
||||
FieldKind::Radio => FieldValue::Radio(v.first().cloned()),
|
||||
FieldKind::Combo | FieldKind::List => FieldValue::Choice(v.clone()),
|
||||
_ => continue,
|
||||
};
|
||||
let mut tmp = f.clone();
|
||||
tmp.flags &= !flags::NO_TOGGLE_TO_OFF;
|
||||
tmp.actions = Default::default();
|
||||
crate::write_value(doc, &tmp, &value, &mut NoScripts)?;
|
||||
}
|
||||
let mut ff = f.flags;
|
||||
for (on, bit) in [(c.read_only, flags::READ_ONLY), (c.required, flags::REQUIRED)] {
|
||||
match on {
|
||||
Some(true) => ff |= bit,
|
||||
Some(false) => ff &= !bit,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
if ff != f.flags {
|
||||
doc.update_dict(f.obj, |d| d.set(b"Ff".to_vec(), Object::Int(ff as i64)))?;
|
||||
}
|
||||
if let Some(display) = c.display {
|
||||
for w in &f.widgets {
|
||||
let old = doc.get(w.obj).as_dict().and_then(|d| d.int(b"F")).unwrap_or(0);
|
||||
// Hidden 2, Print 4, NoView 32 (Table 167).
|
||||
let base = old & !(2 | 4 | 32);
|
||||
let new = match display {
|
||||
1 => base | 2 | 4,
|
||||
2 => base,
|
||||
3 => base | 4 | 32,
|
||||
_ => base | 4,
|
||||
};
|
||||
if new != old {
|
||||
doc.update_dict(w.obj, |d| d.set(b"F".to_vec(), Object::Int(new)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply what a button or console script changed (values, read-only, required, visibility),
|
||||
/// then recalculate.
|
||||
pub fn apply_script_changes(doc: &mut Document, changes: &[FieldChange], scripts: &mut dyn Scripts) -> Result<(), FormError> {
|
||||
apply_changes(doc, changes, "")?;
|
||||
crate::recalculate_with(doc, scripts)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Document-level JavaScripts (the catalog's `/Names /JavaScript` tree), in name order: they
|
||||
/// define the functions field scripts call.
|
||||
pub fn document_scripts(doc: &Document) -> Vec<String> {
|
||||
document_scripts_named(doc).into_iter().map(|(_, js)| js).collect()
|
||||
}
|
||||
|
||||
/// [`document_scripts`] with their names.
|
||||
pub fn document_scripts_named(doc: &Document) -> Vec<(String, String)> {
|
||||
let Some(root) = doc.root() else { return Vec::new() };
|
||||
let cat = doc.get(root);
|
||||
let Some(names) = cat.as_dict().and_then(|c| c.get(b"Names")).map(|n| doc.resolve(n)) else { return Vec::new() };
|
||||
let Some(tree) = names.as_dict().and_then(|n| n.get(b"JavaScript")).map(|t| doc.resolve(t)) else { return Vec::new() };
|
||||
let mut out: Vec<(String, String)> = Vec::new();
|
||||
let mut stack = vec![(*tree).clone()];
|
||||
let mut seen = 0;
|
||||
while let Some(node) = stack.pop() {
|
||||
seen += 1;
|
||||
if seen > 10_000 {
|
||||
break;
|
||||
}
|
||||
let Some(d) = node.as_dict() else { continue };
|
||||
if let Some(kids) = d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()) {
|
||||
stack.extend(kids.iter().map(|k| (*doc.resolve(k)).clone()));
|
||||
}
|
||||
if let Some(pairs) = d.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_array().cloned()) {
|
||||
for pair in pairs.chunks(2) {
|
||||
let [k, v] = pair else { continue };
|
||||
let key = match &*doc.resolve(k) {
|
||||
Object::String(s) => s.to_text(),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(js) = crate::script(doc, v) {
|
||||
out.push((key, js));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort();
|
||||
out
|
||||
}
|
||||
|
||||
/// Add (or replace) a document-level JavaScript named `name`.
|
||||
pub fn set_document_script(doc: &mut Document, name: &str, js: Option<&str>) -> Result<(), FormError> {
|
||||
let root = doc.root().ok_or(FormError::NoForm)?;
|
||||
let cat = doc.get(root).as_dict().cloned().unwrap_or_default();
|
||||
let mut names = cat.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_dict().cloned()).unwrap_or_default();
|
||||
let tree = names.get(b"JavaScript").map(|t| doc.resolve(t)).and_then(|t| t.as_dict().cloned()).unwrap_or_default();
|
||||
if tree.contains(b"Kids") {
|
||||
return Err(FormError::Invalid("the document's JavaScript name tree has kids; edit it with a full editor".into()));
|
||||
}
|
||||
let mut pairs: Vec<Object> = tree.get(b"Names").map(|n| doc.resolve(n)).and_then(|n| n.as_array().cloned()).unwrap_or_default();
|
||||
let mut kept = Vec::new();
|
||||
for pair in pairs.chunks(2) {
|
||||
if let [k, v] = pair
|
||||
&& !matches!(&*doc.resolve(k), Object::String(s) if s.to_text() == name)
|
||||
{
|
||||
kept.push((k.clone(), v.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(js) = js {
|
||||
let mut action = printcraft_cos::Dict::new();
|
||||
action.set(b"S".to_vec(), Object::name("JavaScript"));
|
||||
action.set(b"JS".to_vec(), PdfString::text(js));
|
||||
let a = doc.add(Object::Dict(action));
|
||||
kept.push((Object::String(PdfString::text(name)), Object::Ref(a)));
|
||||
}
|
||||
kept.sort_by_key(|(k, _)| match k {
|
||||
Object::String(s) => s.to_text(),
|
||||
_ => String::new(),
|
||||
});
|
||||
pairs = kept.into_iter().flat_map(|(k, v)| [k, v]).collect();
|
||||
let mut t = printcraft_cos::Dict::new();
|
||||
t.set(b"Names".to_vec(), Object::Array(pairs));
|
||||
names.set(b"JavaScript".to_vec(), Object::Dict(t));
|
||||
doc.update_dict(root, |c| c.set(b"Names".to_vec(), Object::Dict(names)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Field Properties ▸ Format / Validate / Calculate ▸ custom script, or Actions ▸ Mouse Up ▸
|
||||
/// Run a JavaScript: set (or remove, with `None`) field `name`'s script for `event` — one of
|
||||
/// `keystroke`, `format`, `validate`, `calculate` (the field's `/AA` K, F, V, C) or `mouse_up`
|
||||
/// (its widgets' `/A`). Calculated fields join the calculation order (`/CO`).
|
||||
pub fn set_field_script(doc: &mut Document, name: &str, event: &str, js: Option<&str>) -> Result<(), FormError> {
|
||||
let all = fields(doc);
|
||||
let f = all.iter().find(|f| f.name == name).ok_or_else(|| FormError::NoSuchField(name.into()))?.clone();
|
||||
let action = |doc: &mut Document, js: &str| {
|
||||
let mut a = printcraft_cos::Dict::new();
|
||||
a.set(b"S".to_vec(), Object::name("JavaScript"));
|
||||
a.set(b"JS".to_vec(), PdfString::text(js));
|
||||
Object::Ref(doc.add(Object::Dict(a)))
|
||||
};
|
||||
if event == "mouse_up" {
|
||||
for w in &f.widgets {
|
||||
let a = js.map(|j| action(doc, j));
|
||||
doc.update_dict(w.obj, |d| match a {
|
||||
Some(a) => d.set(b"A".to_vec(), a),
|
||||
None => {
|
||||
d.remove(b"A");
|
||||
}
|
||||
})?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let key: &[u8] = match event {
|
||||
"keystroke" => b"K",
|
||||
"format" => b"F",
|
||||
"validate" => b"V",
|
||||
"calculate" => b"C",
|
||||
other => return Err(FormError::Invalid(format!("unknown field event {other:?} (keystroke, format, validate, calculate, mouse_up)"))),
|
||||
};
|
||||
let mut aa = doc.get(f.obj).as_dict().and_then(|d| d.get(b"AA").map(|a| doc.resolve(a))).and_then(|a| a.as_dict().cloned()).unwrap_or_default();
|
||||
match js {
|
||||
Some(j) => {
|
||||
let a = action(doc, j);
|
||||
aa.set(key.to_vec(), a);
|
||||
}
|
||||
None => {
|
||||
aa.remove(key);
|
||||
}
|
||||
}
|
||||
doc.update_dict(f.obj, |d| {
|
||||
if aa.is_empty() {
|
||||
d.remove(b"AA");
|
||||
} else {
|
||||
d.set(b"AA".to_vec(), Object::Dict(aa));
|
||||
}
|
||||
})?;
|
||||
if key == b"C" {
|
||||
let af_ref = crate::author::ensure_form(doc)?;
|
||||
let mut co: Vec<Object> = doc
|
||||
.get(af_ref)
|
||||
.as_dict()
|
||||
.and_then(|d| d.get(b"CO").cloned())
|
||||
.map(|c| doc.resolve(&c))
|
||||
.and_then(|c| c.as_array().cloned())
|
||||
.unwrap_or_default();
|
||||
co.retain(|o| o.as_ref() != Some(f.obj));
|
||||
if js.is_some() {
|
||||
co.push(Object::Ref(f.obj));
|
||||
}
|
||||
doc.update_dict(af_ref, |d| {
|
||||
if co.is_empty() {
|
||||
d.remove(b"CO");
|
||||
} else {
|
||||
d.set(b"CO".to_vec(), Object::Array(co));
|
||||
}
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "printcraft-js"
|
||||
description = "Acrobat JavaScript: a sandboxed interpreter (boa) with the form object model subset (event, getField, app, util, console)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
boa_engine = { version = "0.22.0", default-features = false, features = ["annex-b"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
boa_engine = { version = "0.22.0", default-features = false, features = ["annex-b", "js"] }
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,150 @@
|
|||
use super::*;
|
||||
|
||||
fn fields() -> Vec<FieldState> {
|
||||
let mut agree = FieldState::new("agree", FieldType::CheckBox, vec![]);
|
||||
agree.options = vec![("Yes".into(), "Yes".into())];
|
||||
let mut size = FieldState::new("size", FieldType::ComboBox, vec!["m".into()]);
|
||||
size.options = vec![("s".into(), "Small".into()), ("m".into(), "Medium".into()), ("l".into(), "Large".into())];
|
||||
vec![
|
||||
FieldState::new("price", FieldType::Text, vec!["12.5".into()]),
|
||||
FieldState::new("qty", FieldType::Text, vec!["4".into()]),
|
||||
FieldState::new("total", FieldType::Text, vec![]),
|
||||
FieldState::new("zip", FieldType::Text, vec!["02134".into()]),
|
||||
agree,
|
||||
size,
|
||||
]
|
||||
}
|
||||
|
||||
fn doc() -> DocInfo {
|
||||
DocInfo { file_name: "order.pdf".into(), num_pages: 3, page: 0, info: vec![("title".into(), "Order".into())] }
|
||||
}
|
||||
|
||||
fn go(script: &str, event: &Event) -> Outcome {
|
||||
run(script, event, &doc(), &fields(), &[], Limits::default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_scripts_read_fields_and_set_the_value() {
|
||||
let o = go("event.value = this.getField('price').value * getField('qty').value;", &Event::field("Calculate", "total", ""));
|
||||
assert_eq!(o.error, None);
|
||||
assert_eq!(o.value, "50");
|
||||
assert!(o.rc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numbers_with_leading_zeros_stay_text() {
|
||||
let o = go("event.value = typeof getField('zip').value + ':' + typeof getField('qty').value;", &Event::field("Calculate", "total", ""));
|
||||
assert_eq!(o.value, "string:number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_scripts_reject_with_rc_and_alert() {
|
||||
let o = go("if (event.value > 10) { app.alert('Too many'); event.rc = false; }", &Event::field("Validate", "qty", "11"));
|
||||
assert!(!o.rc);
|
||||
assert_eq!(o.alerts, ["Too many"]);
|
||||
let o = go("if (event.value > 10) { event.rc = false; }", &Event::field("Validate", "qty", "3"));
|
||||
assert!(o.rc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keystroke_scripts_see_the_change() {
|
||||
let mut e = Event::field("Keystroke", "qty", "1");
|
||||
e.change = "x".into();
|
||||
e.will_commit = false;
|
||||
let o = go("event.rc = /^[0-9]*$/.test(event.change);", &e);
|
||||
assert!(!o.rc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scripts_change_other_fields_and_their_properties() {
|
||||
let o = go(
|
||||
"var t = getField('total'); t.value = 99; t.readonly = true; t.display = display.hidden; \
|
||||
getField('agree').checkThisBox(0, true); getField('size').value = 'l'; t.textColor = color.red;",
|
||||
&Event::field("Mouse Up", "agree", ""),
|
||||
);
|
||||
assert_eq!(o.error, None);
|
||||
let total = o.changed.iter().find(|f| f.name == "total").unwrap();
|
||||
assert_eq!(total.value, ["99"]);
|
||||
assert!(total.readonly);
|
||||
assert_eq!(total.display, DISPLAY_HIDDEN);
|
||||
assert_eq!(total.text_color.as_deref(), Some(&["RGB".to_string(), "1".into(), "0".into(), "0".into()][..]));
|
||||
assert_eq!(o.changed.iter().find(|f| f.name == "agree").unwrap().value, ["Yes"]);
|
||||
assert_eq!(o.changed.iter().find(|f| f.name == "size").unwrap().value, ["l"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn choice_and_check_box_object_model() {
|
||||
let o = go(
|
||||
"var s = getField('size'); event.value = [s.numItems, s.getItemAt(2, false), s.currentValueIndices, s.valueAsString, \
|
||||
getField('agree').value, getField('agree').isBoxChecked(0), s.type].join('|');",
|
||||
&Event::field("Calculate", "total", ""),
|
||||
);
|
||||
assert_eq!(o.value, "3|Large|1|m|Off|false|combobox");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn util_printf_printd_and_printx() {
|
||||
let o = go(
|
||||
"event.value = [util.printf('%,0.2f', 1234567.891), util.printf('%05d|%s|%x', 42, 'hi', 255), util.printf('%,2.2f', 1234.5), \
|
||||
util.printd('mmm d, yyyy HH:MM', new Date(2024, 0, 5, 9, 7)), util.printd('dddd', new Date(2024, 0, 5)), \
|
||||
util.printx('(999) 999-9999', '5551234567'), util.printx('>AAA', 'abc')].join('|');",
|
||||
&Event::field("Calculate", "total", ""),
|
||||
);
|
||||
assert_eq!(o.error, None);
|
||||
assert_eq!(o.value, "1,234,567.89|00042|hi|FF|1.234,50|Jan 5, 2024 09:07|Friday|(555) 123-4567|ABC");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_requests_and_console() {
|
||||
let o = go(
|
||||
"console.println('hello ' + this.documentFileName + ' ' + numPages + ' ' + info.title); this.pageNum = 2; \
|
||||
this.resetForm(['qty']); this.print(); app.launchURL('https://example.org'); this.submitForm({cURL: 'https://example.org/f'});",
|
||||
&Event::field("Mouse Up", "agree", ""),
|
||||
);
|
||||
assert_eq!(o.error, None);
|
||||
assert_eq!(o.console, ["hello order.pdf 3 Order"]);
|
||||
assert_eq!(
|
||||
o.requests,
|
||||
[
|
||||
Request::GoToPage(2),
|
||||
Request::Reset(vec!["qty".into()]),
|
||||
Request::Print,
|
||||
Request::LaunchUrl("https://example.org".into()),
|
||||
Request::Submit("https://example.org/f".into())
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_level_functions_are_available() {
|
||||
let o = run(
|
||||
"event.value = double(getField('qty').value);",
|
||||
&Event::field("Calculate", "total", ""),
|
||||
&doc(),
|
||||
&fields(),
|
||||
&["function double(x) { return x * 2; }".into()],
|
||||
Limits::default(),
|
||||
);
|
||||
assert_eq!(o.value, "8");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_and_runaway_scripts_are_contained() {
|
||||
let o = go("event.value = nosuch.thing;", &Event::field("Calculate", "total", "keep"));
|
||||
assert!(o.error.is_some());
|
||||
let o = run("while (true) {}", &Event::doc("Open"), &doc(), &fields(), &[], Limits { loop_iterations: 10_000, recursion: 64 });
|
||||
assert!(o.error.is_some(), "the loop limit stops it");
|
||||
let o = go("function f() { return f(); } f();", &Event::doc("Open"));
|
||||
assert!(o.error.is_some(), "the recursion limit stops it");
|
||||
let o = go("getField('nope').value", &Event::doc("Open"));
|
||||
assert!(o.error.as_deref().unwrap_or("").contains("null") || o.error.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sandbox_has_no_host_access() {
|
||||
let o = go(
|
||||
"event.value = [typeof require, typeof process, typeof fetch, typeof XMLHttpRequest, typeof importScripts].join(',');",
|
||||
&Event::doc("Open"),
|
||||
);
|
||||
assert_eq!(o.value, "undefined,undefined,undefined,undefined,undefined");
|
||||
}
|
||||
|
|
@ -1694,7 +1694,10 @@ fn run_button(app: &mut PrintCraftApp, index: usize, ctx: &egui::Context, name:
|
|||
app.notify(format!("{name} submits the form to {url}; PrintCraft doesn't send form data. Save the document to keep your entries."))
|
||||
}
|
||||
B::ImportIcon => app.choose_field_image(name),
|
||||
B::Script(_) => app.notify(format!("{name} runs a script that needs the JavaScript engine (M6)")),
|
||||
B::Script(js) => {
|
||||
let id = app.views[index].id;
|
||||
app.run_button_script(id, name, &js);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -224,6 +224,12 @@ impl PrintCraftApp {
|
|||
"export.text" => self.dialog = Some(Dialog::Export(crate::export_ui::ExportKind::Text)),
|
||||
"a11y.check" => self.start_accessibility_check(),
|
||||
"ocr.recognize" => self.dialog = Some(Dialog::RecognizeText),
|
||||
"tools.js_console" => self.dialog = Some(Dialog::JsConsole),
|
||||
"tools.document_js" => {
|
||||
self.doc_js = Default::default();
|
||||
self.dialog = Some(Dialog::DocumentJs);
|
||||
}
|
||||
"app.preferences" => self.dialog = Some(Dialog::Preferences),
|
||||
"ocr.recognize_batch" => self.ocr_files_dialog(),
|
||||
"edit.edit_text" => {
|
||||
self.quick_tool = crate::QuickTool::EditText;
|
||||
|
|
|
|||
|
|
@ -819,6 +819,20 @@ pub fn show(app: &mut PrintCraftApp, ctx: &egui::Context) {
|
|||
close = go || cancel;
|
||||
return;
|
||||
}
|
||||
Dialog::JsConsole => {
|
||||
ui.set_width(640.0);
|
||||
close = crate::js_ui::console_body(ui, app, &t);
|
||||
return;
|
||||
}
|
||||
Dialog::DocumentJs => {
|
||||
ui.set_width(640.0);
|
||||
close = crate::js_ui::document_js_body(ui, app, &t);
|
||||
return;
|
||||
}
|
||||
Dialog::Preferences => {
|
||||
close = crate::js_ui::preferences_body(ui, app, &t);
|
||||
return;
|
||||
}
|
||||
Dialog::RecognizeText => {
|
||||
let (go, cancel) = crate::ocr_ui::body(ui, app, &t);
|
||||
ocr_now = go;
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ impl PrintCraftApp {
|
|||
if std::mem::take(&mut view.comments.tool_done) && !self.comment_prefs.pinned {
|
||||
self.quick_tool = crate::QuickTool::Select;
|
||||
}
|
||||
let out = self.session.take_js_output(id);
|
||||
self.handle_js(id, out);
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ pub static ICONS: &[(&str, &[u8])] = &[
|
|||
("accessibility", include_bytes!("../../../assets/icons/accessibility.svg")),
|
||||
("align-center", include_bytes!("../../../assets/icons/align-center.svg")),
|
||||
("align-justify", include_bytes!("../../../assets/icons/align-justify.svg")),
|
||||
("file-code", include_bytes!("../../../assets/icons/file-code.svg")),
|
||||
("square-terminal", include_bytes!("../../../assets/icons/square-terminal.svg")),
|
||||
("align-left", include_bytes!("../../../assets/icons/align-left.svg")),
|
||||
("align-right", include_bytes!("../../../assets/icons/align-right.svg")),
|
||||
("app-window", include_bytes!("../../../assets/icons/app-window.svg")),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
//! Acrobat JavaScript in the shell: what scripts ask for (alerts, printing, navigation, links),
|
||||
//! the JavaScript console (⌘J), Document JavaScripts, and Preferences ▸ JavaScript.
|
||||
|
||||
use egui::{Align, Layout};
|
||||
use printcraft_engine::js::{JsOutput, Request};
|
||||
use printcraft_engine::{DocId, Edit};
|
||||
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{PrintCraftApp, widgets};
|
||||
|
||||
/// The JavaScript console: the input and the output so far.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct JsConsole {
|
||||
pub input: String,
|
||||
pub log: Vec<String>,
|
||||
}
|
||||
|
||||
/// Document JavaScripts: the script being edited.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct DocJsDraft {
|
||||
pub name: String,
|
||||
pub script: String,
|
||||
}
|
||||
|
||||
impl PrintCraftApp {
|
||||
/// Act on what scripts produced in document `id`: alerts are shown, console output goes to
|
||||
/// the console, and print / page / link requests are carried out (form submissions are
|
||||
/// reported, never sent).
|
||||
pub fn handle_js(&mut self, id: DocId, out: JsOutput) {
|
||||
if out.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.js_console.log.extend(out.console.iter().cloned());
|
||||
for e in &out.errors {
|
||||
self.js_console.log.push(format!("Error: {e}"));
|
||||
}
|
||||
let view = self.views.iter().position(|v| v.id == id);
|
||||
for r in out.requests {
|
||||
match r {
|
||||
Request::Print => self.open_print(),
|
||||
Request::GoToPage(p) => {
|
||||
if let Some(i) = view {
|
||||
self.views[i].go_to_page(p);
|
||||
}
|
||||
}
|
||||
Request::LaunchUrl(u) => {
|
||||
if let Some(ctx) = &self.ctx {
|
||||
ctx.open_url(egui::OpenUrl::new_tab(u));
|
||||
}
|
||||
}
|
||||
Request::Submit(u) => self.notify(format!(
|
||||
"The form asks to be submitted to {u}; PrintCraft doesn't send form data. Save the document to keep your entries."
|
||||
)),
|
||||
Request::Focus(_) | Request::Beep | Request::Reset(_) => {}
|
||||
}
|
||||
}
|
||||
if let Some(a) = out.alerts.last() {
|
||||
self.notify(a.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a push button's JavaScript (its Mouse Up action).
|
||||
pub fn run_button_script(&mut self, id: DocId, field: &str, script: &str) {
|
||||
match self.session.run_javascript(id, script, Some(field)) {
|
||||
Ok(o) => {
|
||||
if let Some(i) = self.views.iter().position(|v| v.id == id)
|
||||
&& let Some(info) = self.session.get(id).map(|d| d.info.clone())
|
||||
{
|
||||
self.views[i].document_changed(&info);
|
||||
}
|
||||
let out = JsOutput { alerts: o.alerts, console: o.console, requests: o.requests, errors: o.error.into_iter().collect() };
|
||||
self.handle_js(id, out);
|
||||
}
|
||||
Err(e) => self.notify(format!("{field}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The console's Run: evaluate the input in the active document.
|
||||
pub fn run_console(&mut self) {
|
||||
let Some((i, id)) = self.active_ids() else { return };
|
||||
let script = self.js_console.input.clone();
|
||||
if script.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
self.js_console.log.push(format!("> {}", script.trim()));
|
||||
match self.session.run_javascript(id, &script, None) {
|
||||
Ok(o) => {
|
||||
if let Some(info) = self.session.get(id).map(|d| d.info.clone()) {
|
||||
self.views[i].document_changed(&info);
|
||||
}
|
||||
let (error, result) = (o.error.clone(), o.result.clone());
|
||||
let out = JsOutput { alerts: o.alerts, console: o.console, requests: o.requests, errors: Vec::new() };
|
||||
self.handle_js(id, out);
|
||||
match error {
|
||||
Some(e) => self.js_console.log.push(format!("Error: {e}")),
|
||||
None => self.js_console.log.extend(result),
|
||||
}
|
||||
}
|
||||
Err(e) => self.js_console.log.push(format!("Error: {e}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buttons(ui: &mut egui::Ui, primary: &str, others: &[&str]) -> Option<String> {
|
||||
let mut clicked = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
// Stable ids: the console's output above changes how many widgets come first.
|
||||
if ui.push_id(primary, |ui| widgets::pill_button(ui, primary, true)).inner.clicked() {
|
||||
clicked = Some(primary.to_string());
|
||||
}
|
||||
for o in others {
|
||||
if ui.push_id(o, |ui| widgets::pill_button(ui, o, false)).inner.clicked() {
|
||||
clicked = Some(o.to_string());
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
clicked
|
||||
}
|
||||
|
||||
/// The JavaScript console. Returns `true` to close.
|
||||
pub(crate) fn console_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("JavaScript Console").font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
if !app.session.javascript() {
|
||||
ui.label(egui::RichText::new("JavaScript is turned off (Preferences ▸ JavaScript).").small().color(t.text_muted));
|
||||
}
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
egui::ScrollArea::vertical().max_height(220.0).stick_to_bottom(true).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.set_min_height(160.0);
|
||||
if app.js_console.log.is_empty() {
|
||||
ui.label(egui::RichText::new("Output appears here.").color(t.text_muted));
|
||||
}
|
||||
for line in &app.js_console.log {
|
||||
ui.label(egui::RichText::new(line).monospace());
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
let input = ui.add(
|
||||
egui::TextEdit::multiline(&mut app.js_console.input)
|
||||
.code_editor()
|
||||
.desired_rows(4)
|
||||
.desired_width(f32::INFINITY)
|
||||
.hint_text("JavaScript, e.g. getField(\"total\").value")
|
||||
.id_salt("js-console-input"),
|
||||
);
|
||||
let run_key = input.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter) && i.modifiers.command);
|
||||
ui.add_space(8.0);
|
||||
match buttons(ui, "Run", &["Close", "Clear"]).as_deref() {
|
||||
Some("Run") => app.run_console(),
|
||||
Some("Clear") => app.js_console.log.clear(),
|
||||
Some("Close") => return true,
|
||||
_ if run_key => app.run_console(),
|
||||
_ => {}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Document JavaScripts: list, edit, add and delete. Returns `true` to close.
|
||||
pub(crate) fn document_js_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("Document JavaScripts").font(theme::semibold(18.0)));
|
||||
ui.add_space(6.0);
|
||||
let scripts = app.active_ids().and_then(|(_, id)| app.session.get(id)).map(|d| d.document_scripts()).unwrap_or_default();
|
||||
let mut edit: Option<Edit> = None;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Script Name:");
|
||||
ui.add(egui::TextEdit::singleline(&mut app.doc_js.name).desired_width(240.0).id_salt("doc-js-name"));
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
egui::ScrollArea::vertical().max_height(120.0).id_salt("doc-js-list").show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
if scripts.is_empty() {
|
||||
ui.label(egui::RichText::new("This document has no document-level scripts.").color(t.text_muted));
|
||||
}
|
||||
for (name, js) in &scripts {
|
||||
if ui.selectable_label(app.doc_js.name == *name, name).clicked() {
|
||||
app.doc_js = DocJsDraft { name: name.clone(), script: js.clone() };
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.add(egui::TextEdit::multiline(&mut app.doc_js.script).code_editor().desired_rows(8).desired_width(f32::INFINITY).id_salt("doc-js-script"));
|
||||
ui.add_space(8.0);
|
||||
let name = app.doc_js.name.trim().to_string();
|
||||
let close = match buttons(ui, "Save", &["Close", "Delete"]).as_deref() {
|
||||
Some("Save") if !name.is_empty() => {
|
||||
edit = Some(Edit::SetDocumentScript { name, script: Some(app.doc_js.script.clone()) });
|
||||
false
|
||||
}
|
||||
Some("Delete") if scripts.iter().any(|(n, _)| *n == name) => {
|
||||
edit = Some(Edit::SetDocumentScript { name, script: None });
|
||||
app.doc_js = DocJsDraft::default();
|
||||
false
|
||||
}
|
||||
Some("Close") => true,
|
||||
_ => false,
|
||||
};
|
||||
if let Some(e) = edit {
|
||||
app.apply_edit(e);
|
||||
}
|
||||
close
|
||||
}
|
||||
|
||||
/// Preferences (the JavaScript category for now). Returns `true` to close.
|
||||
pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PrintCraftApp, t: &Tokens) -> bool {
|
||||
ui.label(egui::RichText::new("Preferences").font(theme::semibold(18.0)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(egui::RichText::new("JavaScript").font(theme::semibold(13.0)));
|
||||
egui::Frame::new().fill(t.hover).corner_radius(egui::CornerRadius::same(6)).inner_margin(egui::Margin::same(10)).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
let mut on = app.session.javascript();
|
||||
if ui.checkbox(&mut on, "Enable Acrobat JavaScript").changed() {
|
||||
app.session.set_javascript(on);
|
||||
}
|
||||
ui.label(
|
||||
egui::RichText::new("Scripts run in a sandbox without file or network access. With JavaScript off, Acrobat's standard format, validate and calculate functions still work.")
|
||||
.small()
|
||||
.color(t.text_muted),
|
||||
);
|
||||
});
|
||||
ui.add_space(10.0);
|
||||
buttons(ui, "OK", &[]).is_some()
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ pub mod control;
|
|||
mod create_ui;
|
||||
mod crop;
|
||||
mod export_ui;
|
||||
mod js_ui;
|
||||
mod marks_ui;
|
||||
mod ocr_ui;
|
||||
mod optimize_ui;
|
||||
|
|
@ -187,6 +188,12 @@ pub enum Dialog {
|
|||
AccessibilityOptions,
|
||||
/// Scan & OCR ▸ Recognize text.
|
||||
RecognizeText,
|
||||
/// The JavaScript console (⌘J).
|
||||
JsConsole,
|
||||
/// Document JavaScripts.
|
||||
DocumentJs,
|
||||
/// Preferences.
|
||||
Preferences,
|
||||
/// Combine files: the files, their order and pages.
|
||||
Combine,
|
||||
/// Custom stamps ▸ Create.
|
||||
|
|
@ -302,6 +309,9 @@ pub struct PrintCraftApp {
|
|||
pub ocr_run: Option<ocr_ui::OcrRun>,
|
||||
pub ocr_batch: Option<std::sync::Arc<std::sync::Mutex<ocr_ui::BatchProgress>>>,
|
||||
pub ocr_sync: bool,
|
||||
/// The JavaScript console and the Document JavaScripts draft.
|
||||
pub js_console: js_ui::JsConsole,
|
||||
pub doc_js: js_ui::DocJsDraft,
|
||||
pub a11y: a11y_ui::A11yState,
|
||||
pub a11y_skipped: std::collections::BTreeSet<printcraft_engine::a11y::Rule>,
|
||||
pub alt_draft: a11y_ui::AltDraft,
|
||||
|
|
@ -441,6 +451,8 @@ impl PrintCraftApp {
|
|||
ocr_run: None,
|
||||
ocr_batch: None,
|
||||
ocr_sync: false,
|
||||
js_console: Default::default(),
|
||||
doc_js: Default::default(),
|
||||
a11y: a11y_ui::A11yState::default(),
|
||||
a11y_skipped: Default::default(),
|
||||
alt_draft: Default::default(),
|
||||
|
|
@ -778,6 +790,7 @@ impl PrintCraftApp {
|
|||
"digital_ids": self.digital_ids.iter().filter(|e| !e.path.starts_with("keychain:")).collect::<Vec<_>>(),
|
||||
"trusted": trusted,
|
||||
"custom_stamps": stamps_ui::encode(&self.custom_stamps),
|
||||
"javascript": self.session.javascript(),
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
|
@ -809,6 +822,9 @@ impl PrintCraftApp {
|
|||
self.digital_ids = ids;
|
||||
}
|
||||
self.custom_stamps = stamps_ui::decode(&v["custom_stamps"]);
|
||||
if let Some(on) = v["javascript"].as_bool() {
|
||||
self.session.set_javascript(on);
|
||||
}
|
||||
if let Ok(pems) = serde_json::from_value::<Vec<String>>(v["trusted"].clone()) {
|
||||
let certs = pems.iter().filter_map(|p| printcraft_engine::sign::x509::load_certificates(p.as_bytes()).ok()).flatten().collect();
|
||||
self.session.set_trusted_certificates(certs);
|
||||
|
|
@ -879,6 +895,9 @@ impl PrintCraftApp {
|
|||
"export-all-images" => Some(Dialog::Export(export_ui::ExportKind::AllImages)),
|
||||
"accessibility-options" => Some(Dialog::AccessibilityOptions),
|
||||
"recognize-text" => Some(Dialog::RecognizeText),
|
||||
"js-console" => Some(Dialog::JsConsole),
|
||||
"document-js" => Some(Dialog::DocumentJs),
|
||||
"preferences" => Some(Dialog::Preferences),
|
||||
"signature" => Some(Dialog::Signature),
|
||||
"optimize" => Some(Dialog::Optimize),
|
||||
"sign" | "certify" => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
//! Acrobat JavaScript in the shell (egui_kittest): the console, Document JavaScripts, a button
|
||||
//! script and Preferences ▸ JavaScript.
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
use printcraft_ui_egui::{Dialog, PrintCraftApp};
|
||||
|
||||
/// A form with a text field and a push button whose Mouse Up script fills it in.
|
||||
fn form() -> Vec<u8> {
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 6 0 R >> >> >> >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 300] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (greeting) /Rect [20 250 280 270] /P 3 0 R /F 4 >>",
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (hello) /Rect [20 200 120 230] /P 3 0 R /F 4 /A << /S /JavaScript /JS (getField\\('greeting'\\).value = 'Hello from JavaScript'; app.alert\\('Filled in'\\);) >> >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
|
||||
];
|
||||
let mut out = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (i, o) in objs.iter().enumerate() {
|
||||
offsets.push(out.len());
|
||||
out.extend_from_slice(format!("{} 0 obj\n{o}\nendobj\n", i + 1).as_bytes());
|
||||
}
|
||||
let xref = out.len();
|
||||
out.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for o in offsets {
|
||||
out.extend_from_slice(format!("{o:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
out.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
out
|
||||
}
|
||||
|
||||
fn harness() -> Harness<'static, PrintCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut app = PrintCraftApp::new();
|
||||
app.open_bytes("form.pdf", None, form()).unwrap();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn value(h: &Harness<'static, PrintCraftApp>, name: &str) -> Vec<String> {
|
||||
let app = h.state();
|
||||
let id = app.active_ids().unwrap().1;
|
||||
app.session.get(id).unwrap().form.iter().find(|f| f.name == name).unwrap().value.clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_runs_scripts_against_the_document() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("tools.js_console"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("JavaScript Console");
|
||||
h.state_mut().js_console.input = "getField('greeting').value = 'typed'; console.println(numFields); 6 * 7".into();
|
||||
h.get_by_label("Run").click();
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "greeting"), ["typed"]);
|
||||
let log = h.state().js_console.log.clone();
|
||||
assert!(log.contains(&"2".to_string()) && log.contains(&"42".to_string()), "{log:?}");
|
||||
h.state_mut().js_console.input = "throw new Error('boom')".into();
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Run").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().js_console.log.last().unwrap().contains("boom"));
|
||||
h.run_steps(4);
|
||||
h.get_by_label("Clear").click();
|
||||
h.run_steps(2);
|
||||
assert!(h.state().js_console.log.is_empty(), "{:?} {:?}", h.state().js_console.log, h.state().dialog);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn button_scripts_run_and_alert() {
|
||||
let mut h = harness();
|
||||
let id = h.state().active_ids().unwrap().1;
|
||||
h.state_mut().run_button_script(id, "hello", "getField('greeting').value = 'Hello from JavaScript'; app.alert('Filled in');");
|
||||
h.run_steps(2);
|
||||
assert_eq!(value(&h, "greeting"), ["Hello from JavaScript"]);
|
||||
assert_eq!(h.state().toast.clone().unwrap().0, "Filled in");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_scripts_and_preferences() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("tools.document_js"));
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Document JavaScripts");
|
||||
h.state_mut().doc_js.name = "init".into();
|
||||
h.state_mut().doc_js.script = "function hi() { return 'hi'; }".into();
|
||||
h.get_by_label("Save").click();
|
||||
h.run_steps(2);
|
||||
let id = h.state().active_ids().unwrap().1;
|
||||
assert_eq!(h.state().session.get(id).unwrap().document_scripts()[0].0, "init");
|
||||
h.state_mut().dialog = Some(Dialog::Preferences);
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Enable Acrobat JavaScript").click();
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().session.javascript());
|
||||
let saved = h.state().persist();
|
||||
let mut fresh = PrintCraftApp::new();
|
||||
fresh.restore(&saved);
|
||||
assert!(!fresh.session.javascript(), "the preference is remembered");
|
||||
}
|
||||
|
|
@ -4338,8 +4338,8 @@ milestone = "M6"
|
|||
status = "partial"
|
||||
commands = ["form.fields"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/buttons.rs::buttons_reset_navigate_and_print", "crates/forms/src/af.rs::common_button_scripts_are_recognised"]
|
||||
notes = "Reset form, Named (print, page navigation), URI, Go to, app.alert, this.print/resetForm/pageNum scripts; submit explained, not sent; other scripts need the JS engine"
|
||||
evidence = ["crates/ui-egui/tests/buttons.rs::buttons_reset_navigate_and_print", "crates/forms/src/af.rs::common_button_scripts_are_recognised", "crates/ui-egui/tests/javascript.rs::button_scripts_run_and_alert"]
|
||||
notes = "Reset form, Named (print, page navigation), URI, Go to, and any JavaScript (run in the sandboxed engine); submit explained, not sent"
|
||||
|
||||
[[feature]]
|
||||
id = "form.field-navigation"
|
||||
|
|
@ -4467,11 +4467,11 @@ title = "Custom format, keystroke, validate and calculate scripts"
|
|||
area = "F"
|
||||
tier = "P0"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["form.prepare"]
|
||||
tools = ["form_set_script"]
|
||||
evidence = ["crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/automation.rs::javascript_through_tools"]
|
||||
notes = "Custom keystroke (commit), validate, calculate and format scripts run in Acrobat's event order; AF calls still run natively."
|
||||
|
||||
[[feature]]
|
||||
id = "form.calculation-order"
|
||||
|
|
@ -4491,11 +4491,11 @@ title = "Keystroke, validate, calculate, format event order"
|
|||
area = "F"
|
||||
tier = "P0"
|
||||
milestone = "M6"
|
||||
status = "partial"
|
||||
commands = ["form.field.properties"]
|
||||
tools = ["form_set_props", "form_fill"]
|
||||
evidence = ["crates/forms/src/tests.rs::formats_validation_and_calculations_run_like_acrobat"]
|
||||
notes = "keystroke (on commit) → validate → calculate → format, natively for the AF functions; custom scripts need the JavaScript engine"
|
||||
status = "shipped"
|
||||
commands = ["form.prepare"]
|
||||
tools = ["form_fill"]
|
||||
evidence = ["crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats"]
|
||||
notes = "Keystroke (commit) \u2192 Validate \u2192 value \u2192 Calculate (in /CO order) \u2192 Format, for AF calls and custom scripts."
|
||||
|
||||
[[feature]]
|
||||
id = "form.date-picker"
|
||||
|
|
@ -4983,11 +4983,11 @@ title = "Document-level JavaScripts"
|
|||
area = "F"
|
||||
tier = "P1"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["tools.document_js"]
|
||||
tools = ["js_document_scripts", "js_set_document_script"]
|
||||
evidence = ["crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences", "crates/automation/tests/automation.rs::javascript_through_tools", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats"]
|
||||
notes = "Names /JavaScript tree: list, add, edit, delete; functions are available to field scripts."
|
||||
|
||||
[[feature]]
|
||||
id = "form.document-actions"
|
||||
|
|
@ -5007,11 +5007,11 @@ title = "JavaScript engine with the Acrobat object model subset"
|
|||
area = "F"
|
||||
tier = "P0"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "partial"
|
||||
commands = ["tools.js_console"]
|
||||
tools = ["js_run"]
|
||||
evidence = ["crates/js/src/tests.rs::calculate_scripts_read_fields_and_set_the_value", "crates/engine/src/tests.rs::form_javascript_validates_calculates_and_formats", "crates/automation/tests/automation.rs::javascript_through_tools"]
|
||||
notes = "boa (pure Rust) with the forms object model: event, getField/Field (value, readonly, display, colours, items, check boxes), app, util.printf/printd/printx, console, display, color; no annotations, layers or dialogs (app.response returns null) yet."
|
||||
|
||||
[[feature]]
|
||||
id = "form.js-console"
|
||||
|
|
@ -5019,11 +5019,11 @@ title = "JavaScript console"
|
|||
area = "F"
|
||||
tier = "P1"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["tools.js_console"]
|
||||
tools = ["js_run"]
|
||||
evidence = ["crates/ui-egui/tests/javascript.rs::console_runs_scripts_against_the_document", "crates/automation/tests/automation.rs::javascript_through_tools"]
|
||||
notes = "\u2318J: run JavaScript against the document, with console output, results and errors."
|
||||
|
||||
[[feature]]
|
||||
id = "form.js-debugger"
|
||||
|
|
@ -5055,11 +5055,11 @@ title = "JavaScript enable switch and sandboxing"
|
|||
area = "F"
|
||||
tier = "P0"
|
||||
milestone = "M6"
|
||||
status = "planned"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = []
|
||||
notes = ""
|
||||
status = "shipped"
|
||||
commands = ["app.preferences"]
|
||||
tools = ["js_enabled"]
|
||||
evidence = ["crates/js/src/tests.rs::the_sandbox_has_no_host_access", "crates/js/src/tests.rs::errors_and_runaway_scripts_are_contained", "crates/ui-egui/tests/javascript.rs::document_scripts_and_preferences"]
|
||||
notes = "Preferences \u25b8 JavaScript \u25b8 Enable Acrobat JavaScript (remembered); no host access; loop and recursion limits."
|
||||
|
||||
[[feature]]
|
||||
id = "form.folder-level-js"
|
||||
|
|
|
|||
Loading…
Reference in New Issue