Skip to content

Instantly share code, notes, and snippets.

@grumpyshoe
Created August 22, 2022 08:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grumpyshoe/cffd0bb54b8819e5e562e033445ec2f6 to your computer and use it in GitHub Desktop.
Save grumpyshoe/cffd0bb54b8819e5e562e033445ec2f6 to your computer and use it in GitHub Desktop.
Show PDF in zoomable Image in Jetpack Compose
package de.grumpyshoe.pdfviewer
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.Bundle
import android.os.ParcelFileDescriptor
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RawRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import de.grumpyshoe.pdfviewer.ui.theme.PdfViewerTheme
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
/*
* Based on:
* https://stackoverflow.com/questions/66005066/android-jetpack-compose-how-to-zoom-a-image-in-a-box
*/
class MainActivity : ComponentActivity() {
private lateinit var mPdfRenderer: PdfRenderer
private lateinit var mPdfPage: PdfRenderer.Page
private var outputFile: File = File("Output.pdf")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PdfViewerTheme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
ZoomableImage(openPdfFromRaw(1))
}
}
}
}
@Throws(IOException::class)
fun openPdfFromRaw(pageNumber: Int): Bitmap {
// Copy sample.pdf from 'res/raw' folder into cache so PdfRenderer can handle it
val fileCopy = File(cacheDir, "test.pdf")
copyToCache(fileCopy, R.raw.test)
// We get a page from the PDF doc by calling 'open'
val fileDescriptor = ParcelFileDescriptor.open(
fileCopy,
ParcelFileDescriptor.MODE_READ_ONLY
)
mPdfRenderer = PdfRenderer(fileDescriptor)
mPdfPage = mPdfRenderer.openPage(pageNumber)
// Create a new bitmap and render the page contents into it
val bitmap = Bitmap.createBitmap(
mPdfPage.width,
mPdfPage.height,
Bitmap.Config.ARGB_8888
)
mPdfPage.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
// return bitmap
return bitmap
}
@Throws(IOException::class)
fun copyToCache(file: File?, @RawRes pdfResource: Int) {
if (!outputFile.exists()) {
//Get input stream object to read the pdf
val input: InputStream = resources.openRawResource(pdfResource)
val output = FileOutputStream(file)
val buffer = ByteArray(1024)
var size: Int
// Copy the entire contents of the file
while (input.read(buffer).also { size = it } != -1) {
output.write(buffer, 0, size)
}
//Close the buffer
input.close()
output.close()
}
}
}
@Composable
fun ZoomableImage(bitmap: Bitmap) {
val scale = remember { mutableStateOf(1f) }
val rotationState = remember { mutableStateOf(0f) }
val angle = remember { mutableStateOf(0f) }
val offsetX = remember { mutableStateOf(0f) }
val offsetY = remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.clip(RectangleShape) // Clip the box content
.fillMaxSize() // Give the size you want...
.background(Color.Red)
.pointerInput(Unit) {
detectTransformGestures { _, pan, zoom, rotation ->
scale.value *= zoom
rotationState.value += rotation
val x = pan.x * zoom
val y = pan.y * zoom
val angleRad = angle.value * PI / 180.0
offsetX.value += (x * cos(angleRad) - y * sin(angleRad)).toFloat()
offsetY.value += (x * sin(angleRad) + y * cos(angleRad)).toFloat()
}
}
) {
Image(
modifier = Modifier
.align(Alignment.Center) // keep the image centralized into the Box
.graphicsLayer(
scaleX = scale.value.coerceIn(.5f, 3f),
scaleY = scale.value.coerceIn(.5f, 3f),
translationX = offsetX.value,
translationY = offsetY.value
// rotationZ = rotationState.value // Not needed for now
)
.background(Color.Yellow)
.fillMaxSize(),
contentDescription = null,
bitmap = bitmap.asImageBitmap()
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment