M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text (#741)

* M8: redaction code sets (U.S. FOIA, Privacy Act) as overlay text

Redaction properties offers "Custom text" or "Redaction code" under "Use overlay
text". Picking the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and
ticking codes makes new marks carry them as overlay text, in the set's order.
redact_mark takes code_set and codes and rejects unknown sets or codes with the
allowed list.

Also merges a duplicated evidence/notes pair in form.field-list that made
parity/acrobat-features.toml fail to parse on main.

* M8: say why the default redaction code set can't fail

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* Tests: pin the clock in form_bulk (saves a second apart differed in /ModDate on Windows CI)

---------

Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-10 06:27:26 -04:00
1 parent e742e29771
commit bfa65a9bfc
27 files changed
+254 -35

No files matched your search

+13 -13
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 = "abb6981d86d4df565c72f2741b931052462c6689eb5415b888991f40af1adcb7"
sha256 = "b130884fb43c0b69122b649e94726213782beff5a88c7efbc9b83f21d41f6800"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "9bf3f80e72839e983b9ffb9bc3b44db4e29ebdaf2933852a8a600582b851a3fe"
sha256 = "d00c946c83bf590dc23971b77f94f51017223cb7587055f83715490a1d152e66"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.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 = "e7bf0d59c35cc0f65139a7fe08e7bb213e79820611e7992392d4af2457759253"
sha256 = "6e59e0df51801b317b1e707cb1eab54287ff3c6afdb660eab31683ddfde86342"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "f0c6fcf6fe391c4e7784b64acfb0c91d53ffbe18d9e7708ad663e2afd29a0760"
sha256 = "cd27ba57af6b8016a30b29ff8c3fdb39805192711162517e6e0d608ac18dcec6"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.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 = "6f6fd18c652127a3e5dbc3a0a7c2d24e2cb47ec79ec58ac4170632e37f1e7b1c"
sha256 = "f24ffb1d5b239575ba605592da4502f24d755fae26126107d791d1e9e5d6eea7"
[[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 = "180b624986752af06b173a00899ca38d6ba4965b68ba7d3b8a926cf7e5b942de"
sha256 = "23b96e2bd6d12f0f338638c8a9ca487f89de796f2c153341d28cb04c5b600262"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "5d4fe0c8f680a0ecf72f8d23d4f075e3944f71e2c7594aac8a8e59c6cda29043"
sha256 = "e0062d55c47461bae2e9ce201ce125080736534592e2028eb71831696237979d"
[[asset]]
path = "crates/ui-egui/src/i18n/it.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 = "7b7edbe92edea1809710c9b89df05a5618fc1cf581b907521351b06eccd5cf3a"
sha256 = "8292677569d108bf6c0aec18a2843865980481036de12bda599e4b085ab67b66"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -118,7 +118,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 = "362b3c8014cda1fe8c682b75c96bc5f630e8648cb05a431eb1448714cb8bc435"
sha256 = "948acd6dcd5e64686ae3d573656bd689e089085da8d0ee19dcaebb5a0678756f"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -129,7 +129,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 = "b3fe50274b3117535bb35e935737206417322deb280a643f59855974931b1d9c"
sha256 = "4f612f1314c29534e389fd576af1c30f3ef85509598e6545dbd187ec0a4e5efe"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -140,7 +140,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 = "002a2ba35999da21d8c2ceb383c38f0bdf0aa43f2fe60015835d2f03bac8d576"
sha256 = "3791ef5ee3070a46474495ce1a6ed773518a1e874263413a6885bb2de13aed15"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -151,7 +151,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 = "b268c077b44635ca346d4a2372bef6eea8c5c891e82df549228e68bf4ec4baa5"
sha256 = "07fc4ec18275cb70fc66052005566b8c180fc07d6f73f61b2dc11d1ceefc95d4"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -162,7 +162,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 = "7c043ba0237c3cb7cb0107066722dee309c3265ac8c930d3eadb3a9ac9d4d345"
sha256 = "775a93bd50a66af7a94c8fb35c4592934d278a3d0c24f37d93f38056922f5143"
[[asset]]
path = "assets/icons/accessibility.svg"
+3 -2
View File
@@ -57,7 +57,7 @@ Read this before choosing work. The feature table above counts what exists; this
| M Accessibility | 62% | Checker (all 32 rules). Missing: autotag, Tags/Order/Content panels, Reading Order tool, keyboard-only operation |
| B View | 55% | Shell, find, panels, tiles, web build. **Rendering is borrowed (`hayro`)**, so the 7 rendering P0s are only partial |
| D Organize | 55% | Pages, combine, split, bookmarks, labels. Missing: replace pages, transitions |
| G Protect | 54% | Passwords, permissions, redaction, sanitize. Missing: certificate security, redaction codes |
| G Protect | 54% | Passwords, permissions, redaction, sanitize. Redaction codes (built-in FOIA and Privacy Act sets). Missing: certificate security, custom redaction code sets |
| H Sign | 48% | PAdES B-B signing and validation; macOS Keychain and Windows CNG certificate store signing. Missing: timestamps (B-T), LTV (DSS/OCSP/CRL), FieldMDP, smart cards, PKCS #11 |
| C Edit | 46% | Added text and images stay editable; header/footer/watermark. Missing: robust editing of existing text and images (fonts, subsets, reflow) |
| L Print | 36% | Acrobat-style sizing, n-up, booklet, CUPS. Missing: Windows and web printing, production options |
@@ -102,7 +102,7 @@ Hours are for a single agent (low–high). "Done" is the estimated fraction of t
| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 15–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 | 45% | 95–185 | 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 | 17% | 210–420 | 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 |
| M8 | Security + redaction | 100–180 | 66% | 35–65 | 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. Missing: certificate security, redaction codes and pattern locales, DCT re-encoding |
| M8 | Security + redaction | 100–180 | 66% | 35–65 | 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 | 55% | 70–125 | 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% | 130–235 | 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, CUPS spooler, 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, Windows/web printing |
| M11 | Optimize, preflight, PDF/A/X/UA, print production | 200–350 | 18% | 165–290 | Done: new `optimize` crate: Reduce File Size and the PDF Optimizer (images measured where drawn, bicubic downsampling, JPEG/ZIP recompression only when smaller, discard objects and user data, Flate clean-up, resource merging, object streams). Missing: fonts and transparency panels, space audit, preflight, PDF/A/X/UA |
@@ -132,6 +132,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run
## Log
- **2026-10-10 (M14, side panel width):** The side panel keeps the width it is dragged to. The Bookmarks search row was 2 px wider than the panel, and a resizable egui panel keeps the width its content used, so every repaint (any pointer move) widened it up to 520 px. The Comments reply row did the same in languages with a longer "Post" (Russian, Spanish, …); Post now sits under a full-width reply field. Tests hold both panels' widths steady across 30 repaints. Overall estimate unchanged (about 30–35%).
- **2026-10-10 (M8):** Redaction codes. Redaction properties offers "Custom text" or "Redaction code" under "Use overlay text": pick the U.S. FOIA (5 U.S.C. 552(b)) or U.S. Privacy Act (552a) set and tick codes, and new marks carry them as overlay text in the set's order ("(b)(6), (b)(7)(C)"). `redact_mark` takes `code_set` and `codes`, rejecting unknown sets and codes with the allowed list. Tests cover the code sets, the tool end to end (mark, apply, text) and the dialog; the new label is translated in all 13 UI catalogs. `protect.redaction-codes` is partial: Edit Codes, custom sets and their import/export are still missing. Estimates unchanged.
- **2026-10-10 (M6, #95):** Prepare a form edits shared General, Appearance and applicable Options properties in one atomic undo step. Explicit opt-in controls keep mixed values unchanged; partial appearance patches preserve custom font resources, each widget's style, indirect dictionary contents and unknown keys. Ctrl/Command+A selects the current page; rectangle selection and the Fields panel support multi-selection, with panel double-click opening properties. CLI/MCP `form_set_props` accepts a bounded, unique `fields` list. Regression tests cover save/reopen, undo/redo, locked-field rollback, stale dialogs, invalid arguments and selection. Names, positions, choices/defaults and script tabs remain individual GUI edits. Integrated upstream 0.5.0 and completed Portuguese/Italian catalogs without losing translator credit; shared-property labels now cover all 14 bundled translations. Kept the new Italian interface and Fill & Sign date-language registries aligned, including accented date output; the About-dialog regression waits for the larger contributor list to settle before clicking. Milestone and overall effort estimates unchanged (about 30–35%).
- **2026-10-09 (M5, line-ending labels):** Comment properties names each line ending in words ("Reverse open arrow", "Butt", …) and those labels are translated. The PDF `/LE` token is unchanged. Overall estimate unchanged (about 30–35%).
+20 -2
View File
@@ -1,7 +1,7 @@
//! Redaction tools: mark areas, text, patterns or whole pages for redaction, apply the marks
//! (removing what they cover for good) or clear them.
use pdfcraft_engine::{Edit, Hidden, NewAnnotation, RedactPattern, Shape, Style, find_pattern, rect_quad};
use pdfcraft_engine::{Edit, Hidden, NewAnnotation, REDACTION_CODE_SETS, RedactPattern, RedactionCodeSet, Shape, Style, find_pattern, rect_quad};
use serde_json::{Value, json};
use crate::comments::{DEFAULT_AUTHOR, parse_color};
@@ -15,7 +15,25 @@ impl Automation {
Some(_) => self.pages(a, "pages")?,
None => (0..n).collect(),
};
let overlay = a.opt_str("overlay")?.unwrap_or("").to_string();
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())),
(Some(o), None) => o.to_string(),
(None, Some(id)) => {
let set = RedactionCodeSet::from_id(id).ok_or_else(|| {
let ids: Vec<_> = REDACTION_CODE_SETS.iter().map(|s| s.id).collect();
ToolError::InvalidArgs(format!("unknown code_set {id:?}; allowed: {}", ids.join(", ")))
})?;
let picked = a.strs("codes")?;
let allowed = || format!("allowed in {}: {}", set.id, set.codes.join(", "));
match set.overlay(&picked) {
Ok(o) if !o.is_empty() => o,
Ok(_) => return Err(ToolError::InvalidArgs(format!("codes is empty; {}", allowed()))),
Err(bad) => return Err(ToolError::InvalidArgs(format!("unknown code {bad:?}; {}", allowed()))),
}
}
(None, None) if a.get("codes").is_some() => return Err(ToolError::InvalidArgs("codes needs code_set".into())),
(None, None) => String::new(),
};
let author = a.opt_str("author")?.unwrap_or(DEFAULT_AUTHOR).to_string();
let mut style = Style::default_for(&Shape::Redact { quads: Vec::new(), overlay: String::new(), look: Default::default() });
if let Some(c) = a.opt_str("fill")? {
+3 -1
View File
@@ -352,7 +352,7 @@ 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); pattern (phone, email, credit-card, ssn, date: every match, Acrobat's Search & Redact patterns); whole_pages: true. find, pattern and whole_pages work on pages (default all). overlay: text shown on the box once applied; 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); pattern (phone, email, credit-card, ssn, date: every match, Acrobat's Search & Redact patterns); whole_pages: true. find, 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.",
)
.with(schema(
json!({
@@ -364,6 +364,8 @@ pub fn tools() -> Vec<ToolDef> {
"whole_pages": { "type": "boolean" },
"pages": pages("to search or mark (default: all)"),
"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)\"]." },
"fill": { "type": "string", "description": "#RRGGBB or a colour name." },
"author": { "type": "string" },
}),
+27
View File
@@ -1946,6 +1946,33 @@ fn redacting_through_tools() {
assert!(matches!(a.call("redact_mark", &json!({ "doc": doc })), Err(ToolError::InvalidArgs(_))));
}
#[test]
fn redacting_with_codes_through_tools() {
let dir = workdir("redact-codes");
std::fs::write(dir.join("memo.txt"), "Informant Jane Roe met the agent\nPublic line").unwrap();
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_create", json!({ "from": "text", "path": "memo.txt" }))["doc"].as_u64().unwrap();
let bad = |a: &mut Automation, args: Value| matches!(a.call("redact_mark", &args), Err(ToolError::InvalidArgs(_)));
assert!(bad(&mut a, json!({ "doc": doc, "find": "Jane Roe", "code_set": "gdpr", "codes": ["(b)(6)"] })));
assert!(bad(&mut a, json!({ "doc": doc, "find": "Jane Roe", "code_set": "foia", "codes": ["(k)(1)"] })));
assert!(bad(&mut a, json!({ "doc": doc, "find": "Jane Roe", "code_set": "foia", "codes": [] })));
assert!(bad(&mut a, json!({ "doc": doc, "find": "Jane Roe", "codes": ["(b)(6)"] })));
assert!(bad(&mut a, json!({ "doc": doc, "find": "Jane Roe", "overlay": "X", "code_set": "foia", "codes": ["(b)(6)"] })));
let Err(ToolError::InvalidArgs(msg)) =
a.call("redact_mark", &json!({ "doc": doc, "find": "Jane Roe", "code_set": "foia", "codes": ["(b)(10)"] }))
else {
panic!("an unknown code is rejected")
};
assert!(msg.contains("(b)(7)(C)"), "the error lists the allowed codes: {msg}");
assert_eq!(
ok(&mut a, "redact_mark", json!({ "doc": doc, "find": "Jane Roe", "code_set": "foia", "codes": ["(b)(7)(C)", "(b)(6)"] }))["marked"],
1
);
ok(&mut a, "redact_apply", json!({ "doc": doc }));
let text = page_text(&mut a, doc)[0].clone();
assert!(!text.contains("Jane") && text.contains("(b)(6), (b)(7)(C)"), "{text}");
}
#[test]
fn removing_hidden_information_through_tools() {
let dir = workdir("hidden");
+2 -1
View File
@@ -41,7 +41,8 @@ fn setup(test: &str) -> (Automation, u64, std::path::PathBuf) {
.unwrap();
}
std::fs::write(dir.join("form.pdf"), s.save_bytes(id).unwrap().as_slice()).unwrap();
let mut a = Automation::new().with_root(&dir).unwrap();
// Saves stamp /ModDate from the clock: pin it, or two saves a second apart differ.
let mut a = Automation::new().with_root(&dir).unwrap().with_clock(|| 1_700_000_000);
let id = tool(&mut a, "doc_open", json!({"path": "form.pdf"}))["doc"].as_u64().unwrap();
(a, id, dir)
}
+1
View File
@@ -103,6 +103,7 @@ pub use pdfcraft_forms::detect;
pub use pdfcraft_optimize as optimize;
pub use pdfcraft_preflight as pdfa;
pub use pdfcraft_print as print;
pub use pdfcraft_redact::codes::{CODE_SETS as REDACTION_CODE_SETS, CodeSet as RedactionCodeSet};
pub use pdfcraft_redact::patterns::{PATTERNS as REDACT_PATTERNS, Pattern as RedactPattern, find as find_pattern};
pub use pdfcraft_redact::sanitize::{HIDDEN, Hidden};
pub use pdfcraft_sign as sign;
+4
View File
@@ -49,5 +49,9 @@ and `/PieceInfo` private data. Both make the next save a full rewrite.
`patterns` finds phone numbers, e-mail addresses, credit card numbers (Luhn-checked), US Social
Security numbers and dates for Search & Redact.
`codes` holds the redaction code sets (U.S. FOIA and U.S. Privacy Act exemptions):
`CodeSet::from_id("foia")?.overlay(&["(b)(6)"])` gives the overlay text for the picked codes.
Custom code sets aren't supported yet.
Not yet: pattern locales, struct-tree/alt-text cleanup, metadata and hidden-information
sanitising (M8.4), re-encoding of DCT images (they are removed instead).
+56
View File
@@ -0,0 +1,56 @@
//! Redaction code sets (Acrobat's Redaction Properties ▸ Redaction Code): the exemption codes
//! that justify a redaction, used as its overlay text. The built-in sets are the exemptions of
//! the U.S. Freedom of Information Act (5 U.S.C. 552(b)) and the U.S. Privacy Act
//! (5 U.S.C. 552a(d), (j) and (k)).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CodeSet {
pub id: &'static str,
pub name: &'static str,
pub codes: &'static [&'static str],
}
pub const CODE_SETS: [CodeSet; 2] = [
CodeSet {
id: "foia",
name: "U.S. FOIA",
codes: &[
"(b)(1)(A)",
"(b)(1)(B)",
"(b)(2)",
"(b)(3)(A)",
"(b)(3)(B)",
"(b)(4)",
"(b)(5)",
"(b)(6)",
"(b)(7)(A)",
"(b)(7)(B)",
"(b)(7)(C)",
"(b)(7)(D)",
"(b)(7)(E)",
"(b)(7)(F)",
"(b)(8)",
"(b)(9)",
],
},
CodeSet {
id: "privacy-act",
name: "U.S. Privacy Act",
codes: &["(d)(5)", "(j)(1)", "(j)(2)", "(k)(1)", "(k)(2)", "(k)(3)", "(k)(4)", "(k)(5)", "(k)(6)", "(k)(7)"],
},
];
impl CodeSet {
pub fn from_id(id: &str) -> Option<Self> {
CODE_SETS.into_iter().find(|s| s.id == id)
}
/// The overlay text for the picked codes: in the set's order, once each, joined by ", ".
/// Fails with the first picked code that isn't in the set.
pub fn overlay<'a>(&self, picked: &[&'a str]) -> Result<String, &'a str> {
if let Some(bad) = picked.iter().find(|p| !self.codes.contains(p)) {
return Err(bad);
}
Ok(self.codes.iter().filter(|c| picked.contains(c)).copied().collect::<Vec<_>>().join(", "))
}
}
+1
View File
@@ -16,6 +16,7 @@
use pdfcraft_cos::{Dict, Document, ObjRef, Object, Stream};
pub mod codes;
mod image;
mod interp;
pub mod patterns;
+13
View File
@@ -430,6 +430,19 @@ fn overlay_text_takes_its_font_size_colour_alignment_and_repeats() {
assert!(fonts.contains(b"PCCour") && !fonts.contains(b"PCTimes"));
}
#[test]
fn redaction_codes_join_in_set_order_and_reject_strangers() {
use crate::codes::{CODE_SETS, CodeSet};
let foia = CodeSet::from_id("foia").unwrap();
assert_eq!(foia.overlay(&["(b)(6)", "(b)(1)(A)", "(b)(6)"]), Ok("(b)(1)(A), (b)(6)".into()));
assert_eq!(foia.overlay(&[]), Ok(String::new()));
assert_eq!(foia.overlay(&["(b)(6)", "(k)(1)"]), Err("(k)(1)"));
let privacy = CodeSet::from_id("privacy-act").unwrap();
assert_eq!(privacy.overlay(&["(k)(7)", "(d)(5)"]), Ok("(d)(5), (k)(7)".into()));
assert_eq!(CodeSet::from_id("gdpr"), None);
assert!(CODE_SETS.iter().all(|s| !s.codes.is_empty() && s.codes.iter().all(|c| !c.is_empty())));
}
#[test]
fn tags_lose_what_redaction_removed() {
let content = b"/P <</MCID 0>> BDC BT /F1 10 Tf 10 200 Td (SECRET) Tj ET EMC /P <</MCID 1>> BDC BT /F1 10 Tf 10 100 Td (Public) Tj ET EMC";
+1
View File
@@ -791,6 +791,7 @@
Redacted area fill colour: Цвят на запълване на заличената област:
Use overlay text Използвай текст върху заличаването
Custom text: Потребителски текст:
Redaction code: Код за заличаване:
Font: Шрифт:
Font size: Размер на шрифта:
Auto-size text to fit redaction region Автоматично оразмеряване на текста според областта на заличаване
Can't render this file because it contains an unexpected character in line 8 and column 16.
+1
View File
@@ -1079,6 +1079,7 @@
Redacted area fill colour: Füllfarbe geschwärzter Bereiche:
Use overlay text Überlagerungstext verwenden
Custom text: Eigener Text:
Redaction code: Schwärzungscode:
Font: Schrift:
Font size: Schriftgröße:
Auto-size text to fit redaction region Textgröße automatisch an Schwärzungsbereich anpassen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+1
View File
@@ -1092,6 +1092,7 @@
Redacted area fill colour: Color de relleno del área censurada:
Use overlay text Usar texto superpuesto
Custom text: Texto personalizado:
Redaction code: Código de censura:
Font: Fuente:
Font size: Tamaño de fuente:
Auto-size text to fit redaction region Ajustar automáticamente el tamaño del texto a la zona censurada
Can't render this file because it contains an unexpected character in line 6 and column 60.
+1
View File
@@ -1078,6 +1078,7 @@
Redacted area fill colour: Couleur de remplissage de la zone biffée :
Use overlay text Utiliser un texte en superposition
Custom text: Texte personnalisé :
Redaction code: Code de biffure :
Font: Police :
Font size: Corps :
Auto-size text to fit redaction region Ajuster automatiquement le corps du texte à la zone de biffure
Can't render this file because it contains an unexpected character in line 6 and column 39.
+1
View File
@@ -1068,6 +1068,7 @@
Redacted area fill colour: Kitakart terület kitöltőszíne:
Use overlay text Fedőszöveg használata
Custom text: Saját szöveg:
Redaction code: Kitakarási kód:
Font: Betűtípus:
Font size: Betűméret:
Auto-size text to fit redaction region A szöveg automatikus méretezése a kitakarási területhez
Can't render this file because it contains an unexpected character in line 7 and column 44.
+1
View File
@@ -1094,6 +1094,7 @@
Redacted area fill colour: Colore di riempimento dell'area oscurata:
Use overlay text Usa testo sovrapposto
Custom text: Testo personalizzato:
Redaction code: Codice di oscuramento:
Font: Carattere:
Font size: Dimensione carattere:
Auto-size text to fit redaction region Adatta automaticamente il testo all'area oscurata
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -1098,6 +1098,7 @@
Redacted area fill colour: 墨消し領域の塗りつぶし色:
Use overlay text オーバーレイテキストを使用
Custom text: カスタムテキスト:
Redaction code: 墨消しコード:
Font: フォント:
Font size: フォントサイズ:
Auto-size text to fit redaction region 墨消し領域に合わせて文字サイズを自動調整
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -735,6 +735,7 @@
Redacted area fill colour: Cor de preenchimento da área ocultada:
Use overlay text Usar texto de sobreposição
Custom text: Texto personalizado:
Redaction code: Código de ocultação:
Font: Fonte:
Font size: Tamanho da fonte:
Auto-size text to fit redaction region Ajustar tamanho do texto automaticamente à região de ocultação
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1099,6 +1099,7 @@
Redacted area fill colour: Цвет заливки скрытой области:
Use overlay text Использовать текст поверх
Custom text: Свой текст:
Redaction code: Код сокрытия:
Font: Шрифт:
Font size: Размер шрифта:
Auto-size text to fit redaction region Автоподбор размера текста по области сокрытия
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -1073,6 +1073,7 @@ comment menu Edit సవరించు
Redacted area fill colour: రెడాక్ట్ చేసిన ప్రాంతం నింపు రంగు:
Use overlay text ఓవర్‌లే టెక్స్ట్‌ను ఉపయోగించు
Custom text: అనుకూల టెక్స్ట్:
Redaction code: రెడాక్షన్ కోడ్:
Font: ఫాంట్:
Font size: ఫాంట్ పరిమాణం:
Auto-size text to fit redaction region రెడాక్షన్ ప్రాంతానికి సరిపోయేలా టెక్స్ట్ పరిమాణాన్ని స్వయంచాలకంగా మార్చు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+1
View File
@@ -1096,6 +1096,7 @@
Redacted area fill colour: Колір заливки вилученої області:
Use overlay text Використовувати накладений текст
Custom text: Власний текст:
Redaction code: Код вилучення:
Font: Шрифт:
Font size: Розмір шрифту:
Auto-size text to fit redaction region Автоматично добирати розмір тексту до області вилучення
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1
View File
@@ -798,6 +798,7 @@
Redacted area fill colour: 密文区域填充颜色:
Use overlay text 使用覆盖文本
Custom text: 自定义文本:
Redaction code: 密文代码:
Font: 字体:
Font size: 字号:
Auto-size text to fit redaction region 自动调整文本大小以适应密文区域
Can't render this file because it contains an unexpected character in line 9 and column 16.
+1
View File
@@ -747,6 +747,7 @@
Redacted area fill colour: 遮蔽內容區域填充顏色:
Use overlay text 使用覆蓋文字
Custom text: 自定義文字:
Redaction code: 遮蔽代碼:
Font: 字型:
Font size: 字號:
Auto-size text to fit redaction region 自動調整文字大小以適應遮蔽內容區域
Can't render this file because it contains an unexpected character in line 9 and column 16.
+63 -6
View File
@@ -4,7 +4,7 @@
//! confirmation, as Acrobat asks) or clear them.
use egui::{Color32, CornerRadius, Pos2, Rect, Stroke};
use pdfcraft_engine::{Edit, NewAnnotation, REDACT_PATTERNS, RedactPattern, Rgb, Shape, Style, rect_quad};
use pdfcraft_engine::{Edit, NewAnnotation, REDACT_PATTERNS, REDACTION_CODE_SETS, RedactPattern, RedactionCodeSet, Rgb, Shape, Style, rect_quad};
use pdfcraft_render::DocInfo;
use crate::canvas::{DocView, PageXform};
@@ -20,19 +20,41 @@ pub struct RedactPrefs {
pub fill: Option<Rgb>,
pub use_overlay: bool,
pub overlay: String,
/// The overlay text is the picked redaction codes instead of `overlay`.
pub use_code: bool,
pub code_set: RedactionCodeSet,
pub codes: Vec<&'static str>,
/// Overlay text font, size (0 = auto), colour, alignment and repetition.
pub look: pdfcraft_engine::OverlayLook,
}
impl Default for RedactPrefs {
fn default() -> Self {
Self { fill: Some([0.0, 0.0, 0.0]), use_overlay: false, overlay: String::new(), look: Default::default() }
Self {
fill: Some([0.0, 0.0, 0.0]),
use_overlay: false,
overlay: String::new(),
use_code: false,
// Infallible: a non-empty constant array.
code_set: REDACTION_CODE_SETS[0],
codes: Vec::new(),
look: Default::default(),
}
}
}
impl RedactPrefs {
pub fn overlay_text(&self) -> String {
match (self.use_overlay, self.use_code) {
(false, _) => String::new(),
(true, false) => self.overlay.clone(),
// `codes` only ever holds codes of `code_set`, so this can't fail.
(true, true) => self.code_set.overlay(&self.codes).unwrap_or_default(),
}
}
pub fn mark(&self, page: usize, quads: Vec<[f64; 8]>, author: &str) -> Edit {
let shape = Shape::Redact { quads, overlay: if self.use_overlay { self.overlay.clone() } else { String::new() }, look: self.look };
let shape = Shape::Redact { quads, overlay: self.overlay_text(), look: self.look };
let mut style = Style::default_for(&shape);
style.fill = self.fill;
Edit::AddAnnotation(NewAnnotation { page, shape, style, contents: String::new(), author: author.to_string() })
@@ -275,10 +297,45 @@ pub(crate) fn props_body(ui: &mut egui::Ui, d: &mut RedactPrefs, _t: &Tokens) ->
ui.label("");
ui.checkbox(&mut d.use_overlay, tl!("Use overlay text"));
ui.end_row();
let l = ui.label(tl!("Custom text:"));
ui.add_enabled(d.use_overlay, egui::TextEdit::singleline(&mut d.overlay).desired_width(220.0)).labelled_by(l.id);
ui.end_row();
let on = d.use_overlay;
let l = ui.add_enabled(on, egui::RadioButton::new(!d.use_code, tl!("Custom text:")));
if l.clicked() {
d.use_code = false;
}
ui.add_enabled(on && !d.use_code, egui::TextEdit::singleline(&mut d.overlay).desired_width(220.0)).labelled_by(l.id);
ui.end_row();
if ui.add_enabled(on, egui::RadioButton::new(d.use_code, tl!("Redaction code:"))).clicked() {
d.use_code = true;
}
let codes_on = on && d.use_code;
ui.add_enabled_ui(codes_on, |ui| {
egui::ComboBox::from_id_salt("redaction-code-set").selected_text(d.code_set.name).show_ui(ui, |ui| {
for set in REDACTION_CODE_SETS {
if ui.selectable_label(d.code_set == set, set.name).clicked() && d.code_set != set {
d.code_set = set;
d.codes.clear();
}
}
});
});
ui.end_row();
for row in d.code_set.codes.chunks(4) {
ui.label("");
ui.add_enabled_ui(codes_on, |ui| {
ui.horizontal(|ui| {
for &code in row {
let mut picked = d.codes.contains(&code);
if ui.add_sized([62.0, 18.0], egui::Checkbox::new(&mut picked, code)).changed() {
d.codes.retain(|c| *c != code);
if picked {
d.codes.push(code);
}
}
}
});
});
ui.end_row();
}
let look = &mut d.look;
ui.label(tl!("Font:"));
ui.add_enabled_ui(on, |ui| {
+27
View File
@@ -80,6 +80,33 @@ fn marking_applying_and_clearing() {
assert!(doc.form.is_empty(), "the whole page was redacted, fields included");
}
#[test]
fn redaction_codes_become_the_overlay_text() {
let mut h = harness();
assert!(h.state_mut().execute("redact.properties"));
h.run_steps(2);
h.get_by_label("Redaction Tool Properties");
h.get_by_label("Use overlay text").click();
h.run_steps(2);
h.state_mut().redact_prefs.overlay = "CONFIDENTIAL".into();
h.get_by_label("Redaction code:").click();
h.run_steps(2);
h.get_by_label("(b)(7)(C)").click();
h.run_steps(2);
h.get_by_label("(b)(6)").click();
h.run_steps(2);
h.get_by_label("OK").click();
h.run_steps(2);
let prefs = &h.state().redact_prefs;
assert_eq!(prefs.overlay_text(), "(b)(6), (b)(7)(C)", "codes in the set's order");
let pdfcraft_engine::Edit::AddAnnotation(a) = prefs.mark(0, vec![pdfcraft_engine::rect_quad([10.0, 10.0, 50.0, 30.0])], "T") else {
panic!("a mark adds an annotation")
};
assert!(matches!(a.shape, pdfcraft_engine::Shape::Redact { ref overlay, .. } if overlay == "(b)(6), (b)(7)(C)"));
h.state_mut().redact_prefs.use_code = false;
assert_eq!(h.state().redact_prefs.overlay_text(), "CONFIDENTIAL", "custom text is kept while codes are used");
}
#[test]
fn removing_hidden_information_and_sanitizing() {
let mut h = harness();
+8 -10
View File
@@ -4302,10 +4302,8 @@ milestone = "M3"
status = "shipped"
commands = ["form.fields"]
tools = ["doc_info"]
evidence = ["crates/ui-egui/tests/view.rs::field_list_jumps_to_a_fields_page", "crates/render/src/inspect.rs::fields_listed_only_on_the_pages_are_listed"]
notes = "read-only list with jump-to-field; filling and editing are separate features (M6); fields a file lists only as page widgets (empty /Fields) are adopted too"
evidence = ["crates/ui-egui/tests/view.rs::field_list_jumps_to_a_fields_page", "crates/ui-egui/tests/bulk_fields.rs::fields_panel_selects_multiple_fields_and_opens_their_shared_properties", "crates/ui-egui/tests/bulk_fields.rs::double_clicking_a_field_in_the_panel_opens_its_individual_properties"]
notes = "list with jump-to-field; in Prepare a form, Shift/Ctrl/Command-click selects several fields and Properties edits their shared properties; double-click opens field properties"
evidence = ["crates/ui-egui/tests/view.rs::field_list_jumps_to_a_fields_page", "crates/render/src/inspect.rs::fields_listed_only_on_the_pages_are_listed", "crates/ui-egui/tests/bulk_fields.rs::fields_panel_selects_multiple_fields_and_opens_their_shared_properties", "crates/ui-egui/tests/bulk_fields.rs::double_clicking_a_field_in_the_panel_opens_its_individual_properties"]
notes = "list with jump-to-field; in Prepare a form, Shift/Ctrl/Command-click selects several fields and Properties edits their shared properties; double-click opens field properties; fields a file lists only as page widgets (empty /Fields) are adopted too"
[[feature]]
id = "form.highlight-fields"
@@ -5581,7 +5579,7 @@ status = "partial"
commands = ["redact.properties"]
tools = ["redact_mark"]
evidence = ["crates/ui-egui/tests/redact.rs::marking_applying_and_clearing", "crates/redact/src/tests.rs::overlay_text_takes_its_font_size_colour_alignment_and_repeats"]
notes = "fill colour or none; overlay text with font, size or auto-size, colour, alignment and repeat; redaction codes pending"
notes = "fill colour or none; overlay text with font, size or auto-size, colour, alignment and repeat, or redaction codes (protect.redaction-codes)"
[[feature]]
id = "protect.redaction-codes"
@@ -5589,11 +5587,11 @@ title = "Redaction code sets (e.g. FOIA exemptions)"
area = "G"
tier = "P1"
milestone = "M8"
status = "planned"
commands = []
tools = []
evidence = []
notes = ""
status = "partial"
commands = ["redact.properties"]
tools = ["redact_mark"]
evidence = ["crates/redact/src/tests.rs::redaction_codes_join_in_set_order_and_reject_strangers", "crates/automation/tests/automation.rs::redacting_with_codes_through_tools", "crates/ui-egui/tests/redact.rs::redaction_codes_become_the_overlay_text"]
notes = "built-in U.S. FOIA (5 U.S.C. 552(b)) and U.S. Privacy Act (552a) sets; picked codes become the overlay text, in the set's order. Missing: Edit Codes, custom code sets, import/export of code sets"
[[feature]]
id = "protect.apply-redactions"