From fc34de07ea67e6903ed0fbfee7b1d946df08e72e Mon Sep 17 00:00:00 2001 From: miseke-ffw Date: Mon, 11 Nov 2024 11:33:05 +0100 Subject: [PATCH 1/3] Add PdfSource and RenderQuality --- .../rajat/sample/pdfviewer/ComposeActivity.kt | 10 +- .../rajat/sample/pdfviewer/MainActivity.kt | 5 +- .../com/rajat/pdfviewer/PdfRendererView.kt | 91 +++-- .../java/com/rajat/pdfviewer/PdfSource.kt | 10 + .../com/rajat/pdfviewer/PdfViewAdapter.kt | 321 +----------------- .../com/rajat/pdfviewer/PdfViewerActivity.kt | 4 +- .../rajat/pdfviewer/PinchZoomRecyclerView.kt | 10 +- .../java/com/rajat/pdfviewer/RenderQuality.kt | 5 + .../pdfviewer/compose/PdfRendererCompose.kt | 35 +- 9 files changed, 132 insertions(+), 359 deletions(-) create mode 100644 pdfViewer/src/main/java/com/rajat/pdfviewer/PdfSource.kt create mode 100644 pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt diff --git a/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt b/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt index 1b7d954..214c7a1 100644 --- a/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt +++ b/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt @@ -19,14 +19,14 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.rajat.pdfviewer.PdfRendererView -import com.rajat.pdfviewer.compose.PdfRendererViewCompose +import com.rajat.pdfviewer.PdfSource +import com.rajat.pdfviewer.compose.Pdf import com.rajat.sample.pdfviewer.ui.theme.AndroidpdfviewerTheme import java.io.File @@ -103,7 +103,7 @@ fun MyPdfScreenFromUri(uri: Uri, modifier: Modifier = Modifier) { val lifecycleOwner = LocalLifecycleOwner.current PdfRendererViewCompose( modifier = modifier, - uri = uri, + source = PdfSource.FromUri(uri), lifecycleOwner = lifecycleOwner, statusCallBack = object : PdfRendererView.StatusCallBack { override fun onPdfLoadStart() { @@ -138,7 +138,7 @@ fun MyPdfScreenFromUrl(url: String, modifier: Modifier = Modifier) { val lifecycleOwner = LocalLifecycleOwner.current PdfRendererViewCompose( modifier = modifier, - url = url, + source = PdfSource.FromUrl(url), lifecycleOwner = lifecycleOwner, statusCallBack = object : PdfRendererView.StatusCallBack { override fun onPdfLoadStart() { @@ -174,7 +174,7 @@ fun MyPdfScreenFromFile() { val lifecycleOwner = LocalLifecycleOwner.current val pdfFile = File("path/to/your/file.pdf") // Replace with your file path PdfRendererViewCompose( - file = pdfFile, + source = PdfSource.FromFile(pdfFile), lifecycleOwner = lifecycleOwner ) } diff --git a/app/src/main/java/com/rajat/sample/pdfviewer/MainActivity.kt b/app/src/main/java/com/rajat/sample/pdfviewer/MainActivity.kt index 8497f02..171a087 100644 --- a/app/src/main/java/com/rajat/sample/pdfviewer/MainActivity.kt +++ b/app/src/main/java/com/rajat/sample/pdfviewer/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.rajat.pdfviewer.PdfRendererView +import com.rajat.pdfviewer.PdfSource import com.rajat.pdfviewer.PdfViewerActivity import com.rajat.pdfviewer.util.saveTo import com.rajat.sample.pdfviewer.databinding.ActivityMainBinding @@ -96,8 +97,8 @@ class MainActivity : AppCompatActivity() { //Page change. Not require } } - binding.pdfView.initWithUrl( - url = download_file_url2, + binding.pdfView.init( + source = PdfSource.FromUrl(download_file_url2), lifecycleCoroutineScope = lifecycleScope, lifecycle = lifecycle ) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt index 0a06273..58e3643 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt @@ -48,12 +48,15 @@ class PdfRendererView @JvmOverloads constructor( private var runnable = Runnable {} private var enableLoadingForPages: Boolean = false private var pdfRendererCoreInitialised = false - private var pageMargin: Rect = Rect(0,0,0,0) + private var pageMargin: Rect = Rect(0, 0, 0, 0) var statusListener: StatusCallBack? = null private var positionToUseForState: Int = 0 private var restoredScrollPosition: Int = NO_POSITION private var disableScreenshots: Boolean = false private var postInitializationAction: (() -> Unit)? = null + var pdfSource: PdfSource? = null + private set + var renderQuality: RenderQuality = RenderQuality.LOW val totalPageCount: Int get() { @@ -73,28 +76,51 @@ class PdfRendererView @JvmOverloads constructor( fun onPageChanged(currentPage: Int, totalPage: Int) {} } - fun initWithUrl( + fun init( + source: PdfSource, headers: HeaderData = HeaderData(), + lifecycleCoroutineScope: LifecycleCoroutineScope, + lifecycle: Lifecycle + ) { + pdfSource = source + when (source) { + is PdfSource.FromFile -> initWithFile(source.file) + is PdfSource.FromUri -> initWithUri(source.uri) + is PdfSource.FromUrl -> { + initWithUrl( + url = source.url, + headers = headers, + lifecycleCoroutineScope = lifecycleCoroutineScope, + lifecycle = lifecycle, + ) + } + } + } + + private fun initWithUrl( url: String, headers: HeaderData = HeaderData(), lifecycleCoroutineScope: LifecycleCoroutineScope, lifecycle: Lifecycle ) { lifecycle.addObserver(this) // Register as LifecycleObserver - PdfDownloader(lifecycleCoroutineScope,headers,url, object : PdfDownloader.StatusListener { + PdfDownloader(lifecycleCoroutineScope, headers, url, object : PdfDownloader.StatusListener { override fun getContext(): Context = context override fun onDownloadStart() { statusListener?.onPdfLoadStart() } + override fun onDownloadProgress(currentBytes: Long, totalBytes: Long) { var progress = (currentBytes.toFloat() / totalBytes.toFloat() * 100F).toInt() if (progress >= 100) progress = 100 statusListener?.onPdfLoadProgress(progress, currentBytes, totalBytes) } + override fun onDownloadSuccess(absolutePath: String) { initWithFile(File(absolutePath)) statusListener?.onPdfLoadSuccess(absolutePath) } + override fun onError(error: Throwable) { error.printStackTrace() statusListener?.onError(error) @@ -103,7 +129,8 @@ class PdfRendererView @JvmOverloads constructor( } fun initWithFile(file: File) { - init(file) + val fileDescriptor = PdfRendererCore.getFileDescriptor(file) + init(fileDescriptor) } @Throws(FileNotFoundException::class) @@ -126,7 +153,7 @@ class PdfRendererView @JvmOverloads constructor( var savedState = state if (savedState is Bundle) { val superState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - savedState.getParcelable("superState",Parcelable::class.java) + savedState.getParcelable("superState", Parcelable::class.java) } else { savedState.getParcelable("superState") } @@ -137,16 +164,17 @@ class PdfRendererView @JvmOverloads constructor( } } - private fun init(file: File) { - val fileDescriptor = PdfRendererCore.getFileDescriptor(file) - init(fileDescriptor) - } - private fun init(fileDescriptor: ParcelFileDescriptor) { // Proceed with safeFile pdfRendererCore = PdfRendererCore(context, fileDescriptor) pdfRendererCoreInitialised = true - pdfViewAdapter = PdfViewAdapter(context,pdfRendererCore, pageMargin, enableLoadingForPages) + pdfViewAdapter = PdfViewAdapter( + context = context, + renderer = pdfRendererCore, + pageSpacing = pageMargin, + enableLoadingForPages = enableLoadingForPages, + renderQuality = renderQuality + ) val v = LayoutInflater.from(context).inflate(R.layout.pdf_rendererview, this, false) addView(v) recyclerView = findViewById(R.id.recyclerView) @@ -191,7 +219,8 @@ class PdfRendererView @JvmOverloads constructor( val layoutManager = recyclerView.layoutManager as LinearLayoutManager val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition() - val firstCompletelyVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition() + val firstCompletelyVisiblePosition = + layoutManager.findFirstCompletelyVisibleItemPosition() val isPositionChanged = firstVisiblePosition != lastFirstVisiblePosition || firstCompletelyVisiblePosition != lastCompletelyVisiblePosition if (isPositionChanged) { @@ -204,14 +233,15 @@ class PdfRendererView @JvmOverloads constructor( updatePageNumberDisplay(positionToUse) lastFirstVisiblePosition = firstVisiblePosition lastCompletelyVisiblePosition = firstCompletelyVisiblePosition - }else{ + } else { positionToUseForState = firstVisiblePosition } } private fun updatePageNumberDisplay(position: Int) { if (position != NO_POSITION) { - pageNo.text = context.getString(R.string.pdfView_page_no, position + 1, totalPageCount) + pageNo.text = + context.getString(R.string.pdfView_page_no, position + 1, totalPageCount) pageNo.visibility = View.VISIBLE if (position == 0) { pageNo.postDelayed({ pageNo.visibility = View.GONE }, 3000) @@ -257,24 +287,43 @@ class PdfRendererView @JvmOverloads constructor( engine = PdfEngine.values().first { it.value == engineValue } showDivider = typedArray.getBoolean(R.styleable.PdfRendererView_pdfView_showDivider, true) divider = typedArray.getDrawable(R.styleable.PdfRendererView_pdfView_divider) - enableLoadingForPages = typedArray.getBoolean(R.styleable.PdfRendererView_pdfView_enableLoadingForPages, enableLoadingForPages) - val marginDim = typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_margin, 0) + enableLoadingForPages = typedArray.getBoolean( + R.styleable.PdfRendererView_pdfView_enableLoadingForPages, + enableLoadingForPages + ) + val marginDim = + typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_margin, 0) pageMargin = Rect(marginDim, marginDim, marginDim, marginDim).apply { - top = typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_marginTop, top) - left = typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_marginLeft, left) - right = typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_marginRight, right) - bottom = typedArray.getDimensionPixelSize(R.styleable.PdfRendererView_pdfView_page_marginBottom, bottom) + top = typedArray.getDimensionPixelSize( + R.styleable.PdfRendererView_pdfView_page_marginTop, + top + ) + left = typedArray.getDimensionPixelSize( + R.styleable.PdfRendererView_pdfView_page_marginLeft, + left + ) + right = typedArray.getDimensionPixelSize( + R.styleable.PdfRendererView_pdfView_page_marginRight, + right + ) + bottom = typedArray.getDimensionPixelSize( + R.styleable.PdfRendererView_pdfView_page_marginBottom, + bottom + ) } - disableScreenshots = typedArray.getBoolean(R.styleable.PdfRendererView_pdfView_disableScreenshots, false) + disableScreenshots = + typedArray.getBoolean(R.styleable.PdfRendererView_pdfView_disableScreenshots, false) applyScreenshotSecurity() typedArray.recycle() } + private fun applyScreenshotSecurity() { if (disableScreenshots) { // Disables taking screenshots and screen recording (context as? Activity)?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } } + fun closePdfRender() { if (pdfRendererCoreInitialised) { pdfRendererCore.closePdfRender() diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfSource.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfSource.kt new file mode 100644 index 0000000..efa6955 --- /dev/null +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfSource.kt @@ -0,0 +1,10 @@ +package com.rajat.pdfviewer + +import android.net.Uri +import java.io.File + +sealed class PdfSource { + data class FromUrl(val url: String): PdfSource() + data class FromFile(val file: File): PdfSource() + data class FromUri(val uri: Uri): PdfSource() +} \ No newline at end of file diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index ef2528b..19fcac1 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -21,257 +21,22 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.* +import kotlin.compareTo +import kotlin.div +import kotlin.text.toFloat +import kotlin.text.toInt +import kotlin.times /** * Created by Rajat on 11,July,2020 */ -//internal class PdfViewAdapter1( -// private val context: Context, -// private val renderer: PdfRendererCore, -// private val pageSpacing: Rect, -// private val enableLoadingForPages: Boolean -//) : -// RecyclerView.Adapter() { -// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder { -// return PdfPageViewHolder( -// ListItemPdfPageBinding.inflate( -// LayoutInflater.from(parent.context), -// parent, -// false -// ) -// ) -// } -// -// override fun getItemCount(): Int { -// return renderer.getPageCount() -// } -// -// override fun onBindViewHolder(holder: PdfPageViewHolder, position: Int) { -// holder.bind(position) -// } -// -// inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { -// fun bind(position: Int) { -// with(itemBinding) { -// handleLoadingForPage(position) -// if (pageView.width == 0 || pageView.height == 0) { -// pageView.post { bind(position) } // Postpone if layout not ready -// return -// } -// val width = pageView.width -// val height = calculateBitmapHeight(width, position) -// val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) -// -// val itemHeight = calculateBitmapHeight(itemBinding.root.width, position) -// val layoutParams = itemBinding.root.layoutParams as ViewGroup.MarginLayoutParams -// Log.i("Item height","$width-$height-$itemHeight-${layoutParams.height}") -// -// layoutParams.height = itemHeight -// layoutParams.setMargins( -// pageSpacing.left, -// pageSpacing.top, -// pageSpacing.right, -// pageSpacing.bottom -// ) -// itemBinding.root.layoutParams = layoutParams -// Log.d("PdfViewAdapter", "BEFORE Bitmap Width: $width, Device Width: ${context.resources.displayMetrics.widthPixels}") -// -// renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> -// if (success && pageNo == position) { -// CoroutineScope(Dispatchers.Main).launch { -// itemBinding.pageView.scaleType = ImageView.ScaleType.FIT_CENTER -// renderedBitmap?.let { -// Log.d("PdfViewAdapter", "renderedBitmap Width: ${it.width}, Bitmap Height: ${it.height}") -// } -// bitmap?.let { -// Log.d("PdfViewAdapter", "Bitmap Width: ${it.width}, Bitmap Height: ${it.height}") -// } -// -// itemBinding.pageView.apply { -// setImageBitmap(renderedBitmap ?: bitmap) -// } -// applyFadeInAnimation(pageView) -// pageLoadingLayout.pdfViewPageLoadingProgress.hide() -// } -// // Prefetch pages after rendering the current page -// renderer.prefetchPages(position, width, height) -// } else { -// CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) -// } -// } -// } -// } -// -// private fun calculateBitmapHeight(width: Int, position: Int): Int { -// // Get the actual dimensions of the PDF page -// val pageDimensions = renderer.getPageDimensions(position) -// // Calculate the aspect ratio of the PDF page -// val aspectRatio = pageDimensions.width.toFloat() / pageDimensions.height.toFloat() -// // Calculate the height based on the width of the ImageView and the aspect ratio -// return (width / aspectRatio).toInt() -// } -// -// private fun applyFadeInAnimation(view: View) { -// view.animation = AlphaAnimation(0F, 1F).apply { -// interpolator = LinearInterpolator() -// duration = 300 -// start() -// } -// } -// -// private fun handleLoadingForPage(position: Int) { -// with(itemBinding) { -// if (!enableLoadingForPages || renderer.pageExistInCache(position)) { -// pageLoadingLayout.pdfViewPageLoadingProgress.hide() -// } else { -// pageLoadingLayout.pdfViewPageLoadingProgress.show() -// } -// } -// } -// } -// -//} -// -//internal class PdfViewAdapter2( -// private val context: Context, -// private val renderer: PdfRendererCore, -// private val pageSpacing: Rect, -// private val enableLoadingForPages: Boolean -//) : RecyclerView.Adapter() { -// -// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder = -// PdfPageViewHolder(ListItemPdfPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) -// -// override fun getItemCount(): Int = renderer.getPageCount() -// -// override fun onBindViewHolder(holder: PdfPageViewHolder, position: Int) { -// holder.bind(position) -// } -// -// inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { -// fun bind(position: Int) { -// with(itemBinding) { -// handleLoadingForPage(position) -// if (pageView.width == 0 || pageView.height == 0) { -// pageView.post { bind(position) } // Postpone if layout not ready -// return -// } -// -// val pageDimensions = renderer.getPageDimensions(position) -// val aspectRatio = pageDimensions.width.toFloat() / pageDimensions.height.toFloat() -// val width = pageView.width -// val height = (width / aspectRatio).toInt() -// -// val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) -// updateLayoutParams(height) -// -// renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> -// if (success && pageNo == position) { -// CoroutineScope(Dispatchers.Main).launch { -// itemBinding.pageView.setImageBitmap(renderedBitmap ?: bitmap) -// applyFadeInAnimation(pageView) -// pageLoadingLayout.pdfViewPageLoadingProgress.hide() -// } -// } else { -// CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) -// } -// } -// } -// } -// -// private fun ListItemPdfPageBinding.updateLayoutParams(height: Int) { -// root.layoutParams = root.layoutParams.apply { -// this.height = height -// if (this is ViewGroup.MarginLayoutParams) { -// setMargins(pageSpacing.left, pageSpacing.top, pageSpacing.right, pageSpacing.bottom) -// } -// } -// } -// -// private fun applyFadeInAnimation(view: View) { -// view.startAnimation(AlphaAnimation(0F, 1F).apply { -// interpolator = LinearInterpolator() -// duration = 300 -// }) -// } -// -// private fun handleLoadingForPage(position: Int) { -// with(itemBinding) { -// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = -// if (enableLoadingForPages && !renderer.pageExistInCache(position)) View.VISIBLE else View.GONE -// } -// } -// } -//} - -// -//internal class PdfViewAdapter3( -// private val context: Context, -// private val renderer: PdfRendererCore, -// private val pageSpacing: Rect, -// private val enableLoadingForPages: Boolean -//) : RecyclerView.Adapter() { -// -// override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder = -// PdfPageViewHolder(ListItemPdfPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) -// -// override fun getItemCount(): Int = renderer.getPageCount() -// -// override fun onBindViewHolder(holder: PdfPageViewHolder, position: Int) { -// holder.bind(position) -// } -// -// inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { -// fun bind(position: Int) { -// with(itemBinding) { -// // Load a placeholder or progress bar while the actual content is being prepared. -// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = if (enableLoadingForPages) View.VISIBLE else View.GONE -// -// // Calculate the required bitmap dimensions only once and reuse if already calculated. -// val dimensions = renderer.getPageDimensionsAsync(position) { width, height -> -// val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) -// updateLayoutParams(height) -// -// renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> -// if (success && pageNo == position) { -// CoroutineScope(Dispatchers.Main).launch { -// pageView.setImageBitmap(renderedBitmap ?: bitmap) -// applyFadeInAnimation(pageView) -// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE -// } -// } else { -// CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) -// } -// } -// } -// } -// } -// -// private fun ListItemPdfPageBinding.updateLayoutParams(height: Int) { -// root.layoutParams = root.layoutParams.apply { -// this.height = height -// if (this is ViewGroup.MarginLayoutParams) { -// setMargins(pageSpacing.left, pageSpacing.top, pageSpacing.right, pageSpacing.bottom) -// } -// } -// } -// -// private fun applyFadeInAnimation(view: View) { -// view.startAnimation(AlphaAnimation(0F, 1F).apply { -// interpolator = LinearInterpolator() -// duration = 300 -// }) -// } -// } -//} - - internal class PdfViewAdapter( private val context: Context, private val renderer: PdfRendererCore, private val pageSpacing: Rect, - private val enableLoadingForPages: Boolean + private val enableLoadingForPages: Boolean, + private val renderQuality: RenderQuality, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder = @@ -284,85 +49,21 @@ internal class PdfViewAdapter( } inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { -// fun bind(position: Int) { -//// with(itemBinding) { -//// handleLoadingForPage(position) -//// if (pageView.width == 0 || pageView.height == 0) { -//// pageView.post { bind(position) } // Delay the binding if the layout isn't ready. -//// return -//// } -//// -//// val pageDimensions = renderer.getPageDimensions(position) -//// val aspectRatio = pageDimensions.width.toFloat() / pageDimensions.height.toFloat() -//// val width = pageView.width -//// val height = (width / aspectRatio).toInt() -//// -//// // Use cached or dynamically calculated height here as per your original logic. -//// val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) -//// updateLayoutParams(height) -//// -//// renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> -//// if (success && pageNo == position) { -//// CoroutineScope(Dispatchers.Main).launch { -//// itemBinding.pageView.setImageBitmap(renderedBitmap ?: bitmap) -//// applyFadeInAnimation(pageView) -//// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE -//// } -//// } else { -//// CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) -//// } -//// } -//// } -// -// with(itemBinding) { -// // Show a placeholder or loading indicator -// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.VISIBLE -// -// // Fetch dimensions asynchronously -// renderer.getPageDimensionsAsync(position) { size -> -// val aspectRatio = size.width.toFloat() / size.height.toFloat() -// val width = pageView.width -// val height = (width / aspectRatio).toInt() -// -// // Update layout params based on the actual page size -// updateLayoutParams(height) -// -// // Now load the actual page -// val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) -// renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> -// if (success && pageNo == position) { -// pageView.setImageBitmap(renderedBitmap ?: bitmap) -// applyFadeInAnimation(pageView) -// pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE -// } else { -// CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) -// } -// } -// } -// } -// -// } -// -// private fun ListItemPdfPageBinding.updateLayoutParams(height: Int) { -// root.layoutParams.height = height -// (root.layoutParams as? ViewGroup.MarginLayoutParams)?.setMargins( -// pageSpacing.left, pageSpacing.top, pageSpacing.right, pageSpacing.bottom -// ) -// } - fun bind(position: Int) { with(itemBinding) { pageLoadingLayout.pdfViewPageLoadingProgress.visibility = if (enableLoadingForPages) View.VISIBLE else View.GONE renderer.getPageDimensionsAsync(position) { size -> - val width = pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels + val width = (pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels) val aspectRatio = size.width.toFloat() / size.height.toFloat() + val bitmapWidth = (width * (renderQuality.ordinal * aspectRatio + 1)).toInt() val height = (width / aspectRatio).toInt() + val bitmapHeight = (height * (renderQuality.ordinal * aspectRatio + 1)).toInt() updateLayoutParams(height) - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, maxOf(1, height)) + val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(bitmapWidth, maxOf(1, bitmapHeight)) renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> if (success && pageNo == position) { CoroutineScope(Dispatchers.Main).launch { diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt index b6271db..71f87e3 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewerActivity.kt @@ -357,8 +357,8 @@ class PdfViewerActivity : AppCompatActivity() { if (TextUtils.isEmpty(fileUrl)) onPdfError("") //Initiating PDf Viewer with URL try { - binding.pdfView.initWithUrl( - fileUrl!!, + binding.pdfView.init( + PdfSource.FromUrl(fileUrl!!), headers, lifecycleScope, lifecycle = lifecycle diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt index ca21009..5b6c965 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt @@ -22,6 +22,8 @@ class PinchZoomRecyclerView : RecyclerView { private var mLastTouchY = 0f private var mPosX = 0f private var mPosY = 0f + private var onTop = true + var onTopChange: (Boolean) -> Unit = {} constructor(context: Context) : super(context) { initializeScaleDetector(context) @@ -87,7 +89,13 @@ class PinchZoomRecyclerView : RecyclerView { mPosX = (maxWidth - width * mScaleFactor).coerceAtLeast(mPosX.coerceAtMost(0f)) mPosY = (maxHeight - height * mScaleFactor).coerceAtLeast(mPosY.coerceAtMost(0f)) } - + if (mPosY == 0f && !onTop) { + onTopChange(true) + onTop = true + } else if (mPosY != 0f && onTop) { + onTopChange(false) + onTop = false + } mLastTouchX = x mLastTouchY = y invalidate() diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt new file mode 100644 index 0000000..1a3e419 --- /dev/null +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt @@ -0,0 +1,5 @@ +package com.rajat.pdfviewer + +enum class RenderQuality { + LOW, MEDIUM, HIGH +} \ No newline at end of file diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt index b70788d..8143106 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt @@ -1,6 +1,5 @@ package com.rajat.pdfviewer.compose -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner @@ -9,37 +8,37 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.rajat.pdfviewer.HeaderData import com.rajat.pdfviewer.PdfRendererView -import java.io.File +import com.rajat.pdfviewer.PdfSource +import com.rajat.pdfviewer.PinchZoomRecyclerView +import com.rajat.pdfviewer.RenderQuality @Composable -fun PdfRendererViewCompose( +fun Pdf( modifier: Modifier = Modifier, - url: String? = null, - file: File? = null, - uri: Uri? = null, + source: PdfSource, + renderQuality: RenderQuality = RenderQuality.LOW, headers: HeaderData = HeaderData(), lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - statusCallBack: PdfRendererView.StatusCallBack? = null + statusCallBack: PdfRendererView.StatusCallBack? = null, + scrolledToTop: (Boolean) -> Unit = {}, ) { val lifecycleScope = lifecycleOwner.lifecycleScope AndroidView( factory = { context -> PdfRendererView(context).apply { - if (statusCallBack != null) { - statusListener = statusCallBack - } - if (file != null) { - initWithFile(file) - } else if (url != null) { - initWithUrl(url, headers, lifecycleScope, lifecycleOwner.lifecycle) - } else if (uri != null) { - initWithUri(uri) + if (statusCallBack != null) statusListener = statusCallBack + this.renderQuality = renderQuality + init(source, headers, lifecycleScope, lifecycleOwner.lifecycle) + (recyclerView as? PinchZoomRecyclerView)?.onTopChange = { + scrolledToTop(it) } } }, - update = { view -> - // Update logic if needed + update = { pdfView -> + if (pdfView.pdfSource != source) { + pdfView.init(source, headers, lifecycleScope, lifecycleOwner.lifecycle) + } }, modifier = modifier ) From 85e7a0615dbe9732b000339b5d80886d79db11aa Mon Sep 17 00:00:00 2001 From: miseke-ffw Date: Mon, 11 Nov 2024 11:42:51 +0100 Subject: [PATCH 2/3] Fix CodeFactor --- .../com/rajat/pdfviewer/PdfViewAdapter.kt | 2 +- .../rajat/pdfviewer/PinchZoomRecyclerView.kt | 126 +++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index 19fcac1..44b6773 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -55,7 +55,7 @@ internal class PdfViewAdapter( pageLoadingLayout.pdfViewPageLoadingProgress.visibility = if (enableLoadingForPages) View.VISIBLE else View.GONE renderer.getPageDimensionsAsync(position) { size -> - val width = (pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels) + val width = pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels val aspectRatio = size.width.toFloat() / size.height.toFloat() val bitmapWidth = (width * (renderQuality.ordinal * aspectRatio + 1)).toInt() val height = (width / aspectRatio).toInt() diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt index 5b6c965..2b7ce3f 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt @@ -70,72 +70,74 @@ class PinchZoomRecyclerView : RecyclerView { mGestureDetector?.onTouchEvent(ev) mScaleDetector?.onTouchEvent(ev) when (ev.action and MotionEvent.ACTION_MASK) { - MotionEvent.ACTION_DOWN -> { - mLastTouchX = ev.x - mLastTouchY = ev.y - mActivePointerId = ev.getPointerId(0) - } - MotionEvent.ACTION_MOVE -> { - val pointerIndex = ev.findPointerIndex(mActivePointerId) - val x = ev.getX(pointerIndex) - val y = ev.getY(pointerIndex) - - if (mScaleFactor > 1f) { - val dx = x - mLastTouchX - val dy = y - mLastTouchY - - mPosX += dx - mPosY += dy - mPosX = (maxWidth - width * mScaleFactor).coerceAtLeast(mPosX.coerceAtMost(0f)) - mPosY = (maxHeight - height * mScaleFactor).coerceAtLeast(mPosY.coerceAtMost(0f)) - } - if (mPosY == 0f && !onTop) { - onTopChange(true) - onTop = true - } else if (mPosY != 0f && onTop) { - onTopChange(false) - onTop = false - } - mLastTouchX = x - mLastTouchY = y - invalidate() - } - MotionEvent.ACTION_POINTER_UP -> { - // Extract the index of the pointer that left the touch sensor - val pointerIndex = (ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK) shr MotionEvent.ACTION_POINTER_INDEX_SHIFT - val pointerId = ev.getPointerId(pointerIndex) - - if (pointerId == mActivePointerId) { - // This was our active pointer going up. Choose a new active pointer and adjust accordingly. - val newPointerIndex = if (pointerIndex == 0) 1 else 0 - - mLastTouchX = ev.getX(newPointerIndex) - mLastTouchY = ev.getY(newPointerIndex) - mActivePointerId = ev.getPointerId(newPointerIndex) - } - } - MotionEvent.ACTION_CANCEL -> mActivePointerId = INVALID_POINTER_ID - MotionEvent.ACTION_POINTER_UP -> { - val pointerIndex = ev.actionIndex - val pointerId = ev.getPointerId(pointerIndex) - if (pointerId == mActivePointerId) { - val newPointerIndex = if (pointerIndex == 0) 1 else 0 - mLastTouchX = ev.getX(newPointerIndex) - mLastTouchY = ev.getY(newPointerIndex) - mActivePointerId = ev.getPointerId(newPointerIndex) - } - } - MotionEvent.ACTION_SCROLL -> { - val dy = ev.getAxisValue(MotionEvent.AXIS_VSCROLL) * mScaleFactor - mPosY += dy - clampPosition() - invalidate() - } + MotionEvent.ACTION_DOWN -> down(ev) + MotionEvent.ACTION_MOVE -> move(ev) + MotionEvent.ACTION_POINTER_UP -> pointerUp(ev) + MotionEvent.ACTION_CANCEL -> cancel() + MotionEvent.ACTION_SCROLL -> scroll(ev) } - return superHandled || mScaleFactor > 1f } + private fun cancel() { + mActivePointerId = INVALID_POINTER_ID + } + + private fun down(ev: MotionEvent) { + mLastTouchX = ev.x + mLastTouchY = ev.y + mActivePointerId = ev.getPointerId(0) + } + + private fun scroll(ev: MotionEvent) { + val dy = ev.getAxisValue(MotionEvent.AXIS_VSCROLL) * mScaleFactor + mPosY += dy + clampPosition() + invalidate() + } + + private fun pointerUp(ev: MotionEvent) { + // Extract the index of the pointer that left the touch sensor + val pointerIndex = + (ev.action and MotionEvent.ACTION_POINTER_INDEX_MASK) shr MotionEvent.ACTION_POINTER_INDEX_SHIFT + val pointerId = ev.getPointerId(pointerIndex) + + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new active pointer and adjust accordingly. + val newPointerIndex = if (pointerIndex == 0) 1 else 0 + + mLastTouchX = ev.getX(newPointerIndex) + mLastTouchY = ev.getY(newPointerIndex) + mActivePointerId = ev.getPointerId(newPointerIndex) + } + } + + private fun move(ev: MotionEvent) { + val pointerIndex = ev.findPointerIndex(mActivePointerId) + val x = ev.getX(pointerIndex) + val y = ev.getY(pointerIndex) + + if (mScaleFactor > 1f) { + val dx = x - mLastTouchX + val dy = y - mLastTouchY + + mPosX += dx + mPosY += dy + mPosX = (maxWidth - width * mScaleFactor).coerceAtLeast(mPosX.coerceAtMost(0f)) + mPosY = (maxHeight - height * mScaleFactor).coerceAtLeast(mPosY.coerceAtMost(0f)) + } + if (mPosY == 0f && !onTop) { + onTopChange(true) + onTop = true + } else if (mPosY != 0f && onTop) { + onTopChange(false) + onTop = false + } + mLastTouchX = x + mLastTouchY = y + invalidate() + } + override fun onDraw(canvas: Canvas) { canvas.save() canvas.translate(mPosX, mPosY) From d9c8fa3dd88a74da774d58a052bd0632fed5b146 Mon Sep 17 00:00:00 2001 From: miseke-ffw Date: Thu, 12 Dec 2024 10:03:53 +0100 Subject: [PATCH 3/3] Correctly render pdf in background, add callback for when a page has been rendered --- .../rajat/sample/pdfviewer/ComposeActivity.kt | 6 +- .../com/rajat/pdfviewer/PdfRenderResult.kt | 8 + .../com/rajat/pdfviewer/PdfRendererCore.kt | 350 +++++++++--------- .../com/rajat/pdfviewer/PdfRendererView.kt | 6 +- .../com/rajat/pdfviewer/PdfViewAdapter.kt | 47 ++- .../pdfviewer/compose/PdfRendererCompose.kt | 2 + 6 files changed, 233 insertions(+), 186 deletions(-) create mode 100644 pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRenderResult.kt diff --git a/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt b/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt index 214c7a1..734287a 100644 --- a/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt +++ b/app/src/main/java/com/rajat/sample/pdfviewer/ComposeActivity.kt @@ -101,7 +101,7 @@ fun MyPdfScreenFromUri(modifier: Modifier = Modifier) { @Composable fun MyPdfScreenFromUri(uri: Uri, modifier: Modifier = Modifier) { val lifecycleOwner = LocalLifecycleOwner.current - PdfRendererViewCompose( + Pdf( modifier = modifier, source = PdfSource.FromUri(uri), lifecycleOwner = lifecycleOwner, @@ -136,7 +136,7 @@ fun MyPdfScreenFromUri(uri: Uri, modifier: Modifier = Modifier) { @Composable fun MyPdfScreenFromUrl(url: String, modifier: Modifier = Modifier) { val lifecycleOwner = LocalLifecycleOwner.current - PdfRendererViewCompose( + Pdf( modifier = modifier, source = PdfSource.FromUrl(url), lifecycleOwner = lifecycleOwner, @@ -173,7 +173,7 @@ fun MyPdfScreenFromUrl(url: String, modifier: Modifier = Modifier) { fun MyPdfScreenFromFile() { val lifecycleOwner = LocalLifecycleOwner.current val pdfFile = File("path/to/your/file.pdf") // Replace with your file path - PdfRendererViewCompose( + Pdf( source = PdfSource.FromFile(pdfFile), lifecycleOwner = lifecycleOwner ) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRenderResult.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRenderResult.kt new file mode 100644 index 0000000..f518a74 --- /dev/null +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRenderResult.kt @@ -0,0 +1,8 @@ +package com.rajat.pdfviewer + +import android.graphics.Bitmap + +sealed class PdfRenderResult(open val pageNo: Int) { + data class Error(override val pageNo: Int, val cause: Throwable? = null) : PdfRenderResult(pageNo) + data class Success(override val pageNo: Int, val bitmap: Bitmap) : PdfRenderResult(pageNo) +} diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt index 4663019..df25696 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererCore.kt @@ -1,216 +1,228 @@ - package com.rajat.pdfviewer - import android.content.Context - import android.graphics.Bitmap - import android.graphics.Bitmap.CompressFormat - import android.graphics.Color - import android.graphics.pdf.PdfRenderer - import android.os.Build - import android.os.ParcelFileDescriptor - import android.util.Log - import android.util.Size - import com.rajat.pdfviewer.util.CacheManager - import com.rajat.pdfviewer.util.CacheManager.Companion.CACHE_PATH - import com.rajat.pdfviewer.util.CommonUtils - import com.rajat.pdfviewer.util.CommonUtils.Companion.calculateDynamicPrefetchCount - import kotlinx.coroutines.CoroutineScope - import kotlinx.coroutines.Dispatchers - import kotlinx.coroutines.launch - import kotlinx.coroutines.withContext - import java.io.File - import java.io.FileOutputStream - import java.nio.file.Files - import java.nio.file.Paths - import java.util.concurrent.ConcurrentHashMap - - /** - * Created by Rajat on 11,July,2020 - */ - - internal class PdfRendererCore( - private val context: Context, - fileDescriptor: ParcelFileDescriptor - ) { - - private var isRendererOpen = false - - constructor(context: Context, file: File) : this( - context = context, - fileDescriptor = getFileDescriptor(file) - ) - - private val openPages = ConcurrentHashMap() - private var pdfRenderer: PdfRenderer? = null - private val cacheManager = CacheManager(context) - - companion object { - - private fun sanitizeFilePath(filePath: String): String { - return try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val path = Paths.get(filePath) - if (Files.exists(path)) { - filePath - } else { - "" // Return a default safe path or handle the error - } - } else { +package com.rajat.pdfviewer + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Bitmap.CompressFormat +import android.graphics.Color +import android.graphics.pdf.PdfRenderer +import android.os.Build +import android.os.ParcelFileDescriptor +import android.util.Log +import android.util.Size +import com.rajat.pdfviewer.util.CacheManager +import com.rajat.pdfviewer.util.CacheManager.Companion.CACHE_PATH +import com.rajat.pdfviewer.util.CommonUtils +import com.rajat.pdfviewer.util.CommonUtils.Companion.calculateDynamicPrefetchCount +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.io.FileOutputStream +import java.lang.IllegalArgumentException +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap + +/** + * Created by Rajat on 11,July,2020 + */ + +internal class PdfRendererCore( + private val context: Context, + fileDescriptor: ParcelFileDescriptor +) { + + private var isRendererOpen = false + + constructor(context: Context, file: File) : this( + context = context, + fileDescriptor = getFileDescriptor(file) + ) + + private val openPages = ConcurrentHashMap() + private var pdfRenderer: PdfRenderer? = null + private val cacheManager = CacheManager(context) + + companion object { + + private fun sanitizeFilePath(filePath: String): String { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val path = Paths.get(filePath) + if (Files.exists(path)) { filePath + } else { + "" // Return a default safe path or handle the error } - } catch (e: Exception) { - "" // Handle the exception and return a safe default path + } else { + filePath } + } catch (e: Exception) { + "" // Handle the exception and return a safe default path } + } - internal fun getFileDescriptor(file: File): ParcelFileDescriptor { - val safeFile = File(sanitizeFilePath(file.path)) - return ParcelFileDescriptor.open(safeFile, ParcelFileDescriptor.MODE_READ_ONLY) - } + internal fun getFileDescriptor(file: File): ParcelFileDescriptor { + val safeFile = File(sanitizeFilePath(file.path)) + return ParcelFileDescriptor.open(safeFile, ParcelFileDescriptor.MODE_READ_ONLY) } + } - init { - pdfRenderer = PdfRenderer(fileDescriptor).also { isRendererOpen = true } - cacheManager.initCache() - } + init { + pdfRenderer = PdfRenderer(fileDescriptor).also { isRendererOpen = true } + cacheManager.initCache() + } - internal fun getBitmapFromCache(pageNo: Int): Bitmap? = - cacheManager.getBitmapFromCache(pageNo) + internal fun getBitmapFromCache(pageNo: Int): Bitmap? = + cacheManager.getBitmapFromCache(pageNo) - private fun addBitmapToMemoryCache(pageNo: Int, bitmap: Bitmap) = - cacheManager.addBitmapToCache(pageNo, bitmap) + private fun addBitmapToMemoryCache(pageNo: Int, bitmap: Bitmap) = + cacheManager.addBitmapToCache(pageNo, bitmap) - private fun writeBitmapToCache(pageNo: Int, bitmap: Bitmap, shouldCache: Boolean = true) { - if (!shouldCache) return - CoroutineScope(Dispatchers.IO).launch { - try { - val savePath = File(File(context.cacheDir, CACHE_PATH), pageNo.toString()) - FileOutputStream(savePath).use { fos -> - bitmap.compress(CompressFormat.JPEG, 75, fos) // Compress as JPEG - } - } catch (e: Exception) { - Log.e("PdfRendererCore", "Error writing bitmap to cache: ${e.message}") + private fun writeBitmapToCache(pageNo: Int, bitmap: Bitmap, shouldCache: Boolean = true) { + if (!shouldCache) return + CoroutineScope(Dispatchers.IO).launch { + try { + val savePath = File(File(context.cacheDir, CACHE_PATH), pageNo.toString()) + FileOutputStream(savePath).use { fos -> + bitmap.compress(CompressFormat.JPEG, 75, fos) // Compress as JPEG } + } catch (e: Exception) { + Log.e("PdfRendererCore", "Error writing bitmap to cache: ${e.message}") } } - fun pageExistInCache(pageNo: Int): Boolean = - cacheManager.pageExistsInCache(pageNo) - - fun prefetchPages(currentPage: Int, width: Int, height: Int) { - val dynamicPrefetchCount = calculateDynamicPrefetchCount(context, pdfRenderer!!) - (currentPage - dynamicPrefetchCount..currentPage + dynamicPrefetchCount) - .filter { it in 0 until pdfRenderer!!.pageCount && !pageExistInCache(it) } - .forEach { pageNo -> - CoroutineScope(Dispatchers.IO).launch { - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) - renderPage(pageNo, bitmap) { success, _, _ -> - if (success) writeBitmapToCache(pageNo, bitmap) - else CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) + } + + fun pageExistInCache(pageNo: Int): Boolean = + cacheManager.pageExistsInCache(pageNo) + + fun prefetchPages(currentPage: Int, width: Int, height: Int) { + val dynamicPrefetchCount = calculateDynamicPrefetchCount(context, pdfRenderer!!) + (currentPage - dynamicPrefetchCount..currentPage + dynamicPrefetchCount) + .filter { it in 0 until pdfRenderer!!.pageCount && !pageExistInCache(it) } + .forEach { pageNo -> + CoroutineScope(Dispatchers.IO).launch { + val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) + val renderResult = renderPage(pageNo, bitmap) + when (renderResult) { + is PdfRenderResult.Error -> { + CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) + } + + is PdfRenderResult.Success -> { + writeBitmapToCache(pageNo, bitmap) } } } - } - private fun openPdfFile(fileDescriptor: ParcelFileDescriptor) { - pdfRenderer = PdfRenderer(fileDescriptor) - } - - fun getPageCount(): Int { - synchronized(this) { - if (!isRendererOpen) return 0 - return pdfRenderer?.pageCount ?: 0 } + } + + private fun openPdfFile(fileDescriptor: ParcelFileDescriptor) { + pdfRenderer = PdfRenderer(fileDescriptor) + } + + fun getPageCount(): Int { + synchronized(this) { + if (!isRendererOpen) return 0 + return pdfRenderer?.pageCount ?: 0 } + } - fun renderPage(pageNo: Int, bitmap: Bitmap, onBitmapReady: ((success: Boolean, pageNo: Int, bitmap: Bitmap?) -> Unit)? = null) { + suspend fun renderPage(pageNo: Int, bitmap: Bitmap): PdfRenderResult = + withContext(Dispatchers.IO) { if (pageNo >= getPageCount()) { - onBitmapReady?.invoke(false, pageNo, null) - return + return@withContext PdfRenderResult.Error( + pageNo, + IllegalArgumentException("Illegal pageNo. pageNo=$pageNo, pageCount=${getPageCount()}") + ) } val cachedBitmap = getBitmapFromCache(pageNo) if (cachedBitmap != null) { - CoroutineScope(Dispatchers.Main).launch { onBitmapReady?.invoke(true, pageNo, cachedBitmap) } - return + return@withContext PdfRenderResult.Success(pageNo, cachedBitmap) } - CoroutineScope(Dispatchers.IO).launch { - synchronized(this@PdfRendererCore) { - if (!isRendererOpen) return@launch - openPageSafely(pageNo)?.use { pdfPage -> - try { - bitmap.eraseColor(Color.WHITE) // Clear the bitmap with white color - pdfPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - addBitmapToMemoryCache(pageNo, bitmap) - CoroutineScope(Dispatchers.IO).launch { writeBitmapToCache(pageNo, bitmap) } - CoroutineScope(Dispatchers.Main).launch { onBitmapReady?.invoke(true, pageNo, bitmap) } - } catch (e: Exception) { - CoroutineScope(Dispatchers.Main).launch { onBitmapReady?.invoke(false, pageNo, null) } - } - } + if (!isRendererOpen) return@withContext PdfRenderResult.Error( + pageNo, + IllegalStateException("Renderer is not open") + ) + val page = openPageSafely(pageNo) + page?.use { pdfPage -> + return@withContext try { + bitmap.eraseColor(Color.WHITE) // Clear the bitmap with white color + pdfPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + addBitmapToMemoryCache(pageNo, bitmap) + writeBitmapToCache(pageNo, bitmap) + PdfRenderResult.Success(pageNo, bitmap) + } catch (e: Exception) { + PdfRenderResult.Error(pageNo, e) } - } + } ?: return@withContext PdfRenderResult.Error(pageNo, IllegalStateException("Page is null")) } - private suspend fun withPdfPage(pageNo: Int, block: (PdfRenderer.Page) -> T): T? = - withContext(Dispatchers.IO) { - synchronized(this@PdfRendererCore) { - pdfRenderer?.openPage(pageNo)?.use { page -> - return@withContext block(page) - } + private suspend fun withPdfPage(pageNo: Int, block: (PdfRenderer.Page) -> T): T? = + withContext(Dispatchers.IO) { + synchronized(this@PdfRendererCore) { + pdfRenderer?.openPage(pageNo)?.use { page -> + return@withContext block(page) } - null } + null + } - private val pageDimensionCache = mutableMapOf() + private val pageDimensionCache = mutableMapOf() - fun getPageDimensionsAsync(pageNo: Int, callback: (Size) -> Unit) { - pageDimensionCache[pageNo]?.let { - callback(it) - return - } - - CoroutineScope(Dispatchers.IO).launch { - val size = withPdfPage(pageNo) { page -> - Size(page.width, page.height).also { pageSize -> - pageDimensionCache[pageNo] = pageSize - } - } ?: Size(1, 1) // Fallback to a default minimal size + fun getPageDimensionsAsync(pageNo: Int, callback: (Size) -> Unit) { + pageDimensionCache[pageNo]?.let { + callback(it) + return + } - withContext(Dispatchers.Main) { - callback(size) + CoroutineScope(Dispatchers.IO).launch { + val size = withPdfPage(pageNo) { page -> + Size(page.width, page.height).also { pageSize -> + pageDimensionCache[pageNo] = pageSize } + } ?: Size(1, 1) // Fallback to a default minimal size + + withContext(Dispatchers.Main) { + callback(size) } } + } - private fun openPageSafely(pageNo: Int): PdfRenderer.Page? { - synchronized(this) { - if (!isRendererOpen) return null - closeAllOpenPages() - return pdfRenderer?.openPage(pageNo)?.also { page -> - openPages[pageNo] = page - } + private fun openPageSafely(pageNo: Int): PdfRenderer.Page? { + synchronized(this) { + if (!isRendererOpen) return null + closeAllOpenPages() + return pdfRenderer?.openPage(pageNo)?.also { page -> + openPages[pageNo] = page } } + } - private fun closeAllOpenPages() { - synchronized(this) { - openPages.values.forEach { page -> - try { - page.close() - } catch (e: IllegalStateException) { - Log.e("PDFRendererCore","Page was already closed") - } + private fun closeAllOpenPages() { + synchronized(this) { + openPages.values.forEach { page -> + try { + page.close() + } catch (e: IllegalStateException) { + Log.e("PDFRendererCore", "Page was already closed") } - openPages.clear() // Clear the map after closing all pages. } + openPages.clear() // Clear the map after closing all pages. } + } - fun closePdfRender() { - synchronized(this) { - closeAllOpenPages() - if (isRendererOpen) { - pdfRenderer?.close() - isRendererOpen = false - } - cacheManager.clearCache() + fun closePdfRender() { + synchronized(this) { + closeAllOpenPages() + if (isRendererOpen) { + pdfRenderer?.close() + isRendererOpen = false } + cacheManager.clearCache() } - } + +} diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt index 58e3643..15da6c8 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt @@ -50,6 +50,7 @@ class PdfRendererView @JvmOverloads constructor( private var pdfRendererCoreInitialised = false private var pageMargin: Rect = Rect(0, 0, 0, 0) var statusListener: StatusCallBack? = null + var pageRenderListener: (position: Int) -> Unit = {} private var positionToUseForState: Int = 0 private var restoredScrollPosition: Int = NO_POSITION private var disableScreenshots: Boolean = false @@ -173,7 +174,10 @@ class PdfRendererView @JvmOverloads constructor( renderer = pdfRendererCore, pageSpacing = pageMargin, enableLoadingForPages = enableLoadingForPages, - renderQuality = renderQuality + renderQuality = renderQuality, + onPageRendered = { + pageRenderListener(it) + } ) val v = LayoutInflater.from(context).inflate(R.layout.pdf_rendererview, this, false) addView(v) diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index 44b6773..775b9e7 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -19,6 +19,7 @@ import com.rajat.pdfviewer.util.hide import com.rajat.pdfviewer.util.show import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import java.util.* import kotlin.compareTo @@ -37,10 +38,17 @@ internal class PdfViewAdapter( private val pageSpacing: Rect, private val enableLoadingForPages: Boolean, private val renderQuality: RenderQuality, + private val onPageRendered: (position: Int) -> Unit, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder = - PdfPageViewHolder(ListItemPdfPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + PdfPageViewHolder( + ListItemPdfPageBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) override fun getItemCount(): Int = renderer.getPageCount() @@ -48,14 +56,17 @@ internal class PdfViewAdapter( holder.bind(position) } - inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : RecyclerView.ViewHolder(itemBinding.root) { + inner class PdfPageViewHolder(private val itemBinding: ListItemPdfPageBinding) : + RecyclerView.ViewHolder(itemBinding.root) { fun bind(position: Int) { with(itemBinding) { - pageLoadingLayout.pdfViewPageLoadingProgress.visibility = if (enableLoadingForPages) View.VISIBLE else View.GONE + pageLoadingLayout.pdfViewPageLoadingProgress.visibility = + if (enableLoadingForPages) View.VISIBLE else View.GONE renderer.getPageDimensionsAsync(position) { size -> - val width = pageView.width.takeIf { it > 0 } ?: context.resources.displayMetrics.widthPixels + val width = pageView.width.takeIf { it > 0 } + ?: context.resources.displayMetrics.widthPixels val aspectRatio = size.width.toFloat() / size.height.toFloat() val bitmapWidth = (width * (renderQuality.ordinal * aspectRatio + 1)).toInt() val height = (width / aspectRatio).toInt() @@ -63,16 +74,26 @@ internal class PdfViewAdapter( updateLayoutParams(height) - val bitmap = CommonUtils.Companion.BitmapPool.getBitmap(bitmapWidth, maxOf(1, bitmapHeight)) - renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap -> - if (success && pageNo == position) { - CoroutineScope(Dispatchers.Main).launch { - pageView.setImageBitmap(renderedBitmap ?: bitmap) - applyFadeInAnimation(pageView) - pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View.GONE + val bitmap = CommonUtils.Companion.BitmapPool.getBitmap( + bitmapWidth, + maxOf(1, bitmapHeight) + ) + CoroutineScope(Dispatchers.IO).launch { + val renderResult = renderer.renderPage(position, bitmap) + onPageRendered(renderResult.pageNo) + when (renderResult) { + is PdfRenderResult.Success -> if (renderResult.pageNo == position) { + CoroutineScope(Dispatchers.Main).launch { + pageView.setImageBitmap(renderResult.bitmap) + applyFadeInAnimation(pageView) + pageLoadingLayout.pdfViewPageLoadingProgress.visibility = + View.GONE + } + } else { + CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) } - } else { - CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) + + else -> CommonUtils.Companion.BitmapPool.recycleBitmap(bitmap) } } } diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt index 8143106..b54bc88 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt @@ -21,6 +21,7 @@ fun Pdf( lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, statusCallBack: PdfRendererView.StatusCallBack? = null, scrolledToTop: (Boolean) -> Unit = {}, + onPageRendered: (page: Int) -> Unit = {}, ) { val lifecycleScope = lifecycleOwner.lifecycleScope @@ -33,6 +34,7 @@ fun Pdf( (recyclerView as? PinchZoomRecyclerView)?.onTopChange = { scrolledToTop(it) } + pageRenderListener = onPageRendered } }, update = { pdfView ->