Rendering PDFs on Android the easy way
How to take the most out of the PdfRenderer
Developers or not, most of us are familiar with PDFs and are used to work with them all the time, so rendering PDFs may seem like an ordinary task that any platform should be able to perform with minimal effort. Surprisingly, that's not exactly the case on Android. Browsers (including WebViews) struggle to render PDFs there, and the task is usually delegated to a separate specialized app.
For a long time there was no easy way to render a PDF inside our apps and we had to rely on questionable solutions. A few years ago, the popular AndroidPdfViewer took the stage and things got better, but we still had to pay an expensive price: an extra 15 MB in our APK size (which can be reduced, though, especially today with app bundles). There are also many paid options out there, but I'm here to talk about the great PdfRenderer
.
Starting on API 21, we can use PdfRenderer
as an abstraction on top of PDFium — same as AndroidPdfViewer but without the APK size hit or the need of any extra library at all. The documentation around it is good but brief, and there aren't many examples around, so the goal here is to walk through the process of rendering a PDF as a RecyclerView
so you can scroll through the pages as you'd expect to whenever interacting with a PDF file in a mobile device.
TL;DR:
Check this gist with the final version of the most relevant snippets mentioned in the article.
The basics
Let's assume we have a PDF file available in a given filePath
. This is how we can create a PdfRenderer
:
Nothing particularly interesting about the ParcelFileDescriptor
there, and that's most likely how you'll want to create it every time. Now that we have the renderer
, we can open pages and render them individually. There are a few important things here:
- We're responsible for closing the
renderer
and each page we open. - We can have only a single page open at any given time.
- The current page needs to be closed before we can close the
renderer
.
With that in mind, this is how we'd render the first page of our PDF file:
First line is easy, whenever we want to open a page we can simply call openPage()
and pass the index of the page we want to open.
The bitmap creation is a bit more interesting. We're passing Bitmap.Config.ARGB_8888
as the Config
, and even though there are other configurations available, that's our only option when we're working with the PdfRenderer
:
We also need to define the width
and height
of our Bitmap
. A PdfRenderer.Page
has a width
and height
, but they're measured in points, while the width
and height
we pass when we create a Bitmap
should be in pixels, so we definitely don't want to use our page's width and height when creating our Bitmap
. Furthermore, even if the page size was in pixel, that's not the dimension we should use to determine our Bitmap
's size. We should instead look at where we'll be displaying it and take the dimensions there so we create a Bitmap
that will nicely fit its destination.
In our case (and I'd guess in most cases), we want the PDF to fit the whole width of the screen (maybe minus some margins) and the height should be whatever is necessary to maintain the aspect ratio. This is how we could achieve that:
We use the page's original dimensions just to make sure we set a height that will respect the page's aspect ratio. Going back to our example, this is what we have now:
We've been through the first two statements and the last two are pretty self-explanatory, so let's talk about that render()
call. This is where the magic happens and we turn our PDF page into an image that we can display wherever we want. The first parameter there is our bitmap, the second and third are the clip and transformation:
Not particularly interesting for our simple scenario, so those two null
s will do. For the last parameter we have two options: RENDER_MODE_FOR_DISPLAY
and RENDER_MODE_FOR_PRINT
. If we look at the native code behind this, these two constants are basically translated into two render flags: FPDF_LCD_TEXT
and FPDF_PRINTING
:
Since we want to display the PDF in our app, we definitely want that LCD rendering optimization, so that's an easy choice. With everything in place, we can take the resulting Bitmap
and place it in an ImageView
with a simple setImageBitmap()
call!
We've been neglecting threading so far, but whenever we're dealing with files and Bitmap
s it's usually a good idea to offload the work to a background thread.
PdfRenderer
's docs aren't explicit about this, so I'm relying on the platform's best practices — use your best judgement and benchmark your app. If you're using coroutines, this would be as simple as turning what we have so far into a suspend
function and wrapping it in withContext(Dispatchers.IO)
, but feel free to achieve that with your preferred tool.
This covers the basics but is pretty promising. Even though there's very little code involved, this already works really well. But there are a few interesting points that go a little beyond that.
Transparent PDFs
If you run that code and your PDF is transparent (which is way more common than I'd ever suspect), you're gonna have a bad time if you expect to see a white page being rendered. So it's a good idea to play safe and ensure we have a background color set to handle these cases.
The solution here is pretty straightforward:
That will ensure that we have a white background in case the PDF is transparent, which is what happens in most (all?) PDF readers. It'd be nice if this was handled by the PdfRenderer
itself, but it's a small burden we can carry.
Zoom support
People expect to be able to pinch to zoom when they're interacting with a PDF file on a mobile device. The easiest way to achieve that is to resort to your favorite ImageView
zoom solution — PhotoView is probably the easiest choice. If you place the Bitmap
you get from the PdfRenderer
into a PhotoView
, zooming in and out should work like a charm.
Since we're working with a Bitmap
, we won't get the super crisp result we're used to with PDF readers, but it'll hopefully be good enough for your application. If zooming is really important for your use case, one easy option to improve this is to render larger Bitmap
s. Proper zoom support would require some extra effort: we'd have to write our own zoom logic by taking advantage of the clip and transformation parameters that we've ignored so far.
PDF file as a RecyclerView
Based on what we've seen so far, let's define some extensions to make things easier for us:
If we just want to render the first page of the PDF as a preview, we can define this other function to help us out:
But now let's render each page of the PDF as a RecyclerView
item. Our item will be a simple ImageView
:
And this is how our adapter could look like:
We're receiving a ready to use PdfRenderer
and the pageWidth
. We could also receive the filePath
and create the PdfRenderer
ourselves, but it's usually easier to manage it outside the adapter. The ViewHolder
is extremely straightforward: it simply receives the rendered Bitmap
and places it into its ImageView
. And the adapter itself isn't bad either:
- We implement
getItemCount()
with a simplepageCount
call; - And for each
ViewHolder
, we open, render, and close a page.
And that's it! ✨
…or is it? What about threading again? It can get really tricky to get threading right here. We can't have a naive implementation in place since we can't ever have two pages open at the same time. If we simply offload the rendering work to a thread pool (e.g. Dispatchers.IO
), we run the risk of trying to open a page in one thread while a different thread is still rendering another one, causing this:
java.lang.IllegalStateException: Current page not closed
An alternative is to offload work to a single background thread (something like an Executors.newSingleThreadExecutor().asCoroutineDispatcher()
as suggested here), but we still need to make sure we don't close our PdfRenderer
if there are still open pages.
I'll leave this as an exercise to the reader — I'm actually still experimenting here. The good news is that in many cases you can probably get away with rendering on the main thread, so make sure to profile your app and do the right thing for you here.
And what about zooming? It'd be really hard to implement a way to zoom in and out of the whole RecyclerView
, unfortunately. So an easy solution is to allow users to click on a page and go to a screen where only that page is shown, so we can use a simple PhotoView
to enable zoom support. Make sure to reuse the Bitmap
used in the RecyclerView
to avoid rendering it again — it might be necessary to avoid navigating to a new fragment/activity since it wouldn't be possible to pass the Bitmap
as an Intent
extra or fragment argument.
PdfRenderer limitations
The PdfRenderer
isn't meant to be a full blown PDF solution. It'll do a good job on simple cases but that's pretty much it. If you need to cover more advanced cases, it probably won't be enough for you. As an example, it doesn't support annotations and it has issues dealing with password protected and corrupted files. Keep that in mind while you’re working with it, and for more information around those cases, make sure to check Muthu Raj's comment.
Extra: downloading a PDF file
This is a bit off-topic, but just in case it might be interesting to anyone, I'm dumping a reasonable snippet for downloading a PDF file with Retrofit.
This was the first time I had to work with PDF files on Android, so even though what I'm presenting here has been battle tested in production, these are fresh ideas from a PDF apprentice. Let me know if you see any room for improvements and hit me up here or on Twitter!