Redact Text and Personal Data in PDF

The PdfRedactor class permanently removes the text, images and graphics of some areas of an existing PDF document. The texts to remove can be searched in all the pages, the personal data like email addresses, phone numbers, bank accounts and payment card numbers is found by its structure and the redacted areas are covered with a fill color and an overlay text. The removed content cannot be recovered from the saved document, unlike the content hidden under a shape drawn over the page.

The demo application redacts a name, the texts that match a regular expression and the personal data of the sample document. It also removes the metadata, the scripts, the attached files and the hidden content of the document. The sample document contains fictitious data.

Redact Texts

PdfRedactorRedactText(String, Boolean, Boolean) marks for redaction every occurrence of a text, with the options to match the case of the letters and to find only whole words. PdfRedactorRedactText(Regex) marks for redaction every text that matches a regular expression. The white space of the searched text matches any white space, including a line break, so a text that continues on the next line is found and redacted on both lines. The methods return the number of texts found.

The texts redacted by search are also replaced with the overlay text, or with the word Redacted when no overlay text is set, in the document information, the metadata, the bookmarks, the annotation and form field values and the alternate descriptions of the document structure.

Redact Texts
using PdfRedactor pdfRedactor = new PdfRedactor(inputPdfBytes, password);

// Every occurrence of a name, as a whole word
pdfRedactor.RedactText("Jane Smith", false, true);

// The texts that match a regular expression, for example the customer numbers
pdfRedactor.RedactText(new Regex(@"C-\d{4}-\d{5}"));

byte[] redactedPdf = pdfRedactor.Save();

Redact Personal Data

PdfRedactorRedactPersonalData(PdfPersonalDataTypes) marks for redaction the personal data of the kinds given by a combination of PdfPersonalDataTypes values: email addresses, phone numbers, international bank account numbers, payment card numbers, IPv4 and IPv6 addresses, US social security numbers and Romanian personal numeric codes. The bank account numbers, the payment card numbers and the personal numeric codes are reported only when their check digits are valid.

To review the data before the redaction, PdfRedactorFindPersonalData(PdfPersonalDataTypes) returns the texts found as PdfTextMatch objects with the page number, the text, the bounds and the PersonalDataType of each text. PdfRedactorAddRedaction(PdfTextMatch) marks one of them for redaction. PdfRedactorFindText(String, Boolean, Boolean) finds a text in the same way without marking it.

Redact Personal Data
// All the email addresses, phone numbers and bank accounts
pdfRedactor.RedactPersonalData(PdfPersonalDataTypes.EmailAddress | PdfPersonalDataTypes.PhoneNumber |
    PdfPersonalDataTypes.Iban);

// The payment card numbers found, reviewed one by one
foreach (PdfTextMatch match in pdfRedactor.FindPersonalData(PdfPersonalDataTypes.PaymentCardNumber))
{
    if (match.PageNumber == 1)
        pdfRedactor.AddRedaction(match);
}

Appearance of the Redacted Areas

FillColor covers the redacted areas after their content is removed and is black by default; a null color leaves the areas empty. OverlayText is written over each area with the color of OverlayTextColor, in a font size that fits the area or in the size of OverlayFontSize. RepeatOverlayText repeats the text to fill each area.

Remove Other Information

RemoveMetadata removes the metadata of the document, pages, images and fonts, the page thumbnails and the document information, RemoveJavaScript removes the document scripts and the scripts run when the document is opened, RemoveEmbeddedFiles removes the attached files and RemoveHiddenContent removes from all the pages the content a viewer does not show, like invisible text, text outside the visible area of the page, layers turned off and hidden annotations.

The annotations, links and form fields that intersect a redacted area are removed by default, as controlled by RemoveIntersectingAnnotations. The signatures of the document no longer match the redacted content, so their values are removed and the signature fields are left empty.

PDF/A and PDF/UA Documents

A redacted PDF/A or PDF/UA document keeps the identification of its standard. The overlays are marked as artifacts, the colors of the overlays use the color space of the PDF/A output intent, the structure elements of the removed content lose their actual text, their alternate descriptions are replaced with the overlay text or with the word Redacted and the metadata is written to match the document information. The title of a PDF/UA document is kept when the metadata is removed and the overlay text receives the language of the document, or the language set with OverlayTextLanguage.

Save the Redacted Document

PdfRedactorSave applies the redactions and returns the redacted document, while PdfRedactorSaveToFile(String) and PdfRedactorSaveToFileAsync(String, CancellationToken) write it to a file. After the document is saved, Result gives the number of removed characters, redacted images and shapes and removed annotations, form fields and signatures.

Redaction Marks for a Review

PdfRedactorSaveRedactionMarks saves the document with the marked areas as redaction annotations, without removing any content. The marks carry the fill color and the overlay text of the areas, can be reviewed and changed in a PDF viewer and are applied later with PdfRedactorAddRedactionAnnotations, as described in the Apply Redaction Annotations in PDF section. The demo application saves the marks instead of the redacted document when the option is checked.

Redaction Marks for a Review
using PdfRedactor pdfRedactor = new PdfRedactor(inputPdfBytes, password);
pdfRedactor.RedactText("Jane Smith", false, true);
pdfRedactor.RedactPersonalData(PdfPersonalDataTypes.All);
// The document with the redaction marks, to review before the content is removed
byte[] markedPdf = pdfRedactor.SaveRedactionMarks();

Code Sample - Redact Text and Personal Data

C#
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using EvoPdf_Next_AspNetDemo.Models;
using EvoPdf_Next_AspNetDemo.Models.Redact_PDF;

// Use EVO PDF Namespace
using EvoPdf.Next;

namespace EvoPdf_Next_AspNetDemo.Controllers.Redact_PDF
{
    public class Redact_Text_and_Personal_DataController : Controller
    {
        private readonly IWebHostEnvironment m_hostingEnvironment;

        public Redact_Text_and_Personal_DataController(IWebHostEnvironment hostingEnvironment)
        {
            m_hostingEnvironment = hostingEnvironment;
        }

        public IActionResult Index()
        {
            var model = SetViewModel();
            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> RedactText(Redact_Text_and_Personal_Data_ViewModel model)
        {
            if (!ModelState.IsValid)
            {
                var errorMessage = ModelStateHelper.GetModelErrors(ModelState);
                throw new ValidationException(errorMessage);
            }

            // Set the license key received after purchase to use the library in licensed mode; leave it commented for demo mode
            // Licensing.LicenseKey = "your-license-key";

            byte[] inputPdfBytes = await ReadInputPdf(model);
            string password = !string.IsNullOrEmpty(model.OwnerPassword) ? model.OwnerPassword :
                !string.IsNullOrEmpty(model.UserPassword) ? model.UserPassword : null;

            using PdfRedactor pdfRedactor = new PdfRedactor(inputPdfBytes, password);

            // The appearance of the redacted areas: a fill color and a text written over each area
            pdfRedactor.FillColor = GetFillColor(model.FillColor);
            if (!string.IsNullOrWhiteSpace(model.OverlayText))
            {
                pdfRedactor.OverlayText = model.OverlayText.Trim();
                pdfRedactor.OverlayTextColor = model.FillColor == "White" ? PdfColor.Black : PdfColor.White;
                pdfRedactor.RepeatOverlayText = model.RepeatOverlayText;
            }

            // Each text is searched in all the pages and every occurrence is marked for redaction. The same texts
            // are also replaced in the document information, bookmarks, annotations and form field values
            if (!string.IsNullOrWhiteSpace(model.TextsToRedact))
            {
                string[] texts = model.TextsToRedact.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
                foreach (string text in texts)
                {
                    if (!string.IsNullOrWhiteSpace(text))
                        pdfRedactor.RedactText(text.Trim(), model.MatchCase, model.WholeWord);
                }
            }

            // The texts that match a regular expression, for example the customer numbers
            if (model.UseRegularExpression && !string.IsNullOrWhiteSpace(model.RegularExpression))
            {
                Regex pattern;
                try
                {
                    pattern = new Regex(model.RegularExpression, RegexOptions.CultureInvariant, TimeSpan.FromSeconds(5));
                }
                catch (ArgumentException ex)
                {
                    throw new Exception("The regular expression is not valid", ex);
                }

                pdfRedactor.RedactText(pattern);
            }

            // The personal data is found by its structure and validated, for example by the check digits of
            // bank account and payment card numbers
            PdfPersonalDataTypes personalDataTypes = GetPersonalDataTypes(model);
            if (personalDataTypes != PdfPersonalDataTypes.None)
                pdfRedactor.RedactPersonalData(personalDataTypes);

            // Remove the information that can remain in the document outside the redacted areas
            pdfRedactor.RemoveMetadata = model.RemoveMetadata;
            pdfRedactor.RemoveJavaScript = model.RemoveJavaScript;
            pdfRedactor.RemoveEmbeddedFiles = model.RemoveEmbeddedFiles;
            pdfRedactor.RemoveHiddenContent = model.RemoveHiddenContent;

            byte[] outPdfBuffer;
            string fileName;
            if (model.SaveRedactionMarks)
            {
                // The marked areas are saved as redaction annotations, for a review in a PDF viewer before the
                // content is removed by applying them
                outPdfBuffer = pdfRedactor.SaveRedactionMarks();
                fileName = "Redaction_Marks.pdf";
            }
            else
            {
                // Apply the redactions. The removed content cannot be recovered from the saved document and
                // pdfRedactor.Result gives the number of removed characters, images, annotations and form fields
                outPdfBuffer = pdfRedactor.Save();
                fileName = "Redact_Text_and_Personal_Data.pdf";
            }

            FileResult fileResult = new FileContentResult(outPdfBuffer, "application/pdf");
            fileResult.FileDownloadName = fileName;
            return fileResult;
        }

        private static PdfColor GetFillColor(string fillColor)
        {
            switch (fillColor)
            {
                case "Black":
                    return PdfColor.Black;
                case "DarkGray":
                    return PdfColor.DarkGray;
                case "White":
                    return PdfColor.White;
                case "None":
                    // The content is removed and the areas are left empty
                    return null;
                default:
                    return PdfColor.Black;
            }
        }

        private static PdfPersonalDataTypes GetPersonalDataTypes(Redact_Text_and_Personal_Data_ViewModel model)
        {
            PdfPersonalDataTypes types = PdfPersonalDataTypes.None;
            if (model.RedactEmailAddresses)
                types |= PdfPersonalDataTypes.EmailAddress;
            if (model.RedactPhoneNumbers)
                types |= PdfPersonalDataTypes.PhoneNumber;
            if (model.RedactIbans)
                types |= PdfPersonalDataTypes.Iban;
            if (model.RedactPaymentCardNumbers)
                types |= PdfPersonalDataTypes.PaymentCardNumber;
            if (model.RedactIpAddresses)
                types |= PdfPersonalDataTypes.IpAddress;
            if (model.RedactSocialSecurityNumbers)
                types |= PdfPersonalDataTypes.UsSocialSecurityNumber;
            if (model.RedactPersonalNumericCodes)
                types |= PdfPersonalDataTypes.RomanianPersonalNumericCode;
            return types;
        }

        private async Task<byte[]> ReadInputPdf(Redact_Text_and_Personal_Data_ViewModel model)
        {
            // If an uploaded file exists, use it with priority
            if (model.PdfFile != null && model.PdfFile.Length > 0)
            {
                try
                {
                    using var ms = new MemoryStream();
                    await model.PdfFile.CopyToAsync(ms);
                    return ms.ToArray();
                }
                catch (Exception ex)
                {
                    throw new Exception("Failed to read the uploaded PDF file", ex);
                }
            }

            // Otherwise, fall back to the URL
            string pdfUrl = model.PdfFileUrl?.Trim();
            if (string.IsNullOrWhiteSpace(pdfUrl))
                throw new Exception("No PDF file provided: upload a file or specify a URL");

            try
            {
                if (pdfUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
                {
                    string localPath = new Uri(pdfUrl).LocalPath;
                    return await System.IO.File.ReadAllBytesAsync(localPath);
                }

                using var httpClient = new System.Net.Http.HttpClient();
                return await httpClient.GetByteArrayAsync(pdfUrl);
            }
            catch (Exception ex)
            {
                throw new Exception("Could not download the PDF file from URL", ex);
            }
        }

        private Redact_Text_and_Personal_Data_ViewModel SetViewModel()
        {
            var model = new Redact_Text_and_Personal_Data_ViewModel();

            HttpRequest request = ControllerContext.HttpContext.Request;
            UriBuilder uriBuilder = new UriBuilder
            {
                Scheme = request.Scheme,
                Host = request.Host.Host,
                Path = request.PathBase.ToString() + request.Path.ToString(),
                Query = request.QueryString.ToString()
            };
            if (request.Host.Port != null)
                uriBuilder.Port = (int)request.Host.Port;

            string currentPageUrl = uriBuilder.Uri.AbsoluteUri;
            string rootUrl = currentPageUrl.Substring(0, currentPageUrl.Length - "Redact_Text_and_Personal_Data".Length);

            model.PdfFileUrl = rootUrl + "/DemoAppFiles/Input/PDF_Files/Personal_Data_Document.pdf";

            return model;
        }
    }
}

See Also