Rendering annotations on Android
If you need to render single annotation
objects into Bitmap
instances, Nutrient offers several options for doing this. This article serves as a step-by-step guide to get you started quickly.
ℹ️ Note: Before loading a document and rendering its annotations, you have to initialize Nutrient by providing your license. Make sure to follow the steps in our [integrating Nutrient][] guide before continuing with this guide, in order to have Nutrient fully initialized.
Loading a document
The first step in rendering annotations of a document is to load the document into memory. The PdfDocumentLoader
class offers a variety of static methods for doing this. The following example loads a PDF document from the app’s assets using PdfDocumentLoader.openDocument()
and a file:///android_asset
URI. The complete list of available document sources and ways to load them is outlined in our guide about using an activity:
// Use this `Uri` format to access files inside your app's assets. val documentUri = Uri.parse("file:///android_asset/shopping-center-plan.pdf") // This synchronously opens the document. To keep your app UI responsive, either // don't run this on the main thread, or use `openDocumentAsync()` instead. val document: PdfDocument = PdfDocumentLoader.openDocument(context, documentUri)
// Use this `Uri` format to access files inside your app's assets. final Uri documentUri = Uri.parse("file:///android_asset/shopping-center-plan.pdf"); // This synchronously opens the document. To keep your app UI responsive, either // don't run this on the main thread, or use `openDocumentAsync()` instead. final PdfDocument document = PdfDocumentLoader.openDocument(context, documentUri);
💡 Tip: The synchronous
PdfDocumentLoader.openDocument()
method used in this example will throw an exception if there is an error while loading the document. You should wrap the call intotry/catch
to handle any errors properly.
Retrieving annotations
To retrieve the annotations of your loaded PdfDocument
instance, get a reference of your document’s AnnotationProvider
by calling document.getAnnotationProvider()
. You can then query annotations by page number or annotation type:
// Retrieves all ink annotations of the document. val inkAnnotations = document .getAnnotationProvider() .getAllAnnotationsOfType(EnumSet.of(AnnotationType.INK))
// Retrieves all ink annotations of the document. final List<Annotation> inkAnnotations = document .getAnnotationProvider() .getAllAnnotationsOfType(EnumSet.of(AnnotationType.INK));
💡 Tip: If you want to retrieve all annotations of your document at once, you can call
getAllAnnotationsOfType()
usingAnnotationProvider#ALL_ANNOTATION_TYPES
.
Rendering annotations
The rendering of annotations can be performed synchronously or asynchronously using the renderToBitmap()
and renderToBitmapAsync()
methods of Annotation
, respectively.
Preparing a Bitmap
You first need to create a target Bitmap
instance, which is usually done using Bitmap.createBitmap()
. Since annotations can have transparent regions as well, it makes sense to use a bitmap format that supports this — Bitmap.Config.ARGB_8888
is a good choice:
// Create a bitmap with an alpha channel so that transparent parts // of the annotation are properly displayed. val bitmap = Bitmap.createBitmap( bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888)
// Create a bitmap with an alpha channel so that transparent parts // of the annotation are properly displayed. final Bitmap bitmap = Bitmap.createBitmap( bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
While rendering, the annotation will be stretched to fill the entire bitmap area. If you want to maintain the aspect ratio of the annotation, you need to create a bitmap with the same aspect ratio as the annotation.
Calculating the Bitmap size
You can calculate the Bitmap
size from the annotation size stored within the bounding box of your annotation. To retrieve the bounding box, use annotation.getBoundingBox()
:
// The annotation has a size, which can be calculated from its bounding box. val annotationWidth = annotation.boundingBox.width() val annotationHeight = -annotation.boundingBox.height() // Create a bitmap with a width of 300 pixels. The height of the bitmap is derived // by proportionally fitting the annotation into the width of the bitmap. val bitmapWidth = 300 val heightFactor = bitmapWidth / annotationWidth val bitmapHeight = (annotationHeight * heightFactor).toInt()
// The annotation has a size, which can be calculated from its bounding box. final float annotationWidth = annotation.getBoundingBox().width(); final float annotationHeight = -annotation.getBoundingBox().height(); // Create a bitmap with a width of 300 pixels. The height of the bitmap is derived // by proportionally fitting the annotation into the width of the bitmap. final int bitmapWidth = 300; final float heightFactor = bitmapWidth / annotationWidth; final int bitmapHeight = (int) (annotationHeight * heightFactor);
ℹ️ Note: The bounding box holds PDF coordinates, which, unlike Android’s coordinates, are vertically flipped. As such, the returned
height()
is negative and has to be inverted.
Synchronous rendering
To synchronously render an annotation into a Bitmap
instance, call renderToBitmap()
on your annotation, passing in the already prepared Bitmap
instance:
// This is the bitmap you already prepared (see explanation above). annotation.renderToBitmap(bitmap)
// This is the bitmap you already prepared (see explanation above).
annotation.renderToBitmap(bitmap);
⚠️ Warning: When calling
annotation.renderToBitmap()
, the method will block until rendering has finished, so don’t use it on your main thread! Instead, use asynchronous rendering.
Asynchronous rendering
In most instances, it’s better to render an annotation asynchronously to ensure you do not block the current thread. This can be done by using renderToBitmapAsync()
, which can also be used on the main thread. The method returns Single<Bitmap>
, which will emit the Bitmap
once rendering is done:
val annotationRendering = annotation.renderToBitmapAsync(bitmap) .subscribe { bitmap, throwable -> // This will be called with either your bitmap // or a throwable (if there was an error while rendering). } // If you ever need to cancel the async render operation, use this. annotationRendering.dispose()
Disposable annotationRendering = annotation.renderToBitmapAsync(bitmap) .subscribe((bitmap, throwable) -> { // This will be called with either your bitmap // or a throwable (if there was an error while rendering). }); // If you ever need to cancel the async render operation, use this. annotationRendering.dispose();
💡 Tip: Rendering will only start once you call
subscribe()
onSingle<Bitmap>
. Since rendering is asynchronous, you can cancel it at any time — for example, by callingdispose()
on theDisposable
that is returned bysubscribe()
.
Providing an annotation render configuration
Both synchronous and asynchronous render methods allow you to specify an AnnotationRenderConfiguration
object for defining render options. You can create the configuration using its Builder
class:
val config = AnnotationRenderConfiguration.Builder() .toGrayscale(true) .build() // Renders the annotation to the bitmap in grayscale. annotation.renderToBitmap(bitmap, config)
final AnnotationRenderConfiguration config = new AnnotationRenderConfiguration.Builder() .toGrayscale(true) .build(); // Renders the annotation to the bitmap in grayscale. annotation.renderToBitmap(bitmap, config);
Available render options
It is possible to specify several different render configuration options via the Builder
, including:
-
Inversion of the annotation colors using
invertColors()
. -
Grayscale rendering using
toGrayscale()
. -
Form field highlight color, color of selected form items, and the border color for required form fields using
formHighlightColor()
,formItemHighlightColor()
, andformRequiredFieldBorderColor()
, respectively.