MS Office Converter for iOS

PSPDFKit for iOS can work with a Document Engine instance to provide conversion of Office documents to PDF files in your iOS application.

Architecture

The iOS or Android app using the PSPDFKit SDK uploads the Office file and a correctly formatted and signed JSON Web Token (JWT) to Document Engine. Document Engine will convert the file and immediately return the converted PDF. The app can then display the PDF. For more information, see the Document Engine guide for opening office documents.

Required Setup

To make use of this, the following is required:

  • A running instance of Document Engine

    • Your license also needs to include the Office Files component.

  • A way to obtain 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 Document Engine

We have a full guide explaining how you can run Document Engine 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’ve done this, you should have Document Engine running on your machine.

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’ll need to create a JWT to authenticate your conversion request.

The JWT doesn’t 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’ll need the private key to sign the token, along with the SHA-256 checksum of the file to be converted. For larger files, you shouldn’t 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:

// 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'll 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
}
// 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:

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
}
- (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:

// 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
// necessarily have to 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 Document Engine'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)
    }
}
// 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
// necessarily have to 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 Document Engine'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];
    }
}];

Offline Office Conversion

PSPDFKit for iOS also has the ability to convert Microsoft Office documents to PDF using Processor.generatePDF(from:outputFileURL:options:completionBlock:). This doesn’t require a server or network connection. 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 don’t own these frameworks, nor do we have access to the source code, we’re unable to fix bugs in Apple’s parsers. Therefore, we strongly recommend preferring the server-based conversion architecture described above.