mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
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 <[email protected]>
This commit is contained in:
1 parent
4666e74691
commit
b752827162
22 files changed
+122
-26
No files matched your search
+11
-11
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -764,7 +764,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
}),
|
||||
&["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<ToolDef> {
|
||||
"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"],
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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} ページ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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} 页
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ total matches the parity estimate.
|
||||
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>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.<br>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.<br>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.<br>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.<br>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).<br>Done since: embedded/72/custom DPI choices for image imports; export all images; OCR (searchable image for pages, ranges and multiple files); single-sided cut-and-stack imposition with cut marks, through Print and doc_print; 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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user