2026-06-13 10:18:55 +08:00
|
|
|
"""True redaction: remove every occurrence of the given terms, then VERIFY
|
|
|
|
|
none remain extractable. Args: {"path": in, "out": out, "terms": [".."],
|
|
|
|
|
"caseSensitive": false}. Prints {"found": N, "verified": true}.
|
|
|
|
|
|
2026-06-16 15:48:07 +08:00
|
|
|
Case sensitivity: fitz.Page.search_for is ALWAYS case-insensitive, so when
|
|
|
|
|
caseSensitive=true we post-filter the hits to only those whose on-page glyphs
|
|
|
|
|
match the term's exact case (checked with get_textbox). caseSensitive=false
|
|
|
|
|
redacts every case variant. The verification pass applies the same casing
|
|
|
|
|
rule, so it proves the intended occurrences are gone."""
|
2026-06-13 10:18:55 +08:00
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
|
|
|
|
|
path, out, terms = args.get("path"), args.get("out"), args.get("terms") or []
|
|
|
|
|
case_sensitive = bool(args.get("caseSensitive", False))
|
|
|
|
|
if not path or not out or not isinstance(terms, list) or not terms:
|
|
|
|
|
print(json.dumps({"error": "missing path/out/terms"}))
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
try:
|
|
|
|
|
import fitz
|
|
|
|
|
except ImportError:
|
|
|
|
|
print(json.dumps({"error": "PyMuPDF not installed"}))
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
try:
|
|
|
|
|
doc = fitz.open(path)
|
|
|
|
|
flags = fitz.TEXT_DEHYPHENATE
|
|
|
|
|
found = 0
|
|
|
|
|
for page in doc:
|
|
|
|
|
for term in terms:
|
2026-06-16 15:48:07 +08:00
|
|
|
if case_sensitive:
|
|
|
|
|
# search_for is always case-insensitive; keep only the
|
|
|
|
|
# exact-case hits by checking the glyphs under each quad.
|
|
|
|
|
quads = [
|
|
|
|
|
q
|
|
|
|
|
for q in page.search_for(term, quads=True, flags=flags)
|
|
|
|
|
if term in page.get_textbox(q.rect)
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
quads = page.search_for(term, quads=True)
|
2026-06-13 10:18:55 +08:00
|
|
|
for quad in quads:
|
|
|
|
|
page.add_redact_annot(quad, fill=(0, 0, 0))
|
|
|
|
|
found += 1
|
|
|
|
|
page.apply_redactions()
|
|
|
|
|
doc.save(out, garbage=4, deflate=True)
|
|
|
|
|
doc.close()
|
|
|
|
|
# Verification pass: reopen and prove no term is extractable anymore.
|
|
|
|
|
check = fitz.open(out)
|
|
|
|
|
leaked = []
|
|
|
|
|
for page in check:
|
|
|
|
|
text = page.get_text()
|
|
|
|
|
haystack = text if case_sensitive else text.lower()
|
|
|
|
|
for term in terms:
|
|
|
|
|
needle = term if case_sensitive else term.lower()
|
|
|
|
|
if needle and needle in haystack:
|
|
|
|
|
leaked.append(term)
|
|
|
|
|
check.close()
|
|
|
|
|
if leaked:
|
|
|
|
|
print(json.dumps({"error": f"verification failed: terms still extractable: {sorted(set(leaked))}"}))
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
print(json.dumps({"found": found, "verified": True}))
|
|
|
|
|
except SystemExit:
|
|
|
|
|
raise
|
|
|
|
|
except Exception as exc: # noqa: BLE001
|
|
|
|
|
print(json.dumps({"error": str(exc)}))
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|