mirror of
https://github.com/aphrody-labs/pdfcraft.git
synced 2026-10-13 19:26:52 +00:00
* M3: confirm before opening absolute GoToR paths (#1364) * fix(i18n): translate GoToR confirmation dialog labels --------- Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
36 files changed
+515
-31
No files matched your search
+21
-21
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "9e72de3704f5faf040d951d39f11e32536c40572cb6af4a2c8456c20d6189b81"
|
||||
sha256 = "28141072a01326a59e873958c20780688da68bbce6fee6d8a0598b3b2eaf64c8"
|
||||
|
||||
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
|
||||
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
|
||||
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "bf481bf7dc5b7bbd51541c5a8c3b2fbeaf545b313bd60070993f057f5b6da7f0"
|
||||
sha256 = "55741bc14dd319319db883308cee6400a95cc86a11e54ddcf84921b07d7aa5b6"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
|
||||
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d07268fcab301a296e6e1c56a691331485cbdee92d8082f125df257fbd808ad1"
|
||||
sha256 = "77a0c98f38aceee0a2b63002a0348bc10fb6982ac76fe80d020d030c640c7dfb"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/cs.tsv"
|
||||
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "1b433cb74952dd5eb94e0ea685d6e8c0c715ce9609005483b832ff5c549c4623"
|
||||
sha256 = "b29359b5f0b2be96cdb9c1600187aa738d789db13d74afda26de8d7efbe71d5f"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pt-br.tsv"
|
||||
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "a4d149e4a890a2846d18e9193b45f2b40c2b8cc9743ffef6201acdd1aef4c0ca"
|
||||
sha256 = "69623423479cb67f011a7725d6174385285cc1e6e7f46785d6c2cd1c403f5f71"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/es.tsv"
|
||||
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "aab612c6b21b71f45b09e430a046a5d57d3572b20378caad99e960e0847834f9"
|
||||
sha256 = "b5c78c84b6b4d418e77b1b106f88fbbe4dad8e3faf06e5d134f069d4eeaff49c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/fr.tsv"
|
||||
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "b2cd5d1269a160bc9a02d9a8fc8e7bb0819b40101905ca5552066719134b85f1"
|
||||
sha256 = "e891fcdfa020a94d129f5f56a27a85841153311afd45c1b066895927e24468e0"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/de.tsv"
|
||||
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "35f71ba25148c4d187ef5b1fea0c4ad30d9eac8f6634fdad92cce0328584581f"
|
||||
sha256 = "5f8bca9c22a5a7f0015b3ef4bbc173c3040f23789a145e51119e38419a3e476a"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/te.tsv"
|
||||
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "162544e5cc09a98c3c3ce957223bca08a09f452a0e1ef25192a2ee07a0cec29d"
|
||||
sha256 = "bbc72e480ee0b22da792a95643b851c0752d69543472d79c2edc7978c9afc4fb"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/it.tsv"
|
||||
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "92c062f32c318875153bebd78afef94528ed7a76c074669325e58755e3494e6e"
|
||||
sha256 = "ea26a00d59bca609e44eb2d59e493da0aa41f49c3e206f3a9ba9a4d0886d263c"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/pl.tsv"
|
||||
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "3a39b915005b8139e0505e09484d3532d4d2b3fe57629a8b691397097507c5f3"
|
||||
sha256 = "8f45e60c676b33c2c8298274c44311900364e5ed96258bf86d4cc371a84d65ad"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ru.tsv"
|
||||
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "d6dea9f180e65bc80219c36bf58f3017d71f7419bac97b554946c5d0048eef90"
|
||||
sha256 = "527b6f96e54741387ded3d94484c6f70bd75246a83873beeaad284531cabf710"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hu.tsv"
|
||||
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "026deb06c481650dafcb5237e88f6b60c2ae11df0d6f998727225e3a10fae5aa"
|
||||
sha256 = "c4d0cae1401ac1aae2eb50f426c9ba0361aaeef3ea56835eeada9632b0ba4101"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/bg.tsv"
|
||||
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "2f479cac837466414590f8c4ca5e3d4ff28b45890a60421a3ed6d176bbe8a545"
|
||||
sha256 = "64912fc6c3e232d8a90c6b2fe476a5a383178aeba5f1d32cc27602d5e65f141b"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/kk.tsv"
|
||||
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "a3245989fecc7f8f0adcc19f4290744d291860c175ea8cfb3defa513e6e4708a"
|
||||
sha256 = "f669ad8797a518ad8e849b2332043367b0e04479d4d8de03f7e565dddb53a123"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ja.tsv"
|
||||
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "7f6558c2fac98c05cc67d16d470d3f64ecf121261a46c42476cc083830472a3a"
|
||||
sha256 = "e57905062cc7d54185120c66435be590cd778a98b91d158fe8f40acf17636787"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/uk.tsv"
|
||||
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fb6ff20dc580c7e8fd456fce9857a55a1f2bd9ad560ec9aff886e071040a38fa"
|
||||
sha256 = "24b37ec8b7c16eee4347c3b3e45c574d339a097dc24aad2c35524c2dbf589c40"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ar.tsv"
|
||||
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "fa96cddb9e789d1af12bc0371fd0757c5cbc068884dde19de82d0beeef07ac90"
|
||||
sha256 = "7d3f5fbc9016ad86441bd8b6858627f64971facc8c5bbbcb339521007669fdb1"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/tr.tsv"
|
||||
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "818aadf33298dda57fc82326ade9d05219a04b1ce2ba38ba9af3c47290e341be"
|
||||
sha256 = "bcfe8940ac0f1887b4b2692a7796bbe79048a414891c16159f09040948619a21"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/lt.tsv"
|
||||
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "60b0f9f46417ec7b164b14269a17d63a88c1419002ebc3942fb3412a47c9cb7d"
|
||||
sha256 = "8982df2065edb0aaa98e7e3606521ced403a0cc6e5ff1f70ba0e272dd5dc74e5"
|
||||
|
||||
[[asset]]
|
||||
path = "assets/icons/accessibility.svg"
|
||||
@@ -3351,4 +3351,4 @@ licence = "MIT OR Apache-2.0"
|
||||
licence_file = "LICENSE-MIT"
|
||||
kind = "translation"
|
||||
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "228b1032b114d08aa967cc0935c97225f8bd3d95bfa1d64ba71dc99fa1fd470e"
|
||||
sha256 = "649ead1e9be5f868571144e9964856077aa769e5bdb0b8d3c530d6bce0e29219"
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M3 #1364 candidate: consent before absolute local GoToR access; estimates unchanged); earlier: minor (M1 #1339: an unchanged incremental save of a classic cross-reference table writes a zero-length subsection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M5 #1398: stamps read upright on rotated pages; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (gap 6: cloudy comments stay when flatten cannot draw them, a refused text-box edit keeps its properties, arrow defaults stay off lines, a custom match stops at a separator, and the vertical-text redaction error no longer claims an overlap; #1317, #1327, #1302, #1315, #1340; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M7 #1316: replacing a line keeps a neighbouring line whose glyph box overlaps; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -144,6 +145,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
## Progress log
|
||||
|
||||
- **2026-10-12 (M3, #1364 candidate):** Absolute local GoToR links wait for Allow/Cancel before target-file access. Pending requests preserve their source and page/view destination; relative links retain their existing behavior. Headless `link_open` requires explicit `allow_absolute: true`, preserving existing root, symlink, file and password validation. Estimates unchanged.
|
||||
|
||||
- **2026-10-12 (community, M7, inline Arabic ActualText):** A single-glyph lam-alef `/ActualText` span joins its surrounding word in Edit text instead of splitting it into separate lines. Pure RTL ligature text is put in visual order before line assembly and recovered to typing order with its neighbours. Multi-glyph and mixed-direction replacements retain their authoritative order. Synthetic regressions cover lam-alef variants, marks, two-byte glyph codes, nested spans, replacement and save/reopen. Estimates unchanged.
|
||||
- **2026-10-12 (M5, #1398):** A stamp placed on a page with `/Rotate` reads upright as that page is displayed. Rubber stamps (including Sign Here), natural-size and explicit-rectangle images, and PDF-page stamps counterrotate their appearance; restyling a picture stamp keeps that correction, and a saved file reopens the same way. Estimates unchanged.
|
||||
- **2026-10-12 (M10, file formats):** Create PDF reads BigTIFF scans; a malformed GIF header is no longer detected as an image only to fail conversion. file-format-parity.md now lists XDP data import/export for XFA forms (already shipped under `form.xfa-data`) separately from opening `.xdp` as a document. Estimates unchanged.
|
||||
@@ -414,6 +417,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | M3/#1364 candidate: absolute local GoToR links require desktop consent or headless allow_absolute; existing relative navigation and validation retained. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M8: redaction retires inherited `/Resources` so a parent `/Pages` can't keep a removed XObject reachable; only redacted pages that inherit are pinned (one shared reference), unredacted pages are untouched; estimates unchanged |
|
||||
| 2026-10-12 | minor | M7/#1200 candidate: bounded local GoToR opening, root-confined automation, and action-preserving appearance edits; named destinations and window behavior remain partial; estimates unchanged. |
|
||||
| 2026-10-12 | minor | Exclusive image crops resize with their pixels across nested states, intervening operators and content streams; shared clips remain fixed. Synthetic engine/tool regressions and private image save/reopen checks; estimates unchanged. |
|
||||
|
||||
@@ -164,6 +164,21 @@ undoable and survive saving/reopening. `image_hit` shares the UI's curve and hol
|
||||
The bounds, limits and font requirements are in [existing-text editing](../../docs/editing-existing-text.md)
|
||||
and [image editing](../../docs/editing-images.md).
|
||||
|
||||
## Opening local GoToR links
|
||||
|
||||
`link_open {doc, page, index, password?}` follows a local PDF link using `link_list`
|
||||
numbering. Relative file specifications open normally. An **absolute** file specification
|
||||
requires `allow_absolute: true`, after reviewing the target reported by `link_list`.
|
||||
Omitting the argument or passing false refuses the request before checking or reading the
|
||||
target file. This is an intentional compatibility change for callers that previously opened
|
||||
absolute GoToR targets without explicit consent. Opting in does not bypass `--root`, symlink
|
||||
confinement, regular-file, PDF-extension, size, or password validation. Consent applies only
|
||||
to that call, including each password retry; it is never remembered for other links.
|
||||
|
||||
The desktop UI asks Allow/Cancel before following absolute GoToR targets and displays the
|
||||
complete path. Relative links retain their existing behavior. Named destinations remain
|
||||
unapplied with a warning.
|
||||
|
||||
## Folder batches
|
||||
|
||||
`doc_batch` runs conversion, OCR and compression over files and optionally recursive
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Link tools: list, add, edit and delete links; create links from URLs in the text; remove
|
||||
//! all links. Rectangles are points from the top-left of the displayed page.
|
||||
|
||||
use pdfcraft_engine::{Edit, LinkAction, LinkHighlight, LinkStyle, read_remote_document, resolve_remote_document_path};
|
||||
use pdfcraft_engine::{
|
||||
Edit, LinkAction, LinkHighlight, LinkStyle, read_remote_document, remote_document_path_is_absolute, resolve_remote_document_path,
|
||||
};
|
||||
use pdfcraft_render::{DestView, LinkTarget, RemoteDestination};
|
||||
use serde_json::{Value, json};
|
||||
use std::path::Path;
|
||||
@@ -106,6 +108,12 @@ impl Automation {
|
||||
(source.path.clone(), file.clone(), destination.clone(), *new_window)
|
||||
};
|
||||
let path = resolve_remote_document_path(source_path.as_deref().map(Path::new), &file).map_err(failed)?;
|
||||
let allow_absolute = a.opt_bool("allow_absolute")?.unwrap_or(false);
|
||||
if remote_document_path_is_absolute(&file) && !allow_absolute {
|
||||
return Err(failed(
|
||||
"this GoToR link names an absolute local path; review the target with link_list and pass allow_absolute: true to open it",
|
||||
));
|
||||
}
|
||||
let path = self.resolve(&path.to_string_lossy(), false)?;
|
||||
let bytes = read_remote_document(&path).map_err(|e| failed(format!("{}: {e}", path.display())))?;
|
||||
let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
|
||||
@@ -561,9 +561,9 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
t("link_list", "List links", "Every link: page, 1-based index (for link_edit/link_delete), rect (points from the top-left of the page), and where it goes (url or to_page).")
|
||||
.ro()
|
||||
.with(schema(json!({ "doc": doc() }), &["doc"])),
|
||||
t("link_open", "Open a linked PDF", "Follow a local GoToR link in PdfCraft: open the referenced PDF as a new document, respecting --root. page and index use link_list numbering. Named destinations are reported but not applied.")
|
||||
t("link_open", "Open a linked PDF", "Follow a local GoToR link in PdfCraft: open the referenced PDF as a new document, respecting --root. page and index use link_list numbering. Named destinations are reported but not applied. Absolute file specifications require allow_absolute: true after reviewing the target; relative links need no opt-in.")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "index": { "type": "integer", "minimum": 1 }, "password": { "type": "string", "description": "Optional password for the linked PDF." } }),
|
||||
json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "index": { "type": "integer", "minimum": 1 }, "password": { "type": "string", "description": "Optional password for the linked PDF." }, "allow_absolute": { "type": "boolean", "description": "Explicitly allow an absolute local GoToR target (default false). Existing validation and --root confinement still apply." } }),
|
||||
&["doc", "page", "index"],
|
||||
)),
|
||||
t("link_add", "Add a link", "Add a link over rect on page that opens url or goes to to_page. Appearance: visible (rectangle), color, width 1-3, highlight none/invert/outline/inset. Undoable.")
|
||||
|
||||
@@ -3486,7 +3486,12 @@ fn local_gotor_link_open_applies_destinations_and_honors_root() {
|
||||
assert_eq!(a.session().get(pdfcraft_engine::DocId(target_id)).unwrap().info.pages.len(), 3);
|
||||
|
||||
let absolute = ok(&mut a, "doc_open", json!({ "path": "absolute.pdf" }))["doc"].as_u64().unwrap();
|
||||
let opened = ok(&mut a, "link_open", json!({ "doc": absolute, "page": 1, "index": 1 }));
|
||||
let before = a.session().docs().len();
|
||||
for args in [json!({ "doc": absolute, "page": 1, "index": 1 }), json!({ "doc": absolute, "page": 1, "index": 1, "allow_absolute": false })] {
|
||||
assert!(matches!(err(&mut a, "link_open", args), ToolError::Failed(m) if m.contains("allow_absolute")));
|
||||
assert_eq!(a.session().docs().len(), before);
|
||||
}
|
||||
let opened = ok(&mut a, "link_open", json!({ "doc": absolute, "page": 1, "index": 1, "allow_absolute": true }));
|
||||
assert_eq!(opened["path"], "target.pdf");
|
||||
assert_eq!(opened["destination"]["page"], 2);
|
||||
|
||||
@@ -5623,3 +5628,47 @@ fn private_crops_with_nested_state_resize_save_reopen_through_tools() {
|
||||
assert_eq!(ok(&mut a, "page_images", json!({"doc":doc,"page":1})), original);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_gotor_consent_does_not_bypass_validation_or_passwords() {
|
||||
let (base, root) = sandbox("gotor-absolute-consent");
|
||||
std::fs::create_dir_all(root.join("folder.pdf")).unwrap();
|
||||
std::fs::write(root.join("notes.txt"), fixture(1)).unwrap();
|
||||
std::fs::write(root.join("encrypted.pdf"), protected_fixture("secret")).unwrap();
|
||||
link_dir(&root, "outside-link", &base.join("outside"));
|
||||
let mut a = auto(&root);
|
||||
for (name, target, reason) in [
|
||||
("missing", root.join("missing.pdf"), "missing.pdf"),
|
||||
("folder", root.join("folder.pdf"), "not a regular file"),
|
||||
("text", root.join("notes.txt"), "not a .pdf file"),
|
||||
("outside", base.join("outside/secret.pdf"), "outside"),
|
||||
("symlink", root.join("outside-link/secret.pdf"), "outside"),
|
||||
] {
|
||||
let path = format!("{name}-source.pdf");
|
||||
std::fs::write(root.join(&path), remote_link_pdf(target.to_str().unwrap(), Some("[0 /Fit]"))).unwrap();
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": path }))["doc"].as_u64().unwrap();
|
||||
let before = a.session().docs().len();
|
||||
// The absence of a target or a root escape must not be probed before consent.
|
||||
assert!(
|
||||
matches!(err(&mut a, "link_open", json!({ "doc": doc, "page": 1, "index": 1 })), ToolError::Failed(m) if m.contains("allow_absolute"))
|
||||
);
|
||||
assert!(
|
||||
matches!(err(&mut a, "link_open", json!({ "doc": doc, "page": 1, "index": 1, "allow_absolute": true })), ToolError::Failed(m) if m.contains(reason)),
|
||||
"{name}"
|
||||
);
|
||||
assert_eq!(a.session().docs().len(), before);
|
||||
}
|
||||
std::fs::write(root.join("password-source.pdf"), remote_link_pdf(root.join("encrypted.pdf").to_str().unwrap(), Some("[0 /Fit]"))).unwrap();
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "password-source.pdf" }))["doc"].as_u64().unwrap();
|
||||
let before = a.session().docs().len();
|
||||
for password in ["", "wrong"] {
|
||||
assert!(matches!(
|
||||
err(&mut a, "link_open", json!({ "doc": doc, "page": 1, "index": 1, "allow_absolute": true, "password": password })),
|
||||
ToolError::Failed(_)
|
||||
));
|
||||
assert_eq!(a.session().docs().len(), before);
|
||||
}
|
||||
let opened = ok(&mut a, "link_open", json!({ "doc": doc, "page": 1, "index": 1, "allow_absolute": true, "password": "secret" }));
|
||||
assert_eq!(opened["document"]["encrypted"], true);
|
||||
assert_eq!(opened["destination_applied"], true);
|
||||
}
|
||||
@@ -114,7 +114,9 @@ pub fn typed_signature_graphic(text: &str) -> Option<pdfcraft_sign::Graphic> {
|
||||
o.contours.iter().any(|c| c.len() > 2).then_some(pdfcraft_sign::Graphic::Outlines(o.contours))
|
||||
}
|
||||
|
||||
pub use links::{MAX_REMOTE_DOCUMENT_BYTES, RemoteDocumentPathError, read_remote_document, resolve_remote_document_path};
|
||||
pub use links::{
|
||||
MAX_REMOTE_DOCUMENT_BYTES, RemoteDocumentPathError, read_remote_document, remote_document_path_is_absolute, resolve_remote_document_path,
|
||||
};
|
||||
/// Comment geometry helpers (text-box line breaking) for frontends.
|
||||
pub use pdfcraft_annot::appearance as annot_text;
|
||||
pub use pdfcraft_annot::links::{Highlight as LinkHighlight, LinkAction, LinkItem, LinkStyle};
|
||||
|
||||
@@ -165,6 +165,13 @@ impl std::fmt::Display for RemoteDocumentPathError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the original GoToR file specification is absolute on this platform.
|
||||
/// Check this before resolving relative paths against the source document. This does no I/O;
|
||||
/// the resolver must still reject URL/network paths and malformed file specifications.
|
||||
pub fn remote_document_path_is_absolute(raw: &str) -> bool {
|
||||
Path::new(raw).is_absolute()
|
||||
}
|
||||
|
||||
/// Resolve a PDF remote-Go-To file specification to a local path without opening it.
|
||||
/// Relative paths are based on the source document; an in-memory source has no fallback to CWD.
|
||||
pub fn resolve_remote_document_path(source: Option<&Path>, raw: &str) -> Result<PathBuf, RemoteDocumentPathError> {
|
||||
@@ -181,7 +188,7 @@ pub fn resolve_remote_document_path(source: Option<&Path>, raw: &str) -> Result<
|
||||
return Err(RemoteDocumentPathError::UrlOrNetwork);
|
||||
}
|
||||
let path = Path::new(raw);
|
||||
let drive_absolute = path.is_absolute();
|
||||
let drive_absolute = remote_document_path_is_absolute(raw);
|
||||
if let Some((scheme, _)) = raw.split_once(':') {
|
||||
let valid_scheme = scheme.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
|
||||
&& scheme.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
|
||||
@@ -450,6 +457,13 @@ mod tests {
|
||||
assert_eq!(read(&dir.join("folder.pdf")), Err(E::NotAFile), "a directory is refused");
|
||||
assert_eq!(read(&dir.join("notes.txt")), Err(E::NotPdf));
|
||||
assert_eq!(read(&dir), Err(E::NotPdf));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// Sparse: the size guard must reject this without allocating or reading a GiB.
|
||||
let large = dir.join("large.pdf");
|
||||
std::fs::File::create(&large).unwrap().set_len(super::MAX_REMOTE_DOCUMENT_BYTES + 1).unwrap();
|
||||
assert_eq!(read(&large), Err(E::TooLarge));
|
||||
}
|
||||
assert!(matches!(read(&dir.join("missing.pdf")), Err(E::Read(_))));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -464,6 +478,26 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_document_absolute_classification_uses_the_original_platform_path() {
|
||||
use super::remote_document_path_is_absolute as absolute;
|
||||
for relative in ["target.pdf", "../appendix/target.pdf", "folder/target.pdf"] {
|
||||
assert!(!absolute(relative), "{relative}");
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
assert!(absolute("/manual/target.pdf"));
|
||||
assert!(!absolute(r"C:\manual\target.pdf"));
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
assert!(absolute(r"C:\manual\target.pdf"));
|
||||
assert!(absolute("C:/manual/target.pdf"));
|
||||
assert!(!absolute(r"C:target.pdf"), "drive-relative is not absolute");
|
||||
assert!(!absolute(r"\manual\target.pdf"), "rooted without a drive is not absolute");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_document_paths_are_local_and_source_relative() {
|
||||
use super::{RemoteDocumentPathError as E, resolve_remote_document_path as resolve};
|
||||
|
||||
@@ -2343,6 +2343,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
|| app.palette_open
|
||||
|| app.password_prompt.is_some()
|
||||
|| app.pending_link.is_some()
|
||||
|| app.pending_remote_link.is_some()
|
||||
|| app.updates.open;
|
||||
let mut clicked_link: Option<LinkTarget> = None;
|
||||
let mut remote_open: Option<(pdfcraft_engine::DocId, String, pdfcraft_render::RemoteDestination)> = None;
|
||||
|
||||
@@ -19,6 +19,7 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
password(app, ctx);
|
||||
save_prompt(app, ctx);
|
||||
link_prompt(app, ctx);
|
||||
remote_link_prompt(app, ctx);
|
||||
crate::updates::dialog(app, ctx);
|
||||
let Some(dialog) = app.dialog else {
|
||||
app.props_draft = None;
|
||||
@@ -1511,6 +1512,38 @@ fn link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute document-controlled local paths require consent before any filesystem access.
|
||||
fn remote_link_prompt(app: &mut PdfCraftApp, ctx: &egui::Context) {
|
||||
let Some(pending) = app.pending_remote_link.clone() else { return };
|
||||
let mut choice = None;
|
||||
let modal = widgets::show_modal(ctx, "remote_link_prompt", |ui| {
|
||||
ui.set_width(460.0);
|
||||
ui.label(egui::RichText::new(tl!("Open this local PDF?")).font(theme::semibold(16.0)));
|
||||
ui.add_space(6.0);
|
||||
ui.label(tl!("This document is trying to open a local file:"));
|
||||
egui::ScrollArea::vertical().max_height(160.0).show(ui, |ui| {
|
||||
ui.add(egui::Label::new(egui::RichText::new(pending.path.to_string_lossy()).monospace()).wrap().selectable(true));
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(tl!("Only allow this if you trust the source document."));
|
||||
ui.add_space(14.0);
|
||||
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
|
||||
if widgets::pill_button(ui, tl!("Cancel"), true).clicked() {
|
||||
choice = Some(false);
|
||||
}
|
||||
if widgets::pill_button(ui, tl!("Allow"), false).clicked() {
|
||||
choice = Some(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
if choice.is_none() && modal.should_close() {
|
||||
choice = Some(false);
|
||||
}
|
||||
if let Some(open) = choice {
|
||||
app.resolve_pending_remote_link(open);
|
||||
}
|
||||
}
|
||||
|
||||
fn yes(b: bool) -> String {
|
||||
tl!(if b { "Yes" } else { "No" }).to_string()
|
||||
}
|
||||
|
||||
@@ -2359,5 +2359,11 @@ animation effects Off معطّلة
|
||||
The linked PDF opened, but its named destination was not applied. فُتح ملف PDF المرتبط، لكن لم تُطبَّق وجهته المسماة.
|
||||
The linked PDF opened, but its page destination was out of range. فُتح ملف PDF المرتبط، لكن وجهة الصفحة خارج النطاق.
|
||||
Local cross-PDF links require the desktop app تتطلب الروابط المحلية بين ملفات PDF تطبيق سطح المكتب
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow السماح
|
||||
Only allow this if you trust the source document. لا تسمح بذلك إلا إذا كنت تثق بالمستند المصدر.
|
||||
Open this local PDF? هل تريد فتح ملف PDF المحلي هذا؟
|
||||
This document is trying to open a local file: يحاول هذا المستند فتح ملف محلي:
|
||||
Move tag earlier نقل العلامة للأعلى
|
||||
Move tag later نقل العلامة للأسفل
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2301,5 +2301,11 @@ animation effects Off Изключени
|
||||
The linked PDF opened, but its named destination was not applied. Свързаният PDF се отвори, но именуваната му цел не беше приложена.
|
||||
The linked PDF opened, but its page destination was out of range. Свързаният PDF се отвори, но целевата му страница е извън обхвата.
|
||||
Local cross-PDF links require the desktop app Локалните връзки между PDF файлове изискват настолното приложение
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Разреши
|
||||
Only allow this if you trust the source document. Разрешете това само ако имате доверие на изходния документ.
|
||||
Open this local PDF? Да се отвори ли този локален PDF?
|
||||
This document is trying to open a local file: Този документ се опитва да отвори локален файл:
|
||||
Move tag earlier Преместване на етикета по-рано
|
||||
Move tag later Преместване на етикета по-късно
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -2412,5 +2412,11 @@ animation effects Off Vypnuto
|
||||
The linked PDF opened, but its named destination was not applied. Odkazovaný PDF se otevřel, ale jeho pojmenovaný cíl nebyl použit.
|
||||
The linked PDF opened, but its page destination was out of range. Odkazovaný PDF se otevřel, ale cílová stránka je mimo rozsah.
|
||||
Local cross-PDF links require the desktop app Místní odkazy mezi soubory PDF vyžadují desktopovou aplikaci
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Povolit
|
||||
Only allow this if you trust the source document. Povolte tuto akci pouze v případě, že důvěřujete zdrojovému dokumentu.
|
||||
Open this local PDF? Otevřít tento místní soubor PDF?
|
||||
This document is trying to open a local file: Tento dokument se pokouší otevřít místní soubor:
|
||||
Move tag earlier Přesunout značku dříve
|
||||
Move tag later Přesunout značku později
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2339,5 +2339,11 @@ animation effects Off Aus
|
||||
The linked PDF opened, but its named destination was not applied. Die verknüpfte PDF wurde geöffnet, aber ihr benanntes Ziel wurde nicht angewendet.
|
||||
The linked PDF opened, but its page destination was out of range. Die verknüpfte PDF wurde geöffnet, aber ihr Seitenziel liegt außerhalb des Bereichs.
|
||||
Local cross-PDF links require the desktop app Lokale Verknüpfungen zwischen PDFs erfordern die Desktop-App
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Zulassen
|
||||
Only allow this if you trust the source document. Lassen Sie dies nur zu, wenn Sie dem Quelldokument vertrauen.
|
||||
Open this local PDF? Diese lokale PDF-Datei öffnen?
|
||||
This document is trying to open a local file: Dieses Dokument versucht, eine lokale Datei zu öffnen:
|
||||
Move tag earlier Tag früher verschieben
|
||||
Move tag later Tag später verschieben
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -2348,5 +2348,11 @@ animation effects Off Ανενεργά
|
||||
Local cross-PDF links require the desktop app Οι τοπικοί σύνδεσμοι μεταξύ PDF απαιτούν την εφαρμογή υπολογιστή
|
||||
The existing action will be preserved. Η υπάρχουσα ενέργεια θα διατηρηθεί.
|
||||
This form's scripts are turned off because one ran too long. Reopen the document to run them again. Τα σενάρια αυτής της φόρμας είναι απενεργοποιημένα, επειδή ένα από αυτά εκτελούνταν για πολύ ώρα. Ανοίξτε ξανά το έγγραφο για να εκτελεστούν ξανά.
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Να επιτραπεί
|
||||
Only allow this if you trust the source document. Επιτρέψτε το μόνο αν εμπιστεύεστε το έγγραφο προέλευσης.
|
||||
Open this local PDF? Να ανοίξει αυτό το τοπικό PDF;
|
||||
This document is trying to open a local file: Αυτό το έγγραφο προσπαθεί να ανοίξει ένα τοπικό αρχείο:
|
||||
Move tag earlier Μετακίνηση ετικέτας νωρίτερα
|
||||
Move tag later Μετακίνηση ετικέτας αργότερα
|
||||
|
Can't render this file because it contains an unexpected character in line 479 and column 12.
|
@@ -2468,5 +2468,11 @@ animation effects Off Desactivados
|
||||
The linked PDF opened, but its page destination was out of range. El PDF vinculado se abrió, pero su página de destino está fuera de rango.
|
||||
Local cross-PDF links require the desktop app Los enlaces locales entre PDF requieren la aplicación de escritorio
|
||||
The existing action will be preserved. Se conservará la acción existente.
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Permitir
|
||||
Only allow this if you trust the source document. Permite esta acción solo si confías en el documento de origen.
|
||||
Open this local PDF? ¿Abrir este PDF local?
|
||||
This document is trying to open a local file: Este documento intenta abrir un archivo local:
|
||||
Move tag earlier Mover etiqueta antes
|
||||
Move tag later Mover etiqueta después
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -2338,5 +2338,11 @@ animation effects Off Désactivés
|
||||
The linked PDF opened, but its named destination was not applied. Le PDF lié s'est ouvert, mais sa destination nommée n'a pas été appliquée.
|
||||
The linked PDF opened, but its page destination was out of range. Le PDF lié s'est ouvert, mais sa page de destination est hors limites.
|
||||
Local cross-PDF links require the desktop app Les liens locaux entre PDF nécessitent l'application de bureau
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Autoriser
|
||||
Only allow this if you trust the source document. N'autorisez cette action que si vous faites confiance au document source.
|
||||
Open this local PDF? Ouvrir ce PDF local ?
|
||||
This document is trying to open a local file: Ce document tente d’ouvrir un fichier local :
|
||||
Move tag earlier Déplacer la balise plus tôt
|
||||
Move tag later Déplacer la balise plus tard
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -2337,5 +2337,11 @@ animation effects Off Ki
|
||||
The linked PDF opened, but its named destination was not applied. A hivatkozott PDF megnyílt, de a nevesített cél nem lett alkalmazva.
|
||||
The linked PDF opened, but its page destination was out of range. A hivatkozott PDF megnyílt, de a céloldal a tartományon kívül esik.
|
||||
Local cross-PDF links require the desktop app A PDF-ek közötti helyi hivatkozásokhoz az asztali alkalmazás szükséges
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Engedélyezés
|
||||
Only allow this if you trust the source document. Csak akkor engedélyezze ezt, ha megbízik a forrásdokumentumban.
|
||||
Open this local PDF? Megnyitja ezt a helyi PDF-fájlt?
|
||||
This document is trying to open a local file: Ez a dokumentum egy helyi fájlt próbál megnyitni:
|
||||
Move tag earlier Címke korábbra helyezése
|
||||
Move tag later Címke későbbre helyezése
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
@@ -2347,5 +2347,11 @@ animation effects Off Disattivati
|
||||
The linked PDF opened, but its named destination was not applied. Il PDF collegato è stato aperto, ma la sua destinazione con nome non è stata applicata.
|
||||
The linked PDF opened, but its page destination was out of range. Il PDF collegato è stato aperto, ma la pagina di destinazione è fuori intervallo.
|
||||
Local cross-PDF links require the desktop app I collegamenti locali tra PDF richiedono l'app desktop
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Consenti
|
||||
Only allow this if you trust the source document. Consenti questa operazione solo se ti fidi del documento di origine.
|
||||
Open this local PDF? Aprire questo PDF locale?
|
||||
This document is trying to open a local file: Questo documento sta tentando di aprire un file locale:
|
||||
Move tag earlier Sposta tag prima
|
||||
Move tag later Sposta tag dopo
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2338,5 +2338,11 @@ animation effects Off オフ
|
||||
The linked PDF opened, but its named destination was not applied. リンク先の PDF を開きましたが、名前付き移動先は適用されませんでした。
|
||||
The linked PDF opened, but its page destination was out of range. リンク先の PDF を開きましたが、移動先のページが範囲外です。
|
||||
Local cross-PDF links require the desktop app PDF 間のローカルリンクにはデスクトップアプリが必要です
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow 許可
|
||||
Only allow this if you trust the source document. 元の文書を信頼できる場合にのみ許可してください。
|
||||
Open this local PDF? このローカルPDFを開きますか?
|
||||
This document is trying to open a local file: この文書はローカルファイルを開こうとしています:
|
||||
Move tag earlier タグを前へ移動
|
||||
Move tag later タグを後へ移動
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2336,5 +2336,11 @@ animation effects Off Өшірулі
|
||||
The linked PDF opened, but its named destination was not applied. Байланысты PDF ашылды, бірақ оның аталған мақсаты қолданылмады.
|
||||
The linked PDF opened, but its page destination was out of range. Байланысты PDF ашылды, бірақ оның мақсатты беті ауқымнан тыс.
|
||||
Local cross-PDF links require the desktop app PDF файлдары арасындағы жергілікті сілтемелер үшін жұмыс үстелі қолданбасы қажет
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Рұқсат беру
|
||||
Only allow this if you trust the source document. Бастапқы құжатқа сенсеңіз ғана рұқсат беріңіз.
|
||||
Open this local PDF? Осы жергілікті PDF файлын ашу керек пе?
|
||||
This document is trying to open a local file: Бұл құжат жергілікті файлды ашуға әрекеттеніп жатыр:
|
||||
Move tag earlier Тегті ертерек жылжыту
|
||||
Move tag later Тегті кейінірек жылжыту
|
||||
|
Can't render this file because it contains an unexpected character in line 829 and column 12.
|
@@ -2582,5 +2582,11 @@ key Esc Esc
|
||||
key Home Home
|
||||
key End End
|
||||
key Space Tarpas
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Leisti
|
||||
Only allow this if you trust the source document. Leiskite tai tik tuo atveju, jei pasitikite pirminiu dokumentu.
|
||||
Open this local PDF? Atverti šį vietinį PDF failą?
|
||||
This document is trying to open a local file: Šis dokumentas bando atverti vietinį failą:
|
||||
Move tag earlier Perkelti žymą anksčiau
|
||||
Move tag later Perkelti žymą vėliau
|
||||
|
Can't render this file because it contains an unexpected character in line 482 and column 12.
|
@@ -920,6 +920,24 @@ mod tests {
|
||||
literals
|
||||
}
|
||||
|
||||
/// Partial catalogs also translate every label of the absolute local GoToR consent prompt.
|
||||
#[test]
|
||||
fn local_gotor_confirmation_is_translated_in_all_supported_languages() {
|
||||
for language in LANGUAGES.iter().filter(|language| !language.source.is_empty()) {
|
||||
let lang = Lang::from_code(language.code).expect("registered language");
|
||||
for label in [
|
||||
"Open this local PDF?",
|
||||
"This document is trying to open a local file:",
|
||||
"Only allow this if you trust the source document.",
|
||||
"Allow",
|
||||
"Cancel",
|
||||
] {
|
||||
assert!(has(lang, label), "{} lacks GoToR confirmation label: {label}", language.code);
|
||||
assert_ne!(tr(lang, label), label, "{} falls back to English for {label}", language.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Japanese translates every `tl!("…")` literal in the UI source, so a new string can't ship in
|
||||
/// English by accident (the same check French, German, Russian and Simplified Chinese have).
|
||||
#[test]
|
||||
|
||||
@@ -2318,5 +2318,11 @@ animation effects Off Wyłączone
|
||||
Local cross-PDF links require the desktop app Lokalne łącza między plikami PDF wymagają aplikacji komputerowej
|
||||
The existing action will be preserved. Istniejąca akcja zostanie zachowana.
|
||||
This form's scripts are turned off because one ran too long. Reopen the document to run them again. Skrypty tego formularza są wyłączone, ponieważ jeden z nich działał zbyt długo. Otwórz dokument ponownie, aby znów je uruchomić.
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Zezwól
|
||||
Only allow this if you trust the source document. Zezwól na to tylko wtedy, gdy ufasz dokumentowi źródłowemu.
|
||||
Open this local PDF? Otworzyć ten lokalny plik PDF?
|
||||
This document is trying to open a local file: Ten dokument próbuje otworzyć plik lokalny:
|
||||
Move tag earlier Przenieś znacznik wcześniej
|
||||
Move tag later Przenieś znacznik później
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2352,5 +2352,11 @@ animation effects Off Desativados
|
||||
The linked PDF opened, but its named destination was not applied. O PDF vinculado foi aberto, mas seu destino nomeado não foi aplicado.
|
||||
The linked PDF opened, but its page destination was out of range. O PDF vinculado foi aberto, mas sua página de destino está fora do intervalo.
|
||||
Local cross-PDF links require the desktop app Links locais entre PDFs exigem o aplicativo para desktop
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Permitir
|
||||
Only allow this if you trust the source document. Permita esta ação somente se confiar no documento de origem.
|
||||
Open this local PDF? Abrir este PDF local?
|
||||
This document is trying to open a local file: Este documento está tentando abrir um arquivo local:
|
||||
Move tag earlier Mover tag para antes
|
||||
Move tag later Mover tag para depois
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2251,5 +2251,11 @@ animation effects Off Выкл
|
||||
The linked PDF opened, but its named destination was not applied. Связанный PDF открыт, но его именованное назначение не применено.
|
||||
The linked PDF opened, but its page destination was out of range. Связанный PDF открыт, но страница назначения вне допустимого диапазона.
|
||||
Local cross-PDF links require the desktop app Локальные ссылки между PDF работают только в настольном приложении
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Разрешить
|
||||
Only allow this if you trust the source document. Разрешите это действие, только если доверяете исходному документу.
|
||||
Open this local PDF? Открыть этот локальный PDF?
|
||||
This document is trying to open a local file: Этот документ пытается открыть локальный файл:
|
||||
Move tag earlier Переместить тег раньше
|
||||
Move tag later Переместить тег позже
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2343,5 +2343,11 @@ animation effects Off Izklopljeni
|
||||
The linked PDF opened, but its named destination was not applied. Povezani PDF se je odprl, vendar njegov poimenovani cilj ni bil uporabljen.
|
||||
The linked PDF opened, but its page destination was out of range. Povezani PDF se je odprl, vendar je ciljna stran zunaj obsega.
|
||||
Local cross-PDF links require the desktop app Lokalne povezave med PDF-ji zahtevajo namizno aplikacijo
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Dovoli
|
||||
Only allow this if you trust the source document. To dovolite le, če zaupate izvornemu dokumentu.
|
||||
Open this local PDF? Želite odpreti ta lokalni PDF?
|
||||
This document is trying to open a local file: Ta dokument poskuša odpreti lokalno datoteko:
|
||||
Move tag earlier Premakni oznako prej
|
||||
Move tag later Premakni oznako pozneje
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2251,5 +2251,11 @@ animation effects Off ఆఫ్
|
||||
Local cross-PDF links require the desktop app PDFల మధ్య స్థానిక లింక్లకు డెస్క్టాప్ యాప్ అవసరం
|
||||
The existing action will be preserved. ఉన్న చర్య అలాగే ఉంచబడుతుంది.
|
||||
This form's scripts are turned off because one ran too long. Reopen the document to run them again. ఒక స్క్రిప్ట్ చాలా సేపు నడిచినందున ఈ ఫారమ్ స్క్రిప్ట్లు ఆపివేయబడ్డాయి. వాటిని మళ్లీ నడపడానికి పత్రాన్ని మళ్లీ తెరవండి.
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow అనుమతించు
|
||||
Only allow this if you trust the source document. మూల పత్రాన్ని విశ్వసిస్తే మాత్రమే దీన్ని అనుమతించండి.
|
||||
Open this local PDF? ఈ స్థానిక PDF ఫైల్ను తెరవాలా?
|
||||
This document is trying to open a local file: ఈ పత్రం ఒక స్థానిక ఫైల్ను తెరవడానికి ప్రయత్నిస్తోంది:
|
||||
Move tag earlier ట్యాగ్ను ముందుకు తరలించు
|
||||
Move tag later ట్యాగ్ను వెనుకకు తరలించు
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -2334,5 +2334,11 @@ animation effects Off Kapalı
|
||||
The linked PDF opened, but its named destination was not applied. Bağlantılı PDF açıldı, ancak adlandırılmış hedefi uygulanmadı.
|
||||
The linked PDF opened, but its page destination was out of range. Bağlantılı PDF açıldı, ancak sayfa hedefi aralık dışında.
|
||||
Local cross-PDF links require the desktop app PDF'ler arası yerel bağlantılar masaüstü uygulamasını gerektirir
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow İzin ver
|
||||
Only allow this if you trust the source document. Buna yalnızca kaynak belgeye güveniyorsanız izin verin.
|
||||
Open this local PDF? Bu yerel PDF açılsın mı?
|
||||
This document is trying to open a local file: Bu belge yerel bir dosya açmaya çalışıyor:
|
||||
Move tag earlier Etiketi daha erken taşı
|
||||
Move tag later Etiketi daha geç taşı
|
||||
|
Can't render this file because it contains an unexpected character in line 828 and column 12.
|
@@ -2360,5 +2360,11 @@ animation effects Off Вимкнено
|
||||
The linked PDF opened, but its named destination was not applied. Пов’язаний PDF відкрито, але його іменоване призначення не застосовано.
|
||||
The linked PDF opened, but its page destination was out of range. Пов’язаний PDF відкрито, але сторінка призначення поза межами діапазону.
|
||||
Local cross-PDF links require the desktop app Локальні посилання між PDF працюють лише в настільному застосунку
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow Дозволити
|
||||
Only allow this if you trust the source document. Дозволяйте це лише тоді, коли довіряєте вихідному документу.
|
||||
Open this local PDF? Відкрити цей локальний PDF?
|
||||
This document is trying to open a local file: Цей документ намагається відкрити локальний файл:
|
||||
Move tag earlier Перемістити тег раніше
|
||||
Move tag later Перемістити тег пізніше
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -2302,5 +2302,11 @@ animation effects Off 关
|
||||
The linked PDF opened, but its named destination was not applied. 已打开链接的 PDF,但未应用其命名目标。
|
||||
The linked PDF opened, but its page destination was out of range. 已打开链接的 PDF,但其页面目标超出范围。
|
||||
Local cross-PDF links require the desktop app PDF 之间的本地链接需要桌面应用
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow 允许
|
||||
Only allow this if you trust the source document. 仅在您信任源文档时才允许此操作。
|
||||
Open this local PDF? 是否打开此本地 PDF?
|
||||
This document is trying to open a local file: 此文档正尝试打开本地文件:
|
||||
Move tag earlier 将标签前移
|
||||
Move tag later 将标签后移
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -2212,5 +2212,11 @@ animation effects Off 關
|
||||
The linked PDF opened, but its page destination was out of range. 已開啟連結的 PDF,但其頁面目的地超出範圍。
|
||||
Local cross-PDF links require the desktop app PDF 之間的本機連結需要桌面應用程式
|
||||
The existing action will be preserved. 將保留現有動作。
|
||||
|
||||
# Absolute local GoToR confirmation (#1364).
|
||||
Allow 允許
|
||||
Only allow this if you trust the source document. 只有在您信任來源文件時,才允許此操作。
|
||||
Open this local PDF? 要開啟此本機 PDF 嗎?
|
||||
This document is trying to open a local file: 此文件正嘗試開啟本機檔案:
|
||||
Move tag earlier 將標記前移
|
||||
Move tag later 將標記後移
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -771,6 +771,8 @@ pub struct PdfCraftApp {
|
||||
pub last_opened_url: Option<String>,
|
||||
/// A document asked to open this address; the user hasn't answered yet (#90, #91).
|
||||
pub pending_link: Option<PendingLink>,
|
||||
/// An absolute GoToR target waiting for approval, before any target-file I/O (#1364).
|
||||
pub pending_remote_link: Option<PendingRemoteLink>,
|
||||
}
|
||||
|
||||
/// Where in a document a request to open an address came from.
|
||||
@@ -801,6 +803,14 @@ pub struct PendingLink {
|
||||
pub origin: LinkOrigin,
|
||||
}
|
||||
|
||||
/// A resolved local target and its original source/destination, independent of the active tab.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PendingRemoteLink {
|
||||
pub source: DocId,
|
||||
pub path: std::path::PathBuf,
|
||||
pub destination: pdfcraft_render::RemoteDestination,
|
||||
}
|
||||
|
||||
/// Settings for the Number pages dialog.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct NumberDraft {
|
||||
@@ -990,6 +1000,7 @@ impl PdfCraftApp {
|
||||
bookmark_rename: None,
|
||||
last_opened_url: None,
|
||||
pending_link: None,
|
||||
pending_remote_link: None,
|
||||
protect_draft: Default::default(),
|
||||
boxes_draft: Default::default(),
|
||||
marks_draft: Default::default(),
|
||||
@@ -1560,7 +1571,11 @@ impl PdfCraftApp {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn open_remote_link(&mut self, source: DocId, file: String, destination: pdfcraft_render::RemoteDestination) {
|
||||
let source_path = self.session.get(source).and_then(|doc| doc.path.clone());
|
||||
if self.pending_remote_link.is_some() || self.pending_link.is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(source_doc) = self.session.get(source) else { return };
|
||||
let source_path = source_doc.path.clone();
|
||||
let path = match pdfcraft_engine::resolve_remote_document_path(source_path.as_deref().map(std::path::Path::new), &file) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
@@ -1568,6 +1583,26 @@ impl PdfCraftApp {
|
||||
return;
|
||||
}
|
||||
};
|
||||
if pdfcraft_engine::remote_document_path_is_absolute(&file) {
|
||||
self.pending_remote_link = Some(PendingRemoteLink { source, path, destination });
|
||||
return;
|
||||
}
|
||||
self.open_resolved_remote_link(path, destination);
|
||||
}
|
||||
|
||||
/// Take a pending request exactly once. Closing its source invalidates the request.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn resolve_pending_remote_link(&mut self, open: bool) {
|
||||
if let Some(pending) = self.pending_remote_link.take()
|
||||
&& open
|
||||
&& self.session.get(pending.source).is_some()
|
||||
{
|
||||
self.open_resolved_remote_link(pending.path, pending.destination);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn open_resolved_remote_link(&mut self, path: std::path::PathBuf, destination: pdfcraft_render::RemoteDestination) {
|
||||
let path = path.to_string_lossy().into_owned();
|
||||
let name = std::path::Path::new(&path).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| path.clone());
|
||||
let stamp = file_watch::Stamp::at(&path);
|
||||
@@ -1593,6 +1628,11 @@ impl PdfCraftApp {
|
||||
self.notify_tr("Local cross-PDF links require the desktop app");
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn resolve_pending_remote_link(&mut self, _open: bool) {
|
||||
self.pending_remote_link = None;
|
||||
}
|
||||
|
||||
fn apply_remote_destination(&mut self, destination: pdfcraft_render::RemoteDestination) {
|
||||
let Some(index) = self.views.len().checked_sub(1) else { return };
|
||||
let Some(doc) = self.session.get(self.views[index].id) else { return };
|
||||
@@ -1723,6 +1763,7 @@ impl PdfCraftApp {
|
||||
|| self.password_prompt.is_some()
|
||||
|| self.unlock_prompt.is_some()
|
||||
|| self.pending_link.is_some()
|
||||
|| self.pending_remote_link.is_some()
|
||||
|| self.updates.open
|
||||
}
|
||||
|
||||
@@ -1750,7 +1791,7 @@ impl PdfCraftApp {
|
||||
pub fn request_document_url(&mut self, url: &str, origin: LinkOrigin) {
|
||||
match pdfcraft_engine::links::document_url(url) {
|
||||
Ok(url) => {
|
||||
if self.pending_link.is_none() {
|
||||
if self.pending_link.is_none() && self.pending_remote_link.is_none() {
|
||||
self.pending_link = Some(PendingLink { url, origin });
|
||||
}
|
||||
}
|
||||
@@ -2725,6 +2766,7 @@ impl eframe::App for PdfCraftApp {
|
||||
&& self.dialog.is_none()
|
||||
&& self.close_request.is_none()
|
||||
&& self.pending_link.is_none()
|
||||
&& self.pending_remote_link.is_none()
|
||||
{
|
||||
let startup = self.startup_inbox.lock().map(|mut q| std::mem::take(&mut *q)).unwrap_or_default();
|
||||
for (name, bytes) in startup {
|
||||
|
||||
@@ -391,6 +391,154 @@ fn a_numeric_host_is_shown_as_the_address_it_goes_to() {
|
||||
assert!(h.query_by_label_contains("alphabet").is_none());
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn an_absolute_gotor_waits_for_consent_and_keeps_its_destination_across_tabs() {
|
||||
let _gpu = gpu();
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-ui-gotor-consent-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let target = dir.join("target with spaces.pdf");
|
||||
let file = target.to_str().unwrap();
|
||||
let mut h = remote_harness(None, file, "[1 /FitH 200]");
|
||||
let source = h.state().views[0].id;
|
||||
click_remote_link(&mut h);
|
||||
let pending = h.state().pending_remote_link.as_ref().unwrap();
|
||||
assert_eq!(pending.source, source);
|
||||
assert_eq!(pending.path, target);
|
||||
h.get_by_label(file);
|
||||
h.get_by_label("Allow");
|
||||
assert_eq!(h.state().views.len(), 1);
|
||||
assert_eq!(h.state().session.docs().len(), 1);
|
||||
assert!(h.state().toast.is_none(), "the nonexistent target is not accessed before consent");
|
||||
assert!(h.state().password_prompt.is_none());
|
||||
// The target only appears AFTER the confirmation is displayed.
|
||||
std::fs::write(&target, linked_pages(2, None)).unwrap();
|
||||
h.state_mut().open_bytes("other.pdf", None, fixture()).unwrap();
|
||||
assert_eq!(h.state().active, Some(1));
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Allow").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().pending_remote_link.is_none());
|
||||
assert_eq!(h.state().views.len(), 3);
|
||||
assert_eq!(h.state().views[2].current, 1);
|
||||
assert_eq!(h.state().views[2].fit, pdfcraft_ui_egui::canvas::Fit::Width);
|
||||
assert_eq!(h.state().active, Some(2));
|
||||
assert!(!h.state().session.get(source).unwrap().dirty);
|
||||
assert_eq!(h.state().last_opened_url, None);
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn cancelling_an_absolute_gotor_by_button_escape_or_dismissal_changes_nothing() {
|
||||
let _gpu = gpu();
|
||||
let target = std::env::temp_dir().join(format!("pdfcraft-ui-gotor-cancel-{}.pdf", std::process::id()));
|
||||
for answer in ["button", "escape", "outside"] {
|
||||
let mut h = remote_harness(None, target.to_str().unwrap(), "[1 /Fit]");
|
||||
let source = h.state().views[0].id;
|
||||
let before = h.state().session.get(source).unwrap().bytes.clone();
|
||||
click_remote_link(&mut h);
|
||||
assert!(h.state().pending_remote_link.is_some());
|
||||
match answer {
|
||||
"button" => h.get_by_label("Cancel").click(),
|
||||
"escape" => h.key_press(egui::Key::Escape),
|
||||
_ => click_at(&mut h, egui::pos2(5.0, 5.0)),
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert!(h.state().pending_remote_link.is_none(), "{answer}");
|
||||
assert_eq!(h.state().views.len(), 1);
|
||||
assert_eq!(h.state().session.docs().len(), 1);
|
||||
assert_eq!(h.state().active, Some(0));
|
||||
assert_eq!(h.state().views[0].current, 0);
|
||||
let doc = h.state().session.get(source).unwrap();
|
||||
assert_eq!(doc.bytes, before);
|
||||
assert!(!doc.dirty);
|
||||
assert!(doc.can_undo().is_none());
|
||||
assert!(h.state().toast.is_none());
|
||||
assert!(h.state().password_prompt.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn consenting_to_absolute_gotor_still_validates_the_target() {
|
||||
let _gpu = gpu();
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-ui-gotor-consent-invalid-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("folder.pdf")).unwrap();
|
||||
std::fs::write(dir.join("notes.txt"), b"text").unwrap();
|
||||
std::fs::write(dir.join("broken.pdf"), b"not a PDF").unwrap();
|
||||
#[cfg_attr(not(unix), allow(unused_mut))]
|
||||
let mut cases = vec!["missing.pdf", "folder.pdf", "notes.txt", "broken.pdf"];
|
||||
#[cfg(unix)]
|
||||
{
|
||||
std::os::unix::fs::symlink("/dev/zero", dir.join("zero.pdf")).unwrap();
|
||||
cases.push("zero.pdf");
|
||||
std::fs::File::create(dir.join("large.pdf")).unwrap().set_len(pdfcraft_engine::MAX_REMOTE_DOCUMENT_BYTES + 1).unwrap();
|
||||
cases.push("large.pdf");
|
||||
}
|
||||
for name in cases {
|
||||
let target = dir.join(name);
|
||||
let mut h = remote_harness(None, target.to_str().unwrap(), "[0 /Fit]");
|
||||
click_remote_link(&mut h);
|
||||
assert!(h.state().pending_remote_link.is_some());
|
||||
assert!(h.state().toast.is_none(), "{name} isn't checked before consent");
|
||||
h.get_by_label("Allow").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().pending_remote_link.is_none());
|
||||
assert_eq!(h.state().views.len(), 1, "{name}");
|
||||
assert!(h.state().toast.is_some(), "{name} is refused after consent");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn an_approved_absolute_gotor_preserves_password_retries_and_cancellation() {
|
||||
let _gpu = gpu();
|
||||
let dir = std::env::temp_dir().join(format!("pdfcraft-ui-gotor-consent-password-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let target = dir.join("encrypted.pdf");
|
||||
std::fs::write(&target, linked_pages(2, Some("test-password"))).unwrap();
|
||||
for cancel in [false, true] {
|
||||
let mut h = remote_harness(None, target.to_str().unwrap(), "[1 /FitH 200]");
|
||||
click_remote_link(&mut h);
|
||||
assert!(h.state().password_prompt.is_none());
|
||||
h.get_by_label("Allow").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().password_prompt.is_some());
|
||||
h.state_mut().submit_password(Some("wrong-password".into()));
|
||||
assert!(h.state().password_prompt.as_ref().unwrap().error.is_some());
|
||||
assert_eq!(h.state().views.len(), 1);
|
||||
if cancel {
|
||||
h.state_mut().submit_password(None);
|
||||
assert_eq!(h.state().views.len(), 1);
|
||||
} else {
|
||||
h.state_mut().submit_password(Some("test-password".into()));
|
||||
assert_eq!(h.state().views.len(), 2);
|
||||
assert_eq!(h.state().views[1].current, 1);
|
||||
}
|
||||
assert!(h.state().pending_remote_link.is_none());
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn closing_the_source_invalidates_pending_absolute_gotor_consent() {
|
||||
let _gpu = gpu();
|
||||
let target = std::env::temp_dir().join(format!("pdfcraft-ui-gotor-closed-source-{}.pdf", std::process::id()));
|
||||
let mut h = remote_harness(None, target.to_str().unwrap(), "[0 /Fit]");
|
||||
click_remote_link(&mut h);
|
||||
h.state_mut().close_tab(0);
|
||||
h.state_mut().resolve_pending_remote_link(true);
|
||||
assert!(h.state().views.is_empty());
|
||||
assert!(h.state().session.docs().is_empty());
|
||||
assert!(h.state().pending_remote_link.is_none());
|
||||
assert!(h.state().toast.is_none());
|
||||
}
|
||||
|
||||
fn overlapping_annotation_fixture(subtype: &str, internal: bool) -> Vec<u8> {
|
||||
overlapping_annotation_fixture_with_form(subtype, internal, false)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PdfCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M3 #1364 candidate: consent before absolute local GoToR access; estimates unchanged); earlier: minor (M1 #1339: unchanged incremental saves of a classic cross-reference table write a zero-length subsection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M5 #1398: stamps read upright on rotated pages; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (gap 6: #1317, #1327, #1302, #1315, #1340; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M7 #1316: replacing a line keeps a neighbouring line whose glyph box overlaps; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -84,6 +85,8 @@ total matches the parity estimate.
|
||||
|---|---|---|---|---|---|
|
||||
| M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates |
|
||||
| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. A custom text match that ends on a reconstructed separator stops before the next glyph (#1315). Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); Lab Bradford adaptation uses LMS white-point ratios, covered by independent neutral/chromatic and direct/Indexed render oracles; inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). |
|
||||
@@ -152,6 +155,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | M3/#1364 candidate: absolute local GoToR links require desktop consent or headless allow_absolute; existing relative navigation and validation retained. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M7: single-glyph RTL ActualText ligatures join their word in Edit text. Regressions cover marks, two-byte codes, replacement and save/reopen; multi-glyph logical spans retain their order. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M5 #1398: rubber stamps, explicit image rectangles and PDF-page stamps read upright on rotated pages. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M12: text-only auto-tag Apply writes real structure and parent-tree links, with undo and Tags-panel review; mixed content remains unsupported. Estimates unchanged. |
|
||||
|
||||
@@ -1166,8 +1166,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = ["link_open"]
|
||||
evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination", "crates/ui-egui/tests/view.rs::link_destinations_scroll_to_their_position", "crates/render/src/inspect.rs::bookmarks_and_links_keep_their_destination_view", "crates/render/src/inspect.rs::links_read_bounded_local_remote_go_to_actions", "crates/automation/tests/automation.rs::local_gotor_link_open_applies_destinations_and_honors_root", "crates/ui-egui/tests/document_links.rs::a_local_gotor_link_opens_in_a_tab_and_applies_its_destination"]
|
||||
notes = "GoTo and URI links plus local-PDF GoToR links: source-relative or local absolute /F paths, /UF preferred in file-spec dictionaries, explicit numeric page/view destinations, and internal tabs. link_open honors --root. Named remote destinations open at default view with a warning; /NewWindow is a tab hint. URL/network file systems, GoToE, Launch and other actions are not executed. Same-document destinations retain their supported /XYZ, /Fit, /FitH, /FitV and /FitR views; /FitB* is read as whole-page fit."
|
||||
evidence = ["crates/ui-egui/tests/view.rs::clicking_a_link_goes_to_its_destination", "crates/ui-egui/tests/view.rs::link_destinations_scroll_to_their_position", "crates/render/src/inspect.rs::bookmarks_and_links_keep_their_destination_view", "crates/render/src/inspect.rs::links_read_bounded_local_remote_go_to_actions", "crates/automation/tests/automation.rs::local_gotor_link_open_applies_destinations_and_honors_root", "crates/ui-egui/tests/document_links.rs::a_local_gotor_link_opens_in_a_tab_and_applies_its_destination", "crates/ui-egui/tests/document_links.rs::an_absolute_gotor_waits_for_consent_and_keeps_its_destination_across_tabs", "crates/ui-egui/tests/document_links.rs::cancelling_an_absolute_gotor_by_button_escape_or_dismissal_changes_nothing", "crates/automation/tests/automation.rs::absolute_gotor_consent_does_not_bypass_validation_or_passwords"]
|
||||
notes = "GoTo and URI links plus local-PDF GoToR links: source-relative or local absolute /F paths, /UF preferred in file-spec dictionaries, explicit numeric page/view destinations, and internal tabs. Absolute local GoToR targets require desktop Allow/Cancel confirmation or an explicit allow_absolute opt-in in link_open; relative links open normally. link_open honors --root. Named remote destinations open at default view with a warning; /NewWindow is a tab hint. URL/network file systems, GoToE, Launch and other actions are not executed. Same-document destinations retain their supported /XYZ, /Fit, /FitH, /FitV and /FitR views; /FitB* is read as whole-page fit."
|
||||
|
||||
[[feature]]
|
||||
id = "view.annotation-hover"
|
||||
|
||||
Reference in new issue
Block a user