Merge PR #3411: Honor single-page redaction selections

This commit is contained in:
aiongg
2026-10-22 01:44:46 +00:00
3 changed files with 46 additions and 6 deletions
+13 -3
View File
@@ -14,9 +14,19 @@ impl Automation {
pub(crate) fn redact_mark(&mut self, a: &Args) -> Result<Value> {
let doc = self.doc(a)?;
let (id, n) = (doc.id, doc.info.pages.len());
let pages: Vec<usize> = match a.opt_ints("pages")? {
Some(_) => self.pages(a, "pages")?,
None => (0..n).collect(),
if a.get("page").is_some() && a.get("pages").is_some() {
return Err(ToolError::InvalidArgs("pass page or pages, not both".into()));
}
if a.get("rect").is_some() && a.get("pages").is_some() {
return Err(ToolError::InvalidArgs("rect requires page, not pages".into()));
}
let pages: Vec<usize> = if a.get("page").is_some() {
vec![self.page(a)?]
} else {
match a.opt_ints("pages")? {
Some(_) => self.pages(a, "pages")?,
None => (0..n).collect(),
}
};
let overlay = match (a.opt_str("overlay")?, a.opt_str("code_set")?) {
(Some(_), Some(_)) => return Err(ToolError::InvalidArgs("pass overlay or code_set with codes, not both".into())),
+3 -3
View File
@@ -427,12 +427,12 @@ pub fn tools() -> Vec<ToolDef> {
t(
"redact_mark",
"Mark for redaction",
"Mark content for redaction (nothing is removed until redact_apply). One of: rect [x0, y0, x1, y1] (points from the top-left of the displayed page) with page; find (text, every match); words (a list of words or phrases, every match of each; the result's matched_words says how many marks each made); pattern (phone, email, credit-card, ssn, date: every match, Acrobat's Search & Redact patterns); whole_pages: true. find and words accept case_sensitive and whole_words (both default false). find, words, pattern and whole_pages work on pages (default all). overlay: text shown on the box once applied, or code_set (foia: U.S. FOIA (b)(1)(A)…(b)(9); privacy-act: U.S. Privacy Act (d)(5), (j)(1)…(k)(7)) with codes, shown as \"(b)(6), (b)(7)(C)\"; fill: box colour (default black). Undoable.",
"Mark content for redaction (nothing is removed until redact_apply). One of: rect [x0, y0, x1, y1] (points from the top-left of the displayed page) with page; find (text, every match); words (a list of words or phrases, every match of each; the result's matched_words says how many marks each made); pattern (phone, email, credit-card, ssn, date: every match, Acrobat's Search & Redact patterns); whole_pages: true. find and words accept case_sensitive and whole_words (both default false). Select one page with page or multiple with pages (default all); do not pass both. rect requires page. overlay: text shown on the box once applied, or code_set (foia: U.S. FOIA (b)(1)(A)…(b)(9); privacy-act: U.S. Privacy Act (d)(5), (j)(1)…(k)(7)) with codes, shown as \"(b)(6), (b)(7)(C)\"; fill: box colour (default black). Undoable.",
)
.with(schema(
json!({
"doc": doc(),
"page": { "type": "integer", "minimum": 1 },
"page": { "type": "integer", "minimum": 1, "description": "Single page to mark; mutually exclusive with pages." },
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4 },
"find": { "type": "string", "minLength": 1 },
"words": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "maxItems": 1000 },
@@ -440,7 +440,7 @@ pub fn tools() -> Vec<ToolDef> {
"whole_words": { "type": "boolean", "description": "Match whole words for find or words (default false)." },
"pattern": { "type": "string", "enum": ["phone", "email", "credit-card", "ssn", "date"] },
"whole_pages": { "type": "boolean" },
"pages": pages("to search or mark (default: all)"),
"pages": pages("to search or mark (default: all); mutually exclusive with page, and not valid with rect"),
"overlay": { "type": "string" },
"code_set": { "type": "string", "enum": ["foia", "privacy-act"] },
"codes": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "Redaction codes from code_set, e.g. [\"(b)(6)\"]." },
+30
View File
@@ -3391,6 +3391,36 @@ fn rotating_a_field_through_tools() {
assert_eq!((rot, back), (0, before));
}
#[test]
fn redact_mark_respects_single_page_and_rejects_conflicting_selectors() {
let dir = workdir("redact-single-page");
std::fs::write(dir.join("memo.txt"), "Private 123-45-6789").unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_create", json!({ "from": "text", "path": "memo.txt" }))["doc"].as_u64().unwrap();
ok(&mut a, "page_duplicate", json!({ "doc": doc, "pages": [1] }));
for selector in [json!({ "find": "Private" }), json!({ "words": ["Private"] }), json!({ "pattern": "ssn" }), json!({ "whole_pages": true })] {
let mut args = selector;
args["doc"] = json!(doc);
args["page"] = json!(1);
let result = ok(&mut a, "redact_mark", args);
assert_eq!(result["marked"], 1);
ok(&mut a, "redact_clear", json!({ "doc": doc }));
}
for args in [
json!({ "doc": doc, "find": "Private", "page": 1, "pages": [2] }),
json!({ "doc": doc, "rect": [1, 1, 10, 10], "pages": [2] }),
json!({ "doc": doc, "find": "Private", "page": 3 }),
] {
assert!(matches!(a.call("redact_mark", &args), Err(ToolError::InvalidArgs(_))));
}
let result = ok(&mut a, "redact_mark", json!({ "doc": doc, "find": "Private", "page": 1 }));
assert_eq!(result["marks_pending"], 1, "invalid selections must not add marks");
ok(&mut a, "redact_apply", json!({ "doc": doc }));
let text = page_text(&mut a, doc);
assert!(!text[0].contains("Private"));
assert!(text[1].contains("Private"));
}
#[test]
fn redacting_through_tools() {
let dir = workdir("redact");