PDF Viewer using Coil
When I set out to integrate a PDF viewer into my Android app, I was surprised to find a lack of clear solutions on Google. After much research and experimentation, I developed a method using Coil, a popular image loading library for Android. In this article, I’ll walk you through the steps to set up a PDF viewer using Coil.

Step 1: Create a PDF Decoder for Coil
First, we need a custom decoder to handle PDF files in Coil. Here’s the code snippet for the PdfPageDecoder
:
class PdfPageDecoder(
private val context: Context,
private val data: PdfPage
) : Decoder {
companion object {
const val NUMBER_PDF_PAGES = "numberPages"
const val SPACE_BETWEEN_PAGES = "spaceBetweenPages"
}
override suspend fun decode(): DecodeResult {
val bitmap = withContext(Dispatchers.IO) {
renderPagesToBitmap(
data.file,
context.resources.displayMetrics.density,
data.numberPages,
data.spaceBetweenPages
)
}
val drawable = BitmapDrawable(context.resources, bitmap)
return DecodeResult(drawable = drawable, isSampled = false)
}
class Factory : Decoder.Factory {
override fun create(
result: SourceResult,
options: Options,
imageLoader: ImageLoader
): Decoder? {
return if (isApplicable(result)) {
val numberPages = (options.parameters.values()[NUMBER_PDF_PAGES] as? Int) ?: Int.MAX_VALUE
val spaceBetweenPages =
(options.parameters.values()[SPACE_BETWEEN_PAGES] as? Int) ?: 0
PdfPageDecoder(
context = options.context,
data = PdfPage(
file = result.source.file().toFile(),
numberPages = numberPages,
spaceBetweenPages = spaceBetweenPages
)
)
} else {
null
}
}
private fun isApplicable(result: SourceResult): Boolean {
return result.mimeType?.contains("pdf") == true
}
}
private fun renderPagesToBitmap(
file: File,
density: Float,
pagesNumber: Int,
spaceBetweenPages: Int
): Bitmap {
val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
val pdfRenderer = PdfRenderer(fileDescriptor)
val spaceBetweenPagesPx = (spaceBetweenPages * density).toInt()
val pagesSize = pagesNumber.coerceIn(1, pdfRenderer.pageCount)
val (maxWidth, totalHeight) = calculateTotalHeightAndWidth(
pdfRenderer,
density,
pagesSize,
spaceBetweenPagesPx
)
// Create a bitmap with the total height and max width
val combinedBitmap = Bitmap.createBitmap(maxWidth, totalHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(combinedBitmap)
var currentY = 0
for (i in 0 until pdfRenderer.pageCount) {
pdfRenderer.openPage(i).use { page ->
val pageBitmap = createPageBitmap(page, density)
page.render(pageBitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
// Draw the page bitmap on the combined bitmap
canvas.drawBitmap(pageBitmap, 0f, currentY.toFloat(), null)
currentY += pageBitmap.height + spaceBetweenPagesPx
pageBitmap.recycle() // Recycle the page bitmap to free memory
}
}
pdfRenderer.close()
fileDescriptor.close()
return combinedBitmap
}
private fun calculateTotalHeightAndWidth(
pdfRenderer: PdfRenderer,
density: Float,
pagesSize: Int,
spaceBetweenPagesPx: Int
): Pair<Int, Int> {
var totalHeight = 0
var maxWidth = 0
for (i in 0 until pagesSize) {
pdfRenderer.openPage(i).use { page ->
totalHeight += (page.height * density).toInt() + spaceBetweenPagesPx
maxWidth = maxOf(maxWidth, (page.width * density).toInt())
}
}
totalHeight -= spaceBetweenPagesPx
return Pair(maxWidth, totalHeight)
}
private fun createPageBitmap(page: PdfRenderer.Page, density: Float): Bitmap {
val pageWidth = (page.width * density).toInt()
val pageHeight = (page.height * density).toInt()
return Bitmap.createBitmap(pageWidth, pageHeight, Bitmap.Config.ARGB_8888)
}
data class PdfPage(val file: File, val numberPages: Int, val spaceBetweenPages: Int)
}
Step 2: Add the Decoder in the Application Class
Next, we need to add this decoder to the Coil configuration in our application class. This will ensure that Coil uses our custom decoder for PDF files.
class MyApplication : Application(), ImageLoaderFactory {
override fun newImageLoader(): ImageLoader {
return ImageLoader.Builder(this)
.componentRegistry { add(PdfPageDecoder.Factory()) }
.build()
}
}
Step 3: Using the PDF Decoder in your activity or using compose
Finally, we can use Coil to load and display PDF pages in our activity or fragment.
Short explanation of how the PdfPageDecoder
works:
- Loading the PDF: When Coil encounters a PDF file, it uses the
PdfPageDecoder
to process it. This decoder reads the PDF file and prepares it for rendering. - Rendering the PDF: The
PdfPageDecoder
opens the PDF using Android'sPdfRenderer
class. It then opens the first page of the PDF. - Creating a Bitmap: A
Bitmap
object is created with the dimensions of the PDF page. The PDF page is rendered onto thisBitmap
. - Returning the Result: The
Bitmap
is then wrapped in aDecodeResult
object, which Coil uses to display the image.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val pdfFileUri = "file:///android_asset/sample.pdf"
val imageRequest = ImageRequest.Builder(LocalContext.current)
.setParameter(NUMBER_PDF_PAGES, 1)
.setParameter(SPACE_BETWEEN_PAGES, 20)
.data(pdfFileUri)
.build()
imageView.load(imageRequest)
}
}
Compose example
@Composable
fun PdfViewer(pdfFileUri: String) {
val imageRequest = ImageRequest.Builder(LocalContext.current)
.setParameter(NUMBER_PDF_PAGES, 1)
.setParameter(SPACE_BETWEEN_PAGES, 20)
.data(pdfFileUri)
.build()
AsyncImage(
model = imageRequest,
contentDescription = null,
)
}
With these steps, you should be able to integrate a PDF viewer into your Android app using Coil, both in traditional views and with Jetpack Compose. This approach leverages Coil’s extensibility to handle non-standard image formats, providing a seamless way to display PDF pages.
Feel free to adapt the code snippets to suit your needs. I hope this solution saves you time and effort, allowing you to focus on building great apps. If you have any questions or run into issues, please leave a comment below!