Created
June 4, 2024 15:39
-
-
Save Sagar0-0/1451da91478024e524a4a4105d645b4f to your computer and use it in GitHub Desktop.
Complete gist of the Custom Compose PDFViewer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.graphics.Bitmap | |
import android.graphics.pdf.PdfRenderer | |
import android.os.ParcelFileDescriptor | |
import android.util.Log | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.border | |
import androidx.compose.foundation.gestures.rememberTransformableState | |
import androidx.compose.foundation.gestures.transformable | |
import androidx.compose.foundation.layout.Arrangement | |
import androidx.compose.foundation.layout.Box | |
import androidx.compose.foundation.layout.BoxWithConstraints | |
import androidx.compose.foundation.layout.fillMaxSize | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.material.IconButton | |
import androidx.compose.material.MaterialTheme | |
import androidx.compose.material.Text | |
import androidx.compose.material.TextButton | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.rounded.Close | |
import androidx.compose.material3.Icon | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.derivedStateOf | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableFloatStateOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.produceState | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.graphicsLayer | |
import androidx.compose.ui.layout.ContentScale | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.res.stringResource | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import coil.compose.rememberAsyncImagePainter | |
import coil.imageLoader | |
import coil.memory.MemoryCache | |
import coil.request.ImageRequest | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.isActive | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.sync.Mutex | |
import kotlinx.coroutines.sync.withLock | |
import java.io.File | |
import kotlin.math.sqrt | |
import android.content.Context | |
import android.content.Intent | |
import android.graphics.Bitmap | |
import android.graphics.pdf.PdfRenderer | |
import android.net.Uri | |
import android.os.ParcelFileDescriptor | |
import com.dardoc.dardoc.helper.logger.AppLogger | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.withContext | |
import java.io.File | |
import java.io.FileOutputStream | |
import java.io.IOException | |
import java.net.HttpURLConnection | |
import java.net.URL | |
// Manage Imports according to your use | |
@Composable | |
fun AppPdfViewer( | |
modifier: Modifier = Modifier, | |
url: String, | |
fileName: String, | |
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(0.dp), | |
onClose: () -> Unit | |
) { | |
var file: File? by remember { | |
mutableStateOf(null) | |
} | |
LaunchedEffect(key1 = Unit) { | |
file = async { downloadAndGetFile(url, fileName) }.await() | |
} | |
if (file == null) { | |
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { | |
Loader() | |
} | |
} else { | |
val rendererScope = rememberCoroutineScope() | |
val mutex = remember { Mutex() } | |
val renderer by produceState<PdfRenderer?>(null, file) { | |
rendererScope.launch(Dispatchers.IO) { | |
val input = ParcelFileDescriptor.open(file, 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 | |
.fillMaxSize() | |
.background(MaterialTheme.colors.onSecondary) | |
// .aspectRatio(1f / sqrt(2f)) | |
) { | |
val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt() | |
val height = (width * sqrt(2f)).toInt() | |
val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } } | |
var scale by rememberSaveable { | |
mutableFloatStateOf(1f) | |
} | |
var offset by remember { | |
mutableStateOf(Offset.Zero) | |
} | |
val state = | |
rememberTransformableState { zoomChange, panChange, rotationChange -> | |
scale = (scale * zoomChange).coerceIn(1f, 5f) | |
val extraWidth = (scale - 1) * constraints.maxWidth | |
val extraHeight = (scale - 1) * constraints.maxHeight | |
val maxX = extraWidth / 2 | |
val maxY = extraHeight / 2 | |
offset = Offset( | |
x = (offset.x + scale * panChange.x).coerceIn(-maxX, maxX), | |
y = (offset.y + scale * panChange.y).coerceIn(-maxY, maxY), | |
) | |
} | |
LazyColumn( | |
modifier = Modifier | |
.fillMaxSize() | |
.graphicsLayer { | |
scaleX = scale | |
scaleY = scale | |
translationX = offset.x | |
translationX = offset.y | |
} | |
.transformable(state), | |
verticalArrangement = verticalArrangement | |
) { | |
items( | |
count = pageCount, | |
key = { index -> "${file!!.name}-$index" } | |
) { index -> | |
val cacheKey = MemoryCache.Key("${file!!.name}-$index") | |
val cacheValue: Bitmap? = imageLoader.memoryCache?.get(cacheKey)?.bitmap | |
var bitmap: Bitmap? by remember { mutableStateOf(cacheValue) } | |
if (bitmap == null) { | |
DisposableEffect(file, index) { | |
val job = imageLoadingScope.launch(Dispatchers.IO) { | |
val destinationBitmap = | |
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | |
mutex.withLock { | |
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) | |
.fillMaxWidth() | |
) | |
} else { | |
val request = ImageRequest.Builder(context) | |
.size(width, height) | |
.memoryCacheKey(cacheKey) | |
.data(bitmap) | |
.build() | |
Image( | |
modifier = Modifier | |
.background(Color.Transparent) | |
.border(1.dp, MaterialTheme.colors.background) | |
// .aspectRatio(1f / sqrt(2f)) | |
.fillMaxSize(), | |
contentScale = ContentScale.Fit, | |
painter = rememberAsyncImagePainter(request), | |
contentDescription = "Page ${index + 1} of $pageCount" | |
) | |
} | |
} | |
} | |
IconButton( | |
modifier = Modifier | |
.padding(10.dp) | |
.align(Alignment.TopStart), | |
onClick = onClose | |
) { | |
Icon(imageVector = Icons.Rounded.Close, contentDescription = null, tint = Teal) | |
} | |
TextButton( | |
modifier = Modifier | |
.padding(10.dp) | |
.align(Alignment.TopEnd), | |
onClick = { | |
context.sharePdf(file!!) | |
}, | |
) { | |
Text( | |
modifier = Modifier | |
.padding(vertical = 7.dp, horizontal = 15.dp), | |
text = stringResource(id = R.string.share), | |
style = newTitleStyle(fontSize = 14.sp, color = Teal) | |
) | |
} | |
} | |
} | |
} | |
fun Context.sharePdf(file: File) { | |
val uri = Uri.fromFile(file) | |
val shareIntent = Intent() | |
shareIntent.setAction(Intent.ACTION_SEND) | |
shareIntent.setType("application/pdf") | |
shareIntent.putExtra(Intent.EXTRA_STREAM, uri) | |
this.startActivity(Intent.createChooser(shareIntent, "Share via")) | |
} | |
fun isFileExist(path: String): Boolean { | |
val file = File(path) | |
return file.exists() | |
} | |
fun getUniqueFileName(url: String): String { | |
val fileName = URL(url).toURI().path.replace("/", "_") | |
// Hash the filename to create a shorter and more unique identifier | |
val hash = fileName.hashCode().toString(Character.MAX_RADIX) | |
// Combine a sanitized filename with a hash for uniqueness | |
return "$fileName-$hash" | |
} | |
suspend fun downloadAndGetFile(url: String, fileName: String): File? { | |
if (isFileExist(fileName)) return File(fileName) | |
val connection: HttpURLConnection? = null | |
var file: File? = null | |
try { | |
withContext(Dispatchers.IO) { | |
val connectionOne = URL(url).openConnection() as HttpURLConnection | |
connectionOne.connect() | |
if (connectionOne.responseCode != HttpURLConnection.HTTP_OK) { | |
return@withContext null | |
} | |
val inputStream = connectionOne.inputStream | |
file = File.createTempFile(fileName, ".pdf") | |
val outputStream = FileOutputStream(file) | |
inputStream.copyTo(outputStream) | |
outputStream.close() | |
} | |
} catch (e: IOException) { | |
AppLogger.error("File", "ErrorMessage " + e.message + ", ${e.cause}") | |
} finally { | |
connection?.disconnect() | |
} | |
return file | |
} | |
suspend fun downloadAndRenderPdf(url: String): List<Bitmap>? { | |
val connection: HttpURLConnection? = null | |
var parcelFileDescriptor: ParcelFileDescriptor? = null | |
var pdfRenderer: PdfRenderer? = null | |
var bitmaps: MutableList<Bitmap>? = null | |
try { | |
withContext(Dispatchers.IO) { | |
val connectionOne = URL(url).openConnection() as HttpURLConnection | |
connectionOne.connect() | |
if (connectionOne.responseCode != HttpURLConnection.HTTP_OK) { | |
return@withContext null | |
} | |
val inputStream = connectionOne.inputStream | |
val outputFile = File.createTempFile("temp_pdf", ".pdf") | |
val outputStream = FileOutputStream(outputFile) | |
inputStream.copyTo(outputStream) | |
outputStream.close() | |
parcelFileDescriptor = | |
ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY) | |
parcelFileDescriptor?.let { | |
pdfRenderer = PdfRenderer(it) | |
val pageCount = pdfRenderer!!.pageCount | |
bitmaps = mutableListOf() | |
for (i in 0 until pageCount) { | |
pdfRenderer?.let { renderer -> | |
AppLogger.e("pdfRenderer $i $pageCount") | |
val page = renderer.openPage(i) | |
val bitmap = | |
Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888) | |
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) | |
bitmaps?.add(bitmap) | |
page.close() | |
} | |
} | |
} | |
} | |
} catch (e: IOException) { | |
// Handle exception | |
} finally { | |
connection?.disconnect() | |
parcelFileDescriptor?.close() | |
pdfRenderer?.close() | |
} | |
return bitmaps | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment