ganfra
1 year ago
6 changed files with 342 additions and 0 deletions
@ -0,0 +1,33 @@
@@ -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 @@
@@ -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 @@
@@ -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