mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 03:58:45 -06:00
Merge PR #3411: Honor single-page redaction selections
This commit is contained in:
@@ -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())),
|
||||
|
||||
@@ -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)\"]." },
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user