Document Processing

PSPDFKit allows the splitting and merging of documents, as well as annotation flattening using the Processor class.

Extracting Pages

Processor can extract pages from one document and put them into another document. You can choose to extract a single page, a range of pages, or even multiple page ranges. In most cases, you’ll want to use Processor to create a new PDF document on disk based on a current Document:

Copy
1
2
3
4
5
6
7
8
9
10
// Create default configuration.
let configuration = Processor.Configuration(document: document)!
// Change all annotations to be flattened (instead of saved as annotations).
configuration.modifyAnnotations(ofTypes: .all, change: .flatten)
// Only extract pages 0, 1, and 2.
configuration.includeOnlyIndexes(IndexSet(integersIn: 0...2))

// Start the conversion from `document` to `splitURL`.
let processor = Processor(configuration: configuration, securityOptions: nil)
try processor.write(toFileURL: splitURL)
Copy
1
2
3
4
5
6
7
8
9
10
// Create default configuration.
PSPDFProcessorConfiguration *configuration = [[PSPDFProcessorConfiguration alloc] initWithDocument:document];
// Change all annotations to be flattened (instead of saved as annotations).
[configuration modifyAnnotationsOfTypes:PSPDFAnnotationTypeAll change:PSPDFAnnotationChangeFlatten];
// Only extract pages 0, 1, and 2.
[configuration includeOnlyIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 2)]];

// Start the conversion from `document` to `splitURL`.
PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:configuration securityOptions:nil];
[processor writeToFileURL:splitURL];

ℹ️ Note: In PSPDFKit 5.2 and earlier, the above was called generatePDFFromDocument:pageRanges:outputFileURL:options:progressBlock:error:, and it was configured via a dictionary with predefined keys. We replaced this with Processor.Configuration to have an API that is simpler and easier to understand. At the same time, we made it more powerful.

Merging Multiple Documents

A document can be created from one file or multiple files, so the processor can be used to merge multiple files into one without having to configure anything on Processor.Configuration. To achieve this, you have to create multiple data providers and then initialize the Document using init(dataProviders:):

Copy
1
2
3
4
5
6
// Create `FileDataProvider` number one with a URL that points to a PDF.
let fileProvider1 = FileDataProvider(fileURL: writableURL1)
// Create `FileDataProvider` number two with a URL that points to a PDF.
let fileProvider2 = FileDataProvider(fileURL: writableURL2)
// Initialize the `Document` with an array of data providers.
let document = Document(dataProviders:[fileProvider1, fileProvider2])
Copy
1
2
3
4
5
6
// Create `PSPDFFileDataProvider` number one with a URL that points to a PDF.
PSPDFFileDataProvider *fileProvider1 = [[PSPDFFileDataProvider alloc] initWithFileURL:writableURL1];
// Create `PSPDFFileDataProvider` number two with a URL that points to a PDF.
PSPDFFileDataProvider *fileProvider2 = [[PSPDFFileDataProvider alloc] initWithFileURL:writableURL2];
// Initialize the `PSPDFDocument` with an array of data providers.
PSPDFDocument *document = [[PSPDFDocument alloc] initWithDataProviders:@[fileProvider1, fileProvider2]];

Flattening Annotations

When flattening an annotation, the annotation is removed from the document, while its visual representation is kept intact. A flattened annotation is still visible but no longer editable by your users or by your app. This can be used to, for example, fix annotations onto your document or to make annotations visible to viewers that cannot show annotations (like Safari on iOS). If not otherwise specified, the processor will keep all annotations as they are.

To change how annotations are processed, use Processor.Configuration.modifyAnnotations(ofTypes:change:). With the AnnotationChange enum, you can choose between flattening annotations, removing annotations, or saving annotations into the document (which is the default). In most cases, you’ll want to use Annotation.Kind.all to apply this change on all annotations, including forms. In some cases, excluding links but flattening all other types might be desirable. This can be achieved with the following:

1
2
var types = Annotation.Kind.all
types.remove(.link)
1
PSPDFAnnotationTypeAll & ~PSPDFAnnotationTypeLink

Flattening Forms

Form elements are of the special annotation type Annotation.Kind.widget. You can use the above mentioned method to control flattening for all the form types, or you can change them with PDFFormField.Kind, using Processor.Configuration.modifyForms(of:change:).

For example, you might not want to flatten a signature annotation, as only the visual representation of the digital signature, and not the actual digital signature, would be included in the resulting document.

Scaling Pages

You can also use Processor to scale pages of a document:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
// Create default configuration.
let configuration = Processor.Configuration(document: document)!

// Scale page down to half its size.
let pageInfo = document.pageInfoForPage(at: page)!
let pageSize = pageInfo.size
let newPageSize = CGSize(width: pageSize.width / 2, height: pageSize.height / 2)
configuration.scalePage(page, to: newPageSize)

// Start the conversion from `document` to `scaledDocumentURL`.
let processor = Processor(configuration: configuration, securityOptions: nil)
try processor.write(toFileURL: scaledDocumentURL)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
// Create default configuration.
PSPDFProcessorConfiguration *configuration = [[PSPDFProcessorConfiguration alloc] initWithDocument:document];

// Scale page down to half its size.
PSPDFPageInfo *pageInfo = [document pageInfoForPageAtIndex:page];
CGSize pageSize = pageInfo.size;
CGSize newPageSize = CGSizeMake(pageSize.width/2.f, pageSize.height/2.f);
[configuration scalePage:page toSize:newPageSize];

// Start the conversion from `document` to `scaledDocumentURL`.
PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:configuration securityOptions:nil];
[processor writeToFileURL:scaledDocumentURL];

ℹ️ Note: The page scaling feature is only available if you have the Document Editor component enabled in your license.

Adding Watermarks

Processor lets you draw on all pages of a document. This is useful for watermarks or footers:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Create a default configuration.
let configuration = Processor.Configuration(document: document)!

configuration.drawOnAllCurrentPages { context, pageIndex, pageRect, renderOptions in
    // Careful. This code is executed on background threads. Only use thread-safe drawing methods.
    let text = "PSPDF Live Watermark On Page \(pageIndex + 1)"
    let stringDrawingContext = NSStringDrawingContext()
    stringDrawingContext.minimumScaleFactor = 0.1

    // Add text over the diagonal of the page.
    context.translateBy(x: 0, y: pageRect.size.height / 2)
    context.rotate(by: -.pi / 4)
    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.boldSystemFont(ofSize: 30),
        .foregroundColor: UIColor.red.withAlphaComponent(0.5)
    ]
    text.draw(with: pageRect, options: .usesLineFragmentOrigin, attributes: attributes, context: stringDrawingContext)
}

// Start the conversion from `document` to `processedDocumentURL`.
let processor = Processor(configuration: configuration, securityOptions: documentSecurityOptions)
try processor.write(toFileURL: processedDocumentURL)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Create default configuration.
PSPDFProcessorConfiguration *configuration = [[PSPDFProcessorConfiguration alloc] initWithDocument:document];

PSPDFRenderDrawBlock renderBlock = ^(CGContextRef context, PSPDFPageIndex pageIndex, CGRect pageRect, PSPDFRenderOptions *options) {
    // Careful. This code is executed on background threads. Only use thread-safe drawing methods.
    NSString *text = [NSString stringWithFormat:@"PSPDFKit Live Watermark on page %d", pageIndex + 1];
    NSStringDrawingContext *stringDrawingContext = [NSStringDrawingContext new];
    stringDrawingContext.minimumScaleFactor = 0.1f;

    // Add text over the diagonal of the page.
    CGContextTranslateCTM(context, 0.f, pageRect.size.height/2.f);
    CGContextRotateCTM(context, -(CGFloat)M_PI / 4.f);
    [text drawWithRect:pageRect
               options:NSStringDrawingUsesLineFragmentOrigin
            attributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:100],
                         NSForegroundColorAttributeName: [UIColor.redColor colorWithAlphaComponent:0.5f]}
               context:stringDrawingContext];
};

[configuration drawOnAllCurrentPages:renderBlock];

// Start the conversion from `document` to `processedDocumentURL`.
PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:configuration securityOptions:nil];
[processor writeToFileURL:processedDocumentURL];

This can also be added as a rendering on the document without generating a new document. Please also have a look at PSCExportWatermarkExample in the PSPDFCatalog, where this is shown in action:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let renderBlock: PSPDFRenderDrawBlock =  { context, pageIndex, pageRect, renderOptions in
    // Careful. This code is executed on background threads. Only use thread-safe drawing methods.
    let text = "PSPDF Live Watermark On Page \(pageIndex + 1)"
    let stringDrawingContext = NSStringDrawingContext()
    stringDrawingContext.minimumScaleFactor = 0.1

    // Add text over the diagonal of the page.
    context.translateBy(x: 0, y: pageRect.size.height / 2)
    context.rotate(by: -.pi / 4)
    let attributes: [NSAttributedString.Key: Any] = [
        .font: UIFont.boldSystemFont(ofSize: 30),
        .foregroundColor: UIColor.red.withAlphaComponent(0.5)
    ]
    text.draw(with: pageRect, options: .usesLineFragmentOrigin, attributes: attributes, context: stringDrawingContext)
}

document.updateRenderOptions(for: .all) {
    $0.drawBlock = renderBlock
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const PSPDFRenderDrawBlock renderBlock = ^(CGContextRef context, PSPDFPageIndex pageIndex, CGRect pageRect, PSPDFRenderOptions *options) {
    // Careful. This code is executed on background threads. Only use thread-safe drawing methods.
    NSString *text = [NSString stringWithFormat:@"PSPDFKit Live Watermark on page %d", pageIndex + 1];
    NSStringDrawingContext *stringDrawingContext = [NSStringDrawingContext new];
    stringDrawingContext.minimumScaleFactor = 0.1f;

    // Add text over the diagonal of the page.
    CGContextTranslateCTM(context, 0.f, pageRect.size.height/2.f);
    CGContextRotateCTM(context, -(CGFloat)M_PI / 4.f);
    [text drawWithRect:pageRect
               options:NSStringDrawingUsesLineFragmentOrigin
            attributes:@{NSFontAttributeName: [UIFont boldSystemFontOfSize:100],
                         NSForegroundColorAttributeName: [UIColor.redColor colorWithAlphaComponent:0.5f]}
               context:stringDrawingContext];
};

[document updateRenderOptionsForType:PSPDFRenderTypeAll withBlock:^(PSPDFRenderOptions * options){
    options.drawBlock = drawBlock;
}];

Additional Features

Some options in the processor — such as adding pages, rotating pages via Processor.Configuration.rotatePage(_:by:), or moving pages via Processor.Configuration.movePages(_:toDestinationIndex:) — are only available if you have the Document Editor component enabled for your license. Review the header or API documentation for details.

Website and Office Conversion

PSPDFKit also has the ability to convert websites and Microsoft Office documents to PDF via Processor.generatePDF(from:outputFileURL:options:completionBlock:). This is an experimental feature, and while it works reasonably well, we cannot offer support for it. It uses many frameworks that Apple intended for printing support. Since we do not own these frameworks nor do we have access to the source code, we are unable to fix bugs in Apple’s parsers. If you require a pixel-perfect conversion, we recommend converting on a server. (This is what large companies such as Box, Dropbox, and Microsoft are doing.)

The Microsoft Office file format is incredibly complex and can only be correctly supported by Microsoft Office itself. The file format is open, but this doesn’t help in rendering all the details correctly. In many ways, it’s more complex than PDF.

HTML Conversion

Processor can also convert HTML into a PDF. You can use Processor.generatePDF(fromHTMLString:options:completionBlock:) or Processor.generatePDF(fromHTMLString:outputFileURL:options:completionBlock:) for this. Note that it only allows simple HTML tags and doesn’t work with complex HTML pages.

Creating a Password-Protected Document

Processor can generate a password-protected document from another document. You can use Processor to create a new password-protected PDF document on disk based on a current Document. See CreatePasswordProtectedDocumentExample from the Catalog app for a complete example of how to create a password-protected document:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// By default, a newly initialized `Processor.Configuration` results in an exported document that is the same as the input.
guard let processorConfiguration = Processor.Configuration(document: originalDocument) else { return }

do {
    // Set the proper password and key length in `Document.SecurityOptions`.
    let documentSecurityOptions = try Document.SecurityOptions(ownerPassword: ownerPassword, userPassword: userPassword, keyLength: Document.SecurityOptionsKeyLengthAutomatic)
    let processor = Processor(configuration: processorConfiguration, securityOptions: documentSecurityOptions)
    try processor.write(toFileURL: outputFileURL)
} catch {
    // handle error
}

// Initialize the password-protected document.
let passwordProtectedDocument = Document(url: outputFileURL)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
// By default, a newly initialized `PSPDFProcessorConfiguration` results in an exported document that is the same as the input.
PSPDFProcessorConfiguration *processorConfiguration = [[PSPDFProcessorConfiguration alloc] initWithDocument:originalDocument];

// Set the proper password and key length in `PSPDFDocumentSecurityOptions`.
PSPDFDocumentSecurityOptions *documentSecurityOptions = [[PSPDFDocumentSecurityOptions alloc] initWithOwnerPassword:ownerPassword userPassword:userPassword keyLength:PSPDFDocumentSecurityOptionsKeyLengthAutomatic error:&error];

PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:processorConfiguration securityOptions:documentSecurityOptions];
[processor writeToFileURL:outputFileURL];

// Initialize the password-protected document.
PSPDFDocument *passwordProtectedDocument = [[PSPDFDocument alloc] initWithURL:outputFileURL];
PSPDFViewController *pdfController = [[PSPDFViewController alloc] initWithDocument:passwordProtectedDocument];

Creating a Document with Restricted Permissions

When a document is flattened, it’s often desirable to also prevent future editing of it. A document can be configured to lock some permissions behind an owner password to, for example, restrict form filling/any modification. You can accomplish this use case by passing an owner password, a nil user password, and the desired permissions to Document.SecurityOptions(ownerPassword:userPassword:keyLength:), like so:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// By default, a newly initialized `Processor.Configuration` results in an exported document that is the same as the input.
guard let processorConfiguration = Processor.Configuration(document: originalDocument) else { return }

// Flatten all annotations.
processorConfiguration.modifyAnnotations(ofTypes: .all, change: .flatten)

do {
    // Create the document security options with a proper owner password, a `nil` user password, and the permissions.
    let documentSecurityOptions = try Document.SecurityOptions(ownerPassword: password, userPassword: nil, keyLength: Document.SecurityOptionsKeyLengthAutomatic, permissions: [.printing, .printHighQuality])

    // Initialize the processor with the configuration and the security options.
    let processor = Processor(configuration: processorConfiguration, securityOptions: documentSecurityOptions)
    try processor.write(toFileURL: outputFileURL)
} catch {
    // handle error
}

// Initialize the password-protected document.
let passwordProtectedDocument = Document(url: outputFileURL)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// By default, a newly initialized `PSPDFProcessorConfiguration` results in an exported document that is the same as the input.
PSPDFProcessorConfiguration *processorConfiguration = [[PSPDFProcessorConfiguration alloc] initWithDocument:originalDocument];

// Flatten all annotations.
[processorConfiguration modifyAnnotationsOfTypes:PSPDFAnnotationTypeAll change:PSPDFAnnotationChangeFlatten];

// Create the document security options with a proper owner password, a `nil` user password, and the permissions.
PSPDFDocumentSecurityOptions *documentSecurityOptions = [[PSPDFDocumentSecurityOptions alloc] initWithOwnerPassword:password userPassword:nil keyLength:PSPDFDocumentSecurityOptionsKeyLengthAutomatic permissions:PSPDFDocumentPermissionsPrinting | PSPDFDocumentPermissionsPrintHighQuality error:&error];

// Initialize the processor with the configuration and the security options.
PSPDFProcessor *processor = [[PSPDFProcessor alloc] initWithConfiguration:processorConfiguration securityOptions:documentSecurityOptions];
[processor writeToFileURL:outputFileURL];

// Initialize the password-protected document.
PSPDFDocument *passwordProtectedDocument = [[PSPDFDocument alloc] initWithURL:ouputFileURL];

ℹ️ Note: Software such as PSPDFKit or Adobe Acrobat, which comply with the PDF specification, honor document permissions. Some third-party viewers, such as Apple’s Preview app, only have limited handling of permissions and might still allow editing or throw errors after an edit should be saved.

For more details, please refer to our Document Permissions guide.