Save PDF annotations on Android
By default, Nutrient will save annotations asynchronously and automatically when the onStop()
lifecycle event is called.
Nutrient 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 forWRITE_EXTERNAL_STORAGE
permission when opening files from external storage. This will allow Nutrient to bypass the slowContentProvider
APIs. -
If the PDF was opened from a custom
DataProvider
, it has to implementWriteableDataProvider
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 Nutrient can render the content. If Nutrient 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, Nutrient 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.
![]()
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 returnfalse
.
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
.
![]()
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 callfragment.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(); }
![]()
We also have an extensive guide about the
DocumentListener
interface.