Skip to content

Redact a PDF

What it does. When a document is marked Sensitive, the script reads its text, finds email addresses, UK phone numbers and National Insurance numbers, removes them from a copy of the PDF, and saves the copy in a Redacted folder. The original isn’t changed. If a page is a scanned image with no text to search, the script says so and stops, because a page it couldn’t check can’t be called clean.

What you need.

  • The MaSH feature.
  • A Classification field on the share, filled by Captur or a person, with the value Sensitive.
  • Triggers: Captur Extracted (Captur sets the classification) and Fields Changed (a person sets it). Filter both to pdf in Extension. See Events.
  • Script setting: DEST (the folder for copies, default Redacted, created if missing).

The detection is plain pattern matching on text, so you can test and tune it without a PDF:

block findPhrases text
set patterns to [
"/\\b[A-Z]{2}\\d{6}[A-D]\\b/",
"/\\b[\\w.+-]+@[\\w-]+\\.[\\w.-]+\\b/",
"/\\b(?:\\+44 ?|0)7\\d{3} ?\\d{6}\\b/"
]
set phrases to []
each patterns as pattern
set found to mash.str(text).match(pattern, 1)
each found[0] as m
phrases[] = mash.str(m).trim().toString()
end
end
return mash.collect(phrases).unique().values().all()
end
set sample to "Contact Sam on 07700 900123 or sam@example.com. NI number AB123456C. Ref 2041."
each findPhrases(sample) as p
printline p
end
AB123456C
sam@example.com
07700 900123

Order 2041 stays, because no pattern matches it. A pattern that is too loose will remove things you meant to keep. A pattern that is too tight will miss something. Tune them on real text, and look at the result.

set errorblock to "onError"
block onError
printline "Error on line {{mash.error.line}}: {{mash.error.message}}"
end
block findPhrases text
set patterns to [
"/\\b[A-Z]{2}\\d{6}[A-D]\\b/",
"/\\b[\\w.+-]+@[\\w-]+\\.[\\w.-]+\\b/",
"/\\b(?:\\+44 ?|0)7\\d{3} ?\\d{6}\\b/"
]
set phrases to []
each patterns as pattern
set found to mash.str(text).match(pattern, 1)
each found[0] as m
phrases[] = mash.str(m).trim().toString()
end
end
return mash.collect(phrases).unique().values().all()
end
set destName to mash.config.get("DEST") ?? "Redacted"
set doc to mash.event.file
if not doc
return
end
if doc.is_dir
return
end
if doc.extension is not "pdf"
return
end
set classification to mash.str(doc.fields["Classification"] ?? "").lower().toString()
if classification is not "sensitive"
return
end
set text to mash.pdf.text(doc)
set phrases to findPhrases(text)
if count(phrases) is 0
printline "{{doc.name}} is Sensitive but no pattern matched. Check it by hand."
return
end
set result to mash.pdf.redact(doc, phrases)
printline "Redacted {{result.redacted}} areas in {{doc.name}}"
if count(result.scanned_pages) is more than 0
set pages to mash.collect(result.scanned_pages).join(", ")
printline "Not saved: page {{pages}} has no text layer, so it could not be checked. Review {{doc.name}} by hand."
return
end
if count(result.missing) is more than 0
printline "Not found on the page: " ~ mash.collect(result.missing).join(" | ")
end
set destFolder to doc.parent.make(destName, true)
set copy to destFolder.write("redacted-{{doc.name}}", result.file, true)
printline "Saved {{copy.path}}"
  1. mash.pdf.text reads the PDF’s text layer. A scanned page has none, so its text is empty. That’s why the script checks scanned_pages afterwards.
  2. mash.pdf.redact(doc, phrases) removes each phrase from the page itself. It isn’t a black box over the text. The text is gone from the copy and can’t be selected, copied or searched. The result has file (the redacted copy), redacted (how many areas), missing (phrases it couldn’t find) and scanned_pages.
  3. Matching is exact and ignores case. A phrase that is split over two lines on the page isn’t found, and appears in missing.
  4. Nothing is written by redact itself. The copy is a temporary file. destFolder.write(name, result.file, true) saves it, with the run’s own permissions. The true replaces an earlier copy of the same name, so each document keeps one redacted copy.
  5. The original is never touched. That is deliberate. Don’t delete it from the script.
  6. The scanned-page stop is a safety rule. If any page couldn’t be searched, nothing is saved, and the run log says which page needs a person.
  • Fixed areas. For a form where a photo or signature box never moves, pass areas as the third argument: mash.pdf.redact(doc, phrases, [{"page": 1, "rect": [140, 20, 190, 70], "unit": "mm"}]). An area is burned whatever is under it, including a picture, so it also works on a scanned page.
  • Fields as phrases. Add the values of Captur fields such as Name and Address to phrases.
  • Share a link to the redacted copy. copy.sharing.createLink() makes a link, and copy.sharing.link.url is its address. The link needs someone to share as, and the share’s rules may hold it for approval. See the Files library page.
  • Ask Grace for names. Patterns can’t find names or addresses. mash.grace.structured can, at the cost of Grace credits, and with the usual limits on sending personal data to a model. Test it carefully.
  • A person decides. Instead of return on a scanned page, set a field such as Needs review and let people take over.

Library pages used: PDF, Files, String.