From b7528271627ec658710a140adb8d10b213ebe30b Mon Sep 17 00:00:00 2001 From: Alexis Date: Sun, 11 Oct 2026 17:56:06 +0200 Subject: [PATCH] M7: watermark offset from the page centre (edit.watermark-appearance) (#1076) Add Watermark / Update Watermark can now move the mark from the page centre by a horizontal and a vertical offset in points (right and up). - edit: add_watermark refuses a non-finite offset instead of writing it into the content stream; tests for the placement and the refusal. - automation: doc_watermark takes offset_x and offset_y (clamped to +-20,000 points); a test saves, reopens and measures the text moving. - ui-egui: an "Offset from centre" row with Horizontal and Vertical fields; the preview follows it for text and for picture watermarks. - i18n: "Offset from centre" in the catalogs that have a coverage test (ar, bg, de, fr, hu, it, ja, pt-br, ru, uk, zh-hans); hashes updated in ATTRIBUTION.toml. - parity and roadmap: edit.watermark-appearance stays partial (no offset from the page edge, no print/screen-only variants, no repeat); image and PDF watermarks no longer listed as missing; progress log line. Co-authored-by: Brandon Thomas --- ATTRIBUTION.toml | 22 ++++++++--------- ROADMAP.md | 1 + crates/automation/src/lib.rs | 7 +++++- crates/automation/src/tools.rs | 4 ++- crates/automation/tests/automation.rs | 34 ++++++++++++++++++++++++++ crates/edit/src/lib.rs | 7 +++++- crates/edit/src/tests.rs | 20 +++++++++++++++ crates/ui-egui/src/i18n/ar.tsv | 1 + crates/ui-egui/src/i18n/bg.tsv | 1 + crates/ui-egui/src/i18n/de.tsv | 1 + crates/ui-egui/src/i18n/fr.tsv | 1 + crates/ui-egui/src/i18n/hu.tsv | 1 + crates/ui-egui/src/i18n/it.tsv | 1 + crates/ui-egui/src/i18n/ja.tsv | 1 + crates/ui-egui/src/i18n/pt-br.tsv | 1 + crates/ui-egui/src/i18n/ru.tsv | 1 + crates/ui-egui/src/i18n/uk.tsv | 1 + crates/ui-egui/src/i18n/zh-hans.tsv | 1 + crates/ui-egui/src/marks_ui.rs | 35 ++++++++++++++++++++------- crates/ui-egui/tests/marks.rs | 1 + docs/roadmap.md | 2 +- parity/acrobat-features.toml | 4 +-- 22 files changed, 122 insertions(+), 26 deletions(-) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index cfa2e4340..180e80f43 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -19,7 +19,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 = "2c0f7bc1c225a95d2322e5f92e42ca3b07652f54976d754e9901abeddc4d0dc6" +sha256 = "b98e2e4858e635dbc6930f2ff80e7e8a109d27abe996ac29c27b3b13b70e43f2" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" @@ -52,7 +52,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 = "fe8fc8af29190950048c3e3d5108ede6cf2da2f85ed7003fb2035ddb517a600c" +sha256 = "96b0200a9b1f16cb23fb9482a8189b3fd763f31469009c54e59cb0aa1dd5d7b3" [[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 = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6d99c6a85e69b5448609ae54847700c908919be27d904bd0c73bf2c220a2da4e" +sha256 = "9b4eb2a8230e856a2fd2abd672fc954eaa02164dae7be6a4299451ecb7e732bd" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" @@ -85,7 +85,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 = "09f654d07a87878802e87fc0436f9a5426bc27bfb2156aee912463c5dc8a04ea" +sha256 = "b08667b24b7bb8801e656516064125a29e47f2a88caadda4a81c362d680ed457" [[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 = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "8a7a2bfba1a2f9858ad62a74bd8229a1615a5bc6e51023b11b9768e7ea21d79c" +sha256 = "a574d0ebee8184f917354377257d32312f711f4457e855b9d697d6af15ecc886" [[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 = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "dc0af15b819b9059bc23a73a1e794d1ae5291594b2ff0fae2595274fccc9c281" +sha256 = "d4d9a78c19215360e64d1fcf201415546cabc5f8937888f3f5c1616106b94838" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" @@ -140,7 +140,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 = "16c6e6cf857b70f7ade009bc9c87fceb3831f5f3281ac7fabc0fd7bfd2c5d28e" +sha256 = "f5549263f28e22911e2dc8612d8f793112729cecb8d7d8ab6d0faaf3acd4c0b0" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" @@ -151,7 +151,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 = "9e8c0929c5839b2a5f5c93a2d3457bcce24b118a11fd6b37476aa3584fa5ce76" +sha256 = "c4c77b4451a0505bfe7aacea5a9ef3701a10e48060dcb98558716b28592d72a1" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" @@ -162,7 +162,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 = "47720091895fc1a4c1ff332a660192fe4a9666f32002e5899030b3ca3421f337" +sha256 = "28d98970e3b7c11b1468f2ceec182979160681ca2719ec47246a1655ab452054" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" @@ -173,7 +173,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 = "fa2f00414a8f064f4d6e47d57702a100743c8b64b05dcbe93585330427c83f16" +sha256 = "3b732bcfe593c4619909deab926a8aa30826e5effd957866315c7969338eca94" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" @@ -184,7 +184,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 = "271a9b34b0d13d1734de53d88a09b49d589eb556c75fe04dd1fa5172151c5b1a" +sha256 = "263567299cee16b9c2577b6de92e5d0f3bbb82ef334beebbd130c27407db40e2" [[asset]] path = "assets/icons/accessibility.svg" diff --git a/ROADMAP.md b/ROADMAP.md index 9e528c620..c4f216b65 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md Newest first: the date, what moved, and the new overall estimate. +- **2026-10-11 (M7, watermark placement):** A watermark can be moved from the page centre by a horizontal and a vertical offset in points: Add/Update Watermark has an "Offset from centre" row (the preview follows it, for text and for pictures) and `doc_watermark` takes `offset_x` and `offset_y`. A non-finite offset is refused instead of written into the page. `edit.watermark-appearance` stays partial: no offset from the page edge or in page units, no print/screen-only variants, no repeat. Image and PDF watermarks, already shipped, no longer appear as missing. The overall estimate is unchanged. - **2026-10-10 (M14, interface size):** Preferences ▸ Interface size scales the whole interface (50–250 %, setting `ui_scale`, control option `ui-scale`); 50 % brings an interface the system draws at 200 % back to 100 % (#456). Auto, the default, follows the desktop's text scaling: on Linux and the BSDs the desktop app reads GNOME's `text-scaling-factor` once at launch, so with Large Text (1.25) on a 1× display the interface no longer draws 13 px labels that show their pixels next to the system's text. The size is egui's zoom factor, so `pixels_per_point` grows with it and page rasters stay one texel per screen pixel; egui's own ⌘+/⌘− interface zoom is turned off so those keys stay page zoom. Still missing: KDE's font DPI and Windows' "Make text bigger" aren't read (both scale only text there), and egui's text is still greyscale-antialiased without the system's gamma, so small text looks rougher than GTK's at 100 %. - **2026-10-10 (M14, #496):** Shortcut displays share a UI formatter across toolbar search, registry menus, command palette, Help and tooltips. Modifier and key names use a dedicated catalog context in every bundled translation; missing entries keep the English key name rather than a command translation. German shows Strg/Umschalt/Entf, macOS retains its symbols, and runtime language changes refresh the display. Regression coverage checks localized layout widths, English/German switching, platform conventions and unchanged command execution. Milestone and effort estimates unchanged. diff --git a/crates/automation/src/lib.rs b/crates/automation/src/lib.rs index eec1f4360..5a7020ec6 100644 --- a/crates/automation/src/lib.rs +++ b/crates/automation/src/lib.rs @@ -76,6 +76,8 @@ fn failed(e: impl std::fmt::Display) -> ToolError { /// Default and maximum resolution for `page_render`. const DEFAULT_DPI: f64 = 96.0; const MAX_DPI: f64 = 600.0; +/// How far a watermark may be moved from the page centre, in points (a page is at most 14,400). +const WATERMARK_MAX_OFFSET: f64 = 20_000.0; /// Bookmarks in one page of the bookmark list, by default and at most. const BOOKMARK_PAGE: usize = 100; @@ -960,7 +962,10 @@ impl Automation { opacity: a.opt_num("opacity")?.unwrap_or(d.opacity), rotation: a.opt_num("rotation")?.unwrap_or(d.rotation), behind: a.opt_bool("behind")?.unwrap_or(false), - offset: [0.0; 2], + offset: [ + a.opt_num("offset_x")?.unwrap_or(0.0).clamp(-WATERMARK_MAX_OFFSET, WATERMARK_MAX_OFFSET), + a.opt_num("offset_y")?.unwrap_or(0.0).clamp(-WATERMARK_MAX_OFFSET, WATERMARK_MAX_OFFSET), + ], }; Edit::AddWatermark { pages, settings: wm, replace, file } } diff --git a/crates/automation/src/tools.rs b/crates/automation/src/tools.rs index f5771d68c..6c2e03bb0 100644 --- a/crates/automation/src/tools.rs +++ b/crates/automation/src/tools.rs @@ -764,7 +764,7 @@ pub fn tools() -> Vec { }), &["doc"], )), - t("doc_watermark", "Add watermark", "Add a watermark to pages: text, or a picture from `file` (an image, or page `file_page` of a PDF) at `scale` of the page (default 0.5); rotated (degrees counter-clockwise, default 45), semi-transparent (opacity 0–1, default 0.5), text fitted to the page unless font_size is given, on top unless behind: true. replace: true swaps out existing ones. Undoable.") + t("doc_watermark", "Add watermark", "Add a watermark to pages: text, or a picture from `file` (an image, or page `file_page` of a PDF) at `scale` of the page (default 0.5); rotated (degrees counter-clockwise, default 45), semi-transparent (opacity 0–1, default 0.5), text fitted to the page unless font_size is given, on top unless behind: true; moved from the page centre by `offset_x` (right) and `offset_y` (up) points (default 0). replace: true swaps out existing ones. Undoable.") .cmd("edit.watermark") .with(schema( json!({ @@ -779,6 +779,8 @@ pub fn tools() -> Vec { "opacity": { "type": "number", "minimum": 0, "maximum": 1 }, "rotation": { "type": "number" }, "behind": { "type": "boolean" }, + "offset_x": { "type": "number", "description": "Points right of the page centre (negative: left)." }, + "offset_y": { "type": "number", "description": "Points above the page centre (negative: below)." }, "replace": { "type": "boolean" }, }), &["doc"], diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index 26bc2b26f..ed15f1d0f 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -1396,6 +1396,40 @@ fn headers_watermarks_and_backgrounds_through_tools() { ok(&mut a, "doc_save", json!({ "doc": doc, "path": "marked.pdf" })); } +#[test] +fn watermarks_move_from_the_page_centre_through_tools() { + let dir = workdir("wm-offset"); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + // The top-left corner of "DRAFT" on page 1, in points (y grows downwards). + // Saved and reopened, so the search reads the file as another program would. + let mut saved = 0; + let mut corner = |a: &mut Automation| -> (f64, f64) { + saved += 1; + let path = format!("moved{saved}.pdf"); + ok(a, "doc_save", json!({ "doc": doc, "path": path })); + let copy = ok(a, "doc_open", json!({ "path": path }))["doc"].as_u64().unwrap(); + let found = ok(a, "text_find", json!({ "doc": copy, "query": "DRAFT" })); + let rect = + found["matches"][0]["rects"][0].as_array().unwrap_or_else(|| panic!("DRAFT is on the page: {found}; text: {:?}", page_text(a, copy))); + (rect[0].as_f64().unwrap(), rect[1].as_f64().unwrap()) + }; + // Both sit above the fixture's "Page 1" (at the page centre), or their letters would mix. + ok(&mut a, "doc_watermark", json!({ "doc": doc, "pages": [1], "text": "DRAFT", "rotation": 0, "font_size": 20, "offset_y": 60 })); + let centred = corner(&mut a); + ok( + &mut a, + "doc_watermark", + json!({ "doc": doc, "pages": [1], "text": "DRAFT", "rotation": 0, "font_size": 20, "offset_x": 30, "offset_y": 100, "replace": true }), + ); + let moved = corner(&mut a); + assert!((moved.0 - centred.0 - 30.0).abs() < 1.0, "right: {centred:?} → {moved:?}"); + assert!((centred.1 - moved.1 - 40.0).abs() < 1.0, "up is towards the top: {centred:?} → {moved:?}"); + // Huge offsets are clamped, and a value that isn't a number is refused. + ok(&mut a, "doc_watermark", json!({ "doc": doc, "pages": [1], "text": "DRAFT", "offset_y": 1e300, "replace": true })); + assert!(matches!(a.call("doc_watermark", &json!({ "doc": doc, "text": "X", "offset_x": "left" })), Err(ToolError::InvalidArgs(_)))); +} + #[test] fn exporting_images_and_text_through_tools() { let dir = workdir("export"); diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 467f5dd2b..48e83cf4d 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -491,7 +491,12 @@ pub fn add_watermark(doc: &mut Document, pages: &[usize], wm: &Watermark, replac if wm.source.is_none() { drawable(text.lines())?; } - if !(wm.opacity.is_finite() && wm.rotation.is_finite() && wm.font_size.is_finite() && wm.font_size >= 0.0) { + if !(wm.opacity.is_finite() + && wm.rotation.is_finite() + && wm.font_size.is_finite() + && wm.font_size >= 0.0 + && wm.offset.iter().all(|o| o.is_finite())) + { return Err(EditError::Invalid("invalid watermark settings".into())); } if replace { diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index d9f7f5481..f6b5ab7c9 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -150,6 +150,26 @@ fn watermarks_rotate_fade_and_can_go_behind() { assert_eq!(s.len(), 2, "no wrapper needed for content behind"); } +#[test] +fn watermark_offset_moves_the_centre_right_and_up() { + let mut doc = fixture(); + let wm = Watermark { text: "DRAFT".into(), rotation: 0.0, offset: [-100.0, 50.0], ..Watermark::default() }; + add_watermark(&mut doc, &[0], &wm, false).unwrap(); + let s = streams(&doc, 0); + let mark = s.iter().find(|c| c.contains("/PCMark /Watermark")).expect("the watermark stream"); + assert!(mark.contains("1 0 0 1 200 450 cm"), "{mark}"); +} + +#[test] +fn a_watermark_offset_that_is_not_finite_is_refused() { + for offset in [[f64::NAN, 0.0], [0.0, f64::INFINITY], [f64::NEG_INFINITY, 0.0]] { + let mut doc = fixture(); + let wm = Watermark { text: "DRAFT".into(), offset, ..Watermark::default() }; + assert!(matches!(add_watermark(&mut doc, &[0], &wm, false), Err(EditError::Invalid(_))), "{offset:?}"); + assert!(!doc.is_modified()); + } +} + #[test] fn invalid_requests_change_nothing() { let mut doc = fixture(); diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 77e350e0d..09b9b9fb6 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -785,6 +785,7 @@ Rotation التدوير Appear behind page الظهور خلف الصفحة Appear on top of page الظهور فوق الصفحة + Offset from centre الإزاحة عن المركز From colour من لون Page Range Options خيارات نطاق الصفحات Preview: page {p} of {n} معاينة: صفحة {p} من {n} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index c777b2824..b6a899ec7 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -470,6 +470,7 @@ Rotation Завъртане Appear behind page Под съдържанието на страницата Appear on top of page Над съдържанието на страницата + Offset from centre Отместване от центъра From colour От цвят Page Range Options Опции за диапазон на страниците Preview: page {p} of {n} Преглед: страница {p} от {n} diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index db14c5097..2ec96c3c8 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -758,6 +758,7 @@ Rotation Drehung Appear behind page Hinter der Seite anzeigen Appear on top of page Vor der Seite anzeigen + Offset from centre Abstand von der Mitte From colour Aus Farbe Page Range Options Optionen für Seitenbereich Preview: page {p} of {n} Vorschau: Seite {p} von {n} diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 937bed594..fc5c95a93 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -758,6 +758,7 @@ Rotation Rotation Appear behind page Afficher derrière la page Appear on top of page Afficher au-dessus de la page + Offset from centre Décalage par rapport au centre From colour À partir d'une couleur Page Range Options Options d'intervalle de pages Preview: page {p} of {n} Aperçu : page {p} sur {n} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 3abb9ba19..38faaeb33 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -755,6 +755,7 @@ Rotation Elforgatás Appear behind page Az oldal mögött Appear on top of page Az oldal előtt + Offset from centre Eltolás a középponttól From colour Színből Page Range Options Oldaltartomány beállításai Preview: page {p} of {n} Előnézet: {p}. oldal / {n} diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index d718ff512..d0b56fd92 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -781,6 +781,7 @@ Rotation Rotazione Appear behind page Dietro la pagina Appear on top of page Sopra la pagina + Offset from centre Scostamento dal centro From colour Da colore Page Range Options Opzioni intervallo di pagine Preview: page {p} of {n} Anteprima: pagina {p} di {n} diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 36dbcaa25..924fac2da 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -785,6 +785,7 @@ Rotation 回転 Appear behind page ページの背面に表示 Appear on top of page ページの前面に表示 + Offset from centre 中心からのオフセット From colour 色を指定 Page Range Options ページ範囲のオプション Preview: page {p} of {n} プレビュー: {n} ページ中 {p} ページ diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 4004bd9d1..2f400784a 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -423,6 +423,7 @@ Rotation Rotação Appear behind page Exibir atrás da página Appear on top of page Exibir sobre a página + Offset from centre Deslocamento a partir do centro From colour A partir da cor Page Range Options Opções de intervalo de páginas Preview: page {p} of {n} Pré-visualização: página {p} de {n} diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 431958f30..d173b3e3f 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -779,6 +779,7 @@ Rotation Поворот Appear behind page Позади страницы Appear on top of page Поверх страницы + Offset from centre Смещение от центра From colour Из цвета Page Range Options Параметры диапазона страниц Preview: page {p} of {n} Предпросмотр: стр. {p} из {n} diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5c2d8f87a..bd59a47b6 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -783,6 +783,7 @@ Rotation Поворот Appear behind page Позаду сторінки Appear on top of page Поверх сторінки + Offset from centre Зсув від центру From colour Із кольору Page Range Options Параметри діапазону сторінок Preview: page {p} of {n} Попередній перегляд: сторінка {p} із {n} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 6645b973b..3477455f3 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -477,6 +477,7 @@ Rotation 旋转 Appear behind page 显示在页面后面 Appear on top of page 显示在页面上层 + Offset from centre 与中心的偏移 From colour 使用颜色 Page Range Options 页面范围选项 Preview: page {p} of {n} 预览:第 {p} 页,共 {n} 页 diff --git a/crates/ui-egui/src/marks_ui.rs b/crates/ui-egui/src/marks_ui.rs index c92ae1f61..36cf40381 100644 --- a/crates/ui-egui/src/marks_ui.rs +++ b/crates/ui-egui/src/marks_ui.rs @@ -8,6 +8,9 @@ use pdfcraft_engine::{Background, Edit, HeaderFooter, MarkKind, Watermark}; use crate::theme::{self, Tokens}; use crate::{PdfCraftApp, widgets}; +/// How far a watermark may be moved from the page centre, in points. +const MAX_OFFSET: f64 = 20_000.0; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Subset { All, @@ -269,6 +272,13 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M ui.radio_value(&mut d.wm.behind, true, tl!("Appear behind page")); ui.radio_value(&mut d.wm.behind, false, tl!("Appear on top of page")); }); + ui.horizontal(|ui| { + ui.label(tl!("Offset from centre")); + ui.label(tl!("Horizontal")); + ui.add(egui::DragValue::new(&mut d.wm.offset[0]).range(-MAX_OFFSET..=MAX_OFFSET).suffix(" pt")); + ui.label(tl!("Vertical")); + ui.add(egui::DragValue::new(&mut d.wm.offset[1]).range(-MAX_OFFSET..=MAX_OFFSET).suffix(" pt")); + }); }); }); } @@ -318,19 +328,14 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M } MarkKind::Watermark if file_name.is_some() => { let side = r.width().min(r.height()) * wm.scale as f32; + let centre = r.center() + vec2(wm.offset[0] as f32 * s, -wm.offset[1] as f32 * s); p.rect_stroke( - Rect::from_center_size(r.center(), vec2(side, side)), + Rect::from_center_size(centre, vec2(side, side)), CornerRadius::ZERO, egui::Stroke::new(1.0, Color32::GRAY), egui::StrokeKind::Inside, ); - p.text( - r.center(), - egui::Align2::CENTER_CENTER, - file_name.clone().unwrap_or_default(), - egui::FontId::proportional(10.0), - Color32::GRAY, - ); + p.text(centre, egui::Align2::CENTER_CENTER, file_name.clone().unwrap_or_default(), egui::FontId::proportional(10.0), Color32::GRAY); } MarkKind::Watermark => { let text = if wm.text.trim().is_empty() { "CONFIDENTIAL" } else { wm.text.trim() }; @@ -344,7 +349,8 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M let half = galley.size() / 2.0; let (sn, cs) = angle.sin_cos(); let offset = vec2(half.x * cs - half.y * sn, half.x * sn + half.y * cs); - p.add(egui::epaint::TextShape::new(r.center() - offset, galley, Color32::BLACK).with_angle(angle)); + let centre = r.center() + vec2(wm.offset[0] as f32 * s, -wm.offset[1] as f32 * s); + p.add(egui::epaint::TextShape::new(centre - offset, galley, Color32::BLACK).with_angle(angle)); } MarkKind::Background if file_name.is_some() => { p.rect_stroke(r.shrink(6.0), CornerRadius::ZERO, egui::Stroke::new(1.0, Color32::GRAY), egui::StrokeKind::Inside); @@ -481,4 +487,15 @@ mod tests { let r = PageRange { all: true, subset: Subset::Odd, ..PageRange::default() }; assert_eq!(r.pages(5), [0, 2, 4]); } + + #[test] + fn the_watermark_edit_carries_the_offset() { + let mut d = MarksDraft::default(); + d.wm.text = "DRAFT".into(); + d.wm.offset = [-72.0, 36.0]; + match edit(&d, MarkKind::Watermark, 3) { + Edit::AddWatermark { settings, .. } => assert_eq!(settings.offset, [-72.0, 36.0]), + other => panic!("expected AddWatermark, got {other:?}"), + } + } } diff --git a/crates/ui-egui/tests/marks.rs b/crates/ui-egui/tests/marks.rs index 0a216ba5f..285442f97 100644 --- a/crates/ui-egui/tests/marks.rs +++ b/crates/ui-egui/tests/marks.rs @@ -64,6 +64,7 @@ fn watermark_and_background_dialogs() { h.state_mut().execute("edit.watermark"); h.run_steps(2); assert!(h.query_by(|n| n.label().as_deref() == Some("OK") && n.is_disabled()).is_some(), "OK waits for text"); + h.get_by_label("Offset from centre"); h.state_mut().marks_draft.wm.text = "DRAFT".into(); h.run_steps(2); h.get_by_label("OK").click(); diff --git a/docs/roadmap.md b/docs/roadmap.md index aed24a3db..bdef89a38 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -71,7 +71,7 @@ total matches the parity estimate. | M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:
- command registry (menus, shortcuts and palette all use it);
- undo/redo; incremental, atomic and encrypted saves;
- autosave and crash recovery;
- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;
- CLI `edit/combine/extract/split`.
Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web | | M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export; flatten; Fill & Sign; agent tools.
Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page | | M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done.
Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA | -| 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 | +| 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), watermark print/screen visibility and repeat | | 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.
Done since: redaction codes (U.S. FOIA and Privacy Act sets as overlay text, in Redaction properties and `redact_mark`). 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.
Done since: macOS Keychain and Windows Current User Personal store signing (software-backed CNG keys), certificate viewer, signed documents protected from rewrites. 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).
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; Create PDF from multiple files (PDFs, images and text, edited as pages in the grid). Missing: OCR languages beyond Latin, editable-text OCR output, Office export/import, web printing | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e15ed9e36..097f123ad 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -2430,8 +2430,8 @@ milestone = "M7" status = "partial" commands = ["edit.watermark"] tools = ["doc_watermark"] -evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind"] -notes = "rotation, opacity, behind/on top; no position offsets, scale or print/screen-only variants in the UI yet" +evidence = ["crates/edit/src/tests.rs::watermarks_rotate_fade_and_can_go_behind", "crates/edit/src/tests.rs::watermark_offset_moves_the_centre_right_and_up", "crates/edit/src/tests.rs::a_watermark_offset_that_is_not_finite_is_refused", "crates/automation/tests/automation.rs::watermarks_move_from_the_page_centre_through_tools", "crates/ui-egui/src/marks_ui.rs::the_watermark_edit_carries_the_offset"] +notes = "rotation, opacity, behind/on top, and a horizontal/vertical offset from the page centre in points (dialog and offset_x/offset_y); no offset from the page edge or in page units, and no print/screen-only variants yet" [[feature]] id = "edit.background-add"