Save PDF Annotations on Android

By default, PSPDFKit will save annotations asynchronously and automatically when the onStop() lifecycle event is called.

PSPDFKit can write annotations into a PDF under the following conditions:

  • The PDF must be in a writeable location. This means that it either has to be in a writeable directory on the file system, or the ContentProvider from which the file is loaded must allow saving. For maximum performance, ask for WRITE_EXTERNAL_STORAGE permission when opening files from external storage. This will allow PSPDFKit to bypass the slow ContentProvider APIs.

  • If the PDF was opened from a custom DataProvider, it has to implement WriteableDataProvider and properly handle writing to a file.

  • The PDF must be valid according to the Adobe PDF specification. Some PDFs are broken but still work somewhat, so PSPDFKit can render the content. If PSPDFKit detects a mismatch in the object tree or is unable to find objects, annotation saving will be stopped, since there would be a risk of damaging the document.

Annotation Saving

By default, PSPDFKit auto saves changes to a document and to annotations inside PdfFragment#onStop — effectively, this means every time the fragment is sent to the background, e.g. when switching to another application or when leaving the viewer activity. You can disable auto saving via the #autosaveEnabled setter on the PdfConfiguration.Builder:

// By default, auto save is enabled.
val config = PdfConfiguration.Builder()
    .autosaveEnabled(false)
    .build()

val fragment = PdfFragment.newInstance(documentUri, config)
...
// By default, auto save is enabled.
final PdfConfiguration config = new PdfConfiguration.Builder()
    .autosaveEnabled(false)
    .build();

final PdfFragment fragment = PdfFragment.newInstance(documentUri, config);
...

If you’re using the PdfActivity, you can also deactivate auto save via the #autosaveEnabled setter of the PdfActivityConfiguration.Builder:

// By default, auto save is enabled.
val config = PdfActivityConfiguration.Builder(context)
    .autosaveEnabled(false)
    .build()

PdfActivity.showDocument(context, documentUri, config)
...
// By default, auto save is enabled.
final PdfActivityConfiguration config =
    new PdfActivityConfiguration.Builder(context)
        .autosaveEnabled(false)
        .build();

PdfActivity.showDocument(context, documentUri, config);
...

Modifying and Saving Annotations

If an annotation is modified (i.e. if it has been changed since the document has been loaded) a call to Annotation#isModified will return true. Furthermore, the PdfDocument#wasModified method will return true if annotations were added, changed, or removed. Once you save the document and its annotations, they’re no longer marked as modified.

ℹ️ Note: If you’re editing annotations using one of the annotation tools, modifications to the edited annotation and document will only be visible after you exit the current tool mode by calling PdfFragment#exitCurrentlyActiveMode. If the annotation tool is still active (i.e. the tool is selected in the annotation creation toolbar), PdfDocument#wasModified will still return false.

To save a document and its annotations, you can use any of the synchronous or asynchronous save methods on the PdfDocument class. The following example uses PdfDocument#saveIfModified, which writes the document back to its original location after testing if it has been modified:

override fun onDocumentLoaded(document : PdfDocument) {
    assert(document.wasModified() == false)

    // Add an annotation to the document.
    val annotation = NoteAnnotation(0, RectF(100, 132, 132, 100), "Test annotation", NoteAnnotation.CROSS)
    document.annotationProvider.addAnnotationToPage(annotation)

    assert(annotation.isModified() == true)
    assert(document.wasModified() == true)

    // This will write the document back to its original location.
    document.saveIfModified()

    assert(annotation.isModified() == false)
    assert(document.wasModified() == false)
}
@Override public void onDocumentLoaded(@NonNull PdfDocument document) {
    assert document.wasModified() == false;

    // Add an annotation to the document.
    NoteAnnotation annotation = new NoteAnnotation(0, new RectF(100, 132, 132, 100), "Test annotation", NoteAnnotation.CROSS);
    document.getAnnotationProvider().addAnnotationToPage(annotation);

    assert annotation.isModified() == true;
    assert document.wasModified() == true;

    // This will write the document back to its original location.
    document.saveIfModified();

    assert annotation.isModified() == false;
    assert document.wasModified() == false;
}

Document-Saving Callbacks

The DocumentListener interface provides three callback methods that allow you to listen to and intercept saving attempts. #onDocumentSave(PdfDocument, DocumentSaveOptions) is called right before the PdfFragment or the PdfActivity save a document, allowing you to alter the DocumentSaveOptions or cancel the saving attempt completely by returning false.

ℹ️ Note: Saving callbacks aren’t called when using the PdfDocument save methods directly, but only when the fragment or activity is saving the document — for example, when auto save is enabled or when you explicitly call fragment.save().

/**
 * The password used to save the document may be null. Your app can
 * ask for this prior to saving.
 */
private var documentPassword : String? = null

override fun onDocumentSave(document : PdfDocument, saveOptions : DocumentSaveOptions) : Boolean {
    saveOptions.setPassword(documentPassword)

    // By returning `true`, saving is continued. Alternatively, you could return `false` to cancel saving.
    return true
}
/**
 * The password used to save the document may be null. Your app can
 * ask for this prior to saving.
 */
private String documentPassword;

@Override
public boolean onDocumentSave(PdfDocument document, DocumentSaveOptions saveOptions) {
    saveOptions.setPassword(documentPassword);

    // By returning `true`, saving is continued. Alternatively, you could return `false` to cancel saving.
    return true;
}

The DocumentListener#onDocumentSaved(PdfDocument) method is called after the document has been successfully saved. If the document couldn’t be saved due to an error, the #onDocumentSaveFailed(Throwable) method is called instead, and it passes in the exception that caused the failure:

override fun onDocumentSaved(document : PdfDocument) {
    Toast.makeText(context, "Document successfully saved.", Toast.LENGTH_SHORT).show()
}

override fun onDocumentSaveFailed(exception : Throwable?) {
    AlertDialog.Builder(context)
        .setMessage("Error while saving the document. Please try again.")
        .show()
}
@Override
public void onDocumentSaved(@NonNull PdfDocument document) {
    Toast.makeText(context, "Document successfully saved.", Toast.LENGTH_SHORT).show();
}

@Override
public void onDocumentSaveFailed(Throwable exception) {
    new AlertDialog.Builder(context)
        .setMessage("Error while saving the document. Please try again.")
        .show();
}

💡 Tip: We also have an extensive guide about the DocumentListener interface.