Reputation: 1004
I'm trying to create a PDF viewer composable using the PdfRenderer and Coil for loading the bitmaps into a LazyColumn
.
This is what I got so far:
@Composable
fun PdfViewer(
modifier: Modifier = Modifier,
uri: Uri,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp)
) {
val loaderScope = rememberCoroutineScope()
val renderer = remember(uri) {
val input = ParcelFileDescriptor.open(uri.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
PdfRenderer(input)
}
val context = LocalContext.current
val mutex = remember { Mutex() }
val imageLoader = LocalContext.current.imageLoader
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
val height = (width * sqrt(2f)).toInt()
LazyColumn(
verticalArrangement = verticalArrangement
) {
items(
count = renderer.pageCount,
key = { it }
) { index ->
val cacheKey = MemoryCache.Key("$uri-$index")
val bitmap = remember(uri, index) {
val cachedBitmap = imageLoader.memoryCache[cacheKey]
if (cachedBitmap != null) cachedBitmap else {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
loaderScope.launch(Dispatchers.IO) {
mutex.withLock {
Timber.d("Loading $uri - page $index")
renderer.openPage(index).use {
it.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
}
}
}
bitmap
}
}
val request = ImageRequest.Builder(context)
.size(width, height)
.memoryCacheKey(cacheKey)
.data(bitmap)
.build()
Image(
modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth(),
contentScale = ContentScale.Fit,
painter = rememberImagePainter(request),
contentDescription = "Page ${index + 1} of ${renderer.pageCount}"
)
}
}
}
}
This kind of works, however when the bitmap is first loaded, it won't display in the list until I scroll (i.e. after a redraw). I want to make use of the features of LazyColumn
and only load the PDF pages when they become visible.
Is there any better way to achieve this?
Upvotes: 8
Views: 11984
Reputation: 95
The best way is to use a public library.
Bouquet
is a good choice for you!
To use add implementation 'io.github.grizzi91:bouquet:1.1.2'
to your build.gradle (:app)
and Sync your project.
Upvotes: 2
Reputation: 987
If you are lazy like me. It is the @Filippo Vigani answer but with imports. and touches because I got compilation errors And I do not use Timber.
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.core.net.toFile
import coil.compose.rememberImagePainter
import coil.imageLoader
import coil.memory.MemoryCache
import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.math.sqrt
@Composable
fun PdfViewer(
modifier: Modifier = Modifier,
uri: Uri,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp)
) {
val rendererScope = rememberCoroutineScope()
val mutex = remember { Mutex() }
val renderer by produceState<PdfRenderer?>(null, uri) {
rendererScope.launch(Dispatchers.IO) {
val input = ParcelFileDescriptor.open(uri.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
value = PdfRenderer(input)
}
awaitDispose {
val currentRenderer = value
rendererScope.launch(Dispatchers.IO) {
mutex.withLock {
currentRenderer?.close()
}
}
}
}
val context = LocalContext.current
val imageLoader = LocalContext.current.imageLoader
val imageLoadingScope = rememberCoroutineScope()
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
val height = (width * sqrt(2f)).toInt()
val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } }
LazyColumn(
verticalArrangement = verticalArrangement
) {
items(
count = pageCount,
key = { index -> "$uri-$index" }
) { index ->
val cacheKey = MemoryCache.Key("$uri-$index")
val cacheValue : Bitmap? = imageLoader.memoryCache?.get(cacheKey)?.bitmap
var bitmap : Bitmap? by remember { mutableStateOf(cacheValue)}
if (bitmap == null) {
DisposableEffect(uri, index) {
val job = imageLoadingScope.launch(Dispatchers.IO) {
val destinationBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mutex.withLock {
Log.d("PdfGenerator", "Loading PDF $uri - page $index/$pageCount")
if (!coroutineContext.isActive) return@launch
try {
renderer?.let {
it.openPage(index).use { page ->
page.render(
destinationBitmap,
null,
null,
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
}
}
} catch (e: Exception) {
//Just catch and return in case the renderer is being closed
return@launch
}
}
bitmap = destinationBitmap
}
onDispose {
job.cancel()
}
}
Box(modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth())
} else { //bitmap != null
val request = ImageRequest.Builder(context)
.size(width, height)
.memoryCacheKey(cacheKey)
.data(bitmap)
.build()
Image(
modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth(),
contentScale = ContentScale.Fit,
painter = rememberImagePainter(request),
contentDescription = "Page ${index + 1} of $pageCount"
)
}
}
}
}
}
Thank you Filippo.
Upvotes: 4
Reputation: 31
Excellent way to display PDF using Jetpack Compose.
However, to use correctly you have change this line :
var bitmap by remember { mutableStateOf(imageLoader.memoryCache[cacheKey]) }
by this:
var bitmap by remember { mutableStateOf(imageLoader.memoryCache?.get(cacheKey) as? Bitmap?) }
Upvotes: 1
Reputation: 1004
I managed to solve it as follows:
@Composable
fun PdfViewer(
modifier: Modifier = Modifier,
uri: Uri,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp)
) {
val rendererScope = rememberCoroutineScope()
val mutex = remember { Mutex() }
val renderer by produceState<PdfRenderer?>(null, uri) {
rendererScope.launch(Dispatchers.IO) {
val input = ParcelFileDescriptor.open(uri.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
value = PdfRenderer(input)
}
awaitDispose {
val currentRenderer = value
rendererScope.launch(Dispatchers.IO) {
mutex.withLock {
currentRenderer?.close()
}
}
}
}
val context = LocalContext.current
val imageLoader = LocalContext.current.imageLoader
val imageLoadingScope = rememberCoroutineScope()
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
val height = (width * sqrt(2f)).toInt()
val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } }
LazyColumn(
verticalArrangement = verticalArrangement
) {
items(
count = pageCount,
key = { index -> "$uri-$index" }
) { index ->
val cacheKey = MemoryCache.Key("$uri-$index")
var bitmap by remember { mutableStateOf(imageLoader.memoryCache[cacheKey]) }
if (bitmap == null) {
DisposableEffect(uri, index) {
val job = imageLoadingScope.launch(Dispatchers.IO) {
val destinationBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
mutex.withLock {
Timber.d("Loading PDF $uri - page $index/$pageCount")
if (!coroutineContext.isActive) return@launch
try {
renderer?.let {
it.openPage(index).use { page ->
page.render(
destinationBitmap,
null,
null,
PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
)
}
}
} catch (e: Exception) {
//Just catch and return in case the renderer is being closed
return@launch
}
}
bitmap = destinationBitmap
}
onDispose {
job.cancel()
}
}
Box(modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth())
} else {
val request = ImageRequest.Builder(context)
.size(width, height)
.memoryCacheKey(cacheKey)
.data(bitmap)
.build()
Image(
modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth(),
contentScale = ContentScale.Fit,
painter = rememberImagePainter(request),
contentDescription = "Page ${index + 1} of $pageCount"
)
}
}
}
}
}
This should also handle the disposal of the pdf renderer.
Upvotes: 18