mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 04:58:34 -06:00
Merge remote-tracking branch 'upstream/main' into feat/bookmarks-from-text-styles
This commit is contained in:
@@ -21,3 +21,27 @@ jobs:
|
||||
lint:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/packaging-lint.yml@main
|
||||
|
||||
windows-install-policy:
|
||||
timeout-minutes: 10
|
||||
name: Windows installation policy (no app build)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install the pinned packaging toolchain
|
||||
shell: pwsh
|
||||
run: |
|
||||
$tools = Join-Path $env:RUNNER_TEMP 'craft-install-policy-wix'
|
||||
dotnet tool install --tool-path $tools wix --version 5.0.2
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WiX installation failed.' }
|
||||
$tools >> $env:GITHUB_PATH
|
||||
$policy = Get-Content -Raw -LiteralPath packaging/windows/install-policy.json | ConvertFrom-Json
|
||||
foreach ($extension in @($policy.extensions)) {
|
||||
& (Join-Path $tools 'wix.exe') extension add -g "WixToolset.$extension.wixext/5.0.2"
|
||||
if ($LASTEXITCODE -ne 0) { throw "WiX extension $extension installation failed." }
|
||||
}
|
||||
- name: Build and check non-executable installer fixtures
|
||||
shell: pwsh
|
||||
run: ./packaging/windows/test-install-policy.ps1 -OutputDirectory (Join-Path $env:RUNNER_TEMP 'craft-install-policy-tests')
|
||||
|
||||
@@ -27,8 +27,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
windows-arm64:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@main
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f
|
||||
with:
|
||||
app-name: pdfcraft
|
||||
app-dir: PdfCraft
|
||||
install-relative-path: Programs/ArtCraft/PdfCraft
|
||||
install-scope: perUser
|
||||
|
||||
+2
-2
@@ -335,8 +335,8 @@ Fonts are fetched by `cargo xtask demo-pdf` into `target/demo-fonts/`, OCR model
|
||||
| `NotoColorEmoji-Regular.ttf` | Noto Color Emoji | Google Inc. | OFL-1.1 | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/notocoloremoji | Showcase PDF (cargo xtask demo-pdf) |
|
||||
| `Inter[opsz,wght].ttf` | Inter (variable) | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/inter | Showcase PDF (cargo xtask demo-pdf) |
|
||||
| `Inter-Italic[opsz,wght].ttf` | Inter Italic (variable) | The Inter Project Authors (Rasmus Andersson) | OFL-1.1 | https://github.com/google/fonts/tree/9710da1eacb3be272583c3224dcb70f9da6eadbb/ofl/inter | Showcase PDF (cargo xtask demo-pdf) |
|
||||
| `text-detection.rten` | Ocrs text detection model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0) | Scan & OCR: finds words on page images (cargo xtask models) |
|
||||
| `text-recognition.rten` | Ocrs text recognition model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0) | Scan & OCR: reads the words it finds (cargo xtask models) |
|
||||
| `text-detection.onnx` | Ocrs text detection model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: finds words on page images (cargo xtask models) |
|
||||
| `text-recognition.onnx` | Ocrs text recognition model | Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0) | CC-BY-SA-4.0 | https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0) | Scan & OCR: reads the words it finds (cargo xtask models) |
|
||||
|
||||
## Optional build inputs (1)
|
||||
|
||||
|
||||
+8
-8
@@ -3356,30 +3356,30 @@ usage = "1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packa
|
||||
sha256 = "f46881724a6621f3686a612a79dbdce4f7dbe9e2025610e44c9a094341334c2c"
|
||||
|
||||
[[fetched]]
|
||||
file = "text-detection.rten"
|
||||
url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten"
|
||||
file = "text-detection.onnx"
|
||||
url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.onnx"
|
||||
title = "Ocrs text detection model"
|
||||
author = "Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0)"
|
||||
source = "https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0)"
|
||||
source = "https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0)"
|
||||
licence = "CC-BY-SA-4.0"
|
||||
licence_url = "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt"
|
||||
licence_sha256 = "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
|
||||
kind = "model"
|
||||
usage = "Scan & OCR: finds words on page images (cargo xtask models)"
|
||||
sha256 = "f15cfb56bd02c4bf478a20343986504a1f01e1665c2b3a0ad66340f054b1b5ca"
|
||||
sha256 = "a917b23dbd9524b465df7e922641b3ff2981623df4ded5a0234004ef2fee7cfe"
|
||||
|
||||
[[fetched]]
|
||||
file = "text-recognition.rten"
|
||||
url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten"
|
||||
file = "text-recognition.onnx"
|
||||
url = "https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.onnx"
|
||||
title = "Ocrs text recognition model"
|
||||
author = "Robert Knight (ocrs-models; trained on HierText, CC-BY-SA-4.0)"
|
||||
source = "https://github.com/robertknight/ocrs-models (the models ocrs downloads; model card: https://huggingface.co/robertknight/ocrs, cc-by-sa-4.0)"
|
||||
source = "https://github.com/robertknight/ocrs/blob/0c826441362a701c55560fefc17bf356b6d1037e/ocrs/examples/download-models.sh (author-published ONNX models; model card: https://huggingface.co/robertknight/ocrs/blob/df0edd170279ab971b53e094c627255a87e1a503/README.md, cc-by-sa-4.0)"
|
||||
licence = "CC-BY-SA-4.0"
|
||||
licence_url = "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt"
|
||||
licence_sha256 = "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
|
||||
kind = "model"
|
||||
usage = "Scan & OCR: reads the words it finds (cargo xtask models)"
|
||||
sha256 = "e484866d4cce403175bd8d00b128feb08ab42e208de30e42cd9889d8f1735a6e"
|
||||
sha256 = "86c145c2edb96c8caed5b1ebb8f44d706408922211451c309c625157dd6061c5"
|
||||
|
||||
[[build_input]]
|
||||
name = "craft-fonts"
|
||||
|
||||
Generated
+205
-14
@@ -1023,6 +1023,15 @@ dependencies = [
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
@@ -1169,12 +1178,40 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-docpath"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-fonts-build"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
|
||||
[[package]]
|
||||
name = "craft-i18n"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"fixed_decimal",
|
||||
"icu_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback",
|
||||
"icu_normalizer",
|
||||
"icu_plurals",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"icu_provider_adapters",
|
||||
"icu_provider_blob",
|
||||
"log",
|
||||
"objc2-foundation 0.3.2",
|
||||
"sys-locale",
|
||||
"unicode-bidi",
|
||||
"windows-registry",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-keymap"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=edc41f68fa24c46651c3b5117d10f0ead75ec9cb#edc41f68fa24c46651c3b5117d10f0ead75ec9cb"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"serde",
|
||||
@@ -1184,7 +1221,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-menu"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=edc41f68fa24c46651c3b5117d10f0ead75ec9cb#edc41f68fa24c46651c3b5117d10f0ead75ec9cb"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"craft-keymap",
|
||||
"craft-quit",
|
||||
@@ -1197,11 +1234,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-print"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=42e30cd5b3f4b114270dd053fccbc33bedc47f68#42e30cd5b3f4b114270dd053fccbc33bedc47f68"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"craft-winprint-gdi",
|
||||
"miniz_oxide 0.8.9",
|
||||
"roxmltree",
|
||||
"miniz_oxide 0.9.1",
|
||||
"roxmltree 0.21.2",
|
||||
"tempfile",
|
||||
"winprint",
|
||||
"winsafe",
|
||||
@@ -1210,7 +1247,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-quit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=edc41f68fa24c46651c3b5117d10f0ead75ec9cb#edc41f68fa24c46651c3b5117d10f0ead75ec9cb"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"egui",
|
||||
@@ -1221,20 +1258,28 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=edc41f68fa24c46651c3b5117d10f0ead75ec9cb#edc41f68fa24c46651c3b5117d10f0ead75ec9cb"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"craft-fonts-build",
|
||||
"craft-i18n",
|
||||
"egui",
|
||||
"epaint",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-foundation 0.3.2",
|
||||
"serde",
|
||||
"skrifa 0.44.0",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
@@ -2073,6 +2118,17 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
||||
|
||||
[[package]]
|
||||
name = "fixed_decimal"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"smallvec",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.5.7"
|
||||
@@ -2188,7 +2244,7 @@ version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646"
|
||||
dependencies = [
|
||||
"roxmltree",
|
||||
"roxmltree 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2882,6 +2938,23 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_decimal"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eb8f655bba2d0c0459e43a5b0e6cd3cd388109898fb3d85d048165e8b0abd08"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"fixed_decimal",
|
||||
"icu_locale_core",
|
||||
"icu_pattern",
|
||||
"icu_plurals",
|
||||
"icu_provider",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.3.0"
|
||||
@@ -2890,11 +2963,32 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
|
||||
dependencies = [
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
@@ -2907,6 +3001,7 @@ dependencies = [
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
@@ -2917,6 +3012,32 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||
|
||||
[[package]]
|
||||
name = "icu_pattern"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b3db611446bd92c246b03e4aa0bafa71ddb194af431ddbb42b9d70e4bfce1a0"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"either",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_plurals"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e475e6766ef87b1d3c1f97be6363995b0bfaeddbc789671615df598a97ff0593"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"fixed_decimal",
|
||||
"icu_provider",
|
||||
"serde",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "2.3.0"
|
||||
@@ -2946,6 +3067,9 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"postcard",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -2953,6 +3077,30 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_adapters"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df887bc4f55dbc773c7c45a0c7dfa75be886f7b7812830a71ab7271cc1801820"
|
||||
dependencies = [
|
||||
"icu_locale_fallback",
|
||||
"icu_provider",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_blob"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916b44fac319d3f58e4d8d805a627df19e27937b582cd99fcde3bb6b7ce1b0e"
|
||||
dependencies = [
|
||||
"icu_provider",
|
||||
"postcard",
|
||||
"serde",
|
||||
"writeable",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -3251,6 +3399,9 @@ name = "litemap"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
@@ -4340,6 +4491,7 @@ dependencies = [
|
||||
"ocrs",
|
||||
"pdfcraft-fonts",
|
||||
"rten",
|
||||
"tempfile",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
@@ -4391,7 +4543,7 @@ dependencies = [
|
||||
"pdfcraft-content",
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-model",
|
||||
"roxmltree",
|
||||
"roxmltree 0.20.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4554,7 +4706,7 @@ dependencies = [
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-fonts",
|
||||
"png 0.18.1",
|
||||
"roxmltree",
|
||||
"roxmltree 0.20.0",
|
||||
"sha2 0.11.1",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
@@ -4567,7 +4719,7 @@ dependencies = [
|
||||
"pdfcraft-cos",
|
||||
"pdfcraft-forms",
|
||||
"pdfcraft-model",
|
||||
"roxmltree",
|
||||
"roxmltree 0.20.0",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
@@ -4888,12 +5040,24 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -5444,6 +5608,15 @@ version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "roxmltree"
|
||||
version = "0.21.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9da7f59328b09405b181fee1018925fad50f157afb43c0c0f5c522c256dd5907"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.10.0-rc.18"
|
||||
@@ -5473,6 +5646,7 @@ dependencies = [
|
||||
"rten-base",
|
||||
"rten-gemm",
|
||||
"rten-model-file",
|
||||
"rten-onnx",
|
||||
"rten-parallel",
|
||||
"rten-shape-inference",
|
||||
"rten-simd",
|
||||
@@ -5522,6 +5696,12 @@ dependencies = [
|
||||
"rten-base",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rten-onnx"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96944374568990b8d1bbc5bae5957585866f9dd8256130460405730948bfd42e"
|
||||
|
||||
[[package]]
|
||||
name = "rten-parallel"
|
||||
version = "0.26.0"
|
||||
@@ -6515,7 +6695,10 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6710,6 +6893,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -7078,7 +7262,7 @@ dependencies = [
|
||||
"kurbo 0.11.3",
|
||||
"log",
|
||||
"pico-args",
|
||||
"roxmltree",
|
||||
"roxmltree 0.20.0",
|
||||
"simplecss",
|
||||
"siphasher",
|
||||
"strict-num",
|
||||
@@ -8052,6 +8236,9 @@ name = "writeable"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x11-dl"
|
||||
@@ -8376,8 +8563,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde_core",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8386,6 +8576,7 @@ version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
+7
-7
@@ -13,17 +13,17 @@ repository = "https://github.com/storytold/pdfcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
# Shared frontend components; engines remain independent of egui.
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "edc41f68fa24c46651c3b5117d10f0ead75ec9cb", features = ["serde"] }
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["serde"] }
|
||||
# Quit the macOS way (#1136): ⌘Q asks about unsaved changes, and closing quits through `terminate:`.
|
||||
craft-quit = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "edc41f68fa24c46651c3b5117d10f0ead75ec9cb" }
|
||||
craft-quit = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
# One menu definition for the native macOS menu bar and the in-window bar elsewhere.
|
||||
craft-menu = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "edc41f68fa24c46651c3b5117d10f0ead75ec9cb" }
|
||||
craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "edc41f68fa24c46651c3b5117d10f0ead75ec9cb", default-features = false }
|
||||
craft-menu = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
craft-keymap = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", default-features = false }
|
||||
# Shared system spooler; document composition remains in pdfcraft-print.
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "42e30cd5b3f4b114270dd053fccbc33bedc47f68" }
|
||||
craft-print = { git = "https://github.com/storytold/craft-libs", version = "0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
# Whether a path stored in a document may be opened (GoToR link targets).
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
|
||||
# internal
|
||||
pdfcraft-geom = { path = "crates/geom", version = "0.14.0" }
|
||||
pdfcraft-filters = { path = "crates/filters", version = "0.14.0" }
|
||||
|
||||
@@ -459,11 +459,13 @@ The portable zip runs from any folder, a USB stick included. Its `portable.txt`
|
||||
logs and crash recovery in a `PdfCraftData` folder next to `pdfcraft.exe`, so nothing is written to
|
||||
`%APPDATA%`; delete that file to use the normal per-user folders.
|
||||
|
||||
The MSI installs for all users and requires administrator privileges. It adds "Edit with PdfCraft"
|
||||
The MSI installs for your account only by default, without administrator privileges. Choose
|
||||
"Everyone on this computer" on its first page, or pass `ALLUSERS=1` from an elevated terminal, to install
|
||||
for all users. It adds "Edit with PdfCraft"
|
||||
to the right-click menu of PDF files and "Create PDF with PdfCraft…" to the right-click menu of
|
||||
images (.png .jpg .jpeg .tif .tiff .gif .bmp .jp2 .j2k .jpx) in File Explorer (on Windows 11, under
|
||||
"Show more options"), without changing which app opens them by default. For unattended deployment
|
||||
without a desktop shortcut, run from an elevated terminal:
|
||||
without a desktop shortcut (add `ALLUSERS=1`, from an elevated terminal, for an all-users install):
|
||||
|
||||
```powershell
|
||||
msiexec /i "pdfcraft-<ver>-windows-x64.msi" /qn /norestart INSTALLDESKTOPSHORTCUT=0
|
||||
@@ -471,7 +473,7 @@ msiexec /i "pdfcraft-<ver>-windows-x64.msi" /qn /norestart INSTALLDESKTOPSHORTCU
|
||||
|
||||
| Property | `=0` leaves out |
|
||||
|---|---|
|
||||
| `INSTALLDESKTOPSHORTCUT` | the all-users desktop shortcut |
|
||||
| `INSTALLDESKTOPSHORTCUT` | the desktop shortcut |
|
||||
| `INSTALLPDFCONTEXTMENU` | "Edit with PdfCraft" on PDF files |
|
||||
| `INSTALLIMAGECONTEXTMENU` | "Create PDF with PdfCraft…" on images |
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: OCR model downloads use verified ONNX pairs with legacy discovery fallback; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (M14 localization current state: Arabic logical catalog, explicit RTL label direction and mirrored stock chrome; native/browser RTL workflows unqualified. Native release Hans face via LXGW WenKai Lite; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M7, #2636 partial: pasting added text boxes and images in the engine and through `content_copy`; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M11: image optimization accounts for page-local UserUnit; estimates unchanged)
|
||||
@@ -199,6 +200,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-27 (M10, OCR models):** Model staging downloads and verifies the ONNX pair after the upstream RTEN URL brownout; runtime discovery prefers complete ONNX pairs per directory, retains complete RTEN fallback and preserves directory precedence.
|
||||
- **2026-10-24 (M10, #4158 partial):** Word, HTML, RTF and PowerPoint export keep a block that continues another on its line (an italic case name, a link, a superscript) as a run in its own style inside that paragraph, with the source's word space, instead of a paragraph of its own. Style changes that wrap onto the next line still split paragraphs (the reading blocks keep style boundaries). Estimates unchanged.
|
||||
- **2026-10-24 (M3, window move on Wayland, #3970 follow-up):** After a move or resize by PdfCraft's own title bar or edges, the button release given back to egui goes before the pointer leaving the window, so hover no longer sticks at the old place until the pointer moves. Found by CodeRabbit in review of #3970; a unit test covers it. Estimates unchanged.
|
||||
- **2026-10-24 (M14, #4253):** The annotation quick bar remembers Keep tool selected across restarts, including unpinning. Old and malformed settings keep safe defaults; the opt-in UI control channel reads and changes the same preference. Estimates unchanged.
|
||||
@@ -682,6 +684,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-27 | minor | M10: OCR stages SHA-256-verified ONNX models after the upstream RTEN URL brownout; discovery prefers a complete ONNX pair per directory, falls back to a complete RTEN pair, and never mixes formats or directories; language/web limits and estimates unchanged. |
|
||||
| 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. |
|
||||
| 2026-10-23 | minor | M11: image downsampling measures physical resolution after page-local UserUnit scaling, including shared nested forms; synthetic measurement and optimization regressions. Estimates unchanged. |
|
||||
| 2026-10-23 | minor | M10: Word table cells use the grid columns they span, including cells after a merged cell; synthetic export regression and CLI reproduction. Estimates unchanged. |
|
||||
|
||||
@@ -13,3 +13,7 @@ disallowed-methods = [
|
||||
{ path = "egui::Ui::radio_value", reason = "use radio_value from crates/ui-egui/src/widgets.rs, which is outlined and filled" },
|
||||
{ path = "egui::RadioButton::new", reason = "use radio from crates/ui-egui/src/widgets.rs, which is outlined and filled" },
|
||||
]
|
||||
|
||||
disallowed-types = [
|
||||
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
|
||||
]
|
||||
|
||||
@@ -112,13 +112,13 @@ The release packages ship `pdfcraft-cli` alongside the desktop app, so no build
|
||||
|
||||
| Install | CLI |
|
||||
|---|---|
|
||||
| Windows (MSI) | `C:\Program Files\PdfCraft\pdfcraft-cli.exe` by default (wherever you installed it otherwise), not on `PATH` |
|
||||
| Windows (MSI) | `%LOCALAPPDATA%\Programs\ArtCraft\PdfCraft\pdfcraft-cli.exe` by default (`C:\Program Files\ArtCraft\PdfCraft\pdfcraft-cli.exe` for an all-users install, under `Program Files (x86)` instead for the 32-bit build, or wherever you installed it otherwise), not on `PATH` |
|
||||
| Linux (deb, rpm) | `/usr/bin/pdfcraft-cli` |
|
||||
| macOS | the separate `pdfcraft-cli-<version>-macos-<arch>.zip` release asset (the `.app` holds only the desktop app) |
|
||||
|
||||
```sh
|
||||
# Windows, default install folder
|
||||
claude mcp add pdfcraft -- "C:\Program Files\PdfCraft\pdfcraft-cli.exe" mcp --root <dir>
|
||||
# Windows (PowerShell), default install folder
|
||||
claude mcp add pdfcraft -- "$env:LOCALAPPDATA\Programs\ArtCraft\PdfCraft\pdfcraft-cli.exe" mcp --root <dir>
|
||||
# Linux, or macOS with the CLI unzipped onto PATH
|
||||
claude mcp add pdfcraft -- pdfcraft-cli mcp --root <dir>
|
||||
```
|
||||
|
||||
@@ -13,5 +13,8 @@ workspace = true
|
||||
[dependencies]
|
||||
pdfcraft-fonts = { workspace = true }
|
||||
ocrs = { version = "0.13.1", default-features = false, features = ["rten"] }
|
||||
rten = { version = "0.26.0", default-features = false, features = ["rten_format"] }
|
||||
rten = { version = "0.26.0", default-features = false, features = ["onnx_format", "rten_format"] }
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.27"
|
||||
|
||||
+65
-5
@@ -15,8 +15,8 @@ use std::path::{Path, PathBuf};
|
||||
pub use pdfcraft_fonts::helvetica_width;
|
||||
|
||||
/// The model files, as named in ATTRIBUTION.toml.
|
||||
pub const DETECTION_MODEL: &str = "text-detection.rten";
|
||||
pub const RECOGNITION_MODEL: &str = "text-recognition.rten";
|
||||
pub const DETECTION_MODEL: &str = "text-detection.onnx";
|
||||
pub const RECOGNITION_MODEL: &str = "text-recognition.onnx";
|
||||
|
||||
/// The recognition model's alphabet, as it was trained (ocrs-models' `DEFAULT_ALPHABET`): its
|
||||
/// output classes follow this order. The ocrs crate's own copy has an `E` where the training had
|
||||
@@ -48,10 +48,13 @@ pub struct Models {
|
||||
}
|
||||
|
||||
impl Models {
|
||||
/// The models in `dir`, if both files are there.
|
||||
/// A complete model pair in `dir`, preferring current ONNX files over legacy RTEN files.
|
||||
/// An incomplete update never mixes formats or hides an intact legacy pair.
|
||||
pub fn in_dir(dir: &Path) -> Option<Models> {
|
||||
let m = Models { detection: dir.join(DETECTION_MODEL), recognition: dir.join(RECOGNITION_MODEL) };
|
||||
(m.detection.is_file() && m.recognition.is_file()).then_some(m)
|
||||
[(DETECTION_MODEL, RECOGNITION_MODEL), ("text-detection.rten", "text-recognition.rten")].into_iter().find_map(|(detection, recognition)| {
|
||||
let m = Models { detection: dir.join(detection), recognition: dir.join(recognition) };
|
||||
(m.detection.is_file() && m.recognition.is_file()).then_some(m)
|
||||
})
|
||||
}
|
||||
|
||||
/// Look for the models: `$PDFCRAFT_MODELS`, then where the release packages install them
|
||||
@@ -321,6 +324,63 @@ mod tests {
|
||||
assert_eq!(w.up, [-10.0, 0.0]);
|
||||
}
|
||||
|
||||
/// The current ONNX pair wins when an installation also retains both legacy models.
|
||||
#[test]
|
||||
fn model_discovery_prefers_the_complete_onnx_pair() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for file in ["text-detection.onnx", "text-recognition.onnx"] {
|
||||
std::fs::write(dir.path().join(file), b"model").unwrap();
|
||||
}
|
||||
let expected = Some(Models { detection: dir.path().join("text-detection.onnx"), recognition: dir.path().join("text-recognition.onnx") });
|
||||
assert_eq!(Models::in_dir(dir.path()), expected, "a complete ONNX pair is sufficient");
|
||||
for file in ["text-detection.rten", "text-recognition.rten"] {
|
||||
std::fs::write(dir.path().join(file), b"model").unwrap();
|
||||
}
|
||||
assert_eq!(Models::in_dir(dir.path()), expected, "the complete ONNX pair takes precedence over legacy models");
|
||||
}
|
||||
|
||||
/// An interrupted ONNX update still permits an intact legacy pair in the same directory.
|
||||
#[test]
|
||||
fn model_discovery_falls_back_to_legacy_when_the_onnx_pair_is_incomplete() {
|
||||
for current in ["text-detection.onnx", "text-recognition.onnx"] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for file in [current, "text-detection.rten", "text-recognition.rten"] {
|
||||
std::fs::write(dir.path().join(file), b"model").unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
Models::in_dir(dir.path()),
|
||||
Some(Models { detection: dir.path().join("text-detection.rten"), recognition: dir.path().join("text-recognition.rten") }),
|
||||
"{current} alone must not hide a complete legacy pair"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Detection and recognition must come from one complete format pair, never a mixed update.
|
||||
#[test]
|
||||
fn model_discovery_refuses_mixed_format_pairs() {
|
||||
for pair in [["text-detection.onnx", "text-recognition.rten"], ["text-detection.rten", "text-recognition.onnx"]] {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
for file in pair {
|
||||
std::fs::write(dir.path().join(file), b"model").unwrap();
|
||||
}
|
||||
assert_eq!(Models::in_dir(dir.path()), None, "{pair:?} is not a complete model pair");
|
||||
}
|
||||
}
|
||||
|
||||
/// Existing source builds and PDFCRAFT_MODELS overrides retain their complete RTEN pair.
|
||||
#[test]
|
||||
fn model_discovery_keeps_legacy_only_installations() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
assert_eq!(Models::in_dir(dir.path()), None);
|
||||
std::fs::write(dir.path().join("text-detection.rten"), b"model").unwrap();
|
||||
assert_eq!(Models::in_dir(dir.path()), None, "one legacy model is incomplete");
|
||||
std::fs::write(dir.path().join("text-recognition.rten"), b"model").unwrap();
|
||||
assert_eq!(
|
||||
Models::in_dir(dir.path()),
|
||||
Some(Models { detection: dir.path().join("text-detection.rten"), recognition: dir.path().join("text-recognition.rten") })
|
||||
);
|
||||
}
|
||||
|
||||
/// #103: every release package's layout puts the models where the installed app looks.
|
||||
/// Mirrors packaging/{windows/package.ps1,macos/package.sh,linux/package.sh,freebsd/package.sh}.
|
||||
#[test]
|
||||
|
||||
@@ -226,7 +226,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
ui.end_row();
|
||||
let label = ui.label(tl!("Navigation tab"));
|
||||
ui.add_enabled_ui(editable, |ui| {
|
||||
egui::ComboBox::from_id_salt("iv-nav")
|
||||
craft_ui::ComboBox::from_id_salt("iv-nav")
|
||||
.selected_text(tl!(format!("{:?}", v.navigation)
|
||||
.replace("PageOnly", "Page Only")
|
||||
.replace("Pages", "Pages Panel and Page")
|
||||
@@ -258,7 +258,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
(L::TwoUpContinuousCoverPage, tl!("Two-Up Continuous (Cover Page)")),
|
||||
];
|
||||
let shown = names.iter().find(|(l, _)| *l == v.layout).map_or(tl!("Default"), |(_, n)| n);
|
||||
egui::ComboBox::from_id_salt("iv-layout")
|
||||
craft_ui::ComboBox::from_id_salt("iv-layout")
|
||||
.selected_text(shown)
|
||||
.show_ui(ui, |ui| {
|
||||
for (l, n) in names {
|
||||
@@ -284,7 +284,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
M::Percent(p) => format!("{p:.0}%"),
|
||||
m => names.iter().find(|(x, _)| *x == m).map_or(tl!("Default").to_string(), |(_, n)| n.to_string()),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("iv-mag")
|
||||
craft_ui::ComboBox::from_id_salt("iv-mag")
|
||||
.selected_text(shown)
|
||||
.show_ui(ui, |ui| {
|
||||
for (m, n) in names {
|
||||
@@ -799,7 +799,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
ui.add_space(8.0);
|
||||
egui::Grid::new("rotate-pages").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Direction:"));
|
||||
egui::ComboBox::from_id_salt("rotate-dir")
|
||||
craft_ui::ComboBox::from_id_salt("rotate-dir")
|
||||
.selected_text(match d.degrees {
|
||||
270 => tl!("Counterclockwise 90 degrees"),
|
||||
180 => tl!("180 degrees"),
|
||||
@@ -825,7 +825,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Rotate:"));
|
||||
egui::ComboBox::from_id_salt("rotate-parity")
|
||||
craft_ui::ComboBox::from_id_salt("rotate-parity")
|
||||
.selected_text(match d.parity {
|
||||
P::Both => tl!("Even and Odd Pages"),
|
||||
P::Even => tl!("Even Pages Only"),
|
||||
@@ -838,7 +838,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
egui::ComboBox::from_id_salt("rotate-orient")
|
||||
craft_ui::ComboBox::from_id_salt("rotate-orient")
|
||||
.selected_text(match d.orientation {
|
||||
O::Both => tl!("Landscape and Portrait Pages"),
|
||||
O::Landscape => tl!("Landscape Pages"),
|
||||
@@ -947,7 +947,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Sort comments by:"));
|
||||
egui::ComboBox::from_id_salt("summary-sort").selected_text(tl!(app.summary_sort.name())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("summary-sort").selected_text(tl!(app.summary_sort.name())).show_ui(ui, |ui| {
|
||||
for s in pdfcraft_engine::SummarySort::ALL {
|
||||
ui.selectable_value(&mut app.summary_sort, s, tl!(s.name()));
|
||||
}
|
||||
@@ -1227,7 +1227,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
(L::None, tl!("None (prefix only)")),
|
||||
];
|
||||
let current = styles.iter().find(|(s, _)| *s == d.style).map_or("1, 2, 3", |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("label_style").selected_text(current).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("label_style").selected_text(current).show_ui(ui, |ui| {
|
||||
for (s, l) in styles {
|
||||
ui.selectable_value(&mut d.style, s, l);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ pub fn preferences_section(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
ui.label(RichText::new(tl_ctx!("theme", "Appearance")).font(theme::semibold(13.0)));
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Appearance mode"));
|
||||
let combo = egui::ComboBox::from_id_salt("appearance-mode").selected_text(tl!(mode_label(app.theme_preference))).show_ui(ui, |ui| {
|
||||
let combo = craft_ui::ComboBox::from_id_salt("appearance-mode").selected_text(tl!(mode_label(app.theme_preference))).show_ui(ui, |ui| {
|
||||
for preference in [ThemePreference::System, ThemePreference::Light, ThemePreference::Dark] {
|
||||
if ui.selectable_label(app.theme_preference == preference, tl!(mode_label(preference))).clicked() {
|
||||
app.execute(mode_command(preference));
|
||||
|
||||
@@ -207,7 +207,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
egui::Grid::new("comment-props").num_columns(2).spacing([12.0, 10.0]).show(ui, |ui| {
|
||||
if let Some(icon) = e.icon.as_mut() {
|
||||
ui.label(tl!("Icon"));
|
||||
egui::ComboBox::from_id_salt("note-icon").selected_text(icon.name()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("note-icon").selected_text(icon.name()).show_ui(ui, |ui| {
|
||||
for i in ICONS {
|
||||
ui.selectable_value(icon, i, i.name());
|
||||
}
|
||||
@@ -247,7 +247,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
ui.label(tl!("Line ending"));
|
||||
ui.horizontal(|ui| {
|
||||
for (i, ending) in ends.iter_mut().enumerate() {
|
||||
egui::ComboBox::from_id_salt(("line-ending", i)).selected_text(ending_label(*ending)).width(188.0).show_ui(
|
||||
craft_ui::ComboBox::from_id_salt(("line-ending", i)).selected_text(ending_label(*ending)).width(188.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for style in LineEnding::ALL {
|
||||
|
||||
@@ -243,7 +243,7 @@ pub fn contributors_ui(ui: &mut egui::Ui) {
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(crate::i18n::t("Sort"));
|
||||
egui::ComboBox::from_id_salt("credits_sort").selected_text(crate::i18n::t(v.key.label().0)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(crate::i18n::t(v.key.label().0)).show_ui(ui, |ui| {
|
||||
for k in SortKey::ALL {
|
||||
if ui.selectable_label(v.key == k, crate::i18n::t(k.label().0)).clicked() {
|
||||
v.key = k;
|
||||
|
||||
@@ -63,7 +63,7 @@ impl PageRange {
|
||||
Subset::Even => tl!("Even pages only"),
|
||||
Subset::Odd => tl!("Odd pages only"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("mark-subset").selected_text(label).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("mark-subset").selected_text(label).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut self.subset, Subset::All, tl!("All pages in range"));
|
||||
ui.selectable_value(&mut self.subset, Subset::Even, tl!("Even pages only"));
|
||||
ui.selectable_value(&mut self.subset, Subset::Odd, tl!("Odd pages only"));
|
||||
@@ -227,7 +227,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
||||
ui.horizontal(|ui| {
|
||||
// The standard faces a header can be drawn in (#3754).
|
||||
ui.label(tl!("Font:"));
|
||||
egui::ComboBox::from_id_salt("hf-font").selected_text(d.hf.font.label()).width(100.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("hf-font").selected_text(d.hf.font.label()).width(100.0).show_ui(ui, |ui| {
|
||||
for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
|
||||
ui.selectable_value(&mut d.hf.font, f, f.label());
|
||||
}
|
||||
@@ -267,7 +267,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
||||
}
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("hf-page-format").selected_text(PAGE_FORMATS[d.page_format].trim_matches(['<', '>'])).show_ui(
|
||||
craft_ui::ComboBox::from_id_salt("hf-page-format").selected_text(PAGE_FORMATS[d.page_format].trim_matches(['<', '>'])).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (k, f) in PAGE_FORMATS.iter().enumerate() {
|
||||
@@ -279,7 +279,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M
|
||||
d.hf.text[d.focused_box].push_str(PAGE_FORMATS[d.page_format]);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
egui::ComboBox::from_id_salt("hf-date-format").selected_text(DATE_FORMATS[d.date_format].trim_matches(['<', '>'])).show_ui(
|
||||
craft_ui::ComboBox::from_id_salt("hf-date-format").selected_text(DATE_FORMATS[d.date_format].trim_matches(['<', '>'])).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (k, f) in DATE_FORMATS.iter().enumerate() {
|
||||
|
||||
@@ -102,14 +102,14 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
(PageBox::Media, "MediaBox"),
|
||||
];
|
||||
let cur = names.iter().find(|(b, _)| *b == d.which).map_or("CropBox", |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("boxes-which").selected_text(cur).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("boxes-which").selected_text(cur).show_ui(ui, |ui| {
|
||||
for (b, l) in names {
|
||||
ui.selectable_value(&mut d.which, b, l);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Units"));
|
||||
egui::ComboBox::from_id_salt("boxes-unit").selected_text(d.unit.label()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("boxes-unit").selected_text(d.unit.label()).show_ui(ui, |ui| {
|
||||
for u in Unit::ALL {
|
||||
ui.selectable_value(&mut d.unit, u, u.label());
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
(A::Rc4_40, tl!("Acrobat 3.0 and later (40-bit RC4)")),
|
||||
];
|
||||
let cur = algos.iter().find(|(a, _)| *a == p.algorithm).map_or(algos[0].1, |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-algo").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("protect-algo").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (a, l) in algos {
|
||||
ui.selectable_value(&mut p.algorithm, a, l);
|
||||
}
|
||||
@@ -164,7 +164,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
let opts =
|
||||
[(Printing::None, tl!("None")), (Printing::Low, tl!("Low Resolution (150 dpi)")), (Printing::High, tl!("High Resolution"))];
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.printing).map_or(tl!("High Resolution"), |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-print").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("protect-print").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (o, l) in opts {
|
||||
ui.selectable_value(&mut p.printing, o, l);
|
||||
}
|
||||
@@ -179,7 +179,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
(Changes::AnyExceptExtract, tl!("Any except extracting pages")),
|
||||
];
|
||||
let cur = opts.iter().find(|(o, _)| *o == p.changes).map_or(tl!("None"), |(_, l)| *l);
|
||||
egui::ComboBox::from_id_salt("protect-changes").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("protect-changes").width(300.0).selected_text(cur).show_ui(ui, |ui| {
|
||||
for (o, l) in opts {
|
||||
ui.selectable_value(&mut p.changes, o, l);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool
|
||||
let before = app.pdfa.level;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Conformance level:"));
|
||||
egui::ComboBox::from_id_salt("pdfa-level").selected_text(app.pdfa.level.label()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("pdfa-level").selected_text(app.pdfa.level.label()).show_ui(ui, |ui| {
|
||||
for l in [Level::A2b, Level::A3b] {
|
||||
ui.selectable_value(&mut app.pdfa.level, l, l.label());
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
|
||||
|
||||
// The font, across the panel.
|
||||
let width = ui.available_width();
|
||||
egui::ComboBox::from_id_salt("format-font")
|
||||
craft_ui::ComboBox::from_id_salt("format-font")
|
||||
.selected_text(fonts.shown.as_str())
|
||||
.width(width)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -128,7 +128,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
|
||||
|
||||
// Size and colour | bullets, numbering and alignment.
|
||||
row(ui, |ui| {
|
||||
let _ = egui::ComboBox::from_id_salt("format-size")
|
||||
let _ = craft_ui::ComboBox::from_id_salt("format-size")
|
||||
.selected_text(number(look.size))
|
||||
.width(64.0)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -206,7 +206,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
|
||||
labelled_icon(ui, t, "arrow-up-down", tl!("Line spacing"));
|
||||
let own = tl!("Auto").to_string();
|
||||
let shown = if e.line_spacing == 0.0 { own.clone() } else { number(e.line_spacing) };
|
||||
egui::ComboBox::from_id_salt("format-line-spacing").selected_text(shown).width(half).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("format-line-spacing").selected_text(shown).width(half).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut e.line_spacing, 0.0, own.as_str());
|
||||
for v in LINE_SPACINGS {
|
||||
ui.selectable_value(&mut e.line_spacing, v, number(v));
|
||||
@@ -216,7 +216,7 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras:
|
||||
ui.add_enabled_ui(false, |ui| {
|
||||
labelled_icon(ui, t, "pilcrow", tl!("Paragraph spacing"));
|
||||
let tip = format!("{} — {}", tl!("Paragraph spacing"), tl!("Planned"));
|
||||
let _ = egui::ComboBox::from_id_salt("format-paragraph-spacing")
|
||||
let _ = craft_ui::ComboBox::from_id_salt("format-paragraph-spacing")
|
||||
.selected_text("0")
|
||||
.width(half)
|
||||
.show_ui(ui, |_| {})
|
||||
|
||||
@@ -238,7 +238,7 @@ impl crate::PdfCraftApp {
|
||||
|
||||
fn bool_row(ui: &mut egui::Ui, label: &str, c: &mut Change<bool>) {
|
||||
row(ui, label, c, |ui, value| {
|
||||
egui::ComboBox::from_id_salt(label).selected_text(tl!(if *value { "Yes" } else { "No" })).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt(label).selected_text(tl!(if *value { "Yes" } else { "No" })).show_ui(ui, |ui| {
|
||||
ui.selectable_value(value, true, tl!("Yes"));
|
||||
ui.selectable_value(value, false, tl!("No"));
|
||||
});
|
||||
@@ -296,7 +296,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut Draft, t: &theme::Tokens) -> (bool
|
||||
});
|
||||
let font_label = if d.font.apply { d.font.value.label().to_string() } else { d.font_label.clone() };
|
||||
row(ui, "Font:", &mut d.font, |ui, v| {
|
||||
egui::ComboBox::from_id_salt("bulk-font").selected_text(tl!(&font_label)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("bulk-font").selected_text(tl!(&font_label)).show_ui(ui, |ui| {
|
||||
for font in FieldFont::ALL {
|
||||
ui.selectable_value(v, font, font.label());
|
||||
}
|
||||
@@ -314,7 +314,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut Draft, t: &theme::Tokens) -> (bool
|
||||
ui.add(egui::DragValue::new(v).range(0.0..=12.0).speed(0.25).suffix(" pt"));
|
||||
});
|
||||
row(ui, "Line Style:", &mut d.style, |ui, v| {
|
||||
egui::ComboBox::from_id_salt("bulk-style").selected_text(tl!(v.label())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("bulk-style").selected_text(tl!(v.label())).show_ui(ui, |ui| {
|
||||
for style in BorderStyle::ALL {
|
||||
ui.selectable_value(v, style, tl!(style.label()));
|
||||
}
|
||||
@@ -329,7 +329,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut Draft, t: &theme::Tokens) -> (bool
|
||||
2 => "Right",
|
||||
_ => "Left",
|
||||
};
|
||||
egui::ComboBox::from_id_salt("bulk-align").selected_text(tl!(label)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("bulk-align").selected_text(tl!(label)).show_ui(ui, |ui| {
|
||||
for (value, label) in [(0, "Left"), (1, "Center"), (2, "Right")] {
|
||||
ui.selectable_value(v, value, tl!(label));
|
||||
}
|
||||
@@ -344,7 +344,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut Draft, t: &theme::Tokens) -> (bool
|
||||
}
|
||||
if d.check_fields {
|
||||
row(ui, "Button Style:", &mut d.check_style, |ui, v| {
|
||||
egui::ComboBox::from_id_salt("bulk-check-style").selected_text(tl!(v.label())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("bulk-check-style").selected_text(tl!(v.label())).show_ui(ui, |ui| {
|
||||
for style in CheckStyle::ALL {
|
||||
ui.selectable_value(v, style, tl!(style.label()));
|
||||
}
|
||||
|
||||
@@ -229,7 +229,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
||||
widgets::section_title(ui, tl!("Link Appearance"));
|
||||
egui::Grid::new("link-appearance").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Link Type:"));
|
||||
egui::ComboBox::from_id_salt("link-type")
|
||||
craft_ui::ComboBox::from_id_salt("link-type")
|
||||
.selected_text(if d.style.visible { tl!("Visible Rectangle") } else { tl!("Invisible Rectangle") })
|
||||
.show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.style.visible, true, tl!("Visible Rectangle"));
|
||||
@@ -237,7 +237,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Highlight Style:"));
|
||||
egui::ComboBox::from_id_salt("link-hl").selected_text(tl!(d.style.highlight.label())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("link-hl").selected_text(tl!(d.style.highlight.label())).show_ui(ui, |ui| {
|
||||
for h in LinkHighlight::ALL {
|
||||
ui.selectable_value(&mut d.style.highlight, h, tl!(h.label()));
|
||||
}
|
||||
@@ -245,7 +245,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
||||
ui.end_row();
|
||||
if d.style.visible {
|
||||
ui.label(tl!("Line Thickness:"));
|
||||
egui::ComboBox::from_id_salt("link-w")
|
||||
craft_ui::ComboBox::from_id_salt("link-w")
|
||||
.selected_text(if d.style.width <= 1.0 {
|
||||
tl!("Thin")
|
||||
} else if d.style.width <= 2.0 {
|
||||
@@ -267,7 +267,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut LinkDraft, pages: usize, t: &Token
|
||||
} else {
|
||||
0
|
||||
};
|
||||
egui::ComboBox::from_id_salt("link-s").selected_text(tl!(["Solid", "Dashed", "Underline"][style])).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("link-s").selected_text(tl!(["Solid", "Dashed", "Underline"][style])).show_ui(ui, |ui| {
|
||||
for (k, l) in ["Solid", "Dashed", "Underline"].iter().enumerate() {
|
||||
ui.selectable_value(&mut style, k, tl!(l));
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ fn panel_body(app: &mut PdfCraftApp, ui: &mut egui::Ui, _t: &Tokens) {
|
||||
None => state.error = Some(tl!("Enter a scale such as 1:50.").into()),
|
||||
}
|
||||
}
|
||||
egui::ComboBox::from_id_salt("measure-common-scales").selected_text(tl!("Common scales")).width(20.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("measure-common-scales").selected_text(tl!("Common scales")).width(20.0).show_ui(ui, |ui| {
|
||||
for n in RATIOS {
|
||||
if ui.selectable_label(state.ratio_text == format_ratio(n), format_ratio(n)).clicked() {
|
||||
chosen = Some(n);
|
||||
@@ -449,7 +449,7 @@ fn panel_body(app: &mut PdfCraftApp, ui: &mut egui::Ui, _t: &Tokens) {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut state.drawing_points).range(0.000001..=1e9));
|
||||
let page_unit = page_unit_of(state.page_unit, &state.unit, units);
|
||||
egui::ComboBox::from_id_salt("measure-page-unit").selected_text(page_unit.label()).width(44.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("measure-page-unit").selected_text(page_unit.label()).width(44.0).show_ui(ui, |ui| {
|
||||
for u in PageUnit::ALL {
|
||||
if ui.selectable_label(u == page_unit, u.label()).clicked() {
|
||||
state.page_unit = Some(u);
|
||||
|
||||
@@ -17,7 +17,7 @@ fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label:
|
||||
/// Shared PDF `/Q` picker for text and choice-field Options.
|
||||
fn alignment(ui: &mut egui::Ui, quadding: &mut i64) {
|
||||
ui.label(tl!("Alignment:"));
|
||||
egui::ComboBox::from_id_salt("quadding")
|
||||
craft_ui::ComboBox::from_id_salt("quadding")
|
||||
.selected_text(tl!(match (*quadding).clamp(0, 2) {
|
||||
1 => "Center",
|
||||
2 => "Right",
|
||||
@@ -87,7 +87,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
||||
ui.end_row();
|
||||
if let Some(l) = d.look.as_mut() {
|
||||
ui.label(tl!("Font:"));
|
||||
egui::ComboBox::from_id_salt("field-font").selected_text(l.font.label()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("field-font").selected_text(l.font.label()).show_ui(ui, |ui| {
|
||||
for f in FieldFont::ALL {
|
||||
ui.selectable_value(&mut l.font, f, f.label());
|
||||
}
|
||||
@@ -130,14 +130,14 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
||||
tl!("Thick")
|
||||
}
|
||||
};
|
||||
egui::ComboBox::from_id_salt("field-width").selected_text(thick(l.width)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("field-width").selected_text(thick(l.width)).show_ui(ui, |ui| {
|
||||
for (w, label) in [(1.0, tl!("Thin")), (2.0, tl!("Medium")), (3.0, tl!("Thick"))] {
|
||||
ui.selectable_value(&mut l.width, w, label);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Line Style:"));
|
||||
egui::ComboBox::from_id_salt("field-style").selected_text(tl!(l.style.label())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("field-style").selected_text(tl!(l.style.label())).show_ui(ui, |ui| {
|
||||
for s in BorderStyle::ALL {
|
||||
ui.selectable_value(&mut l.style, s, tl!(s.label()));
|
||||
}
|
||||
@@ -162,7 +162,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(tl!("Rotation:"));
|
||||
let before = d.rotation;
|
||||
egui::ComboBox::from_id_salt("field-rotation")
|
||||
craft_ui::ComboBox::from_id_salt("field-rotation")
|
||||
.selected_text(format!("{}°", d.rotation))
|
||||
.show_ui(ui, |ui| {
|
||||
for r in [0, 90, 180, 270] {
|
||||
@@ -237,7 +237,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
||||
if let Some(style) = d.check_style.as_mut() {
|
||||
ui.horizontal(|ui| {
|
||||
let l = ui.label(tl!(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" }));
|
||||
egui::ComboBox::from_id_salt("check-style")
|
||||
craft_ui::ComboBox::from_id_salt("check-style")
|
||||
.selected_text(tl!(style.label()))
|
||||
.show_ui(ui, |ui| {
|
||||
for s in pdfcraft_engine::CheckStyle::ALL {
|
||||
@@ -297,7 +297,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Default:"));
|
||||
let shown = if d.default.is_empty() { tl!("(none)").to_string() } else { d.default.clone() };
|
||||
egui::ComboBox::from_id_salt("choice-default").selected_text(shown).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("choice-default").selected_text(shown).show_ui(ui, |ui| {
|
||||
ui.selectable_value(&mut d.default, String::new(), tl!("(none)"));
|
||||
for o in d.options.clone() {
|
||||
ui.selectable_value(&mut d.default, o.clone(), o);
|
||||
|
||||
@@ -35,7 +35,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
let mut picked = cat;
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Select format category:"));
|
||||
egui::ComboBox::from_id_salt("format-cat")
|
||||
craft_ui::ComboBox::from_id_salt("format-cat")
|
||||
.selected_text(tl!(match cat {
|
||||
Category::None => "None",
|
||||
Category::Number => "Number",
|
||||
@@ -87,7 +87,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-sep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-sep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
ui.end_row();
|
||||
ui.label(tl!("Currency symbol:"));
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("fmt-cur")
|
||||
craft_ui::ComboBox::from_id_salt("fmt-cur")
|
||||
.selected_text(if currency.is_empty() { tl!("None") } else { currency.as_str() })
|
||||
.width(70.0)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -107,7 +107,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Negative number style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-neg").selected_text(NEGATIVES[(*neg).min(3) as usize]).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-neg").selected_text(NEGATIVES[(*neg).min(3) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in NEGATIVES.iter().enumerate() {
|
||||
ui.selectable_value(neg, i as u8, *s);
|
||||
}
|
||||
@@ -121,7 +121,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
ui.add(egui::DragValue::new(decimals).range(0..=10));
|
||||
ui.end_row();
|
||||
ui.label(tl!("Separator style:"));
|
||||
egui::ComboBox::from_id_salt("fmt-psep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-psep").selected_text(SEPARATORS[(*sep).min(4) as usize]).show_ui(ui, |ui| {
|
||||
for (i, s) in SEPARATORS.iter().enumerate() {
|
||||
ui.selectable_value(sep, i as u8, *s);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Format:"));
|
||||
egui::ComboBox::from_id_salt("fmt-date").selected_text(p.as_str()).width(170.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-date").selected_text(p.as_str()).width(170.0).show_ui(ui, |ui| {
|
||||
for pr in presets {
|
||||
ui.selectable_value(p, pr.to_string(), pr);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
}
|
||||
Format::Special(n) => {
|
||||
let mut idx = *n as usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[idx.min(4)])).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[idx.min(4)])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
@@ -161,7 +161,7 @@ pub(super) fn format_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme
|
||||
}
|
||||
Format::Mask(m) => {
|
||||
let mut idx = 4usize;
|
||||
egui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[4])).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("fmt-special").selected_text(tl!(SPECIALS[4])).show_ui(ui, |ui| {
|
||||
for (i, s) in SPECIALS.iter().enumerate() {
|
||||
ui.selectable_value(&mut idx, i, tl!(s));
|
||||
}
|
||||
@@ -220,7 +220,7 @@ pub(super) fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::them
|
||||
ui.label(tl!("Select Trigger:"));
|
||||
// Editing changes the script of one trigger's action: its trigger and kind stay.
|
||||
ui.add_enabled_ui(!editing, |ui| {
|
||||
egui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("action-trigger").selected_text(tl!(a.trigger.label())).width(220.0).show_ui(ui, |ui| {
|
||||
for tr in T::ALL {
|
||||
ui.selectable_value(&mut a.trigger, tr, tl!(tr.label()));
|
||||
}
|
||||
@@ -229,7 +229,7 @@ pub(super) fn actions_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::them
|
||||
ui.end_row();
|
||||
ui.label(tl!("Select Action:"));
|
||||
ui.add_enabled_ui(!editing, |ui| {
|
||||
egui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("action-kind").selected_text(tl!(ACTION_KINDS[a.kind])).width(220.0).show_ui(ui, |ui| {
|
||||
for (i, k) in ACTION_KINDS.iter().enumerate() {
|
||||
ui.selectable_value(&mut a.kind, i, tl!(k));
|
||||
}
|
||||
@@ -350,7 +350,7 @@ pub(super) fn calculate_tab(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::th
|
||||
if let Calculate::Simple { op, fields } = &mut d.calculate {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(24.0);
|
||||
egui::ComboBox::from_id_salt("calc-op").selected_text(tl!(op.label())).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("calc-op").selected_text(tl!(op.label())).show_ui(ui, |ui| {
|
||||
for o in CalcOp::ALL {
|
||||
ui.selectable_value(op, o, tl!(o.label()));
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@ pub(crate) fn search_body(ui: &mut egui::Ui, d: &mut SearchDraft, t: &Tokens) ->
|
||||
ui.add_space(6.0);
|
||||
widgets::radio_value(ui, &mut d.mode, SearchMode::Patterns, tl!("Patterns"));
|
||||
ui.add_enabled_ui(d.mode == SearchMode::Patterns, |ui| {
|
||||
egui::ComboBox::from_id_salt("redact-pattern").selected_text(tl!(d.pattern.label())).width(240.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("redact-pattern").selected_text(tl!(d.pattern.label())).width(240.0).show_ui(ui, |ui| {
|
||||
for p in REDACT_PATTERNS {
|
||||
ui.selectable_value(&mut d.pattern, p, tl!(p.label()));
|
||||
}
|
||||
@@ -475,7 +475,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
||||
}
|
||||
let codes_on = on && d.use_code;
|
||||
ui.add_enabled_ui(codes_on, |ui| {
|
||||
egui::ComboBox::from_id_salt("redaction-code-set").selected_text(d.code_set.name).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("redaction-code-set").selected_text(d.code_set.name).show_ui(ui, |ui| {
|
||||
for set in REDACTION_CODE_SETS {
|
||||
if ui.selectable_label(d.code_set == set, set.name).clicked() && d.code_set != set {
|
||||
d.code_set = set;
|
||||
@@ -505,7 +505,7 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
|
||||
let look = &mut d.look;
|
||||
ui.label(tl!("Font:"));
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
egui::ComboBox::from_id_salt("overlay-font").selected_text(look.font.name()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("overlay-font").selected_text(look.font.name()).show_ui(ui, |ui| {
|
||||
for f in pdfcraft_engine::OverlayFont::ALL {
|
||||
ui.selectable_value(&mut look.font, f, f.name());
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ fn configure(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
ui.add(crate::widgets::line(&mut n.country).hint_text("US").char_limit(2).desired_width(60.0)).labelled_by(l.id);
|
||||
ui.end_row();
|
||||
ui.label(tl!("Key Algorithm"));
|
||||
egui::ComboBox::from_id_salt("key-alg").selected_text(KEY_ALGORITHMS[n.key].0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("key-alg").selected_text(KEY_ALGORITHMS[n.key].0).show_ui(ui, |ui| {
|
||||
for (i, (label, _)) in KEY_ALGORITHMS.iter().enumerate() {
|
||||
ui.selectable_value(&mut n.key, i, *label);
|
||||
}
|
||||
@@ -415,7 +415,7 @@ fn sign_as(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
3 => tl!("Annotations, form fill-in, and digital signatures"),
|
||||
_ => tl!("Form fill-in and digital signatures"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("certify-p").selected_text(label(p)).width(300.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("certify-p").selected_text(label(p)).width(300.0).show_ui(ui, |ui| {
|
||||
for v in [1u8, 2, 3] {
|
||||
ui.selectable_value(&mut p, v, label(v));
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(crate) fn create_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(crate::widgets::line(&mut d.category).desired_width(200.0).hint_text(tl!("e.g. My stamps"))).labelled_by(l.id);
|
||||
if !categories.is_empty() {
|
||||
egui::ComboBox::from_id_salt("stamp-categories").selected_text("").width(24.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("stamp-categories").selected_text("").width(24.0).show_ui(ui, |ui| {
|
||||
for c in &categories {
|
||||
if ui.selectable_label(d.category == *c, c).clicked() {
|
||||
d.category = c.clone();
|
||||
|
||||
@@ -103,7 +103,7 @@ pub(crate) fn options_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens)
|
||||
group(ui, &crate::i18n::fmt(tl!("Checking Options ({on} of 32 in all categories)"), &[("on", &on.to_string())]), &mut |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Category:"));
|
||||
egui::ComboBox::from_id_salt("a11y-category").selected_text(tl!(o.category.label())).width(260.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("a11y-category").selected_text(tl!(o.category.label())).width(260.0).show_ui(ui, |ui| {
|
||||
for c in Category::ALL {
|
||||
ui.selectable_value(&mut o.category, c, tl!(c.label()));
|
||||
}
|
||||
|
||||
@@ -345,11 +345,14 @@ fn edit_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
let all = Step::all();
|
||||
let add = &mut app.wizard.add;
|
||||
ui.horizontal(|ui| {
|
||||
egui::ComboBox::from_id_salt("action-add-step").selected_text(tl!(all[(*add).min(all.len() - 1)].label())).width(240.0).show_ui(ui, |ui| {
|
||||
for (i, s) in all.iter().enumerate() {
|
||||
ui.selectable_value(add, i, tl!(s.label()));
|
||||
}
|
||||
});
|
||||
craft_ui::ComboBox::from_id_salt("action-add-step").selected_text(tl!(all[(*add).min(all.len() - 1)].label())).width(240.0).show_ui(
|
||||
ui,
|
||||
|ui| {
|
||||
for (i, s) in all.iter().enumerate() {
|
||||
ui.selectable_value(add, i, tl!(s.label()));
|
||||
}
|
||||
},
|
||||
);
|
||||
if ui.button(tl!("Add Step")).clicked() {
|
||||
a.steps.push(all[(*add).min(all.len() - 1)].clone());
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
ui.label(tl!("Old file:"));
|
||||
let shown =
|
||||
others.iter().find(|(id, _)| Some(*id) == app.compare_old).map_or(tl!("Open the older version first").to_string(), |(_, n)| n.clone());
|
||||
egui::ComboBox::from_id_salt("compare-old").selected_text(shown).width(320.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("compare-old").selected_text(shown).width(320.0).show_ui(ui, |ui| {
|
||||
for (id, n) in &others {
|
||||
ui.selectable_value(&mut app.compare_old, Some(*id), n);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
||||
if kind == ExportKind::Image {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(tl!("Resolution"));
|
||||
egui::ComboBox::from_id_salt("export-dpi")
|
||||
craft_ui::ComboBox::from_id_salt("export-dpi")
|
||||
.selected_text(crate::i18n::fmt(tl!("{dpi} pixels/inch"), &[("dpi", &d.dpi.to_string())]))
|
||||
.show_ui(ui, |ui| {
|
||||
for dpi in [72.0, 96.0, 150.0, 300.0, 600.0] {
|
||||
@@ -67,7 +67,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
||||
ImageFormat::Jpeg { quality } => quality,
|
||||
_ => 85,
|
||||
};
|
||||
egui::ComboBox::from_id_salt("export-format").selected_text(d.format.label()).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("export-format").selected_text(d.format.label()).show_ui(ui, |ui| {
|
||||
for f in [ImageFormat::Png, ImageFormat::Jpeg { quality }, ImageFormat::Tiff] {
|
||||
let on = std::mem::discriminant(&d.format) == std::mem::discriminant(&f);
|
||||
if ui.selectable_label(on, f.label()).clicked() {
|
||||
@@ -93,7 +93,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: E
|
||||
let label = move |n: u32| {
|
||||
if n == 0 { tl!("No limit").to_string() } else { crate::i18n::trn(crate::i18n::current(), u64::from(n), "{n} pixel", "{n} pixels") }
|
||||
};
|
||||
egui::ComboBox::from_id_salt("export-min").selected_text(label(d.min_side)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("export-min").selected_text(label(d.min_side)).show_ui(ui, |ui| {
|
||||
for n in [0, 16, 32, 64, 128, 256] {
|
||||
ui.selectable_value(&mut d.min_side, n, label(n));
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
||||
(selected, picker)
|
||||
};
|
||||
let before = app.language.clone();
|
||||
egui::ComboBox::from_id_salt("interface-language")
|
||||
craft_ui::ComboBox::from_id_salt("interface-language")
|
||||
.selected_text(selected)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_value(&mut app.language, crate::i18n::AUTO.to_string(), tl!("Auto")).clicked() {
|
||||
@@ -359,7 +359,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
||||
let auto_percent = ui_scale::percent(ui_scale::drawn(None, app.system_text_scale, native));
|
||||
let auto = crate::i18n::fmt(tl!("Auto ({percent}%)"), &[("percent", &auto_percent.to_string())]);
|
||||
let selected = app.ui_scale.map_or_else(|| auto.clone(), |f| format!("{}%", ui_scale::percent(f)));
|
||||
egui::ComboBox::from_id_salt("interface-size")
|
||||
craft_ui::ComboBox::from_id_salt("interface-size")
|
||||
.selected_text(selected)
|
||||
.show_ui(ui, |ui| {
|
||||
if ui.selectable_value(&mut app.ui_scale, None, auto).clicked() {
|
||||
@@ -485,7 +485,7 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
|
||||
ui.horizontal(|ui| {
|
||||
let label = ui.label(tl!("Replace document colours"));
|
||||
let mode = app.reading_colors.mode_name();
|
||||
egui::ComboBox::from_id_salt("reading-colors")
|
||||
craft_ui::ComboBox::from_id_salt("reading-colors")
|
||||
.selected_text(tl!(reading_mode_label(mode)))
|
||||
.show_ui(ui, |ui| {
|
||||
for (m, label) in [
|
||||
@@ -590,7 +590,7 @@ fn date_format(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
let label = ui.label(tl!("Fill & Sign dates"));
|
||||
let current = app.session.date_format().to_string();
|
||||
let selected = if DATE_FORMATS.contains(¤t.as_str()) { sample(app, ¤t) } else { tl!("Custom").to_string() };
|
||||
egui::ComboBox::from_id_salt("date-format")
|
||||
craft_ui::ComboBox::from_id_salt("date-format")
|
||||
.selected_text(selected)
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -652,7 +652,7 @@ fn date_format(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) {
|
||||
let names = DATE_LANGUAGES.iter().map(|l| (l, date_language_picker_name(l, drawable))).collect::<Vec<_>>();
|
||||
(follow, selected, current, names)
|
||||
};
|
||||
egui::ComboBox::from_id_salt("date-language")
|
||||
craft_ui::ComboBox::from_id_salt("date-language")
|
||||
.selected_text(selected)
|
||||
.width(240.0)
|
||||
.show_ui(ui, |ui| {
|
||||
|
||||
@@ -121,21 +121,21 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> (boo
|
||||
egui::Grid::new("ocr-settings").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
ui.label(tl!("Document language"));
|
||||
let name = LANGUAGES.iter().find(|l| l.0 == d.language).map_or("English", |l| l.1);
|
||||
egui::ComboBox::from_id_salt("ocr-language").selected_text(tl!(name)).width(220.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("ocr-language").selected_text(tl!(name)).width(220.0).show_ui(ui, |ui| {
|
||||
for (code, name) in LANGUAGES {
|
||||
ui.selectable_value(&mut d.language, (*code).to_string(), tl!(*name));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Output"));
|
||||
egui::ComboBox::from_id_salt("ocr-output").selected_text(tl!("Searchable Image (Exact)")).width(220.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("ocr-output").selected_text(tl!("Searchable Image (Exact)")).width(220.0).show_ui(ui, |ui| {
|
||||
let _ = ui
|
||||
.selectable_label(true, tl!("Searchable Image (Exact)"))
|
||||
.on_hover_text(tl!("Adds invisible text over each word; the page image is not changed"));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tl!("Downsample to"));
|
||||
egui::ComboBox::from_id_salt("ocr-dpi").selected_text(format!("{} dpi", d.dpi)).width(220.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("ocr-dpi").selected_text(format!("{} dpi", d.dpi)).width(220.0).show_ui(ui, |ui| {
|
||||
for v in [600, 300, 150, 72] {
|
||||
ui.selectable_value(&mut d.dpi, v, format!("{v} dpi"));
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) {
|
||||
Compression::Flate => tl!("ZIP"),
|
||||
Compression::Retain => tl!("Retain existing"),
|
||||
};
|
||||
egui::ComboBox::from_id_salt((id, "compression")).selected_text(label).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt((id, "compression")).selected_text(label).show_ui(ui, |ui| {
|
||||
let q = match s.compression {
|
||||
Compression::Jpeg(q) => q,
|
||||
_ => 60,
|
||||
@@ -133,7 +133,7 @@ fn image_row(ui: &mut egui::Ui, id: &str, title: &str, s: &mut ImageSettings) {
|
||||
if let Compression::Jpeg(q) = &mut s.compression {
|
||||
ui.label(tl!("Quality"));
|
||||
let name = QUALITIES.iter().min_by_key(|(_, v)| (*v as i32 - *q as i32).abs()).map_or(tl!("Medium"), |(n, _)| tl!(n));
|
||||
egui::ComboBox::from_id_salt((id, "quality")).selected_text(name).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt((id, "quality")).selected_text(name).show_ui(ui, |ui| {
|
||||
for (n, v) in QUALITIES {
|
||||
ui.selectable_value(q, v, tl!(n));
|
||||
}
|
||||
@@ -165,7 +165,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
Preset::Mobile => tl!("Mobile"),
|
||||
};
|
||||
let current = Preset::of(&d.settings);
|
||||
egui::ComboBox::from_id_salt("optimize-preset").selected_text(current.map_or(tl!("Custom"), name)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("optimize-preset").selected_text(current.map_or(tl!("Custom"), name)).show_ui(ui, |ui| {
|
||||
for p in Preset::ALL {
|
||||
if ui.selectable_label(current == Some(p), name(p)).clicked() {
|
||||
d.settings = p.settings();
|
||||
@@ -178,7 +178,7 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut OptimizeDraft, t: &Tokens) -> (boo
|
||||
Compatibility::Retain => tl!("Retain existing").to_string(),
|
||||
_ => format!("PDF {}", c.name()),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("optimize-compatibility").selected_text(compat_label(d.settings.compatibility)).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("optimize-compatibility").selected_text(compat_label(d.settings.compatibility)).show_ui(ui, |ui| {
|
||||
for c in Compatibility::ALL {
|
||||
ui.selectable_value(&mut d.settings.compatibility, c, compat_label(c));
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ fn driver_options_panel(ui: &mut egui::Ui, d: &mut PrintDraft, t: &Tokens) {
|
||||
let label_of = |c: &str| o.choices.iter().find(|(k, _)| k == c).map_or_else(|| c.to_string(), |(_, l)| l.clone());
|
||||
let mut pick = current.clone();
|
||||
let option_label = ui.label(&o.label);
|
||||
egui::ComboBox::from_id_salt(("driver-option", &o.key))
|
||||
craft_ui::ComboBox::from_id_salt(("driver-option", &o.key))
|
||||
.selected_text(label_of(¤t))
|
||||
.width(200.0)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -64,7 +64,7 @@ fn driver_options_panel(ui: &mut egui::Ui, d: &mut PrintDraft, t: &Tokens) {
|
||||
/// Draw a combo box whose accessible name does not depend on the selected value.
|
||||
fn combo<T: PartialEq + Copy>(ui: &mut egui::Ui, id: &str, name: &str, value: &mut T, choices: &[(T, &str)], width: f32) {
|
||||
let shown = choices.iter().find(|c| c.0 == *value).map_or("", |c| c.1);
|
||||
egui::ComboBox::from_id_salt(id)
|
||||
craft_ui::ComboBox::from_id_salt(id)
|
||||
.selected_text(shown)
|
||||
.width(width)
|
||||
.show_ui(ui, |ui| {
|
||||
@@ -107,7 +107,7 @@ pub(crate) fn body(
|
||||
None if still_looking(d) => tl!("Looking for printers…").to_string(),
|
||||
None => tl!("Save as PDF").to_string(),
|
||||
};
|
||||
egui::ComboBox::from_id_salt("printer").selected_text(shown).width(260.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("printer").selected_text(shown).width(260.0).show_ui(ui, |ui| {
|
||||
if d.printers_loading {
|
||||
ui.add_enabled(false, egui::Label::new(tl!("Looking for printers…")));
|
||||
}
|
||||
@@ -201,7 +201,7 @@ pub(crate) fn body(
|
||||
let paper_label = ui.label(tl!("Paper:"));
|
||||
let paper_name = PAPERS.get(d.paper).map_or("US Letter", |p| p.0);
|
||||
let before = d.paper;
|
||||
egui::ComboBox::from_id_salt("paper").selected_text(tl!(paper_name)).width(260.0).show_ui(ui, |ui| {
|
||||
craft_ui::ComboBox::from_id_salt("paper").selected_text(tl!(paper_name)).width(260.0).show_ui(ui, |ui| {
|
||||
for (i, (name, _)) in PAPERS.iter().enumerate() {
|
||||
ui.selectable_value(&mut d.paper, i, tl!(name));
|
||||
}
|
||||
|
||||
+17
-8
@@ -69,13 +69,20 @@ pinned in `release.yml` and builds with `CRAFT_FONTS_DIR` and `CRAFT_FONTS_REQUI
|
||||
embed its Japanese fonts and fail rather than ship without them (`AGENTS.md` §1.4). Bump the pin
|
||||
deliberately.
|
||||
|
||||
**OCR models:** every desktop package ships the Scan & OCR models (#103). The packaging scripts call
|
||||
`stage_models` (`packaging/env.sh`; `package.ps1` on Windows), which runs `cargo xtask models` to
|
||||
fetch every `kind = "model"` file in `ATTRIBUTION.toml` (verified by SHA-256) with its licence text
|
||||
and an `ATTRIBUTION.txt`, straight into the package, so packaging needs the network. They go where
|
||||
`pdfcraft_ocr::Models::dirs_beside_exe` looks: `models\` beside `pdfcraft.exe` (MSI, portable zip),
|
||||
`PdfCraft.app/Contents/Resources/models` (macOS), and `share/pdfcraft/models` beside `bin/`
|
||||
(deb, rpm, tar.gz, AppImage, Flatpak, FreeBSD). The web build doesn't include them yet.
|
||||
**OCR models:** desktop packaging stages the Scan & OCR models (#103).
|
||||
The packaging scripts call `stage_models` (`packaging/env.sh`; `package.ps1` on Windows), which runs `cargo xtask models` to fetch every `kind = "model"` file in `ATTRIBUTION.toml` and verify its SHA-256.
|
||||
The current detection and recognition files are `text-detection.onnx` and `text-recognition.onnx`.
|
||||
Each model ships with its licence text and the directory's `ATTRIBUTION.txt`, so packaging needs the network.
|
||||
Upstream announced the legacy RTEN download URLs' brownout in [ocrs #282](https://github.com/robertknight/ocrs/issues/282) on 2026-10-27 and directs consumers to the ONNX models.
|
||||
|
||||
Runtime discovery prefers a complete ONNX detection/recognition pair within each directory, then a complete legacy `text-detection.rten` / `text-recognition.rten` pair.
|
||||
An incomplete ONNX update can therefore use an intact RTEN pair in the same directory.
|
||||
Discovery never combines model formats or files from different directories.
|
||||
Directory precedence stays the same: `$PDFCRAFT_MODELS`, the release-package locations beside the executable, then the source tree's `assets/models/`.
|
||||
The first directory with a complete supported pair wins, even if an earlier legacy pair precedes a later ONNX pair.
|
||||
|
||||
The release-package locations are `models\` beside `pdfcraft.exe` (MSI, portable zip), `PdfCraft.app/Contents/Resources/models` (macOS), and `share/pdfcraft/models` beside `bin/` (deb, rpm, tar.gz, AppImage, Flatpak, FreeBSD).
|
||||
The web build does not include these models, and OCR remains limited to the Latin alphabet without accents.
|
||||
|
||||
### macOS
|
||||
|
||||
@@ -111,7 +118,9 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable.
|
||||
- Before packaging, it reads both executables' PE headers: the machine type must match `-Arch`, and
|
||||
`pdfcraft.exe` must be a GUI-subsystem program (no console window, #57) while `pdfcraft-cli.exe`
|
||||
stays a console program.
|
||||
- `pdfcraft.wxs` (WiX v5) installs per machine into Program Files with a Start Menu shortcut, a
|
||||
- `pdfcraft.wxs` (WiX v5) is a dual-scope package (per-user by default into
|
||||
`%LOCALAPPDATA%\Programs\ArtCraft\PdfCraft`, per-machine into `Program Files\ArtCraft\PdfCraft` (`Program Files (x86)` for the 32-bit build),
|
||||
see `packaging/windows/INSTALLATION.md`) with a Start Menu shortcut, a
|
||||
desktop shortcut (on by default) and an App Paths entry. Both shortcuts are plain links to
|
||||
`pdfcraft.exe`, not advertised MSI shortcuts (#107, #143). The MSI version is the numeric `X.Y.Z`
|
||||
(MSI has no pre-release field), and same-version upgrades are allowed so release candidates replace
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
# PdfCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: OCR model downloads use verified ONNX pairs with legacy discovery fallback; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-25 | **Last updated:** 2026-10-25 | **Change:** minor (M14 localization current state: Arabic logical catalog, explicit RTL label direction and mirrored stock chrome; native/browser RTL workflows unqualified. Native release Hans face via LXGW WenKai Lite; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M7, #2636 partial: pasting added text boxes and images in the engine and through `content_copy`; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-23 · **Last updated:** 2026-10-23 · **Change:** minor (M11: image optimization accounts for page-local UserUnit; estimates unchanged)
|
||||
@@ -193,7 +194,7 @@ total matches the parity estimate.
|
||||
| M7 | Content editing (text, images, header/footer, watermark) | 250–500 | 20% | 100–180 | Done: header & footer, watermarks, backgrounds, Bates; added text and images that stay editable (move, resize, format, rotate, flip, crop, replace); links (Link tool, Link Properties, create from URLs, remove all). Missing: editing existing text and images in place (the longest pole), image/PDF watermarks |
|
||||
| M8 | Security + redaction | 100–180 | 68% | 15–30 | Done: opening protected documents, permissions, Protect Using Password, Remove security; redaction (mark text/areas/pages, Search & Redact with patterns, apply removing glyphs, image pixels, vectors, XObject content, annotations and fields, verification, full rewrite on save); Remove hidden information and Sanitize.<br>Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). Redaction cleans indirect and form structure references, preserving surviving form tags. Redaction retires inherited resource dictionaries, so a parent `/Pages` can't keep a removed XObject reachable; unredacted pages are left untouched. Image redaction removes alternate images and unsupported masks only on affected images. Changed marked-content spans lose replacement descriptions; untouched spans keep theirs. Hidden-text cleanup retires unused named descriptions across inherited/shared page and form resources (64 focused tests and scoped Rust 1.96 Clippy/wasm checks pass; publication CI pending). The redaction audit counts a filled cover that hides half of any one word of a long line (#2662), applied or not. Phone-pattern redaction also matches labelled Italian geographic numbers with `+39` and a slash after the area code (#3205 follow-up); unlabelled international slash numbers remain unsupported. Missing: certificate security, custom redaction code sets and pattern locales, DCT re-encoding |
|
||||
| M9 | Signatures (PAdES, validation) | 160–280 | 60% | 40–70 | Done: new `sign` crate (DER, X.509, CMS, PKCS #12 on RustCrypto; aws-lc-rs for RSA private keys); PAdES B-B signing (visible, invisible, existing fields, certification with DocMDP); validation with trust store and changes-after-signing classification; self-signed digital IDs; Signatures panel, message bar, sign dialogs; agent tools. Checked with pdfsig and OpenSSL.<br>Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites, page validation overlays separate from original signature artwork (baked Not Verified captions remain; #79 open), snapshot tabs that reuse instead of multiplying. Missing: timestamps (B-T), LTV (DSS, OCSP, CRL), FieldMDP, smart cards, PKCS #11 |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Word table cells use the grid columns they span, including cells after a merged cell. Done since: text in a table slot covered by another cell's span remains as paragraph text in Word, HTML, RTF and PowerPoint. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. Image export applies soft-mask Decode opacity intervals before resizing, including indirect entries and inverted ranges. Vector printing gives annotation appearances unoccupied XObject names so existing page artwork survives. |
|
||||
| M10 | OCR, create, export, print | 200–350 | 33% | 120–215 | Done: create from blank/text/PNG/JPEG/TIFF (multi-page)/GIF/BMP; export PNG/JPEG/TIFF and text; Print (Acrobat's sizing, n-up, booklet, poster, comments & forms, preview, the CUPS and Windows spoolers, print-ready PDF).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; architectural/ANSI/large ISO sheet presets, Match page at 100% and PDF-scale preview (#599 follow-up); Create PDF from multiple files (PDFs, images and text, edited as pages in the grid); OCR can also read pictures on pages that have text, without adding that text twice (#1028). Model staging fetches SHA-256-verified ONNX detection/recognition files after the upstream RTEN URL brownout; runtime discovery prefers complete ONNX pairs per directory, then complete legacy RTEN pairs, without mixing formats or changing directory precedence. Done since: Word invoice export uses borderless inferred tables, floating positioned text/artwork only where text overlaps an image or is rotated, and per-page sizes, covered by synthetic invoice automation regressions (#773); printing on the web through the browser's print dialog, sheets drawn by PdfCraft (gap 4). Lossless image export applies per-component Decode, including Indexed mapping (#1201 partial). Existing 16-bit precision and color-management limits remain. #3532 partial: vector printing retains existing appearances with absent Subtype through a print-owned Form copy; source artwork stays unchanged. #3120: Office export reads form XObjects 50 levels deep, as the renderer does, and up to 50 000 a page; pages past that are listed in `truncated_pages`. Word table cells use the grid columns they span, including cells after a merged cell. Done since: text in a table slot covered by another cell's span remains as paragraph text in Word, HTML, RTF and PowerPoint. Missing: OCR languages beyond Latin, editable-text OCR output, full Office layout fidelity/import. Done since: dotted-leader contents entries stay in separate complete paragraphs (#1757) #2156 candidate: last selected print paper persists across sessions; other print preferences unchanged. Image export applies soft-mask Decode opacity intervals before resizing, including indirect entries and inverted ranges. Vector printing gives annotation appearances unoccupied XObject names so existing page artwork survives. |
|
||||
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 80–140 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Target-version compatibility is partial: newer content is not detected or converted. Content stream boundaries preserve comments, split operands and graphics state when image fragments merge. Image downsampling includes page-local `/UserUnit` scaling when measuring physical resolution. Done since: reject invalid image and soft-mask dimensions with checked sizing (#2178). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
|
||||
| M12 | Accessibility, compare, measure, search, XFA | 200–380 | 30% | 90–160 | Done: new `a11y` crate with the Accessibility Checker (all 32 rules, report, Fix/Skip/Explain, options dialog and results panel, agent tools). Locatable figure, annotation and form findings highlight their page bounds. Character encoding accepts Identity-H/V with Adobe CJK collections and predefined CJK CMaps independently (#3673); per-glyph validation and embedded-font extraction remain separate.<br>Done since: 2D distance, perimeter and area measurements with persistent viewport calibration, snapping, live information and CSV export; a bounded, text-only auto-tag proposal tool for one page. Marking a figure decorative rewrites only that stream (#1324). An XFA radio item whose value contains a space is written back unchanged (#1319). Done since: #2569: Growable multiline XFA text reflows after edits, with undo/redo, save/reopen and print coverage; oversized fields still become read-only page text. Structured-data synchronization warnings retain accepted whitespace without relayout from stale data. Done since: #2572: Every repeated page-master button resolves its own field to the shared XFA click script, including print buttons. Done since: #2570: Dynamic XFA rich-text fields carry exData/maxLength into AcroForm MaxLen for reported and enforced Unicode character limits, retained after save/reopen. Done since: #2589: Active XFA skips PDF-container fallback scripts while retaining requested messages and saved scripts; ordinary/static/unlaid-out documents and explicit console/field calls keep their existing behavior. Missing: writing proposed tags and parent tree, field autotag, visual Tags/Order/Content panels, Reading Order tool, compare, geospatial/3D measurement, search index, XFA<br>Reading colours: pending page backgrounds follow the configured colour (#2059). |
|
||||
| M13 | Automation (MCP, Action Wizard, CLI) + AI providers | 60–120 | 45% | 25–45 | Done: MCP resources (document info, text, page images); headless tool table (123 tools incl. signing, optimizing, initial view, links, stamps, data exchange, comment review, forms authoring and scripts, redaction, sanitize, print, add content), opt-in MCP server over stdio, CLI `run`/`tools` (closed stdout pipes exit cleanly), UI control channel with drag. Missing: Action Wizard, AI providers<br>Done since: CLI run and MCP reject missing, empty, repeated and option-valued --root arguments before any tool/script execution or MCP initialization. A single-hyphen directory name remains valid; synthetic CLI regressions preserve a would-be overwritten file. `text_find` also caps the per-page match ranges it materializes at the remaining result limit plus one continuation match. CLI robustness scans visit each directory once through overlapping roots and symlink aliases. |
|
||||
@@ -254,6 +255,7 @@ Estimates unchanged.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-27 | minor | M10: OCR stages SHA-256-verified ONNX models after the upstream RTEN URL brownout; discovery prefers a complete ONNX pair per directory, falls back to a complete RTEN pair, and never mixes formats or directories; language/web limits and estimates unchanged. |
|
||||
| 2026-10-24 | minor | M3, #3970 follow-up: the button release given back after a window move or resize goes before the pointer leaving, so hover doesn't stick. Estimates unchanged. |
|
||||
| 2026-10-23 | minor | M8, #3205 follow-up: phone-pattern redaction includes the +39 country code on labelled Italian geographic numbers with a slash; dates, protocol identifiers and whole-run guards remain. Estimates unchanged. |
|
||||
| 2026-10-23 | minor | M10: Word table cells use the grid columns they span, including cells after a merged cell; synthetic export regression and CLI reproduction. Estimates unchanged. |
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Windows installation policy checks
|
||||
|
||||
New installations default to the current account under `Programs\ArtCraft\<App>`.
|
||||
The first setup page offers installation for everyone. Scope is selected before duplicate
|
||||
checks and directory costing. An update uses the existing context's remembered exact folder;
|
||||
`INSTALLFOLDER` supplied explicitly is never replaced. It is a secure MSI property so the
|
||||
chosen folder reaches the elevated installer. Silent machine updates pass `ALLUSERS=1`.
|
||||
A cross-context copy is refused; changing context means uninstalling the old copy first.
|
||||
|
||||
App registration follows the selected context. Existing optional shortcuts and file-handler
|
||||
choices stay app-owned. No new cleanup operation or model/settings migration is introduced.
|
||||
Packaged resources remain package-owned; separately downloaded shared models are not caches
|
||||
and are outside this installer's ownership.
|
||||
|
||||
## Repeatable metadata tests
|
||||
|
||||
On Windows with WiX 5.0.2 and its matching UI/Util extensions:
|
||||
|
||||
```powershell
|
||||
./packaging/windows/test-install-policy.ps1 -OutputDirectory ./target/install-policy-tests
|
||||
```
|
||||
|
||||
The script builds x64, x86 and arm64 packages with deliberately non-executable payloads. It
|
||||
uses the actual package source and compiled database. A safe Windows Installer session checks
|
||||
24 scope/registration/maintenance cases, remembered and explicit custom folders, sequencing,
|
||||
upgrade identity and the original registration inventory recorded in `install-policy.json`.
|
||||
The release packaging script runs the compiled checker before signing. Packaging CI runs the
|
||||
small fixture builds without compiling the Rust application.
|
||||
|
||||
Never install or distribute the `NOT-INSTALLABLE.msi` fixtures as an app. Passing these checks
|
||||
does not prove real installation, elevation, legacy-version upgrades, registry writes, optional
|
||||
shortcut/menu behaviour, cancellation, rollback or uninstall. Those still require the real
|
||||
payload in an authorised clean Windows test environment. Cross-architecture metadata is not
|
||||
execution evidence on those architectures. Existing open installer/association PRs must be
|
||||
reconciled before publication. Nothing here changes macOS/Linux native packaging conventions.
|
||||
@@ -0,0 +1,296 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check the compiled installation policy without installing any product.
|
||||
.DESCRIPTION
|
||||
Uses Windows Installer's safe session (OpenPackage option 1). Only property-setting custom
|
||||
actions are exercised. No AppSearch, install, repair, removal or file-writing action runs.
|
||||
#>
|
||||
param([Parameter(Mandatory = $true)] [string] $MsiPath)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$policy = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'install-policy.json') | ConvertFrom-Json
|
||||
function Read-Rows($Database, [string] $Sql, [int] $Columns) {
|
||||
$view = $Database.OpenView($Sql)
|
||||
try {
|
||||
[void]$view.Execute()
|
||||
while ($null -ne ($record = $view.Fetch())) {
|
||||
try {
|
||||
$values = @(for ($i = 1; $i -le $Columns; $i++) {
|
||||
$record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, @($i))
|
||||
})
|
||||
,$values
|
||||
} finally { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($record) }
|
||||
}
|
||||
} finally {
|
||||
[void]$view.Close()
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($view)
|
||||
}
|
||||
}
|
||||
function Set-SessionProperty($Session, [string] $Name, [string] $Value) {
|
||||
[void]$Session.GetType().InvokeMember('Property', 'SetProperty', $null, $Session, @($Name, $Value))
|
||||
}
|
||||
function Get-SessionProperty($Session, [string] $Name) {
|
||||
$Session.GetType().InvokeMember('Property', 'GetProperty', $null, $Session, @($Name))
|
||||
}
|
||||
function Get-PackageTemplate($Database) {
|
||||
# Summary information property 7 is the platform: "Intel;1033", "x64;1033" or "Arm64;1033".
|
||||
$summary = $Database.GetType().InvokeMember('SummaryInformation', 'GetProperty', $null, $Database, @(0))
|
||||
try { [string]$summary.GetType().InvokeMember('Property', 'GetProperty', $null, $summary, @(7)) }
|
||||
finally { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($summary) }
|
||||
}
|
||||
$installer = $null; $database = $null; $session = $null
|
||||
try {
|
||||
$installer = New-Object -ComObject WindowsInstaller.Installer
|
||||
$file = (Resolve-Path -LiteralPath $MsiPath).Path
|
||||
$database = $installer.OpenDatabase($file, 0)
|
||||
$session = $installer.OpenPackage($file, 1)
|
||||
$properties = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Property`, `Value` FROM `Property`' 2)) { $properties[$row[0]] = $row[1] }
|
||||
$secureProperties = @($properties['SecureCustomProperties'] -split ';')
|
||||
if ('INSTALLFOLDER' -notin $secureProperties) {
|
||||
throw 'The selected installation folder must survive elevation (secure INSTALLFOLDER).'
|
||||
}
|
||||
if ($policy.mode -eq 'perUser') {
|
||||
if ($properties.ContainsKey('ALLUSERS')) { throw 'Launcher must remain per-user; machine self-update is not part of this change.' }
|
||||
if ($properties['UpgradeCode'].Trim('{}') -ne $policy.upgradeCode) { throw 'Launcher upgrade identity changed.' }
|
||||
$dirs = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`' 3)) { $dirs[$row[0]] = @($row[1],$row[2]) }
|
||||
if ($dirs['INSTALLFOLDER'][0] -ne 'CraftSuiteFolder' -or $dirs['CraftSuiteFolder'][1] -ne 'ArtCraft' -or
|
||||
$dirs['CraftSuiteFolder'][0] -ne 'LocalProgramsFolder' -or $dirs['LocalProgramsFolder'][0] -ne 'LocalAppDataFolder') {
|
||||
throw 'Launcher new-install folder must be under the per-user ArtCraft suite root.'
|
||||
}
|
||||
foreach ($explicit in @('', 'F:\Chosen exact launcher')) {
|
||||
Set-SessionProperty $session 'PREVIOUSINSTALLFOLDER' 'E:\Existing exact launcher'
|
||||
Set-SessionProperty $session 'INSTALLFOLDER' $explicit
|
||||
$row = @(Read-Rows $database 'SELECT `Condition` FROM `InstallUISequence` WHERE `Action` = ''CraftRestoreLauncherFolder''' 1)[0]
|
||||
if ($session.EvaluateCondition($row[0]) -eq 1) {
|
||||
if ($session.DoAction('CraftRestoreLauncherFolder') -ne 1) { throw 'Launcher restore action failed.' }
|
||||
}
|
||||
$expected = if ($explicit) { $explicit } else { 'E:\Existing exact launcher' }
|
||||
if ((Get-SessionProperty $session 'INSTALLFOLDER') -ne $expected) { throw 'Launcher custom or existing path was lost.' }
|
||||
}
|
||||
$rows = @(Read-Rows $database 'SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`' 4)
|
||||
if (@($rows | Where-Object { $_[0] -ne '1' }).Count) { throw 'Launcher registration must stay under the current account.' }
|
||||
foreach ($entry in $policy.registry) {
|
||||
if (@($rows | Where-Object { $_[0] -eq $entry[0] -and $_[1] -eq $entry[1] -and $_[2] -eq $entry[2] -and $_[3] -eq $entry[3] }).Count -ne 1) {
|
||||
throw 'Launcher registration or shortcut preference changed unexpectedly.'
|
||||
}
|
||||
}
|
||||
Write-Output 'PASS ArtCraft Launcher: per-user scope, suite folder, remembered/custom paths and existing registration. No installation performed.'
|
||||
return
|
||||
}
|
||||
if ($properties['ALLUSERS'] -ne '2' -or $properties['MSIINSTALLPERUSER'] -ne '1') {
|
||||
throw 'Policy regression: new packages must default to the per-user dual-scope context.'
|
||||
}
|
||||
if ($properties['UpgradeCode'].Trim('{}') -ne $policy.upgradeCode) { throw 'The existing upgrade identity must not change.' }
|
||||
$directories = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`' 3)) {
|
||||
$directories[$row[0]] = @($row[1], $row[2])
|
||||
}
|
||||
if ($directories['INSTALLFOLDER'][0] -ne 'CraftSuiteFolder' -or
|
||||
$directories['CraftSuiteFolder'][1] -ne 'ArtCraft') { throw 'Expected the app under the ArtCraft suite folder.' }
|
||||
$programs = $directories['CraftSuiteFolder'][0]
|
||||
if ($programs -eq 'ProgramFiles6432Folder') { $programs = $directories[$programs][0] }
|
||||
if ($programs -notin 'ProgramFilesFolder', 'ProgramFiles64Folder') {
|
||||
throw 'Use Windows Installer native folder redirection, not a hard-coded profile path.'
|
||||
}
|
||||
$guards = @(Read-Rows $database 'SELECT `Condition`, `Description` FROM `LaunchCondition`' 2 |
|
||||
Where-Object { $_[0] -match 'CRAFT_(USER|MACHINE)_DIR' })
|
||||
if ($guards.Count -ne 2) { throw 'Expected both duplicate-installation guards.' }
|
||||
$cases = 0
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
foreach ($user in @('', 'D:\Existing user app')) {
|
||||
foreach ($machine in @('', 'E:\Existing machine app')) {
|
||||
foreach ($installed in @('', '1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
Set-SessionProperty $session 'Installed' $installed
|
||||
Set-SessionProperty $session 'CRAFT_USER_DIR' $user
|
||||
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' $machine
|
||||
$allowed = $true
|
||||
foreach ($guard in $guards) {
|
||||
$value = $session.EvaluateCondition($guard[0])
|
||||
if ($value -notin 0, 1) { throw 'Invalid duplicate guard.' }
|
||||
if ($value -eq 0) { $allowed = $false }
|
||||
}
|
||||
$opposite = if ($scope -eq '1') { $user } else { $machine }
|
||||
$expected = [bool]$installed -or -not [bool]$opposite
|
||||
if ($allowed -ne $expected) { throw "Scope regression: ALLUSERS='$scope', user=$([bool]$user), machine=$([bool]$machine), maintenance=$([bool]$installed)." }
|
||||
$cases++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# Execute the actual authored type-51 actions in a safe session, not copied resolver logic.
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
foreach ($explicit in @('', 'F:\Chosen exact app')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
Set-SessionProperty $session 'INSTALLFOLDER' $explicit
|
||||
Set-SessionProperty $session 'CRAFT_USER_DIR' 'D:\Existing user app'
|
||||
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' 'E:\Existing machine app'
|
||||
$action = if ($scope -eq '1') { 'CraftRestoreMachineFolder' } else { 'CraftRestoreUserFolder' }
|
||||
# Evaluate the compiled condition, so the test exercises it instead of calling the action directly.
|
||||
$restore = @(Read-Rows $database ('SELECT `Condition` FROM `InstallExecuteSequence` WHERE `Action` = ''{0}''' -f $action) 1)[0]
|
||||
$selected = $session.EvaluateCondition($restore[0]) -eq 1
|
||||
if ($selected -eq [bool]$explicit) { throw "$action must run only when no folder was supplied (condition: $($restore[0]))." }
|
||||
if ($selected -and $session.DoAction($action) -ne 1) { throw 'Remembered-folder property action failed.' }
|
||||
$expected = if ($explicit) { $explicit } elseif ($scope -eq '1') { 'E:\Existing machine app' } else { 'D:\Existing user app' }
|
||||
if ((Get-SessionProperty $session 'INSTALLFOLDER') -ne $expected) { throw 'Custom or remembered folder was not preserved.' }
|
||||
}
|
||||
}
|
||||
$ui = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Action`, `Sequence` FROM `InstallUISequence`' 2)) { $ui[$row[0]] = [int]$row[1] }
|
||||
if (-not ($ui['AppSearch'] -lt $ui['CraftPreferMachineScope'] -and
|
||||
$ui['CraftPreferMachineScope'] -lt $ui['CraftScopeDlg'] -and
|
||||
$ui['CraftScopeDlg'] -lt $ui['FindRelatedProducts'] -and
|
||||
$ui['FindRelatedProducts'] -lt $ui['LaunchConditions'] -and
|
||||
$ui['CraftScopeDlg'] -lt $ui['CraftRestoreUserFolder'] -and $ui['CraftRestoreUserFolder'] -lt $ui['CostInitialize'])) {
|
||||
throw 'Scope must be chosen after AppSearch and before FindRelatedProducts, launch checks and costing.'
|
||||
}
|
||||
# Windows Installer resolves Program Files for the context it started in, before the wizard's scope choice.
|
||||
# The wizard therefore sets the new-install folder itself, and only in the wizard.
|
||||
$defaults = @(Read-Rows $database 'SELECT `Action`, `Sequence`, `Condition` FROM `InstallUISequence`' 3 | Where-Object { $_[0] -like 'CraftDefault*' })
|
||||
if ($defaults.Count -lt 2) { throw 'The wizard must set the new-install folder after the scope page; Program Files is resolved before the choice.' }
|
||||
foreach ($row in $defaults) {
|
||||
if (-not ($ui['CraftScopeDlg'] -lt [int]$row[1] -and [int]$row[1] -lt $ui['CostInitialize'])) { throw "$($row[0]) must run after the scope page and before costing." }
|
||||
}
|
||||
if (@(Read-Rows $database 'SELECT `Action` FROM `InstallExecuteSequence`' 1 | Where-Object { $_[0] -like 'CraftDefault*' }).Count) {
|
||||
throw 'Silent installs must not use the wizard-only default folder actions.'
|
||||
}
|
||||
# The machine default comes from the environment of the package architecture, not from a folder name.
|
||||
# x86 packages use Program Files (x86) on 64-bit Windows; x64 and Arm64 packages use the 64-bit root.
|
||||
$machineRoot = if ((Get-PackageTemplate $database) -match '^Intel') {
|
||||
if (Get-SessionProperty $session 'VersionNT64') { ${env:ProgramFiles(x86)} } else { $env:ProgramFiles }
|
||||
} elseif ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }
|
||||
$machineFolder = (Join-Path $machineRoot "ArtCraft\$($policy.name)") + '\'
|
||||
foreach ($scope in @('1', '')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
Set-SessionProperty $session 'INSTALLFOLDER' ''
|
||||
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' ''
|
||||
Set-SessionProperty $session 'CRAFT_USER_DIR' ''
|
||||
foreach ($row in @($defaults | Sort-Object { [int]$_[1] })) {
|
||||
if ($session.EvaluateCondition($row[2]) -eq 1 -and $session.DoAction($row[0]) -ne 1) { throw "$($row[0]) failed." }
|
||||
}
|
||||
$folder = Get-SessionProperty $session 'INSTALLFOLDER'
|
||||
$wanted = if ($scope -eq '1') { $machineFolder } else { (Join-Path $env:LOCALAPPDATA "Programs\ArtCraft\$($policy.name)") + '\' }
|
||||
if ($folder -ine $wanted) { throw "Wizard default folder for ALLUSERS='$scope' is '$folder', expected '$wanted'." }
|
||||
}
|
||||
$execute = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Action`, `Sequence` FROM `InstallExecuteSequence`' 2)) { $execute[$row[0]] = [int]$row[1] }
|
||||
foreach ($action in @('CraftRestoreUserFolder','CraftRestoreMachineFolder')) {
|
||||
if (-not ($execute['AppSearch'] -lt $execute[$action] -and $execute[$action] -lt $execute['CostInitialize'])) {
|
||||
throw 'Silent updates must restore the folder before directory costing.'
|
||||
}
|
||||
}
|
||||
foreach ($resolver in @('CraftUFromLegacyExe','CraftMFromLegacyExe')) {
|
||||
if (-not ($execute['AppSearch'] -lt $execute[$resolver] -and $execute[$resolver] -lt $execute['LaunchConditions'])) {
|
||||
throw 'Both legacy folder resolvers must run after AppSearch and before the launch checks.'
|
||||
}
|
||||
}
|
||||
# Switching the first-page choice must reset BOTH properties, including a back-and-forth change.
|
||||
$events = @(Read-Rows $database 'SELECT `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_` = ''CraftScopeDlg'' AND `Control_` = ''Next'' ORDER BY `Ordering`' 4)
|
||||
# Model the full-UI upgrade entry with an existing machine-wide copy. Run
|
||||
# the actual preference action, then the compiled Next events before discovery.
|
||||
Set-SessionProperty $session 'ALLUSERS' '2'
|
||||
Set-SessionProperty $session 'CRAFT_SCOPE' 'user'
|
||||
Set-SessionProperty $session 'CRAFT_USER_DIR' ''
|
||||
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' 'E:\Existing machine app'
|
||||
$preference = @(Read-Rows $database 'SELECT `Condition` FROM `InstallUISequence` WHERE `Action` = ''CraftPreferMachineScope''' 1)[0]
|
||||
if ($session.EvaluateCondition($preference[0]) -ne 1 -or $session.DoAction('CraftPreferMachineScope') -ne 1 -or
|
||||
(Get-SessionProperty $session 'CRAFT_SCOPE') -ne 'machine') {
|
||||
throw 'Full-UI machine upgrades must initially select the existing machine context.'
|
||||
}
|
||||
foreach ($choice in @('machine','user','machine','user')) {
|
||||
Set-SessionProperty $session 'CRAFT_SCOPE' $choice
|
||||
foreach ($event in $events) {
|
||||
if ($event[0] -notmatch '^\[([^\]]+)\]$') { continue }
|
||||
$property = $Matches[1]
|
||||
if ($session.EvaluateCondition($event[2]) -eq 1) {
|
||||
Set-SessionProperty $session $property $(if ($event[1] -eq '{}') { '' } else { $event[1] })
|
||||
}
|
||||
}
|
||||
$allUsers = Get-SessionProperty $session 'ALLUSERS'
|
||||
$perUser = Get-SessionProperty $session 'MSIINSTALLPERUSER'
|
||||
if (($choice -eq 'machine' -and ($allUsers -ne '1' -or $perUser)) -or
|
||||
($choice -eq 'user' -and ($allUsers -or $perUser -ne '1'))) {
|
||||
throw 'Scope selector does not reset the requested installation context.'
|
||||
}
|
||||
}
|
||||
$actions = @(Read-Rows $database 'SELECT `Action`, `Source`, `Target`, `Type` FROM `CustomAction`' 4)
|
||||
$savedOptions = @($actions | Where-Object { $_[0] -match '^CraftLoadOption[0-9]+M$' })
|
||||
foreach ($option in $savedOptions) {
|
||||
if ([int]$option[3] -ne 51) { throw 'Saved-option test may run only property-setting actions.' }
|
||||
$number = [regex]::Match($option[0], '[0-9]+').Value
|
||||
foreach ($scope in @('2','1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
Set-SessionProperty $session "CRAFT_OPTION_${number}_M" 'saved-machine-choice'
|
||||
Set-SessionProperty $session "CRAFT_OPTION_${number}_U" 'saved-user-choice'
|
||||
Set-SessionProperty $session $option[1] ''
|
||||
$action = "CraftLoadOption$number" + $(if ($scope -eq '1') { 'M' } else { 'U' })
|
||||
if ($session.DoAction($action) -ne 1) { throw 'Saved-option property action failed.' }
|
||||
$expected = if ($scope -eq '1') { 'saved-machine-choice' } else { 'saved-user-choice' }
|
||||
if ((Get-SessionProperty $session $option[1]) -ne $expected) { throw 'Saved installer choices leaked across contexts.' }
|
||||
}
|
||||
}
|
||||
# Follow the compiled control conditions, not the source's intended appearance.
|
||||
$controls = @(Read-Rows $database 'SELECT `Dialog_`, `Control`, `Type`, `Attributes`, `Text` FROM `Control`' 5)
|
||||
$conditions = @()
|
||||
if (@(Read-Rows $database 'SELECT `Name` FROM `_Tables` WHERE `Name` = ''ControlCondition''' 1).Count) {
|
||||
$conditions = @(Read-Rows $database 'SELECT `Dialog_`, `Control_`, `Action`, `Condition` FROM `ControlCondition`' 4)
|
||||
}
|
||||
function Test-ControlVisible($Control) {
|
||||
$visible = ([int]$Control[3] -band 1) -ne 0
|
||||
foreach ($condition in $conditions) {
|
||||
if ($condition[0] -eq $Control[0] -and $condition[1] -eq $Control[1] -and $session.EvaluateCondition($condition[3]) -eq 1) {
|
||||
if ($condition[2] -eq 'Show') { $visible = $true }
|
||||
if ($condition[2] -eq 'Hide') { $visible = $false }
|
||||
}
|
||||
}
|
||||
return $visible
|
||||
}
|
||||
foreach ($button in @($controls | Where-Object { $_[0] -in 'InstallWelcome','InstallMaintenance','DesktopShortcutDlg' -and ([int]$_[3] -band 8388608) -ne 0 })) {
|
||||
$plain = @($controls | Where-Object { $_[0] -eq $button[0] -and $_[1] -eq ($button[1] + 'User') })
|
||||
if ($plain.Count -ne 1 -or ([int]$plain[0][3] -band 8388608)) { throw 'Scope-aware buttons need an unshielded per-user counterpart.' }
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
if ((Test-ControlVisible $button) -ne ($scope -eq '1') -or (Test-ControlVisible $plain[0]) -ne ($scope -ne '1')) {
|
||||
throw 'The elevation shield must follow the selected scope.'
|
||||
}
|
||||
}
|
||||
$dialog = $button[0]; $shieldId = $button[1]; $plainId = $plain[0][1]
|
||||
$shieldEvents = @(Read-Rows $database "SELECT ``Event``, ``Argument``, ``Condition``, ``Ordering`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$shieldId' ORDER BY ``Ordering``" 4 | ForEach-Object { $_ -join '|' })
|
||||
$plainEvents = @(Read-Rows $database "SELECT ``Event``, ``Argument``, ``Condition``, ``Ordering`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$plainId' ORDER BY ``Ordering``" 4 | ForEach-Object { $_ -join '|' })
|
||||
if (($shieldEvents -join "`n") -ne ($plainEvents -join "`n")) { throw 'Per-user and machine buttons must perform the same operation.' }
|
||||
}
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
foreach ($text in @($controls | Where-Object { $_[4] -like '*for all users*' })) {
|
||||
if ((Test-ControlVisible $text) -ne ($scope -eq '1')) { throw 'The welcome-page scope text must follow the selected scope.' }
|
||||
}
|
||||
}
|
||||
if ($policy.id -in 'deckcraft','filmcraft','soundcraft') {
|
||||
foreach ($route in @(@('WelcomeDlg','Next','InstallDirDlg'),@('InstallDirDlg','Back','WelcomeDlg'))) {
|
||||
$dialog = $route[0]; $control = $route[1]
|
||||
$routes = @(Read-Rows $database "SELECT ``Argument``, ``Condition`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$control' AND ``Event`` = 'NewDialog' ORDER BY ``Ordering``" 2)
|
||||
Set-SessionProperty $session 'Installed' ''
|
||||
$destination = ''
|
||||
foreach ($event in $routes) { if ($session.EvaluateCondition($event[1]) -eq 1) { $destination = $event[0] } }
|
||||
if ($destination -ne $route[2]) { throw 'Skipping the licence page must work in both navigation directions.' }
|
||||
}
|
||||
}
|
||||
$registry = @(Read-Rows $database 'SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`' 4)
|
||||
if (@($registry | Where-Object { $_[0] -in '1', '2' }).Count) { throw 'Registration must follow the selected context, not force HKCU or HKLM.' }
|
||||
if (@($registry | Where-Object { $_[0] -eq '0' -and $_[1] -match '^Software\\Classes(\\|$)' }).Count) {
|
||||
throw 'HKCR keys must be relative to Classes.'
|
||||
}
|
||||
foreach ($entry in $policy.registry) {
|
||||
$found = @($registry | Where-Object { $_[0] -eq $entry[0] -and $_[1] -eq $entry[1] -and $_[2] -eq $entry[2] -and $_[3] -eq $entry[3] })
|
||||
if ($found.Count -ne 1) { throw "Lost or changed registration: $($entry[1]) / $($entry[2])" }
|
||||
}
|
||||
$installDir = @($registry | Where-Object { $_[0] -eq '-1' -and $_[1] -eq "Software\$($policy.name)" -and $_[2] -eq 'InstallDir' -and $_[3] -eq '[INSTALLFOLDER]' })
|
||||
if ($installDir.Count -ne 1) { throw 'A context-sensitive installation-folder record is required.' }
|
||||
Write-Output "PASS $($policy.name): $cases scope cases, folder preservation, action sequencing and registry contract. No installation performed."
|
||||
} finally {
|
||||
foreach ($com in @($session, $database, $installer)) {
|
||||
if ($null -ne $com) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($com) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"id": "pdfcraft",
|
||||
"name": "PdfCraft",
|
||||
"upgradeCode": "EA00C367-7270-4C50-9D8B-A7A5C88AD27F",
|
||||
"sources": [
|
||||
"pdfcraft.wxs",
|
||||
"installer-ui.wxs"
|
||||
],
|
||||
"icon": "assets/app-icon/pdfcraft.ico",
|
||||
"localization": [],
|
||||
"extensions": [],
|
||||
"payloads": [
|
||||
"LICENSE-APACHE",
|
||||
"LICENSE-MIT",
|
||||
"LICENSE-document.txt",
|
||||
"NOTICE",
|
||||
"pdfcraft-cli.exe",
|
||||
"pdfcraft.exe"
|
||||
],
|
||||
"registry": [
|
||||
[
|
||||
"-1",
|
||||
"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\pdfcraft.exe",
|
||||
"",
|
||||
"[#PdfcraftExe]"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\PdfCraft.Document",
|
||||
"",
|
||||
"PDF Document"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\PdfCraft.Document\\DefaultIcon",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\",-2"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\PdfCraft.Document\\shell\\open\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\PdfCraft.Document",
|
||||
"TypeOverlay",
|
||||
""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\.pdf\\OpenWithProgids",
|
||||
"PdfCraft.Document",
|
||||
""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\Applications\\pdfcraft.exe",
|
||||
"FriendlyAppName",
|
||||
"PdfCraft"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\Applications\\pdfcraft.exe\\DefaultIcon",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\",-2"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\Applications\\pdfcraft.exe\\SupportedTypes",
|
||||
".pdf",
|
||||
""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\Applications\\pdfcraft.exe\\shell\\open\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Capabilities",
|
||||
"ApplicationName",
|
||||
"PdfCraft"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Capabilities",
|
||||
"ApplicationDescription",
|
||||
"Open-source PDF workbench"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Capabilities",
|
||||
"ApplicationIcon",
|
||||
"[#PdfcraftExe],0"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Capabilities\\FileAssociations",
|
||||
".pdf",
|
||||
"PdfCraft.Document"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\RegisteredApplications",
|
||||
"PdfCraft",
|
||||
"Software\\PdfCraft\\Capabilities"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Shortcuts",
|
||||
"StartMenu",
|
||||
"#1"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\PdfCraft\\Shortcuts",
|
||||
"Desktop",
|
||||
"#1"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.pdf\\shell\\PdfCraft.Edit",
|
||||
"",
|
||||
"Edit with PdfCraft"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.pdf\\shell\\PdfCraft.Edit",
|
||||
"Icon",
|
||||
"\"[#PdfcraftExe]\",0"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.pdf\\shell\\PdfCraft.Edit",
|
||||
"MultiSelectModel",
|
||||
"Document"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.pdf\\shell\\PdfCraft.Edit\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.png\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.png\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.png\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpg\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpg\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpg\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpeg\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpeg\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpeg\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tif\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tif\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tif\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tiff\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tiff\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.tiff\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.gif\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.gif\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.gif\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.bmp\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.bmp\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.bmp\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jp2\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jp2\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jp2\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.j2k\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.j2k\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.j2k\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpx\\shell\\PdfCraft.CreatePdf",
|
||||
"",
|
||||
"Create PDF with PdfCraft\u2026"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpx\\shell\\PdfCraft.CreatePdf",
|
||||
"MultiSelectModel",
|
||||
"Single"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\Classes\\SystemFileAssociations\\.jpx\\shell\\PdfCraft.CreatePdf\\command",
|
||||
"",
|
||||
"\"[#PdfcraftExe]\" --create-images \"%1\""
|
||||
]
|
||||
],
|
||||
"mode": "dualScope"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Include xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<!-- Choose scope before launch checks and directory costing. Never move an existing copy. -->
|
||||
<!-- Only for me leaves ALLUSERS unset. Publishing ALLUSERS=2 here makes FindRelatedProducts treat the run as
|
||||
per-machine and skip the existing per-user copy, so an interactive upgrade installs beside it
|
||||
(two registered products, and uninstall leaves files behind). Checked with real msiexec runs. -->
|
||||
<Property Id="INSTALLFOLDER" Secure="yes" />
|
||||
<Property Id="CRAFT_SCOPE" Value="user" Secure="yes" />
|
||||
<Property Id="CRAFT_U_DIR" Secure="yes"><RegistrySearch Id="CRAFT_U_DIRSearch" Root="HKCU" Key="Software\PdfCraft" Name="InstallDir" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromDIR" Value="[CRAFT_U_DIR]" After="AppSearch" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_DIR" />
|
||||
<Property Id="CRAFT_U_FOLDER" Secure="yes"><RegistrySearch Id="CRAFT_U_FOLDERSearch" Root="HKCU" Key="Software\PdfCraft" Name="InstallFolder" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromFOLDER" Value="[CRAFT_U_FOLDER]" After="CraftUFromDIR" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_FOLDER" />
|
||||
<Property Id="CRAFT_U_PATH" Secure="yes"><RegistrySearch Id="CRAFT_U_PATHSearch" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Name="Path" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromPATH" Value="[CRAFT_U_PATH]" After="CraftUFromFOLDER" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_PATH" />
|
||||
<Property Id="CRAFT_U_EXEFOLDER" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyFolder" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Type="file" /></Property>
|
||||
<Property Id="CRAFT_U_EXE" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyExe" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Type="file"><FileSearch Id="CRAFT_ULegacyFile" Name="pdfcraft.exe" /></RegistrySearch></Property>
|
||||
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromLegacyExe" Value="[CRAFT_U_EXEFOLDER]" After="CraftUFromPATH" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_EXEFOLDER AND CRAFT_U_EXE" />
|
||||
<Property Id="CRAFT_USER_DIR" Secure="yes" />
|
||||
<Property Id="CRAFT_M_DIR" Secure="yes"><RegistrySearch Id="CRAFT_M_DIRSearch" Root="HKLM" Key="Software\PdfCraft" Name="InstallDir" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromDIR" Value="[CRAFT_M_DIR]" After="CraftUFromLegacyExe" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_DIR" />
|
||||
<Property Id="CRAFT_M_FOLDER" Secure="yes"><RegistrySearch Id="CRAFT_M_FOLDERSearch" Root="HKLM" Key="Software\PdfCraft" Name="InstallFolder" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromFOLDER" Value="[CRAFT_M_FOLDER]" After="CraftMFromDIR" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_FOLDER" />
|
||||
<Property Id="CRAFT_M_PATH" Secure="yes"><RegistrySearch Id="CRAFT_M_PATHSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Name="Path" Type="raw" /></Property>
|
||||
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromPATH" Value="[CRAFT_M_PATH]" After="CraftMFromFOLDER" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_PATH" />
|
||||
<Property Id="CRAFT_M_EXEFOLDER" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyFolder" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Type="file" /></Property>
|
||||
<Property Id="CRAFT_M_EXE" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyExe" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Type="file"><FileSearch Id="CRAFT_MLegacyFile" Name="pdfcraft.exe" /></RegistrySearch></Property>
|
||||
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromLegacyExe" Value="[CRAFT_M_EXEFOLDER]" After="CraftMFromPATH" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_EXEFOLDER AND CRAFT_M_EXE" />
|
||||
<Property Id="CRAFT_MACHINE_DIR" Secure="yes" />
|
||||
<SetProperty Id="CRAFT_SCOPE" Action="CraftPreferMachineScope" Value="machine" After="CraftMFromLegacyExe" Sequence="ui" Condition="ALLUSERS = 1 OR (CRAFT_MACHINE_DIR AND NOT CRAFT_USER_DIR)" />
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreUserFolder" Value="[CRAFT_USER_DIR]" Before="CostInitialize" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS <> 1 AND CRAFT_USER_DIR" />
|
||||
<!-- The wizard can change the context after Windows Installer has already resolved Program Files for the first one.
|
||||
Give the new-install folder explicitly so Everyone never lands in the profile and Only for me never lands in Program Files. -->
|
||||
<?if $(sys.BUILDARCH) = x86 ?>
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder" Value="[%ProgramFiles(x86)]\ArtCraft\PdfCraft\" After="CraftRestoreUserFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND VersionNT64" />
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder32" Value="[%ProgramFiles]\ArtCraft\PdfCraft\" After="CraftDefaultMachineFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND NOT VersionNT64" />
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultUserFolder" Value="[LocalAppDataFolder]Programs\ArtCraft\PdfCraft\" After="CraftDefaultMachineFolder32" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS <> 1" />
|
||||
<?else?>
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder" Value="[%ProgramW6432]\ArtCraft\PdfCraft\" After="CraftRestoreUserFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1" />
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultUserFolder" Value="[LocalAppDataFolder]Programs\ArtCraft\PdfCraft\" After="CraftDefaultMachineFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS <> 1" />
|
||||
<?endif?>
|
||||
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreMachineFolder" Value="[CRAFT_MACHINE_DIR]" Before="CraftRestoreUserFolder" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND CRAFT_MACHINE_DIR" />
|
||||
<Launch Condition="Installed OR ALLUSERS <> 1 OR NOT CRAFT_USER_DIR" Message="This app is already installed for your account. Choose Only for me to update it, or uninstall that copy before changing who it is installed for." />
|
||||
<Launch Condition="Installed OR ALLUSERS = 1 OR NOT CRAFT_MACHINE_DIR" Message="This app is already installed for everyone. Choose Everyone to update it, or pass ALLUSERS=1 to msiexec. Uninstall that copy before changing who it is installed for." />
|
||||
<UI>
|
||||
<TextStyle Id="CraftScopeText" FaceName="Segoe UI" Size="9" />
|
||||
<TextStyle Id="CraftScopeTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
|
||||
<Dialog Id="CraftScopeDlg" Width="370" Height="220" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="30" Text="{\CraftScopeTitle}Who should use [ProductName]?" />
|
||||
<Control Id="Info" Type="Text" X="20" Y="50" Width="330" Height="30" Text="{\CraftScopeText}An update keeps the existing folder. New copies use the ArtCraft folder under Programs." />
|
||||
<Control Id="Scope" Type="RadioButtonGroup" X="20" Y="88" Width="330" Height="60" Property="CRAFT_SCOPE">
|
||||
<RadioButtonGroup Property="CRAFT_SCOPE">
|
||||
<RadioButton Value="user" X="0" Y="0" Width="330" Height="20" Text="Only for me (no administrator permission needed)" />
|
||||
<RadioButton Value="machine" X="0" Y="30" Width="330" Height="20" Text="Everyone on this computer (asks an administrator)" />
|
||||
</RadioButtonGroup>
|
||||
</Control>
|
||||
<Control Id="Next" Type="PushButton" X="214" Y="182" Width="66" Height="20" Default="yes" Text="Next">
|
||||
<Publish Property="ALLUSERS" Value="1" Order="1" Condition="CRAFT_SCOPE = "machine"" />
|
||||
<Publish Property="ALLUSERS" Value="{}" Order="2" Condition="CRAFT_SCOPE = "user"" />
|
||||
<Publish Property="MSIINSTALLPERUSER" Value="{}" Order="3" Condition="CRAFT_SCOPE = "machine"" />
|
||||
<Publish Property="MSIINSTALLPERUSER" Value="1" Order="4" Condition="CRAFT_SCOPE = "user"" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="5" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="290" Y="182" Width="60" Height="20" Cancel="yes" Text="Cancel"><Publish Event="EndDialog" Value="Exit" /></Control>
|
||||
</Dialog>
|
||||
</UI>
|
||||
<InstallUISequence>
|
||||
<Show Dialog="CraftScopeDlg" After="CraftPreferMachineScope" Condition="NOT Installed AND NOT REMOVE AND UILevel = 5" />
|
||||
<!-- Related-product discovery runs once, so full UI must choose its context first. -->
|
||||
<FindRelatedProducts After="CraftScopeDlg" />
|
||||
</InstallUISequence>
|
||||
</Include>
|
||||
@@ -1,6 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Original, text-only native MSI dialogs. No third-party UI artwork or custom-action DLLs.
|
||||
Full UI gets welcome (with the desktop-shortcut and PDF context-menu checkboxes)/progress/outcome screens; /qn and /qb stay unattended. -->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Fragment>
|
||||
<UI Id="PdfcraftInstallerUI">
|
||||
@@ -10,13 +8,14 @@
|
||||
|
||||
<Dialog Id="InstallWelcome" Width="370" Height="180" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Install [ProductName]" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="40"
|
||||
Text="Setup will install [ProductName] for all users and add it to the Start menu. Click Install to continue." />
|
||||
<Control Id="DesktopShortcut" Type="CheckBox" X="20" Y="105" Width="330" Height="17"
|
||||
Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="Create a &desktop shortcut" />
|
||||
<Control Id="PdfContextMenu" Type="CheckBox" X="20" Y="123" Width="330" Height="17"
|
||||
Property="INSTALLPDFCONTEXTMENU" CheckBoxValue="1" Text="Add "&Edit with PdfCraft" to the right-click menu of PDF files" />
|
||||
<Control Id="Install" Type="PushButton" X="195" Y="145" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&Install">
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="40" Text="Setup will install [ProductName] for all users and add it to the Start menu. Click Install to continue." HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" />
|
||||
<Control Id="DescriptionUser" Type="Text" X="20" Y="55" Width="330" Height="40" Text="Setup will install [ProductName] only for your account and add it to the Start menu. Click Install to continue." HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" />
|
||||
<Control Id="DesktopShortcut" Type="CheckBox" X="20" Y="105" Width="330" Height="17" Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="Create a &desktop shortcut" />
|
||||
<Control Id="PdfContextMenu" Type="CheckBox" X="20" Y="123" Width="330" Height="17" Property="INSTALLPDFCONTEXTMENU" CheckBoxValue="1" Text="Add "&Edit with PdfCraft" to the right-click menu of PDF files" />
|
||||
<Control Id="Install" Type="PushButton" X="195" Y="145" Width="75" Height="23" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="&Install">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
<Control Id="InstallUser" Type="PushButton" DefaultCondition="ALLUSERS <> 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="195" Y="145" Width="75" Height="23" Text="&Install">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="280" Y="145" Width="70" Height="23" Cancel="yes" Text="Cancel">
|
||||
@@ -26,14 +25,22 @@
|
||||
|
||||
<Dialog Id="InstallMaintenance" Width="370" Height="180" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Maintain [ProductName]" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] is already installed. Choose Repair to restore missing files and shortcuts, or Remove to uninstall it." />
|
||||
<Control Id="Repair" Type="PushButton" X="110" Y="145" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&Repair">
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70" Text="[ProductName] is already installed. Choose Repair to restore missing files and shortcuts, or Remove to uninstall it." />
|
||||
<Control Id="Repair" Type="PushButton" X="110" Y="145" Width="75" Height="23" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="&Repair">
|
||||
<Publish Property="REINSTALL" Value="ALL" Order="1" />
|
||||
<Publish Property="REINSTALLMODE" Value="amus" Order="2" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="3" />
|
||||
</Control>
|
||||
<Control Id="Remove" Type="PushButton" X="195" Y="145" Width="75" Height="23" ElevationShield="yes" Text="R&emove">
|
||||
<Control Id="RepairUser" Type="PushButton" DefaultCondition="ALLUSERS <> 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="110" Y="145" Width="75" Height="23" Text="&Repair">
|
||||
<Publish Property="REINSTALL" Value="ALL" Order="1" />
|
||||
<Publish Property="REINSTALLMODE" Value="amus" Order="2" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="3" />
|
||||
</Control>
|
||||
<Control Id="Remove" Type="PushButton" X="195" Y="145" Width="75" Height="23" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="R&emove">
|
||||
<Publish Property="REMOVE" Value="ALL" Order="1" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="2" />
|
||||
</Control>
|
||||
<Control Id="RemoveUser" Type="PushButton" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="195" Y="145" Width="75" Height="23" Text="R&emove">
|
||||
<Publish Property="REMOVE" Value="ALL" Order="1" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="2" />
|
||||
</Control>
|
||||
@@ -66,8 +73,7 @@
|
||||
|
||||
<Dialog Id="InstallComplete" Width="370" Height="180" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup completed successfully" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] setup has completed successfully. Click Finish to close this window." />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70" Text="[ProductName] setup has completed successfully. Click Finish to close this window." />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
@@ -75,8 +81,7 @@
|
||||
|
||||
<Dialog Id="InstallFailed" Width="370" Height="180" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup failed" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] setup could not be completed. Resolve the reported error and run setup again." />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70" Text="[ProductName] setup could not be completed. Resolve the reported error and run setup again." />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
@@ -84,8 +89,7 @@
|
||||
|
||||
<Dialog Id="InstallCancelled" Width="370" Height="180" Title="[ProductName] Setup">
|
||||
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup cancelled" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
|
||||
Text="[ProductName] setup was cancelled. Run setup again when you are ready." />
|
||||
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70" Text="[ProductName] setup was cancelled. Run setup again when you are ready." />
|
||||
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&Finish">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
@@ -96,8 +100,7 @@
|
||||
continues; Cancel ends setup. -->
|
||||
<Dialog Id="MsiRMFilesInUse" Width="370" Height="200" Title="[ProductName] Setup" KeepModeless="yes">
|
||||
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="25" Text="{\InstallerTitle}Files in use" />
|
||||
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="30"
|
||||
Text="These programs are using files that setup needs to update. Click OK to close them and continue, or Cancel to stop setup." />
|
||||
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="30" Text="These programs are using files that setup needs to update. Click OK to close them and continue, or Cancel to stop setup." />
|
||||
<Control Id="List" Type="ListBox" X="20" Y="80" Width="330" Height="55" Property="FileInUseProcess" Sunken="yes" TabSkip="yes" />
|
||||
<Control Id="OK" Type="PushButton" X="195" Y="165" Width="75" Height="23" Default="yes" Text="OK">
|
||||
<Publish Property="MSIRMSHUTDOWN" Value="2" Order="1" />
|
||||
|
||||
@@ -154,6 +154,8 @@ Invoke-Native 'test-msi' {
|
||||
& (Get-Process -Id $PID).Path -NoProfile -File (Join-Path $PSScriptRoot 'test-msi.ps1') $Msi `
|
||||
-CraftFontLicences $HasCraftFontLicences -ExpectedFontNoticesDirectory $Docs
|
||||
}
|
||||
# Check the compiled scope, path and registration contract before signing.
|
||||
& (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $Msi
|
||||
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.
|
||||
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb'))
|
||||
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
|
||||
|
||||
+89
-104
@@ -1,37 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
PdfCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
|
||||
|
||||
wix build pdfcraft.wxs installer-ui.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d ModelsDir=... -d IconPath=... -o out.msi
|
||||
|
||||
Per-machine install into Program Files, a Start Menu shortcut, a desktop shortcut (on by default,
|
||||
optional, and an update keeps it only if it is still there), an App Paths entry, PdfCraft registered as a PDF handler (offered under "Open with"
|
||||
and in Default apps, never forced as the default), "Edit with PdfCraft" on PDF files'
|
||||
right-click menu and "Create PDF with PdfCraft…" on image files' right-click menu (both on by
|
||||
default, optional, and an update keeps the installed version's choice). Files opened with
|
||||
PdfCraft arrive on its command line.
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<?ifndef HasCraftFontLicences ?>
|
||||
<?define HasCraftFontLicences=0 ?>
|
||||
<?endif ?>
|
||||
<Package
|
||||
Name="PdfCraft"
|
||||
Manufacturer="Learning Machines LLC"
|
||||
Version="$(var.Version)"
|
||||
UpgradeCode="EA00C367-7270-4C50-9D8B-A7A5C88AD27F"
|
||||
Scope="perMachine"
|
||||
Compressed="yes"
|
||||
Language="1033">
|
||||
<?endif?>
|
||||
<Package Name="PdfCraft" Manufacturer="Learning Machines LLC" Version="$(var.Version)" UpgradeCode="EA00C367-7270-4C50-9D8B-A7A5C88AD27F" Scope="perUserOrMachine" Compressed="yes" Language="1033">
|
||||
<SummaryInformation Description="PdfCraft PDF workbench" Manufacturer="Learning Machines LLC" />
|
||||
<?include $(sys.SOURCEFILEDIR)install-policy.wxi?>
|
||||
<!-- HKLM registrations must never point into a per-user installation (#305). Allow removal
|
||||
of an existing installation even if an older MSI let it install per-user. -->
|
||||
<Launch Condition="ALLUSERS = 1 OR (Installed AND REMOVE = "ALL")"
|
||||
Message="PdfCraft must be installed for all users. Run setup with administrator privileges and ALLUSERS=1; per-user installation is not supported." />
|
||||
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
|
||||
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes"
|
||||
DowngradeErrorMessage="A newer version of PdfCraft is already installed." />
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of PdfCraft is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||
|
||||
<!-- Windows Installer requires an Icon id ending in .ico or .exe, or advertised shortcuts and
|
||||
@@ -48,13 +27,21 @@
|
||||
updates with /passive and no properties, so deleting the shortcut is the only way its
|
||||
users can say no. A value given on the command line wins. -->
|
||||
<Property Id="INSTALLDESKTOPSHORTCUT" Secure="yes" />
|
||||
<Property Id="EXISTINGDESKTOPSHORTCUT">
|
||||
<DirectorySearch Id="DesktopSearch" Path="[DesktopFolder]" Depth="0">
|
||||
<!-- AppSearch runs before the full-UI scope page can choose Everyone, so [DesktopFolder] still
|
||||
names the user's desktop then. Search both desktops and keep the one for the chosen scope. -->
|
||||
<Property Id="EXISTINGDESKTOPSHORTCUT_USER">
|
||||
<DirectorySearch Id="DesktopSearchUser" Path="[DesktopFolder]" Depth="0">
|
||||
<FileSearch Name="PdfCraft.lnk" />
|
||||
</DirectorySearch>
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLDESKTOPSHORTCUT" Value="1" After="AppSearch" Sequence="first"
|
||||
Condition="NOT INSTALLDESKTOPSHORTCUT AND (NOT WIX_UPGRADE_DETECTED OR EXISTINGDESKTOPSHORTCUT)" />
|
||||
<Property Id="EXISTINGDESKTOPSHORTCUT_MACHINE">
|
||||
<DirectorySearch Id="DesktopSearchMachine" Path="[%PUBLIC]\Desktop" Depth="0">
|
||||
<FileSearch Name="PdfCraft.lnk" />
|
||||
</DirectorySearch>
|
||||
</Property>
|
||||
<SetProperty Id="EXISTINGDESKTOPSHORTCUT" Action="CraftExistingMachineShortcut" Value="[EXISTINGDESKTOPSHORTCUT_MACHINE]" After="CraftLoadOption2U" Sequence="first" Condition="ALLUSERS = 1 AND EXISTINGDESKTOPSHORTCUT_MACHINE" />
|
||||
<SetProperty Id="EXISTINGDESKTOPSHORTCUT" Action="CraftExistingUserShortcut" Value="[EXISTINGDESKTOPSHORTCUT_USER]" After="CraftExistingMachineShortcut" Sequence="first" Condition="ALLUSERS <> 1 AND EXISTINGDESKTOPSHORTCUT_USER" />
|
||||
<SetProperty Id="INSTALLDESKTOPSHORTCUT" Value="1" After="CraftExistingUserShortcut" Sequence="first" Condition="NOT INSTALLDESKTOPSHORTCUT AND (NOT WIX_UPGRADE_DETECTED OR EXISTINGDESKTOPSHORTCUT)" />
|
||||
<!-- The File Explorer menus: "Edit with PdfCraft" for PDF files (#682) and "Create PDF with
|
||||
PdfCraft…" for images (#190). Each is on by default for a first install and has a property
|
||||
to leave it out of unattended installs: INSTALLPDFCONTEXTMENU=0 (also a welcome-dialog
|
||||
@@ -69,23 +56,21 @@
|
||||
<UpgradeVersion Minimum="0.4.0" IncludeMinimum="yes" OnlyDetect="yes" Property="IMAGECONTEXTMENURELEASE" />
|
||||
</Upgrade>
|
||||
<Property Id="INSTALLPDFCONTEXTMENU" Secure="yes" />
|
||||
<Property Id="EXISTINGPDFCONTEXTMENU">
|
||||
<RegistrySearch Id="PdfContextMenuSearch" Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Type="raw" />
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLPDFCONTEXTMENU" Value="1" After="AppSearch" Sequence="first"
|
||||
Condition="NOT INSTALLPDFCONTEXTMENU AND (NOT PDFCONTEXTMENURELEASE OR EXISTINGPDFCONTEXTMENU)" />
|
||||
<Property Id="EXISTINGPDFCONTEXTMENU" Secure="yes">
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLPDFCONTEXTMENU" Value="1" After="CraftLoadOption2U" Sequence="first" Condition="NOT INSTALLPDFCONTEXTMENU AND (NOT PDFCONTEXTMENURELEASE OR EXISTINGPDFCONTEXTMENU)" />
|
||||
<Property Id="INSTALLIMAGECONTEXTMENU" Secure="yes" />
|
||||
<Property Id="EXISTINGIMAGECONTEXTMENU">
|
||||
<RegistrySearch Id="ImageContextMenuSearch" Root="HKLM" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Type="raw" />
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLIMAGECONTEXTMENU" Value="1" After="AppSearch" Sequence="first"
|
||||
Condition="NOT INSTALLIMAGECONTEXTMENU AND (NOT IMAGECONTEXTMENURELEASE OR EXISTINGIMAGECONTEXTMENU)" />
|
||||
<Property Id="EXISTINGIMAGECONTEXTMENU" Secure="yes">
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLIMAGECONTEXTMENU" Value="1" After="CraftLoadOption2U" Sequence="first" Condition="NOT INSTALLIMAGECONTEXTMENU AND (NOT IMAGECONTEXTMENURELEASE OR EXISTINGIMAGECONTEXTMENU)" />
|
||||
<UIRef Id="PdfcraftInstallerUI" />
|
||||
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="PdfCraft">
|
||||
<Directory Id="CraftSuiteFolder" Name="ArtCraft">
|
||||
<Directory Id="INSTALLFOLDER" Name="PdfCraft">
|
||||
<Directory Id="ModelsFolder" Name="models" />
|
||||
</Directory>
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
<StandardDirectory Id="ProgramMenuFolder" />
|
||||
<StandardDirectory Id="DesktopFolder" />
|
||||
@@ -93,23 +78,27 @@
|
||||
<Feature Id="Main" Title="PdfCraft" Level="1">
|
||||
<?if $(var.HasCraftFontLicences) = 1 ?>
|
||||
<ComponentGroupRef Id="CraftFontLicences" />
|
||||
<?endif ?>
|
||||
<?endif?>
|
||||
<ComponentGroupRef Id="PdfcraftFiles" />
|
||||
<ComponentGroupRef Id="PdfcraftModels" />
|
||||
</Feature>
|
||||
</Package>
|
||||
<Property Id="CRAFT_OPTION_1_M" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_1_MSearch" Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Type="raw" />
|
||||
</Property><SetProperty Id="EXISTINGPDFCONTEXTMENU" Action="CraftLoadOption1M" Value="[CRAFT_OPTION_1_M]" After="LaunchConditions" Sequence="both" Condition="ALLUSERS = 1" /><Property Id="CRAFT_OPTION_1_U" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_1_USearch" Root="HKCU" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Type="raw" />
|
||||
</Property><SetProperty Id="EXISTINGPDFCONTEXTMENU" Action="CraftLoadOption1U" Value="[CRAFT_OPTION_1_U]" After="CraftLoadOption1M" Sequence="both" Condition="ALLUSERS <> 1" /><Property Id="CRAFT_OPTION_2_M" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_2_MSearch" Root="HKLM" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Type="raw" />
|
||||
</Property><SetProperty Id="EXISTINGIMAGECONTEXTMENU" Action="CraftLoadOption2M" Value="[CRAFT_OPTION_2_M]" After="CraftLoadOption1U" Sequence="both" Condition="ALLUSERS = 1" /><Property Id="CRAFT_OPTION_2_U" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_2_USearch" Root="HKCU" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Type="raw" />
|
||||
</Property><SetProperty Id="EXISTINGIMAGECONTEXTMENU" Action="CraftLoadOption2U" Value="[CRAFT_OPTION_2_U]" After="CraftLoadOption2M" Sequence="both" Condition="ALLUSERS <> 1" /></Package>
|
||||
|
||||
<Fragment>
|
||||
<?if $(var.HasCraftFontLicences) = 1 ?>
|
||||
<ComponentGroup Id="CraftFontLicences" Directory="INSTALLFOLDER">
|
||||
<Files Include="$(var.DocsDir)\**" />
|
||||
</ComponentGroup>
|
||||
<?endif ?>
|
||||
<?endif?>
|
||||
<ComponentGroup Id="PdfcraftFiles" Directory="INSTALLFOLDER">
|
||||
<Component Id="PdfcraftApp" Guid="E724C1C6-091F-4E21-A685-80D089FDBE21">
|
||||
<File Id="PdfcraftExe" Source="$(var.BinDir)\pdfcraft.exe" KeyPath="yes" />
|
||||
|
||||
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Value="[#PdfcraftExe]" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Value="[#PdfcraftExe]" Type="string" />
|
||||
|
||||
<!-- PDF files (#107): PdfCraft is offered under "Open with" and in Settings ▸ Default
|
||||
apps, through Windows' Default Programs registration (a ProgID, OpenWithProgids,
|
||||
@@ -117,26 +106,27 @@
|
||||
.pdf's own default value is left alone, and the user chooses. Without this, the only
|
||||
PdfCraft entry to pick was the advertised Start Menu shortcut, which Windows can't
|
||||
run as a file handler ("This app can't run on your PC"). -->
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document" Value="PDF Document" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\PdfCraft.Document" Value="PDF Document" Type="string" />
|
||||
<!-- A negative index selects the stable icon resource ID, rather than its position.
|
||||
Resource 1 is the app's lion; resource 2 is the PDF document (build.rs). -->
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document\DefaultIcon" Value=""[#PdfcraftExe]",-2" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document\shell\open\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\PdfCraft.Document\DefaultIcon" Value=""[#PdfcraftExe]",-2" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\PdfCraft.Document\shell\open\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
<!-- No overlay on thumbnails and in the preview pane: without TypeOverlay, Windows paints
|
||||
the default program's icon (the lion) over the lower-right corner of every PDF
|
||||
thumbnail, covering part of the page. An empty string means none ("Thumbnail
|
||||
Handlers", Microsoft Learn). -->
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\PdfCraft.Document" Name="TypeOverlay" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\.pdf\OpenWithProgids" Name="PdfCraft.Document" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe" Name="FriendlyAppName" Value="PdfCraft" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe\DefaultIcon" Value=""[#PdfcraftExe]",-2" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe\SupportedTypes" Name=".pdf" Value="" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\Applications\pdfcraft.exe\shell\open\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Capabilities" Name="ApplicationName" Value="PdfCraft" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Capabilities" Name="ApplicationDescription" Value="Open-source PDF workbench" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Capabilities" Name="ApplicationIcon" Value="[#PdfcraftExe],0" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Capabilities\FileAssociations" Name=".pdf" Value="PdfCraft.Document" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\RegisteredApplications" Name="PdfCraft" Value="Software\PdfCraft\Capabilities" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\PdfCraft.Document" Name="TypeOverlay" Value="" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\.pdf\OpenWithProgids" Name="PdfCraft.Document" Value="" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\pdfcraft.exe" Name="FriendlyAppName" Value="PdfCraft" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\pdfcraft.exe\DefaultIcon" Value=""[#PdfcraftExe]",-2" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\pdfcraft.exe\SupportedTypes" Name=".pdf" Value="" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\pdfcraft.exe\shell\open\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Capabilities" Name="ApplicationName" Value="PdfCraft" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Capabilities" Name="ApplicationDescription" Value="Open-source PDF workbench" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Capabilities" Name="ApplicationIcon" Value="[#PdfcraftExe],0" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Capabilities\FileAssociations" Name=".pdf" Value="PdfCraft.Document" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\RegisteredApplications" Name="PdfCraft" Value="Software\PdfCraft\Capabilities" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" />
|
||||
</Component>
|
||||
|
||||
<!-- Shortcuts (#143) are plain links to pdfcraft.exe, not advertised MSI shortcuts: advertised
|
||||
@@ -145,16 +135,13 @@
|
||||
(#143). Each has its own component with an HKLM key path, so Windows Installer removes it
|
||||
on uninstall; in this per-machine package both folders are the all-users ones. -->
|
||||
<Component Id="PdfcraftStartMenuShortcut" Directory="ProgramMenuFolder" Guid="DC028192-C233-4F99-9A77-ED458C60DF26">
|
||||
<Shortcut Id="StartMenuShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench"
|
||||
Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Shortcuts" Name="StartMenu" Type="integer" Value="1" KeyPath="yes" />
|
||||
<Shortcut Id="StartMenuShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench" Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Shortcuts" Name="StartMenu" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<Component Id="PdfcraftDesktopShortcut" Directory="DesktopFolder" Guid="F6948627-32AB-4D78-8865-0FDF417CE442"
|
||||
Condition="INSTALLDESKTOPSHORTCUT = 1">
|
||||
<Shortcut Id="DesktopShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench"
|
||||
Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
|
||||
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Shortcuts" Name="Desktop" Type="integer" Value="1" KeyPath="yes" />
|
||||
<Component Id="PdfcraftDesktopShortcut" Directory="DesktopFolder" Guid="F6948627-32AB-4D78-8865-0FDF417CE442" Condition="INSTALLDESKTOPSHORTCUT = 1">
|
||||
<Shortcut Id="DesktopShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench" Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
|
||||
<RegistryValue Root="HKMU" Key="Software\PdfCraft\Shortcuts" Name="Desktop" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
|
||||
<!-- PDF context menu (#682): "Edit with PdfCraft" opens the file in PdfCraft whichever app is
|
||||
@@ -164,12 +151,11 @@
|
||||
Windows 11 the entry is under "Show more options": the first-level menu only takes
|
||||
packaged apps' (IExplorerCommand) entries. Its own component with an HKLM key path, so
|
||||
INSTALLPDFCONTEXTMENU=0 leaves it out and uninstall removes it. -->
|
||||
<Component Id="PdfcraftPdfContextMenu" Guid="439F084F-657D-4AC2-A4CC-F099C518D71F"
|
||||
Condition="INSTALLPDFCONTEXTMENU = 1">
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Value="Edit with PdfCraft" Type="string" KeyPath="yes" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="Icon" Value=""[#PdfcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="MultiSelectModel" Value="Document" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
<Component Id="PdfcraftPdfContextMenu" Guid="439F084F-657D-4AC2-A4CC-F099C518D71F" Condition="INSTALLPDFCONTEXTMENU = 1">
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Value="Edit with PdfCraft" Type="string" KeyPath="yes" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="Icon" Value=""[#PdfcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit" Name="MultiSelectModel" Value="Document" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit\command" Value=""[#PdfcraftExe]" "%1"" Type="string" />
|
||||
</Component>
|
||||
|
||||
<!-- Image context menu (#190): "Create PDF with PdfCraft…" opens the DPI chooser, without
|
||||
@@ -177,38 +163,37 @@
|
||||
INSTALLIMAGECONTEXTMENU=0 leaves it out and uninstall removes it. These rows were in
|
||||
PdfcraftApp up to 0.7.1; moving them is safe because MajorUpgrade removes the old version
|
||||
before installing (afterInstallValidate), so no old row is left behind. -->
|
||||
<Component Id="PdfcraftImageContextMenu" Guid="1B783BF3-12DF-4588-AF80-E2CEA8BC1E5A"
|
||||
Condition="INSTALLIMAGECONTEXTMENU = 1">
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" KeyPath="yes" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<Component Id="PdfcraftImageContextMenu" Guid="1B783BF3-12DF-4588-AF80-E2CEA8BC1E5A" Condition="INSTALLIMAGECONTEXTMENU = 1">
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" KeyPath="yes" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.png\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpg\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpeg\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tif\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.tiff\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.gif\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.bmp\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jp2\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.j2k\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf" Value="Create PDF with PdfCraft…" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf" Name="MultiSelectModel" Value="Single" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\SystemFileAssociations\.jpx\shell\PdfCraft.CreatePdf\command" Value=""[#PdfcraftExe]" --create-images "%1"" Type="string" />
|
||||
</Component>
|
||||
|
||||
<Component Id="PdfcraftCli">
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build non-executable installer metadata fixtures; never install them.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $OutputDirectory,
|
||||
[ValidateSet('x64','x86','arm64')] [string[]] $Architectures = @('x64','x86','arm64')
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$policy = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'install-policy.json') | ConvertFrom-Json
|
||||
[void](New-Item -ItemType Directory -Force -Path $OutputDirectory)
|
||||
$output = (Resolve-Path -LiteralPath $OutputDirectory).Path
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
foreach ($arch in $Architectures) {
|
||||
$stage = Join-Path $output $arch
|
||||
[void](New-Item -ItemType Directory -Force -Path $stage)
|
||||
foreach ($name in $policy.payloads) {
|
||||
$destination = Join-Path $stage $name
|
||||
[void](New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($destination)))
|
||||
Set-Content -LiteralPath $destination -Encoding Ascii -Value 'NON-EXECUTABLE MSI METADATA TEST FIXTURE'
|
||||
}
|
||||
foreach ($folder in @('docs','models','notices')) {
|
||||
$path = Join-Path $stage $folder
|
||||
[void](New-Item -ItemType Directory -Force -Path $path)
|
||||
Set-Content -LiteralPath (Join-Path $path "$folder-fixture.txt") -Value 'Metadata test fixture, not a user file or model.'
|
||||
}
|
||||
foreach ($model in @('metadata-one.rten','metadata-two.rten')) {
|
||||
Set-Content -LiteralPath (Join-Path (Join-Path $stage 'models') $model) -Encoding Ascii -Value 'NON-EXECUTABLE MODEL METADATA FIXTURE'
|
||||
}
|
||||
foreach ($spec in @(@('banner.bmp',493,58),@('dialog.bmp',493,312))) {
|
||||
$bitmap = [Drawing.Bitmap]::new($spec[1],$spec[2],[Drawing.Imaging.PixelFormat]::Format24bppRgb)
|
||||
try { $bitmap.Save((Join-Path $stage $spec[0]),[Drawing.Imaging.ImageFormat]::Bmp) }
|
||||
finally { $bitmap.Dispose() }
|
||||
}
|
||||
Set-Content -LiteralPath (Join-Path $stage 'licence.rtf') -Encoding Ascii -Value '{\rtf1 Metadata test fixture.}'
|
||||
$sources = @($policy.sources | ForEach-Object { Join-Path $PSScriptRoot $_ })
|
||||
$msi = Join-Path $stage "$($policy.id)-NOT-INSTALLABLE.msi"
|
||||
$extensions = @($policy.extensions | ForEach-Object { '-ext'; "WixToolset.$_.wixext" })
|
||||
& wix build @sources -arch $arch @extensions `
|
||||
-culture en-US -d 'Version=0.0.1' -d 'DisplayVersion=0.0.1-metadata-test' `
|
||||
-d "BinDir=$stage" -d "IconPath=$(Join-Path $root $policy.icon)" `
|
||||
-d "DocsDir=$(Join-Path $stage 'docs')" -d "ModelsDir=$(Join-Path $stage 'models')" `
|
||||
-d "AppNoticesDir=$(Join-Path $stage 'notices')" -d 'HasCraftFontLicences=1' -d 'HasCli=1' `
|
||||
-d "BannerBmp=$(Join-Path $stage 'banner.bmp')" -d "DialogBmp=$(Join-Path $stage 'dialog.bmp')" `
|
||||
-d "UiBannerBmp=$(Join-Path $stage 'banner.bmp')" -d "UiDialogBmp=$(Join-Path $stage 'dialog.bmp')" `
|
||||
-d "LicenseRtf=$(Join-Path $stage 'licence.rtf')" -o $msi @($policy.localization | ForEach-Object { '-loc'; Join-Path $PSScriptRoot $_ })
|
||||
if ($LASTEXITCODE -ne 0) { throw "WiX validation failed for $arch (exit $LASTEXITCODE)." }
|
||||
$shell = (Get-Process -Id $PID).Path
|
||||
& $shell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $msi
|
||||
if ($LASTEXITCODE -ne 0) { throw "Compiled installer policy failed for $arch (exit $LASTEXITCODE)." }
|
||||
Write-Output "PASS $arch metadata-only package. This is not an executable installer."
|
||||
}
|
||||
@@ -38,41 +38,11 @@ function Assert-NoRow([string] $Sql, [string] $What) {
|
||||
} finally { [void] $view.Close() }
|
||||
}
|
||||
|
||||
# Test the compiled condition with Windows Installer's evaluator, in a restricted session that
|
||||
# cannot change machine state. Normal installs/repairs work, per-user overrides fail, and removal
|
||||
# of an older incorrectly scoped installation remains possible (#305).
|
||||
$scopeMessage = 'PdfCraft must be installed for all users. Run setup with administrator privileges and ALLUSERS=1; per-user installation is not supported.'
|
||||
$scopeCondition = Read-Row ('SELECT `Condition` FROM `LaunchCondition` WHERE `Description` = ''' + $scopeMessage + '''') 1
|
||||
# The shared compiled contract covers both scopes, duplicate guards and safe maintenance.
|
||||
# Keep it in a separate process because Windows Installer permits one session per process.
|
||||
& (Get-Process -Id $PID).Path -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $Path
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Dual-scope installer contract failed.' }
|
||||
$Installer.UILevel = 2
|
||||
$session = $null
|
||||
try {
|
||||
$session = $Installer.OpenPackage((Resolve-Path -LiteralPath $Path).Path, 1)
|
||||
foreach ($case in @(
|
||||
@('machine install', '1', '', '', '', 1),
|
||||
@('machine repair', '1', '', '1', '', 1),
|
||||
@('forced per-user install', '2', '1', '', '', 0),
|
||||
@('empty ALLUSERS', '', '', '', '', 0),
|
||||
@('resolved per-user install', '', '1', '', '', 0),
|
||||
@('forced per-user repair', '2', '1', '1', '', 0),
|
||||
@('machine uninstall', '1', '', '1', 'ALL', 1),
|
||||
@('legacy per-user uninstall', '', '1', '1', 'ALL', 1))) {
|
||||
$session.Property('ALLUSERS') = $case[1]
|
||||
$session.Property('MSIINSTALLPERUSER') = $case[2]
|
||||
$session.Property('Installed') = $case[3]
|
||||
$session.Property('REMOVE') = $case[4]
|
||||
Assert-Equal ($session.EvaluateCondition($scopeCondition[0])) $case[5] $case[0]
|
||||
}
|
||||
} finally {
|
||||
if ($session) { [void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($session) }
|
||||
}
|
||||
foreach ($sequence in @('InstallUISequence', 'InstallExecuteSequence')) {
|
||||
$launch = Read-Row ('SELECT `Condition`, `Sequence` FROM `' + $sequence + '` WHERE `Action` = ''LaunchConditions''') 2
|
||||
Assert-Equal $launch[0] '' "$sequence launch conditions are unconditional"
|
||||
$cost = Read-Row ('SELECT `Sequence` FROM `' + $sequence + '` WHERE `Action` = ''CostInitialize''') 1
|
||||
if ([int] $launch[1] -le 0 -or [int] $launch[1] -ge [int] $cost[0]) {
|
||||
throw "$sequence must reject per-user overrides before costing"
|
||||
}
|
||||
}
|
||||
$manufacturer = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''Manufacturer''' 1
|
||||
Assert-Equal $manufacturer[0] 'Learning Machines LLC' 'MSI manufacturer'
|
||||
|
||||
@@ -173,7 +143,7 @@ Assert-Equal $checked[0] '1' 'Welcome checkbox value'
|
||||
$app = Read-Row 'SELECT `KeyPath` FROM `Component` WHERE `Component` = ''PdfcraftApp''' 1
|
||||
Assert-Equal $app[0] 'PdfcraftExe' 'Shortcut executable key path'
|
||||
$scope = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''ALLUSERS''' 1
|
||||
Assert-Equal $scope[0] '1' 'Per-machine shortcut scope'
|
||||
Assert-Equal $scope[0] '2' 'Dual-scope shortcut package'
|
||||
|
||||
# Each File Explorer menu remembers whether it was installed by finding its key path in HKLM, in the
|
||||
# package's own registry view (64-bit for x64 and arm64). Only a release that offered the menu can
|
||||
@@ -181,12 +151,22 @@ Assert-Equal $scope[0] '1' 'Per-machine shortcut scope'
|
||||
$upgradeCode = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''UpgradeCode''' 1
|
||||
$view64 = $Database.SummaryInformation(0).Property(7) -notmatch '^Intel;'
|
||||
function Assert-MenuSearch([string] $Found, [string] $Key, [string] $Release, [string] $Since, [string] $What) {
|
||||
$search = Read-Row ('SELECT `Root`, `Key`, `Name`, `Type` FROM `RegLocator`, `AppSearch` WHERE `AppSearch`.`Signature_` = `RegLocator`.`Signature_` AND `AppSearch`.`Property` = ''' + $Found + '''') 4
|
||||
Assert-Equal $search[0] '2' "$What search HKLM root"
|
||||
Assert-Equal $search[1] $Key "$What search key"
|
||||
Assert-Equal $search[2] '' "$What search reads the default value"
|
||||
# msidbLocatorTypeRawValue (2), plus msidbLocatorType64bit (16) in a 64-bit package.
|
||||
Assert-Equal ([int] $search[3]) $(if ($view64) { 18 } else { 2 }) "$What search type"
|
||||
foreach ($context in @(@('M', '2'), @('U', '1'))) {
|
||||
$cache = ''
|
||||
$loaders = $Database.OpenView('SELECT `Action`, `Target` FROM `CustomAction` WHERE `Source` = ''' + $Found + '''')
|
||||
try {
|
||||
[void]$loaders.Execute()
|
||||
while ($row = $loaders.Fetch()) {
|
||||
if ($row.StringData(1) -match ('^CraftLoadOption[0-9]+' + $context[0] + '$')) { $cache = $row.StringData(2).Trim('[]') }
|
||||
}
|
||||
} finally { [void]$loaders.Close() }
|
||||
if (-not $cache) { throw "$What has no context-specific loader." }
|
||||
$search = Read-Row ('SELECT `Root`, `Key`, `Name`, `Type` FROM `RegLocator`, `AppSearch` WHERE `AppSearch`.`Signature_` = `RegLocator`.`Signature_` AND `AppSearch`.`Property` = ''' + $cache + '''') 4
|
||||
Assert-Equal $search[0] $context[1] "$What context $($context[0]) search root"
|
||||
Assert-Equal $search[1] $Key "$What search key"
|
||||
Assert-Equal $search[2] '' "$What search reads the default value"
|
||||
Assert-Equal ([int] $search[3]) $(if ($view64) { 18 } else { 2 }) "$What search type"
|
||||
}
|
||||
$upgrade = Read-Row ('SELECT `UpgradeCode`, `VersionMin`, `VersionMax`, `Attributes` FROM `Upgrade` WHERE `ActionProperty` = ''' + $Release + '''') 4
|
||||
Assert-Equal $upgrade[0] $upgradeCode[0] "$What release detection upgrade code"
|
||||
Assert-Equal $upgrade[1] $Since "$What release detection minimum"
|
||||
@@ -213,7 +193,7 @@ foreach ($ext in @('png', 'jpg', 'jpeg', 'tif', 'tiff', 'gif', 'bmp', 'jp2', 'j2
|
||||
$menu = Read-Row ('SELECT `Value`, `Component_`, `Root` FROM `Registry` WHERE `Key` = ''' + $key + ''' AND `Name` IS NULL') 3
|
||||
Assert-Equal $menu[0] 'Create PDF with PdfCraft…' "$ext context menu label"
|
||||
Assert-Equal $menu[1] 'PdfcraftImageContextMenu' "$ext context menu component"
|
||||
Assert-Equal $menu[2] '2' "$ext context menu HKLM root"
|
||||
Assert-Equal $menu[2] '-1' "$ext context menu context-sensitive root"
|
||||
$command = Read-Row ('SELECT `Value`, `Component_` FROM `Registry` WHERE `Key` = ''' + $key + '\command''') 2
|
||||
Assert-Equal $command[0] '"[#PdfcraftExe]" --create-images "%1"' "$ext context menu command"
|
||||
Assert-Equal $command[1] 'PdfcraftImageContextMenu' "$ext context menu command component"
|
||||
@@ -246,7 +226,7 @@ $pdfKey = 'Software\Classes\SystemFileAssociations\.pdf\shell\PdfCraft.Edit'
|
||||
$pdfMenu = Read-Row ('SELECT `Registry`, `Value`, `Component_`, `Root` FROM `Registry` WHERE `Key` = ''' + $pdfKey + ''' AND `Name` IS NULL') 4
|
||||
Assert-Equal $pdfMenu[1] 'Edit with PdfCraft' 'PDF context menu label'
|
||||
Assert-Equal $pdfMenu[2] 'PdfcraftPdfContextMenu' 'PDF context menu component'
|
||||
Assert-Equal $pdfMenu[3] '2' 'PDF context menu HKLM root'
|
||||
Assert-Equal $pdfMenu[3] '-1' 'PDF context menu context-sensitive root'
|
||||
foreach ($value in @(@('Icon', '"[#PdfcraftExe]",0'), @('MultiSelectModel', 'Document'))) {
|
||||
$row = Read-Row ('SELECT `Value`, `Component_` FROM `Registry` WHERE `Key` = ''' + $pdfKey + ''' AND `Name` = ''' + $value[0] + '''') 2
|
||||
Assert-Equal $row[0] $value[1] "PDF context menu $($value[0])"
|
||||
@@ -288,15 +268,18 @@ $modelsDir = Read-Row 'SELECT `Directory_Parent`, `DefaultDir` FROM `Directory`
|
||||
Assert-Equal $modelsDir[0] 'INSTALLFOLDER' 'OCR models folder parent'
|
||||
# DefaultDir is `models`, or `SHORT|models` with a generated 8.3 name.
|
||||
if ($modelsDir[1] -notmatch '(^|\|)models$') { throw "OCR models folder name: '$($modelsDir[1])'" }
|
||||
$modelFiles = 0
|
||||
$modelFiles = @()
|
||||
$view = $Database.OpenView('SELECT `File`.`FileName` FROM `File`, `Component` WHERE `File`.`Component_` = `Component`.`Component` AND `Component`.`Directory_` = ''ModelsFolder''')
|
||||
try {
|
||||
[void] $view.Execute()
|
||||
while ($record = $view.Fetch()) { if ($record.StringData(1) -match '\.rten$') { $modelFiles++ } }
|
||||
# FileName can contain a generated 8.3 name before the long name: SHORT|long.
|
||||
while ($record = $view.Fetch()) { $modelFiles += ($record.StringData(1) -split '\|')[-1] }
|
||||
} finally { [void] $view.Close() }
|
||||
if ($modelFiles -lt 2) { throw "expected the OCR models (*.rten) in models\, found $modelFiles" }
|
||||
foreach ($model in @('text-detection.onnx', 'text-recognition.onnx')) {
|
||||
Assert-Equal (@($modelFiles | Where-Object { $_ -ceq $model }).Count) 1 "OCR model $model in models\"
|
||||
}
|
||||
$rm = Read-Row 'SELECT `Dialog` FROM `Dialog` WHERE `Dialog` = ''MsiRMFilesInUse''' 1
|
||||
Assert-Equal $rm[0] 'MsiRMFilesInUse' 'Files-in-use dialog'
|
||||
[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Database)
|
||||
[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Installer)
|
||||
Write-Output 'ok MSI: per-machine scope guard, publisher, persistent progress text, Start Menu shortcut, optional desktop shortcut and PDF and image context menus (on for a first install, kept on update only while present; checkboxes for the first two), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog, OCR models'
|
||||
Write-Output 'ok MSI: dual-scope guards, publisher, persistent progress text, Start Menu shortcut, optional desktop shortcut and PDF and image context menus (on for a first install, kept on update only while present; checkboxes for the first two), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog, OCR models'
|
||||
|
||||
@@ -6556,8 +6556,8 @@ milestone = "M10"
|
||||
status = "shipped"
|
||||
commands = ["ocr.recognize"]
|
||||
tools = ["ocr_recognize", "ocr_status"]
|
||||
evidence = ["crates/engine/src/tests.rs::recognize_text_makes_a_scanned_page_searchable", "crates/automation/tests/it/automation.rs::ocr_tools_make_a_scan_searchable", "crates/ui-egui/tests/it/ocr.rs::recognize_text_dialog_adds_searchable_text"]
|
||||
notes = "ocrs engine (MIT/Apache) with CC-BY-SA models fetched by `cargo xtask models`; the desktop release packages ship them (#103; packaging/env.sh stage_models), the web build does not yet; runs on a worker thread with progress; one undoable step; pages with text are skipped unless asked (ocr.skip-renderable-text)."
|
||||
evidence = ["crates/ocr/src/lib.rs::model_discovery_prefers_the_complete_onnx_pair", "crates/ocr/src/lib.rs::model_discovery_falls_back_to_legacy_when_the_onnx_pair_is_incomplete", "crates/ocr/src/lib.rs::model_discovery_refuses_mixed_format_pairs", "crates/ocr/src/lib.rs::model_discovery_keeps_legacy_only_installations", "crates/engine/src/tests.rs::recognize_text_makes_a_scanned_page_searchable", "crates/automation/tests/it/automation.rs::ocr_tools_make_a_scan_searchable", "crates/ui-egui/tests/it/ocr.rs::recognize_text_dialog_adds_searchable_text"]
|
||||
notes = "ocrs engine (MIT/Apache) with CC-BY-SA ONNX models fetched and SHA-256 checked by `cargo xtask models`; within each search directory, a complete ONNX pair takes precedence over a complete legacy RTEN pair, incomplete and mixed-format pairs are not combined, and directory precedence is unchanged; the desktop release packages ship them (#103; packaging/env.sh stage_models), the web build does not yet; runs on a worker thread with progress; one undoable step; pages with text are skipped unless asked (ocr.skip-renderable-text)."
|
||||
|
||||
[[feature]]
|
||||
id = "ocr.recognize-range"
|
||||
|
||||
Reference in New Issue
Block a user