Office Conversion

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

Architecture

In order to convert Office files, they first get sent to PSPDFKit Server. There, they will be converted as discussed here. Finally, PSPDFKit for Android 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.
    • 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.

Running the Conversion

Now you need to actually do the conversion in your app. Use OfficeToPdfConverter to perform the conversion and jjwt to generate the needed JWTs.

  1. Add jjwt as a dependency to your project.
  2. Put the private key into a string resource named jwt_private_key. Remove the -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY----- sections; only the Base64-encoded key should remain.
  3. Next up, you need to load the private key in your app.
Copy
1
2
3
4
5
6
// First you need the private key you use to sign the JWT.
val base64PrivateKey = getString(R.string.jwt_private_key)
val binaryPrivateKey = Base64.decode(base64PrivateKey, Base64.DEFAULT)
val spec = PKCS8EncodedKeySpec(binaryPrivateKey)
val keyFactory = KeyFactory.getInstance("RSA")
val parsedPrivateKey = keyFactory.generatePrivate(spec)
Copy
1
2
3
4
5
6
// First you need the private key you use to sign the JWT.
final String base64PrivateKey = getString(R.string.jwt_private_key);
final byte[]  binaryPrivateKey = Base64.decode(base64PrivateKey, Base64.DEFAULT);
final PKCS8EncodedKeySpec spec = new  PKCS8EncodedKeySpec(binaryPrivateKey);
final KeyFactory keyFactory  = KeyFactory.getInstance("RSA");
final PrivateKey parsedPrivateKey = keyFactory.generatePrivate(spec);
  1. Now you need to load your Office file and generate the SHA-256 of it, since this is going to be a part of the JWT.

You can use these methods to convert the bytes of your file to a SHA-256:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private fun generateSha256(data: ByteArray): String {
    try {
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(data)
        return bytesToHexString(hash)
    } catch (e: Exception) {
        e.printStackTrace()
    }
    return ""
}

private fun bytesToHexString(bytes: ByteArray): String {
    val sb = StringBuffer()
    for (i in bytes.indices) {
        val hex = Integer.toHexString(0xFF and bytes[i].toInt())
        if (hex.length == 1) {
            sb.append('0')
        }
        sb.append(hex)
    }
    return sb.toString()
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private String generateSha256(final byte[] data) {
    try {
        final MessageDigest digest = MessageDigest.getInstance("SHA-256");
        final byte[] hash = digest.digest(data);
        return bytesToHexString(hash);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return "";
}

private String bytesToHexString(final byte[] bytes) {
    final StringBuffer sb =  new StringBuffer();
    for (int i = 0; i < bytes.length; i++) {
        final String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

For larger files, you should operate on the InputStream directly instead of loading the entire file into memory, but this action is out of scope for this basic example.

Your code should look like this now:

Copy
1
2
3
4
5
6
// First you need the private key you use to sign the JWT.
...

// Next you need to generate the SHA-256 of the file you want to convert.
val fileData = ...
val sha256 = generateSha256(fileData)
Copy
1
2
3
4
5
6
// First you need the private key you use to sign the JWT.
...

// Next you need to generate the SHA-256 of the file you want to convert.
final byte[] fileData = ...
final String sha256 = generateSha256(fileData);
  1. You can now generate the final JWT that you will send to your PSPDFKit Server instance.
Copy
1
2
3
4
5
6
7
// Now create the actual JWT.
// Set the expiration for five minutes in the future.
val claims = Jwts.claims().setExpiration(Date(Date().time + 5 * 60 * 1000))
// Put in your SHA-256.
claims["sha256"] = sha256
// And finally sign the JWT.
val jwt = Jwts.builder().setClaims(claims).signWith(parsedPrivateKey).compact()
Copy
1
2
3
4
5
6
7
// Now create the actual JWT.
// Set the expiration for five minutes in the future.
final Claims claims = Jwts.claims().setExpiration(new Date(new Date().getTime() + 5 * 60 * 1000));
// Put in your SHA-256.
claims.put("sha256", sha256);
// And finally sign the JWT.
final String jwt = Jwts.builder().setClaims(claims).signWith(parsedPrivateKey).compact();
  1. Finally you can now call OfficeToPdfConverter to actually run the conversion.
Copy
1
2
3
4
5
6
7
8
9
// Finally you can perform the actual conversion.
OfficeToPdfConverter.fromUri(this, officeFileUri, Uri.parse("http://localhost:5000/"), jwt)
    .convertToPdfAsync()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { file: File?, throwable: Throwable? ->
        if (file != null) {
            showDocument(this, Uri.fromFile(file), PdfActivityConfiguration.Builder(this).build())
        } else throwable?.printStackTrace()
    }
Copy
1
2
3
4
5
6
7
8
9
10
11
// Finally you can perform the actual conversion.
OfficeToPdfConverter.fromUri(this, officeFileUri, Uri.parse("http://localhost:5000/"), jwt)
    .convertToPdfAsync()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe((file, throwable) -> {
        if (file != null) {
            PdfActivity.showDocument(this, Uri.fromFile(file), new PdfActivityConfiguration.Builder(this).build());
        } else if (throwable != null) {
            throwable.printStackTrace();
        }
    });
  1. There is one last step you need to take before this works: You need to allow your Android device to talk to the locally running server by running adb reverse tcp:5000 tcp:5000.

  2. If you now run this code on your Android device, the converted PDF should be opened as soon as the conversion on the server is done.

Just to recap, here is the final code again:

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
// First you need the private key you use to sign the JWT.
val base64PrivateKey = getString(R.string.jwt_private_key)
val binaryPrivateKey = Base64.decode(base64PrivateKey, Base64.DEFAULT)
val spec = PKCS8EncodedKeySpec(binaryPrivateKey)
val keyFactory = KeyFactory.getInstance("RSA")
val parsedPrivateKey = keyFactory.generatePrivate(spec)

// Next you need to generate the SHA-256 of the file you want to convert.
val fileData = ...
val sha256 = generateSha256(fileData)

// Now create the actual JWT.
// Set the expiration for five minutes in the future.
val claims = Jwts.claims().setExpiration(Date(Date().time + 5 * 60 * 1000))
// Put in your SHA-256.
claims["sha256"] = sha256
// And finally sign the JWT.
val jwt = Jwts.builder().setClaims(claims).signWith(parsedPrivateKey).compact()

// Finally you can perform the actual conversion.
OfficeToPdfConverter.fromUri(this, officeFileUri, Uri.parse("http://localhost:5000/"), jwt)
    .convertToPdfAsync()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { file: File?, throwable: Throwable? ->
        if (file != null) {
            showDocument(this, Uri.fromFile(file), PdfActivityConfiguration.Builder(this).build())
        } else throwable?.printStackTrace()
    }
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
// First you need the private key you use to sign the JWT.
final String base64PrivateKey = getString(R.string.jwt_private_key);
final byte[]  binaryPrivateKey = Base64.decode(base64PrivateKey, Base64.DEFAULT);
final PKCS8EncodedKeySpec spec = new  PKCS8EncodedKeySpec(binaryPrivateKey);
final KeyFactory keyFactory  = KeyFactory.getInstance("RSA");
final PrivateKey parsedPrivateKey = keyFactory.generatePrivate(spec);

// Next you need to generate the SHA-256 of the file you want to convert.
final byte[] fileData = ...
final String sha256 = generateSha256(fileData);

// Now create the actual JWT.
// Set the expiration for five minutes in the future.
final Claims claims = Jwts.claims().setExpiration(new Date(new Date().getTime() + 5 * 60 * 1000));
// Put in your SHA-256.
claims.put("sha256", sha256);
// And finally sign the JWT.
final String jwt = Jwts.builder().setClaims(claims).signWith(parsedPrivateKey).compact();

// Finally you can perform the actual conversion.
OfficeToPdfConverter.fromUri(this, officeFileUri, Uri.parse("http://localhost:5000/"), jwt)
    .convertToPdfAsync()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe((file, throwable) -> {
        if (file != null) {
            PdfActivity.showDocument(this, Uri.fromFile(file), new PdfActivityConfiguration.Builder(this).build());
        } else if (throwable != null) {
            throwable.printStackTrace();
        }
    });