PDF renderer library for Android

You can use Nutrient to render a PDF page into a Bitmap object so that you can use it in your custom application views as well. To do this, use PSPDFKit and PdfDocument class calls.

Initializing Nutrient

Before loading or rendering a document, you have to initialize Nutrient by providing your license. Here’s how to do it.

Loading a document

The PdfDocumentLoader class offers a variety of methods for loading a document. You can load documents from a Uri or a DataProvider. The following example loads a PDF document from the app’s assets. The different available document sources are described in the PdfActivity guide:

val document : PdfDocument

try {
    // 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, you should do this call
    // on a background thread, or use the asynchronous version of this method instead.
    document = PdfDocumentLoader.openDocument(context, documentUri)
} catch (e : IOException) {
    handleDocumentLoadingError(e)
}
final PdfDocument document;

try {
    // 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, you should do this call
    // on a background thread, or use the asynchronous version of this method instead.
    document = PdfDocumentLoder.openDocument(context, documentUri);
} catch (IOException e) {
    handleDocumentLoadingError(e);
}

Now that the PdfDocument has been loaded, you can use it to render PDF pages.

Rendering pages

The rendering of pages can be performed synchronously or asynchronously using the #renderPageToBitmap and #renderPageToBitmapAsync methods of the PdfDocument, respectively.

Synchronous rendering

To synchronously render a page into a Bitmap, use any of the available #renderPageToBitmap methods. These methods will block until rendering has been finished, which means you should only use them on a background thread (not from the main UI thread of your app).

💡 Tip: To keep the original page aspect while rendering and prevent stretching of the resulting image, you can access the original PageSize of the rendered page (in PDF points) and calculate your resulting bitmap size:

val pageIndex = 0
// Page size is in PDF points (not pixels).
val pageSize : Size = document.getPageSize(pageIndex)
// We define a target width for the resulting bitmap and use it to calculate the final height.
val width = 2048
val height = (pageSize.height * (width / pageSize.width)).toInt()

// This will render the first page uniformly into a bitmap with a width of 2,048 pixels.
val pageBitmap : Bitmap = document.renderPageToBitmap(context, pageIndex, width, height)
final int pageIndex = 0;
// Page size is in PDF points (not pixels).
final Size pageSize = document.getPageSize(pageIndex);
// We define a target width for the resulting bitmap and use it to calculate the final height.
final int width = 2048;
final int height = (int) (pageSize.height * (width / pageSize.width));

// This will render the first page uniformly into a bitmap with a width of 2,048 pixels.
final Bitmap pageBitmap = document.renderPageToBitmap(context, pageIndex, width, height);

Asynchronous rendering

To render the document asynchronously in a background thread, you can use any of the available #renderPageToBitmapAsync methods. These methods won’t block, but they will instead return an RxJava Single<Bitmap>, which will emit the Bitmap once rendering has finished (similar to a callback). The rendering itself is performed on a background thread by default, so you can safely call this method from your app’s main thread:

// Render the page on a background thread and return the resulting bitmap on the main thread.
document.renderPageToBitmapAsync(context, pageIndex, width, height)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { bitmap ->
        // Your code can now use the bitmap.
        updateUserInterface(bitmap)
    }
// Render the page on a background thread and return the resulting bitmap on the main thread.
document.renderPageToBitmapAsync(context, pageIndex, width, height)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(bitmap -> {
        // Your code can now use the bitmap.
        updateUserInterface(bitmap);
    });

Providing a render configuration

Both synchronous and asynchronous render methods allow you to specify a PageRenderConfiguration object for defining details about the requested rendering. You can create the configuration using its Builder class. The configuration allows you to specify several options:

  • Enabling or disabling of an in-memory render cache.

  • Rendering of only a specific region on the page (e.g. the upper half).

  • A reuseBitmap object that should be reused for rendering. Use this to optimize memory performance of your app.

  • Background color, grayscale mode, or inversion of all colors.

  • Rendering of renderedDrawables above the page content. This can be used to add watermarks. More information can be found in our drawable API guide.

val renderConfig = PageRenderConfiguration.Builder()
    .toGrayscale(true)
    .build()

val grayscaleBitmap : Bitmap = document.renderPageToBitmap(context, pageIndex, width, height, renderConfig)
final PageRenderConfiguration renderConfig = new PageRenderConfiguration.Builder()
    .toGrayscale(true)
    .build();

final Bitmap grayscaleBitmap = document.renderPageToBitmap(context, pageIndex, width, height, renderConfig);

Rendering a specific part of a page

As previously mentioned, by using PageRenderConfiguration, you can render a specific part of a page. See the PageRenderConfiguration.Builder#region() method for more information.

Here’s an example of rendering the part of the page that is currently zoomed (every step is described by a comment):

// You can get all visible pages by calling `PdfFragment#getVisiblePages()`.
val somePageIndex = ...

// In order to render just a part of the page, you first need the visible rect.
val currentVisibleRect = RectF()

// This saves the visible rect in PDF coordinates to `currentVisibleRect`.
pdfFragment.getVisiblePdfRect(currentVisibleRect, somePageIndex)

// Define a desired width for the bitmap in pixels.
val bitmapWidth = 2000

// Now calculate the height, keeping the aspect ratio of the bitmap as the visible area.
val ratio = bitmapWidth / currentVisibleRect.width

// Invert the height so it's not negative (in PDF, the rect top has a higher value than the bottom).
val bitmapHeight = (-currentVisibleRect.height * ratio) as Int

// Calculate the size of the full page for the desired region bitmap size.
val pageSizeInPdfPoints = document.getPageSize(somePageIndex)
val fullPageWidth = (pageSizeInPdfPoints.width * ratio) as Int
val fullPageHeight = (pageSizeInPdfPoints.height * ratio) as Int

// Now we need to get region coordinates in relation to the full page.
val x = (currentVisibleRect.left * ratio) as Int
val y = ((pageSizeInPdfPoints.height - currentVisibleRect.top) * ratio) as Int

// Offsets define the movement of the full page, so we move the page region in the top/left direction
// by x and y and then capture it.
val renderConfiguration = PageRenderConfiguration.Builder()
    .region(-x, -y, fullPageWidth, fullPageHeight)
    .build()

// Captures the region and renders it to the bitmap.
val regionBitmap = document.renderPageToBitmap(getContext(), 0, bitmapWidth, bitmapHeight, renderConfiguration)
// You can get all visible pages by calling `PdfFragment#getVisiblePages()`.
int somePageIndex = ...

// In order to render just a part of the page, we first need the visible rect.
RectF currentVisibleRect = new RectF();

// This saves the visible rect in PDF coordinates to `currentVisibleRect`.
pdfFragment.getVisiblePdfRect(currentVisibleRect, somePageIndex);

// Define a desired width for the bitmap in pixels.
int bitmapWidth = 2000;

// Now calculate the height, keeping the aspect ratio of the bitmap as the visible area.
float ratio = bitmapWidth / currentVisibleRect.width();

// Invert the height so it's not negative (in PDF, the rect top has a higher value than the bottom).
int bitmapHeight = (int) (-currentVisibleRect.height() * ratio);

// Calculate the size of the full page for the desired region bitmap size.
Size pageSizeInPdfPoints = document.getPageSize(somePageIndex);
int fullPageWidth = (int) (pageSizeInPdfPoints.width * ratio);
int fullPageHeight = (int) (pageSizeInPdfPoints.height * ratio);

// Now we need to get region coordinates in relation to the full page.
int x = (int) (currentVisibleRect.left * ratio);
int y = (int) ((pageSizeInPdfPoints.height - currentVisibleRect.top) * ratio);

// Offsets define the movement of the full page, so we move the page region in the top/left direction
// by x and y and then capture it.
PageRenderConfiguration renderConfiguration = new PageRenderConfiguration.Builder()
    .region(-x, -y, fullPageWidth, fullPageHeight)
    .build();

// Captures the region and renders it to the bitmap.
Bitmap regionBitmap = document.renderPageToBitmap(getContext(), 0, bitmapWidth, bitmapHeight, renderConfiguration);