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:
AlexisandBrandon Thomas authored and GitHub committed 2026-10-11 11:56:06 -04:00
1 parent 4666e74691
commit b752827162
22 files changed
+122 -26

No files matched your search

+11 -11
View File
@@ -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"
+1
View File
@@ -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.
+6 -1
View File
@@ -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 }
}
+3 -1
View 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"],
+34
View File
@@ -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");
+6 -1
View File
@@ -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 {
+20
View File
@@ -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();
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+1
View File
@@ -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.
+26 -9
View File
@@ -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:?}"),
}
}
}
+1
View File
@@ -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
View File
@@ -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 |
+2 -2
View File
@@ -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"