Redacting PDFs Programmatically on iOS

Creating Redactions Programmatically

You can create redactions programmatically via RedactionAnnotation. Use the rects property to set the regions that should be covered by the redaction annotation. Additionally, the boundingBox needs to be set to a frame containing all the specified rects.

You also have a few customization options for how a redaction should look, both while in its marked state, which is when the redaction has been created but not yet applied, and in its redacted state, which is when the redaction has been applied. It isn’t possible to change the appearance once a redaction has been applied, since the redaction annotation will be removed from the document in the process of applying the redactions.

  • overlayText can be used to set the text that should be displayed at the specified region when a redaction has been applied.

  • repeatOverlayText defines whether the overlay text should be drawn only once or repeated to fill the entire redaction area. This defaults to disabled, which means the overlay text is only drawn once. It has no effect if there is no overlay text specified.

  • color can be used to change the color of the overlay text. It has no effect if there is no overlay text specified. This defaults to a red color.

  • fillColor specifies the background color of the redaction area after it has been applied. The color is drawn on all the specified rects. This defaults to black.

  • outlineColor specifies the color used for the redaction’s border in its marked state. This defaults to a red color.

  • lineWidth can be set to change the border width of the redaction in its marked state. This defaults to 5.

This is how a redaction annotation covering the first occurrence of the text “Annual” on the first page of a document is created:

let words = document.textParserForPage(at: 0)!.words
let wordToRedact = words.first(where: { $0.stringValue == "Annual" })!
let redaction = RedactionAnnotation()
redaction.boundingBox = wordToRedact.frame
redaction.rects = [wordToRedact.frame]
redaction.color = .orange
redaction.fillColor = .black
redaction.outlineColor = .yellow
redaction.overlayText = "REDACTED"

document.add(annotations: [redaction])
NSArray<PSPDFWord *> *words = [document textParserForPageAtIndex:0].words;
NSUInteger index = [words indexOfObjectPassingTest:^BOOL(PSPDFWord *word, NSUInteger idx, BOOL *stop) {
    return [word.stringValue isEqualToString:@"Annual"];
}];
if (index == NSNotFound) { return; }
PSPDFWord *wordToRedact = words[index];

PSPDFRedactionAnnotation *redaction = [PSPDFRedactionAnnotation new];
redaction.boundingBox = wordToRedact.frame;
redaction.rects = @[@(wordToRedact.frame)];
redaction.color = UIColor.orangeColor;
redaction.fillColor = UIColor.blackColor;
redaction.outlineColor = UIColor.yellowColor;
redaction.overlayText = @"REDACTED";

[document addAnnotations:@[redaction] options:nil];

The redaction annotation created with the above code snippet would look like what’s shown in the image below.

Marked State
Redacted State

Applying Redactions Programmatically

There are two separate options for applying redactions programmatically.

  • Use Processor. This creates a new document and will leave the original document with the redaction annotations untouched:

let document: Document = // Document containing redaction annotations.
guard let processorConfiguration = Processor.Configuration(document: document) else { return }
processorConfiguration.applyRedactions()

let redactedDocumentURL: URL = // URL to save the redacted document to.
let processor = Processor(configuration: processorConfiguration, securityOptions: nil)

do {
    try processor.write(toFileURL: redactedDocumentURL)
    redactedDocument = Document(url: redactedDocumentURL)
} catch {
    // Handle error.
}
PSPDFDocument *document = // Document containing redaction annotations.
PSPDFProcessorConfiguration *processorConfiguration = [[PSPDFProcessorConfiguration alloc] initWithDocument:document];
[processorConfiguration applyRedactions];

NSURL *redactedDocumentURL = // `NSURL` to save the redacted document to.
PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:processorConfiguration securityOptions:nil];
[processor writeToFileURL:redactedDocumentURL error:&error];

redactedDocument = [[PSPDFDocument alloc] initWithURL:redactedDocumentURL];
  • Use the .applyRedactions option when saving the document via any of the save methods on Document, like save(options:). This will overwrite the existing document, removing content irreversibly:

try document.save(options: [.applyRedactions])
[document saveWithOptions:@{PSPDFDocumentSaveOptionApplyRedactions: @YES} error:&error];

Both options will remove the redaction annotations in the process of redacting the content.

Previewing Redactions Programmatically

To preview redactions and see how they’d look when applied without removing any document content, you can use the RenderOptions.drawRedactionsAsRedacted render option. To set this render option, use updateRenderOptions(for:with:) on the document where you want to render redactions as redacted:

document.updateRenderOptions(for: .all) {
    $0.drawRedactionsAsRedacted = true
}
[document updateRenderOptionsForType:PSPDFRenderTypeAll withBlock:^(PSPDFRenderOptions *options) {
    options.drawRedactionsAsRedacted = YES;
}];

When updating render options while a document is currently being displayed in a PDFViewController, a rerender of the currently displayed pages will need to be triggered. This is so the redaction preview rendering can immediately be applied. There are various ways this can be done. The suggested option is to call update(_:animated:) and pass all the document’s redaction annotations. In this way, only the affected areas, and not the whole document, are rerendered:

let redactions = document.allAnnotations(of: .redaction).values.flatMap { $0 }
document.documentProviders.first?.annotationManager.update(redactions, animated: true)
NSArray<PSPDFAnnotation *> *redactions = [[document allAnnotationsOfType:PSPDFAnnotationTypeRedaction].allValues valueForKeyPath:@"@unionOfArrays.self"];
[document.documentProviders.firstObject.annotationManager updateAnnotations:redactions animated:YES];

Alternatively, this can also be done by invalidating the cache or reloading data in PDFViewController.

Licensing

The following redaction features are part of the Redaction component that must be licensed separately. The following list describes the expected behavior if Redaction isn’t part of your license:

  • The redaction annotation toolbar button and text selection menu item will be hidden.

  • The floating redaction information button won’t be shown.

  • The annotation inspector for existing redaction annotations can’t be shown.

  • RedactionAnnotation can’t be created.

  • RenderOptions.drawRedactionsAsRedacted, applyRedactions(), and similar APIs won’t work or assert when used.