ganfra
1 year ago
6 changed files with 342 additions and 0 deletions
@ -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() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue