Save PDF annotations to an external source on Android

Saving annotations to an external source is an operation that can be tackled in different ways. With Nutrient Android SDK, you can choose the most appropriate solution depending on your needs. If you want a hassle-free solution that works out of the box, we suggest choosing our proprietary synchronization feature, Nutrient Instant.

Saving annotations to an XFDF file

XFDF is an XML-based standard from Adobe XFDF (ISO 19444-1:2016) for encoding annotations and form field values. It has various limitations, but it’s compatible with Adobe Acrobat and several other third-party frameworks.

You can export annotations and form field values from a document to an XFDF file like so:

// List of annotations from the document to be exported.
val listOfAnnotationsToExport = ...

// List of form fields from the document to be exported.
val listOfFormFieldsToExport = ...

// `ByteArrayOutput` stream pointing to the XFDF file into which to write the data.
val byteArrayOutputStream = ...

// The async `write` method is recommended (so you can easily offload writing from the UI thread).
XfdfFormatter.writeXfdfAsync(pdfDocument, listOfAnnotationsToExport, listOfFormFieldsToExport, byteArrayOutputStream)
        .subscribeOn(Schedulers.io()) // Specify the thread on which to write XFDF.
        .subscribe(..)
// List of annotations from the document to be exported.
List<Annotation> listOfAnnotationsToExport = ... ;

// List of form fields from the document to be exported.
List<FormField> listOfFormFieldsToExport = ... ;

// `ByteArrayOutput` stream pointing to the XFDF file into which to write the data.
ByteArrayOutputStream byteArrayOutputStream = ... ;

// The async `write` method is recommended (so you can easily offload writing from the UI thread).
XfdfFormatter.writeXfdfAsync(pdfDocument, listOfAnnotationsToExport, listOfFormFieldsToExport, byteArrayOutputStream)
        .subscribeOn(Schedulers.io()) // Specify the thread on which to write XFDF.
        .subscribe(..);

Once you have the XFDF file saved locally, you can upload it to a remote server using your favorite library. Below you’ll see an example that uses OkHttp. We chose to use it because it’s a common dependency most projects already have, but you’re by no means required to use it if you have a different networking setup already in place:

val data = byteArrayOutputStream.toByteArray()
val requestBody = data.toRequestBody("application/octet-stream".toMediaType())
val request = Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build()
// TODO: Use an `OkHttpClient` to fire the request.
final byte[] data = byteArrayOutputStream.toByteArray();
final RequestBody requestBody = RequestBody.create(data, MediaType.parse("application/octet-stream"));
final Request request = new Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build();
// TODO: Use an `OkHttpClient` to fire the request.

Saving annotations to an Instant JSON file

Instant JSON is our approach to bringing annotations into a modern format while keeping all important properties to make the Instant JSON spec work with PDF. It’s fully documented and supports long-term storage.

Instant JSON stores PDF changes like annotations in a separate JSON file. This means that a PDF document will only need to be transferred once, and all changes will be added as an overlay to the existing PDF. This approach significantly reduces the bandwidth, since you only need to transfer the JSON instead of the complete PDF.

To generate Instant JSON for documents, use the static exportDocumentJson() or exportDocumentJsonAsync() of the DocumentJsonFormatter class. Pass in the document from which you wish to retrieve currently unsaved changes in JSON form, as well as an ByteArrayOutputStream that will receive the JSON string. For example:

val byteArrayOutputStream = ByteArrayOutputStream()
DocumentJsonFormatter.exportDocumentJson(document, byteArrayOutputStream)
val jsonString = byteArrayOutputStream.toString()
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DocumentJsonFormatter.exportDocumentJson(document, byteArrayOutputStream);
final String jsonString = byteArrayOutputStream.toString();

Once you have the Instant JSON file saved locally, you can upload it to a remote server using your favorite library. Below you’ll see an example that uses OkHttp. We chose to use it because it’s a common dependency most projects already have, but you’re by no means required to use it if you have a different networking setup already in place:

val data = byteArrayOutputStream.toByteArray()
val requestBody = data.toRequestBody("application/json".toMediaType())
val request = Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build()
// TODO: Use an `OkHttpClient` to fire the request.
final byte[] data = byteArrayOutputStream.toByteArray();
final RequestBody requestBody = RequestBody.create(data, MediaType.parse("application/json"));
final Request request = new Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build();
// TODO: Use an `OkHttpClient` to fire the request.

Embedding annotations into a PDF document

Nutrient Android SDK automatically saves annotations into a PDF document when their fragment lifecycle onStop() method is called. Manual saving can always be triggered by calling saveIfModifiedAsync() from the main thread:

/** Manually saving inside the activity. **/
fun saveDocument() {
    // Won't save if the document inside `PdfFragment` is `null`.
    val document : PdfDocument = pdfFragment.document ?: return

    document.saveIfModifiedAsync()
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(object : DisposableSingleObserver<Boolean>() {
            override fun onError(e : Throwable) {
                /** Saving has failed. The exception holds additional failure details. **/
                Toast.makeText(context, "Failed to save the document!", Toast.LENGTH_SHORT).show()
            }

            override fun onSuccess(saved : Boolean) {
                if (saved) {
                    /** Changes were saved successfully! **/
                    Toast.makeText(context, "Saved successfully!", Toast.LENGTH_SHORT).show()
                } else {
                    /** There was nothing to save. **/
                    Toast.makeText(context, "There were no changes in the file.", Toast.LENGTH_SHORT).show()
                }
            }
        })
}
/** Manually saving inside the activity. **/
PdfDocument document = getPdfFragment().getDocument();
if (document == null) {
    // No document loaded.
    return;
}
document.saveIfModifiedAsync()
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new DisposableSingleObserver<Boolean>() {
             @Override
             public void onError(Throwable e) {
                 /** Saving has failed. The exception holds additional failure details. **/
                Toast.makeText(context, "Failed to save the document!", Toast.LENGTH_SHORT).show();
             }

             @Override
             public void onSuccess(Boolean saved) {
                 if (saved) {
                     /** Changes were saved successfully! **/
                     Toast.makeText(context, "Saved successfully!", Toast.LENGTH_SHORT).show();
                 } else {
                    /** There was nothing to save. **/
                     Toast.makeText(context, "There were no changes in the file.", Toast.LENGTH_SHORT).show();
                 }
             }
        });

Once the annotations are saved, the PDF document can be uploaded to a remote server. Uploading the whole PDF document over the network may be an expensive solution, as the size may vary depending on many factors like the number of pages and the images embedded.

Below you’ll see an example that uses OkHttp to upload the PDF document to a remote server. We chose to use it because it’s a common dependency most projects already have, but you’re by no means required to use it if you have a different networking setup already in place:

val dataProvider = activity.requirePdfFragment().document?.documentSource?.dataProvider ?: return
val data = dataProvider.read(dataProvider.size, 0)
val requestBody = data.toRequestBody("application/pdf".toMediaType())
val request = Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build()
// TODO: Use an `OkHttpClient` to fire the request.
final PdfDocument document = activity.requirePdfFragment().getDocument();
if (document == null) return;
final DataProvider dataProvider = document.getDocumentSource().getDataProvider();
if (dataProvider == null) return;

final byte[] data = dataProvider.read(dataProvider.getSize(), 0);
final RequestBody requestBody = RequestBody.create(data, MediaType.parse("application/pdf"));
final Request request = new Request.Builder().url("http://127.0.0.1:12345").post(requestBody).build();
// TODO: Use an `OkHttpClient` to fire the request.

Using instant synchronization

By default, Nutrient Instant automatically synchronizes annotations with your Document Engine in real time. This is configurable, and you can instead choose for it to sync manually when your app requests it.

Using a network, especially a cellular network, is one of the most energy-intensive tasks on mobile devices. While we do our best to minimize the energy impact of Instant, it can be reduced further by disabling listening for changes from the server or by syncing less often after local changes are made.

Syncing after making local changes and listening for server changes can be configured separately. However, syncing always sends all local changes and fetches all changes from the server. It isn’t possible to fetch remote changes without pushing local changes or to push local changes without fetching remote changes.

Automatic syncing

By default, when you show a document managed by Instant in an InstantPdfFragment, real-time syncing of annotations is fully automatic: Instant will push local changes to the server as they happen and listen for changes from the server.

If you don’t show the document in an InstantPdfFragment, you can enable listening for changes using setListenToServerChanges on the InstantPdfDocument.

Time lapse of changes

By default, Instant will use the network efficiently by coalescing changes with a one-second delay. You can reduce energy consumption at the cost of less immediate syncing by using setDelayForSyncingLocalChanges on InstantPdfDocument to increase the delay.