Office Conversion

Starting with PSPDFKit for iOS 9.4, we added the ability to leverage a running PSPDFKit Server or PSPDFKit Processor instance to provide conversion of Office documents to PDF files in your iOS application.

Architecture

In order to convert Office files, they first get sent to your server. There, they will be converted as discussed here. Finally, PSPDFKit for iOS downloads the converted PDF.

Required Setup

In order to make use of this, the following is required:

  • A running instance of PSPDFKit Server with a version of at least 2020.2.6 or PSPDFKit Processor.
    • Your license also needs to include the Office Files Support feature.
  • A way to obtain JSON Web Tokens (JWTs) for use with the mobile conversion API, as described here.
    • This can be a separate service providing the tokens. This is the recommended approach for production apps.
    • You can also embed the private key used for signing the JWT in your app and generate the JWTs in your app directly. Keep in mind that this poses a considerable security risk, since anyone with access to your app could extract the private key. For this reason, we only recommend this for development.

⚠️ Warning: For production applications, we recommend using a separate service for token generation. Generating JWTs inside the client app could make your private key vulnerable to reverse engineering.

Minimal Setup for Testing

Let’s walk through configuring the minimal setup to work with this API.

Generating Keys for JWTs

Have a look here to learn how to generate a new key for signing and validating JWTs. You’ll use the public key when running server, and the private key in your sample application for signing the JWTs you use.

Running Server

We have a full guide explaining how you can run PSPDFKit Server in Docker here. Follow it, making sure to put the public key you previously generated into the docker-compose.yml in the JWT_PUBLIC_KEY. Once you have done this, you should have PSPDFKit Server running on your machine.

If you want to use PSPDFKit Processor, have a look here. Again, be sure to specify your key in the JWT_PUBLIC_KEY configuration.

Running the Conversion

Finally, you need to actually do the conversion in your app. Use PSPDFProcessor to perform the conversion. Before converting the file, you will need to create a JWT to authenticate your conversion request.

The JWT does not necessarily have to be created on the iOS app itself — you could also have a web service that takes the SHA-256 checksum of the file and creates a signed JWT for you. This guide covers how to create a JWT on the iOS app itself by using a third-party library.

  1. Add the third-party JWT library as a dependency to your project. This guide assumes the use of IBM-Swift/Swift-JWT for Swift, and yourkarma/JWT (version 3.0.0-beta.12 or above) for Objective-C.
  2. To generate the JWT, you will need the private key to sign the token, along with the SHA-256 checksum of the file to be converted. For larger files, you should not load the entire file into memory when calculating the checksum, but this action is out of scope for this basic example.

Here’s how to calculate the SHA-256 checksum:

Copy
1
2
3
4
5
6
7
8
9
10
11
// This function will calculate the SHA-256 checksum of the given data and return it in hex format.
// To use the `SHA256.hash` function, you will need to add `import CryptoKit` to the list of imports.
func sha256HexChecksumFor(data: Data) -> String {
    // Use CryptoKit to calculate the checksum.
    let checksum = SHA256.hash(data: data)

    // Convert the checksum to hex format.
    let checksumHexString = checksum.compactMap { String(format: "%02x", $0) }.joined()

    return checksumHexString
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// This function will calculate the SHA-256 checksum of the given data and return it in hex format.
// Requires #include <CommonCrypto/CommonDigest.h>
- (NSString *)sha256HexChecksumForData:(NSData *)data {
    // Use CommonCrypto to calculate the checksum.
    NSMutableData *checksum = [NSMutableData dataWithLength:CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(data.bytes, (CC_LONG)data.length, checksum.mutableBytes);

    const unsigned char *dataBuffer = (const unsigned char *)[data bytes];
    if (!dataBuffer) { return [NSString string]; }

    NSUInteger dataLength  = [data length];

    // Convert the checksum to hex format.
    NSMutableString *checksumHexString  = [NSMutableString stringWithCapacity:(dataLength * 2)];
    for (int i = 0; i < dataLength; ++i) {
        [checksumHexString appendString:[NSString stringWithFormat:@"%02lx", (unsigned long)dataBuffer[i]]];
    }

    return checksumHexString;
}
  1. Next up, set up the private key and claims payload, and use the third-party library to generate the JWT. Here’s how that looks:
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
33
34
35
func generateJWTForFileWith(url: URL) throws -> String? {
    // Load the file in memory.
    let fileData = try Data(contentsOf: url)

    // Calculate the SHA-256 checksum.
    let checksum = sha256HexChecksumFor(data: fileData)

    // Make sure to keep `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----`.
    let privateKeyString = "Private Key Here"

    // Convert the private key to `Data`.
    guard let pkData: Data = privateKeyString.data(using: .utf8) else { return nil }

    // Set up the signer, header, and claims.
    let signer = JWTSigner.rs256(privateKey: pkData)
    let header = Header(typ: "JWT")

    struct MyClaims: Claims {
        var exp: Date?
        var sha256: String
    }

    // Create the claims payload. The only two claims required are `exp` and `sha256`.
    let claims = MyClaims(
        exp: Date(timeIntervalSince1970: 9999999999),
        sha256: checksum
    )

    // Generate the JWT.
    let myJWT = JWT(header: header, claims: claims)
    let jwtEncoder = JWTEncoder(jwtSigner: signer)
    let jwtString = try jwtEncoder.encodeToString(myJWT)

    return jwtString
}
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
33
34
35
- (NSString *)generateJWTForFileWithURL:(NSURL *)url {
    // Load the contents of your file into an `NSData` object.
    NSData *fileData = [[NSFileManager defaultManager] contentsAtPath:url.path];

    // Get the SHA-256 checksum.
    NSString *checksum = [self sha256HexChecksumForData:fileData];

    // Make sure to remove `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----`.
    NSString *privateKeyString = @"Private Key Here";

    // Create the claims payload. The only two claims required are `exp` and `sha256`.
    NSDictionary *claimsPayload = @{
        @"exp": @(9999999999),
        @"sha256": checksum
    };

    // Create the signing data holder.
    id <JWTAlgorithmDataHolderProtocol> signDataHolder = [JWTAlgorithmRSFamilyDataHolder new]
                                                            .keyExtractorType([JWTCryptoKeyExtractor privateKeyWithPEMBase64].type)
                                                            .algorithmName(@"RS256")
                                                            .secret(privateKeyString);

    // Perform the signing process.
    JWTCodingResultType *signResult = [JWTEncodingBuilder encodePayload:claimsPayload].addHolder(signDataHolder).result;

    // Extract the JWT if signing was successful.
    NSString *jwt = nil;
    if (signResult.successResult) {
        jwt = signResult.successResult.encoded;
    } else {
        NSLog(@"%@ error: %@", self.debugDescription, signResult.errorResult.error);
    }

    return jwt;
}
  1. Finally, call PSPDFProcessor’s Office document conversion API to actually run the conversion:
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
// Local URL of the Office file that needs to be converted.
guard let officeFileURL = Bundle.main.url(forResource: "document", withExtension: "docx") else { return }

// Create a temporary URL for the converted file. There are two Processor APIs: one that writes
// the converted file to disk, and another one that keeps it in memory. An `outputURL` is needed
// only when using the first API.
let outputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("converted_document.pdf", isDirectory: false)

// Generate the JWT for the file to be converted. As mentioned earlier in the guide, JWTs don't have
// to necessarily be generated on the app itself. This could be a network service as well, which is
// the recommended way for production apps.
guard let jwt = try generateJWTForFileWith(url: officeFileURL) else { return }

// Change the server URL to point to your PSPDFKit Server's PDF conversion endpoint.
let serverURL = URL(string: "http://localhost:5000/i/convert_to_pdf")!

// Start the conversion using the Processor API.
Processor.generatePDF(from: officeFileURL, serverURL: serverURL, jwt: jwt, outputFileURL: outputURL) { actualOutputFileURL, error in
    if let error = error {
        // Conversion was unsuccessful.
        print(error.localizedDescription)
    } else if let actualOutputFileURL = actualOutputFileURL {
        // Conversion was successful. The converted file is now accessible at `actualOutputFileURL`.
        let document = Document(url: actualOutputFileURL)
    }
}
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
33
34
35
36
// Local URL of the Office file that needs to be converted.
NSURL *officeFileURL = [[NSBundle mainBundle] URLForResource:@"document" withExtension:@"docx"];

if (officeFileURL == nil) {
    NSLog(@"The local file was not found.");
    return;
}

// Create a temporary URL for the converted file. There are two Processor APIs: one that writes
// the converted file to disk, and another one that keeps it in memory. An `outputURL` is needed
// only when using the first API.
NSURL *outputURL = [[NSURL fileURLWithPath:NSTemporaryDirectory()] URLByAppendingPathComponent:@"converted_document.pdf" isDirectory:NO];

// Generate the JWT for the file to be converted. As mentioned earlier in the guide, JWTs don't have
// to necessarily be generated on the app itself. This could be a network service as well, which is
// the recommended way for production apps.
NSString *JWT = [self generateJWTForFileWithURL:officeFileURL];

if (JWT == nil) {
    NSLog(@"There was an error while trying to generate JWT.");
    return;
}

// Change the server URL to point to your PSPDFKit Server's PDF conversion endpoint.
NSURL *serverURL = [NSURL URLWithString:@"http://localhost:5000/i/convert_to_pdf"];

// Start the conversion using the Processor API.
[PSPDFProcessor generatePDFFromURL:officeFileURL serverURL:serverURL JWT:JWT outputFileURL:outputURL completionBlock:^(NSURL * _Nullable actualOutputFileURL, NSError * _Nullable error) {
    if (error) {
        // Conversion was unsuccessful.
        NSLog(@"%@", error.localizedDescription);
    } else if (actualOutputFileURL != nil) {
        // Conversion was successful. The converted file is now accessible at `actualOutputFileURL`.
        PSPDFDocument *document = [[PSPDFDocument alloc] initWithURL:actualOutputFileURL];
    }
}];