Rendering PDF vector images using Coil
Using PdfRenderer and custom Decoder implementation

The problem
I recently faced a challenge where I needed to render PDF vector assets/images in the app.
Our existing API was exclusively used on iOS, which natively supports rendering PDFs as images. But, we know this is not the same case on Android.
Possible solutions
One possible solution is to convert all our PDF assets from the server to SVGs. However, we’re talking about hundreds of them, making this our last resort.
We could also manually implement downloading the PDF files, converting them to Bitmap, and displaying them inside an Image. But this would also mean handling the caching manually, along with all the other necessary work to make it performant, all of which come for free when using industry-standard image loading libraries.
A better solution
Coil allows us to write our custom decoder exactly for these types of scenarios. This is the same approach they use for their provided SvgDecoder, GifDecoder, etc.
So, how do we do it?
- First, let’s create a class that implements the Decoder interface from Coil. In this case, we’re calling it PdfDecoder.
class PdfDecoder(
private val source: ImageSource,
private val options: Options,
) : Decoder {
// implementation
}
2. Inside this class, we’re going to create a decoder factory. The purpose of this class is for Coil to be able to tell if this Decoder supports a certain type of file. In this case, we’re checking against application/pdf
mime type.
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
if (!isApplicable(result)) return null
return PdfDecoder(result.source, options)
}
private fun isApplicable(result: SourceResult): Boolean = result.mimeType == "application/pdf"
}
3. Let’s go back to our PdfDecoder and implement the decode()
function. Let’s use the SourceResult
coming from our factory to decode the file. We can obtain the already downloaded file by Coil through ImageSource
.
Let’s convert the PDF file to Drawable using PdfRenderer and return it as a DecodeResult
.
override suspend fun decode(): DecodeResult {
val context = options.context
val pdfRenderer = PdfRenderer(
ParcelFileDescriptor.open(
source.file().toFile(),
ParcelFileDescriptor.MODE_READ_ONLY,
),
)
val page = pdfRenderer.openPage(0)
// For better bitmap quality: https://stackoverflow.com/a/32327174/5285687
val densityDpi = context.resources.displayMetrics.densityDpi
val bitmap = Bitmap.createBitmap(
densityDpi * page.width / 72,
densityDpi * page.height / 72,
Bitmap.Config.ARGB_8888,
)
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page.close()
pdfRenderer.close()
return DecodeResult(
drawable = bitmap.toDrawable(context.resources),
isSampled = false,
)
}
4. Finally, what’s left for us to do is to add this custom PdfDecoder to our ImageLoader instance (ideally inside your Application class).
override fun newImageLoader(): ImageLoader =
ImageLoader.Builder(this)
.components {
add(PdfDecoder.Factory())
}
.build()
5. Now you can render PDF images inside an Image.
// Jetpack Compose
// URL
AsyncImage(model = "https://example.com/image.pdf")
// Views
// URL
imageView.load("https://example.com/image.pdf")
// File
imageView.load(File("/path/to/image.pdf"))
And, that’s it!
I hope I was able to help someone out there with this article. Thank you for reading and happy coding!
You can view the full gist of this solution here.
Important Note: Please adapt the
decode()
function to your specific requirements. The method shown here for converting a PDF to a bitmap might not be ideal for all situations, especially when handling large PDF files. However, it is a suitable solution for my use case, which only involves rendering vector assets and icons from our backend.