Recursive AI

Redact text in a PDF

All onepdf recipes

Find a name on every page of a PDF in C#, remove it from the content, cover the spot with a black box, and clear it from the metadata.

OnePDF removes the text from the file. Nothing is just hidden under a box.

C#
using OnePdf;
using OnePdf.Text;
 
string[] phrase = ["Jane", "Doe"];
 
using PdfDocument doc = PdfDocument.Open("report.pdf", new PdfOpenOptions { AllowEditing = true });
 
// Find the phrase on every page and collect the boxes of its words.
var regions = new List<PdfRedactionRegion>();
for (int p = 0; p < doc.PageCount; p++)
{
    PdfPageText text = doc.Pages[p].ExtractText();
    foreach (PdfTextLine line in text.Blocks.SelectMany(b => b.Lines))
    {
        IReadOnlyList<PdfTextWord> words = line.Words;
        for (int i = 0; i + phrase.Length <= words.Count; i++)
        {
            if (!phrase.Select((s, k) => words[i + k].Text.TrimEnd(',', '.', ';') == s).All(match => match))
                continue;
            PdfRectangle[] boxes = words.Skip(i).Take(phrase.Length).Select(w => w.Bounds).ToArray();
            var area = new PdfRectangle(boxes.Min(r => r.Left), boxes.Min(r => r.Bottom), boxes.Max(r => r.Right), boxes.Max(r => r.Top));
            regions.Add(new PdfRedactionRegion(p, area));
        }
    }
}
 
// Remove what is under the boxes, draw black boxes over them, and clear the name from the metadata.
PdfRedactionResult result = doc.Redact(regions, new PdfRedactionOptions
{
    Overlay = new PdfRedactionOverlay(),
    MetadataTerms = [string.Join(" ", phrase)],
});
Console.WriteLine($"{regions.Count} areas, {result.GlyphsRemoved} glyphs removed on {result.PagesProcessed} pages");
doc.Save("redacted.pdf");                                    // a full save; SaveIncremental is refused after Redact
 
using PdfDocument check = PdfDocument.Open("redacted.pdf");
bool found = check.ExtractText().Any(page => page.Text.Contains("Jane"));
Console.WriteLine($"\"Jane\" still in the text: {found}");
Text
6 areas, 54 glyphs removed on 6 pages
"Jane" still in the text: False

pdftotext agrees: the first line of the page now reads “Prepared by” and then the account number.

What is removed

Redact rewrites the page content. Inside each area it removes:

  • the glyphs whose boxes meet the area,
  • vector paths and shadings that meet it,
  • image samples under it (the image is blanked there and stored again),
  • annotations, and the values of form fields whose widgets meet it,
  • /ActualText and alternate text of the tagged content it removed.

MetadataTerms clears matching entries from the document information, the XMP metadata and bookmark titles. RemoveJavaScript, RemoveEmbeddedFiles and RemoveThumbnails strip those parts too.

Good to know

  • Save with Save. After Redact, SaveIncremental is refused, because the old content would stay in the file’s earlier revision.
  • It fails closed. If any page’s content can’t be read completely, Redact throws PdfRedactionException naming the page and changes nothing.
  • Areas are in the page’s own coordinates, before its rotation. Word boxes from text extraction have the rotation applied, so on a rotated page convert them first. On an unrotated page, as here, they can be used as they are.
  • Matching is by position. Only the places your areas cover are removed. A word drawn in two places needs two areas, and text that exists only as an image, such as a scan without a text layer, is removed only by blanking the image.
  • Signatures become invalid once the redacted document is saved. result.SignaturesInvalidated says so.
  • JPEG and other compressed images are blanked when a decoder is registered in PdfRedactionOptions.Decoders. Without one, an image that meets an area is removed whole.

The full list of what is and isn’t covered is in Redaction. Finding words is covered in Get words and their positions.

For the whole workflow, including image areas, metadata and checking the result, see Redact a PDF in C#.