Drawing content above displayed documents

An app using Nutrient can draw content above a displayed document using PdfDrawable, which is a subtype of Android’s Drawable(opens in a new tab) class and thus is very similar in use. This article guides you through the process of creating a drawable that paints a red rectangle on a document, positioning it using PDF page coordinates.

For more information on how to use the Drawable API to draw watermarks, check out WatermarkExample inside the Catalog app. Another great resource is ScreenReaderExample, which is a complete example of all the features and elements discussed in this article.

Using drawables

To draw a PdfDrawable onto a page, you need to provide it to the PdfFragment using a PdfDrawableProvider. In addition, you can provide it to PdfThumbnailBar and PdfThumbnailGrid in order to draw on thumbnails.

You can think of the PdfDrawableProvider like the adapter of a list view, but instead of creating and serving list view items, it serves drawables. A very simple implementation of a drawable provider that only serves a single RectDrawable that is painted on every page may look like this:

// The drawable provider needs to return a list of drawables for a page.
// Since we're only serving a single drawable, we create a one-element list.
val drawables = Collections.singletonList(SquareDrawable(RectF(0f, 100f, 100f, 0f)))
// This is the drawable provider. You only need to implement a single method.
val provider = object : PdfDrawableProvider() {
override fun getDrawablesForPage(
context: Context,
document: PdfDocument,
pageIndex: Int): List<PdfDrawable>? {
// Return the same drawable for every requested page.
return drawables
}
}

To start drawing the PdfDrawable on the page, you need to add the provider by calling addDrawableProvider(provider) on the fragment:

pdfFragment.addDrawableProvider(provider)

To draw on thumbnails for PdfThumbnailBar and PdfThumbnailGrid, use the following:

// Thumbnail Grid
pspdfKitViews.thumbnailGridView?.addDrawableProvider(provider)
// Thumbnail Bar
pspdfKitViews.thumbnailBarView?.addDrawableProvider(provider)

Creating a custom drawable

Your custom drawable needs to extend the PdfDrawable, which itself extends Android’s abstract Drawable(opens in a new tab) class and requires a couple of methods to be overridden and implemented. We will look at them one after another:

public void draw(Canvas canvas);
public void setAlpha(int alpha);
public void setColorFilter(ColorFilter colorFilter);
public int getOpacity();

The #draw(Canvas)(opens in a new tab) method is where the drawable performs all of its onscreen drawing operations by issuing drawing commands on the provided Canvas(opens in a new tab). #setAlpha(int)(opens in a new tab) and #setColorFilter(ColorFilter)(opens in a new tab) are used to tell the drawable to adapt to presentation specifics, e.g. to enable translucency or blending modes.

Preparing to draw

Drawing on a canvas requires a Paint(opens in a new tab) object specifying drawing settings like color, thickness, opacity, and more. Since subsequent drawing passes should always happen in less than 16 ms(opens in a new tab), the used Paint(opens in a new tab) has to be prepared upfront — for example, in the constructor of your drawable:

class RectDrawable : PdfDrawable() {
private val paint: Paint = Paint()
init {
paint.color = Color.RED
paint.style = Paint.Style.FILL
}
}

Perform drawing

Inside the draw() method, you can use the provided Canvas(opens in a new tab), passing it the paint. There are many different call methods — this example uses the Canvas#drawRect(Rect, Paint)(opens in a new tab) method:

override fun draw(canvas: Canvas) {
canvas.drawRect(bounds, paint)
}

The Drawable#getBounds(opens in a new tab) method used by the above snippet returns the screen rectangle that will be covered by the drawable. Thus, our RectDrawable will take all of the space it is assigned and will draw a single rectangle inside that space.

Next, a drawable must implement the #getOpacity(opens in a new tab) method, which returns a flag specifying whether the background shines through the drawable, or whether the drawable is completely opaque. This is mainly for improving drawing performance — something you should always aim for:

override fun int getOpacity() {
// The drawable paints a solid rectangle. Since nothing from behind the drawable
// is visible inside the drawable bounds, we specify the drawable as `OPAQUE`.
return PixelFormat.OPAQUE
}

ℹ️ Note: The #getOpacity(opens in a new tab) method must return one PixelFormat(opens in a new tab)UNKNOWN(opens in a new tab), OPAQUE(opens in a new tab), TRANSPARENT(opens in a new tab), or TRANSLUCENT(opens in a new tab) — depending on the content that is drawn.

Defining drawable bounds

Setting new bounds of your drawable is as simple as calling this.setBounds(newDrawableBounds)(opens in a new tab) inside your drawable:

// This will cause the drawable to paint a 100x100 px square
// at the very top-left corner of the page.
bounds = Rect(0, 0, 100, 100)

This works fine if you want to draw something in pixels (i.e. in view coordinates). When doing so, the drawable will always be at the top left of the page, keeping the same size without being influenced by the zoom.

Drawing in PDF coordinates

If you want to draw on a page and align your drawables with page text, annotations, or any other content on the page, you will first need to understand how the PDF coordinate space works. Since the Canvas(opens in a new tab) object expects coordinates in pixel space instead of PDF coordinates, the PdfDrawable#getPdfToPageTransformation provides a transformation Matrix(opens in a new tab) that you can use to convert your PDF coordinates to view coordinates:

// This is a 100x100pt square on the bottom left of the page.
// Don't confuse this with pixels. Usually 1pt is 1/72 inch.
val pageCoordinates = RectF(0f, 100f, 100f, 0f)
// This will contain the screen coordinates (in pixels).
val screenCoordinates = RectF()
private fun updateBoundingBox() {
// Transform PDF coordinates into screen coordinates.
pdfToPageTransformation.mapRect(screenCoordinates, pageCoordinates)
// Since the drawable bounds are `Rect` (`int`) and our transformed
// screen coordinates are `RectF` (`float`), we need to round the coordinates before applying.
val newBounds = this.bounds
screenCoordinates.roundOut(newBounds)
this.bounds = newBounds
}

Every time the page you’re drawing to is moved — for example, because of zooming or scrolling — the transformation Matrix(opens in a new tab) is recalculated and the drawable is notified of this via the PdfDrawable#updatePdfToViewTransformation method. You can override this method and use it as a hook for recalculating the screen coordinates:

override fun updatePdfToViewTransformation(matrix: Matrix) {
super.updatePdfToViewTransformation(matrix)
// We simply call the method we implemented earlier. It will take the new matrix
// and use it to calculate screen coordinates.
updateBoundingBox()
}

Important: Don’t forget to call super.updatePdfToViewTransformation(matrix) first inside the method, or PdfDrawable#getPdfToPageTransformation will stop working.

Invalidating the drawable

Whenever the visual representation of your drawable changes, you need to tell the rendering system about that change. For example, when you are updating the alpha value of your drawable (e.g. because of a call to setAlpha()), a call to Drawable#invalidateSelf(opens in a new tab) will trigger an invalidation, which will rerender the updated drawable on the screen:

@UiThread override fun setAlpha(alpha: Int) {
paint.alpha = alpha
// Drawable invalidation is only allowed from a UI thread.
invalidateSelf()
}

Important: invalidateSelf() may only be called from the UI thread. Any call from a background thread will raise an exception.

The final drawable

Here is the final drawable. This version also supports setting new PDF coordinates by calling the RectDrawable#setPageCoordinates method:

class RectDrawable(pageCoordinates: RectF) : PdfDrawable() {
private val paint = Paint()
private val screenCoordinates = RectF()
var pageCoordinates: RectF = pageCoordinates
set(value) {
field.set(value)
updateScreenCoordinates()
}
init {
paint.color = Color.RED
paint.style = Paint.Style.FILL
}
/**
* All the drawing is performed here. Keep this method fast to maintain 60 fps.
*/
override fun draw(canvas: Canvas) {
canvas.drawRect(bounds, paint)
}
/**
* PSPDFKit calls this method every time the page is moved or resized on the screen.
* It will provide a fresh transformation for calculating screen coordinates from
* PDF coordinates.
*/
override fun updatePdfToViewTransformation(matrix: Matrix) {
super.updatePdfToViewTransformation(matrix)
updateScreenCoordinates()
}
@UiThread override fun setAlpha(alpha: Int) {
paint.alpha = alpha
// Drawable invalidation is only allowed from a UI thread.
invalidateSelf()
}
@UiThread override fun setColorFilter(colorFilter: ColorFilter?) {
paint.colorFilter = colorFilter
// Drawable invalidation is only allowed from a UI thread.
invalidateSelf()
}
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
private fun updateScreenCoordinates() {
// Calculate the screen coordinates by applying the PDF-to-view transformation.
pdfToPageTransformation.mapRect(screenCoordinates, pageCoordinates)
// Rounding out ensures no clipping of content.
val newBounds = this.bounds
screenCoordinates.roundOut(newBounds)
this.bounds = newBounds
}
}