Document Processing

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

Page Extraction

PSPDFProcessor can extract pages from one document 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 PSPDFProcessor to create a new PDF document on disk based on a current PSPDFDocument:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Create default configuration.
guard let configuration = PSPDFProcessorConfiguration(document: document) else {
    // Handle failure
    abort()
}
// 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))

do {
    // Start the conversion from `document` to `splitURL`.
    let processor = PSPDFProcessor(configuration: configuration, securityOptions: nil)
    try processor.write(toFileURL: splitURL)
} catch {
    // Handle failure
    abort()
}
Copy
1
2
3
4
5
6
7
8
9
// 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, this was called generatePDFFromDocument:pageRanges:outputFileURL:options:progressBlock:error: and was configured via a dictionary with predefined keys. We replaced this with PSPDFProcessorConfiguration 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 or multiple files, so the processor can be used to merge multiple files into one without having to configure anything on PSPDFProcessorConfiguration. To achieve this, you have to create multiple data providers and then initialize the PSPDFDocument using initWithDataProviders::

Copy
1
2
3
4
5
6
// Create PSPDFFileDataProvider number one with a URL that points to a PDF.
let fileProvider1 = PSPDFFileDataProvider(fileURL: writableURL1)
// Create PSPDFFileDataProvider number two with a URL that points to a PDF.
let fileProvider2 = PSPDFFileDataProvider(fileURL: writableURL2)
// Initialize the PSPDFDocument with an array of data providers.
let document = PSPDFDocument(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]];

Annotation Flattening

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 otherwise (like Safari on iOS). If not otherwise specified, the processor will keep all annotations as they are.

To change how annotations are processed, use PSPDFProcessorConfiguration#modifyAnnotationsOfTypes:change:. With the PSPDFAnnotationChange 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 PSPDFAnnotationTypeAll to apply this change on all annotations. In some cases, excluding links but flattening all other types might be desirable. This can be achieved with the following:

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

Form Flattening

Form elements are of the special annotation type PSPDFAnnotationTypeWidget. You can use the above mentioned method to control flattening for all the form types, or you can change them with PSPDFFormFieldType, using PSPDFProcessorConfiguration#modifyFormsOfType:change:.

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

Page Scaling

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

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

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Create default configuration.
guard let configuration = PSPDFProcessorConfiguration(document: document) else {
    // Handle failure
    abort()
}

// 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)

do {
    // Start the conversion from `document` to `scaledDocumentURL`.
    let processor = PSPDFProcessor(configuration: configuration, securityOptions: nil)
    try processor.write(toFileURL: scaledDocumentURL)
} catch {
    // Handle failure.
    abort()
}
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];

Adding Watermarks

PSPDFProcessor 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
23
24
25
26
27
28
29
30
31
32
// Create default configuration.
guard let configuration = PSPDFProcessorConfiguration(document: document) else {
    // Handle failure.
    abort()
}

let renderDrawBlock: PSPDFRenderDrawBlock = { context, page, cropBox, _, _ in
    // Careful! This code is executed on background threads. Only use thread-safe drawing methods.
    let text = "PSPDF Live Watermark On Page \(page + 1)"
    let stringDrawingContext = NSStringDrawingContext()
    stringDrawingContext.minimumScaleFactor = 0.1

    // Add text over the diagonal of the page.
    context.translateBy(x: 0, y: cropBox.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: cropBox, options: .usesLineFragmentOrigin, attributes: attributes, context: stringDrawingContext)
}

configuration.draw(onAllCurrentPages: renderDrawBlock)

do {
    // Start the conversion from `document` to `processedDocumentURL`.
    let processor = PSPDFProcessor(configuration: configuration, securityOptions: nil)
    try processor.write(toFileURL: processedDocumentURL)
} catch {
    // Handle failure.
    abort()
}
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, NSUInteger page, CGRect cropBox, NSUInteger rotation, NSDictionary *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", page + 1];
    NSStringDrawingContext *stringDrawingContext = [NSStringDrawingContext new];
    stringDrawingContext.minimumScaleFactor = 0.1f;

    // Add text over the diagonal of the page.
    CGContextTranslateCTM(context, 0.f, cropBox.size.height/2.f);
    CGContextRotateCTM(context, -(CGFloat)M_PI / 4.f);
    [text drawWithRect:cropBox
               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
20
typealias RenderDrawBlock = @convention(block) (CGContext, UInt, CGRect, UInt, [String : Any]?) -> Void

let renderDrawBlock: RenderDrawBlock = { context, page, cropBox, _, _ in
    // Careful! This code is executed on background threads. Only use thread-safe drawing methods.
    let text = "PSPDF Live Watermark On Page \(page + 1)"
    let stringDrawingContext = NSStringDrawingContext()
    stringDrawingContext.minimumScaleFactor = 0.1

    // Add text over the diagonal of the page.
    context.translateBy(x: 0, y: cropBox.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: cropBox, options: .usesLineFragmentOrigin, attributes: attributes, context: stringDrawingContext)
}

let blockObject = unsafeBitCast(renderDrawBlock, to: AnyObject.self)
document.updateRenderOptions([PSPDFRenderOption.drawBlockKey: blockObject], type: PSPDFRenderType.all)
Copy
1
2
3
4
5
6
7
8
9
10
11
12
const PSPDFRenderDrawBlock drawBlock = ^(CGContextRef context, NSUInteger page, CGRect cropBox, NSUInteger rotation, NSDictionary *options) {
    // Careful! This code is executed on background threads. Only use thread-safe drawing methods.
    NSString *text = @"PSPDFKit Live Watermark";
    NSStringDrawingContext *stringDrawingContext = [NSStringDrawingContext new];
    stringDrawingContext.minimumScaleFactor = 0.1f;

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

Additional Features

Some options in the processor — such as adding pages, rotating pages via PSPDFProcessorConfiguration#rotatePage:by:, or moving pages via PSPDFProcessorConfiguration#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 Office documents to PDF via -PSPDFProcessor#generatePDFFromURL:outputFileURL: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 doing so on a server. (This is what large companies such as Box, Dropbox, and Microsoft are doing.)

The Microsoft Office file format is insanely 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

PSPDFProcessor can also convert HTML into a PDF. You can use -PSPDFProcessor#convertHTMLString:outputFileURL:completionBlock: for that. Note that it only allows simple HTML tags and doesn’t work with complex HTML pages.

Creating a Password-Protected Document

PSPDFProcessor can generate a password-protected document from another document. You can use PSPDFProcessor to create a new password-protected PDF document on disk based on a current PSPDFDocument. 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
15
// By default, a newly initialized `PSPDFProcessorConfiguration` results in an exported document that is the same as the input.
let processorConfiguration = PSPDFProcessorConfiguration(document: originalDocument)

// Set the proper password and key length in `PSPDFDocumentSecurityOptions`.
let documentSecurityOptions = PSPDFDocumentSecurityOptions(ownerPassword: ownerPassword, userPassword: userPassword, keyLength: PSPDFDocumentSecurityOptionsKeyLengthAutomatic)
do {
    let processor = PSPDFProcessor(configuration: processorConfiguration!, securityOptions: documentSecurityOptions)
    try processor.write(toFileURL: outputFileURL)
} catch {
    // Handle failure.
    abort()
}

// Initialize the password-protected document.
let passwordProtectedDocument = PSPDFDocument(url: ouputFileURL)
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:userPassword userPassword:ownerPassword keyLength:PSPDFDocumentSecurityOptionsKeyLengthAutomatic];

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

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