Pdf: first iteration of pdf renderer
This commit is contained in:
@@ -34,6 +34,7 @@ import io.element.android.features.messages.impl.attachments.Attachment
|
||||
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode
|
||||
import io.element.android.features.messages.impl.media.viewer.MediaViewerNode
|
||||
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent
|
||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||
import io.element.android.libraries.architecture.BackstackNode
|
||||
@@ -129,6 +130,16 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
is TimelineItemFileContent -> {
|
||||
val mediaSource = event.content.fileSource
|
||||
val navTarget = NavTarget.MediaViewer(
|
||||
title = event.content.body,
|
||||
mediaSource = mediaSource,
|
||||
thumbnailSource = event.content.thumbnailSource,
|
||||
mimeType = event.content.mimeType,
|
||||
)
|
||||
backstack.push(navTarget)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,34 @@ import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
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.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.MediaItem
|
||||
@@ -38,8 +57,13 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.AspectRatioFrameLayout
|
||||
import androidx.media3.ui.PlayerView
|
||||
import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper
|
||||
import io.element.android.features.messages.impl.media.local.pdf.ParcelFileDescriptorFactory
|
||||
import io.element.android.features.messages.impl.media.local.pdf.PdfPage
|
||||
import io.element.android.features.messages.impl.media.local.pdf.PdfRendererManager
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import me.saket.telephoto.zoomable.ZoomSpec
|
||||
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
|
||||
import me.saket.telephoto.zoomable.rememberZoomableImageState
|
||||
@@ -64,6 +88,9 @@ fun LocalMediaView(
|
||||
onReady = onReady,
|
||||
modifier = modifier
|
||||
)
|
||||
mimeType == io.element.android.libraries.core.mimetype.MimeTypes.Pdf -> {
|
||||
MediaPDFView(localMedia = localMedia, onReady = onReady, modifier = modifier)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
@@ -154,3 +181,95 @@ fun MediaVideoView(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@Composable
|
||||
fun MediaPDFView(
|
||||
localMedia: LocalMedia?,
|
||||
onReady: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
val maxWidth = this.maxWidth.dpToPx()
|
||||
val lazyState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var pdfRendererManager by remember {
|
||||
mutableStateOf<PdfRendererManager?>(null)
|
||||
}
|
||||
DisposableEffect(localMedia) {
|
||||
ParcelFileDescriptorFactory(context).create(localMedia?.model)
|
||||
.onSuccess {
|
||||
pdfRendererManager = PdfRendererManager(it, maxWidth, coroutineScope).apply {
|
||||
open()
|
||||
}
|
||||
onReady()
|
||||
}
|
||||
onDispose {
|
||||
pdfRendererManager?.close()
|
||||
}
|
||||
}
|
||||
pdfRendererManager?.run {
|
||||
val pdfPages = pdfPages.collectAsState().value
|
||||
PdfPagesView(pdfPages.toImmutableList(), lazyState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfPagesView(
|
||||
pdfPages: ImmutableList<PdfPage>,
|
||||
lazyListState: LazyListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
state = lazyListState
|
||||
) {
|
||||
items(pdfPages.size) { index ->
|
||||
val pdfPage = pdfPages[index]
|
||||
PdfPageView(pdfPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PdfPageView(
|
||||
pdfPage: PdfPage,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val pdfPageState by pdfPage.stateFlow.collectAsState()
|
||||
DisposableEffect(pdfPage) {
|
||||
pdfPage.load()
|
||||
onDispose {
|
||||
pdfPage.close()
|
||||
}
|
||||
}
|
||||
when (val state = pdfPageState) {
|
||||
is PdfPage.State.Loaded -> {
|
||||
Image(
|
||||
bitmap = state.bitmap.asImageBitmap(),
|
||||
contentDescription = "Page ${pdfPage.pageIndex}",
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
is PdfPage.State.Loading -> {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(state.height.pxToDp())
|
||||
.background(color = Color.White)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
|
||||
|
||||
@Composable
|
||||
private fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.roundToPx() }
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.media.local.pdf
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.File
|
||||
|
||||
class ParcelFileDescriptorFactory(private val context: Context) {
|
||||
|
||||
fun create(model: Any?) = runCatching {
|
||||
when (model) {
|
||||
is File -> ParcelFileDescriptor.open(model, ParcelFileDescriptor.MODE_READ_ONLY)
|
||||
is Uri -> context.contentResolver.openFileDescriptor(model, "r")!!
|
||||
else -> error(RuntimeException("Can't handle this model"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.media.local.pdf
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import androidx.compose.runtime.Stable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Stable
|
||||
class PdfPage(
|
||||
maxWidth: Int,
|
||||
val pageIndex: Int,
|
||||
private val mutex: Mutex,
|
||||
private val pdfRenderer: PdfRenderer,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) {
|
||||
|
||||
sealed interface State {
|
||||
data class Loading(val width: Int, val height: Int) : State
|
||||
data class Loaded(val bitmap: Bitmap) : State
|
||||
}
|
||||
|
||||
private val renderWidth = maxWidth
|
||||
private val renderHeight: Int
|
||||
private var loadJob: Job? = null
|
||||
|
||||
init {
|
||||
pdfRenderer.openPage(pageIndex).use { page ->
|
||||
renderHeight = (page.height * (renderWidth.toFloat() / page.width)).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow<State>(
|
||||
State.Loading(
|
||||
width = renderWidth,
|
||||
height = renderHeight
|
||||
)
|
||||
)
|
||||
val stateFlow: StateFlow<State> = mutableStateFlow
|
||||
|
||||
fun load() {
|
||||
loadJob = coroutineScope.launch {
|
||||
val bitmap = mutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
pdfRenderer.openPageRenderAndClose(pageIndex, renderWidth, renderHeight)
|
||||
}
|
||||
}
|
||||
mutableStateFlow.value = State.Loaded(bitmap)
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
loadJob?.cancel()
|
||||
when (val loadingState = stateFlow.value) {
|
||||
is State.Loading -> return
|
||||
is State.Loaded -> {
|
||||
loadingState.bitmap.recycle()
|
||||
mutableStateFlow.value = State.Loading(
|
||||
width = renderWidth,
|
||||
height = renderHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PdfRenderer.openPageRenderAndClose(index: Int, bitmapWidth: Int, bitmapHeight: Int): Bitmap {
|
||||
fun createBitmap(bitmapWidth: Int, bitmapHeight: Int): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(
|
||||
bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888
|
||||
)
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawColor(Color.WHITE)
|
||||
canvas.drawBitmap(bitmap, 0f, 0f, null)
|
||||
return bitmap
|
||||
}
|
||||
return openPage(index).use { page ->
|
||||
createBitmap(bitmapWidth, bitmapHeight).apply {
|
||||
page.render(this, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.media.local.pdf
|
||||
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.os.ParcelFileDescriptor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class PdfRendererManager(
|
||||
private val parcelFileDescriptor: ParcelFileDescriptor,
|
||||
private val width: Int,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
private var pdfRenderer: PdfRenderer? = null
|
||||
private val mutablePdfPages = MutableStateFlow<List<PdfPage>>(emptyList())
|
||||
val pdfPages: StateFlow<List<PdfPage>> = mutablePdfPages
|
||||
|
||||
fun open() {
|
||||
coroutineScope.launch {
|
||||
mutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
pdfRenderer = PdfRenderer(parcelFileDescriptor).apply {
|
||||
(0 until pageCount).map { pageIndex ->
|
||||
PdfPage(width, pageIndex, mutex, this, coroutineScope)
|
||||
}.also {
|
||||
mutablePdfPages.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun close() {
|
||||
coroutineScope.launch {
|
||||
mutex.withLock {
|
||||
mutablePdfPages.value.forEach { pdfPage ->
|
||||
pdfPage.close()
|
||||
}
|
||||
pdfRenderer?.close()
|
||||
parcelFileDescriptor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ object MimeTypes {
|
||||
const val Any: String = "*/*"
|
||||
const val OctetStream = "application/octet-stream"
|
||||
const val Apk = "application/vnd.android.package-archive"
|
||||
const val Pdf = "application/pdf"
|
||||
|
||||
const val Images = "image/*"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user