Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix configuration change crash with PdfNavigatorFragment #398

Merged
merged 14 commits into from
Oct 3, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import org.readium.r2.shared.InternalReadiumApi
import org.readium.r2.shared.extensions.md5
import org.readium.r2.shared.extensions.tryOrNull
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.getOrThrow
import org.readium.r2.shared.resource.ResourceTry
import org.readium.r2.shared.resource.mapCatching
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import org.readium.r2.shared.util.use
Expand Down Expand Up @@ -84,24 +86,28 @@ public class PdfiumDocumentFactory(context: Context) : PdfDocumentFactory<Pdfium

private val core by lazy { PdfiumCore(context.applicationContext) }

override suspend fun open(file: File, password: String?): PdfiumDocument =
core.fromFile(file, password)
override suspend fun open(file: File, password: String?): ResourceTry<PdfiumDocument> =
mickael-menu marked this conversation as resolved.
Show resolved Hide resolved
try {
Try.success(core.fromFile(file, password))
} catch (e: Throwable) {
Try.failure(Resource.Exception.wrap(e))
}

override suspend fun open(resource: Resource, password: String?): PdfiumDocument {
override suspend fun open(resource: Resource, password: String?): ResourceTry<PdfiumDocument> {
// First try to open the resource as a file on the FS for performance improvement, as
// PDFium requires the whole PDF document to be loaded in memory when using raw bytes.
return resource.openAsFile(password)
?: resource.openBytes(password)
}

private suspend fun Resource.openAsFile(password: String?): PdfiumDocument? =
private suspend fun Resource.openAsFile(password: String?): ResourceTry<PdfiumDocument>? =
tryOrNull {
source?.toFile()?.let { open(it, password) }
}

private suspend fun Resource.openBytes(password: String?): PdfiumDocument =
private suspend fun Resource.openBytes(password: String?): ResourceTry<PdfiumDocument> =
use {
core.fromBytes(read().getOrThrow(), password)
read().mapCatching { core.fromBytes(it, password) }
}

private fun PdfiumCore.fromFile(file: File, password: String?): PdfiumDocument =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,47 @@ import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.github.barteksc.pdfviewer.PDFView
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.readium.adapters.pdfium.document.PdfiumDocumentFactory
import org.readium.r2.navigator.input.InputListener
import org.readium.r2.navigator.input.TapEvent
import org.readium.r2.navigator.pdf.PdfDocumentFragment
import org.readium.r2.navigator.preferences.Axis
import org.readium.r2.navigator.preferences.Fit
import org.readium.r2.navigator.preferences.ReadingProgression
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Link
import org.readium.r2.shared.publication.Publication
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.util.SingleJob
import org.readium.r2.shared.util.Url
import org.readium.r2.shared.util.getOrElse
import timber.log.Timber

@ExperimentalReadiumApi
public class PdfiumDocumentFragment internal constructor(
private val publication: Publication,
private val link: Link,
private val href: Url,
private val initialPageIndex: Int,
settings: PdfiumSettings,
private val appListener: Listener?,
private val navigatorListener: PdfDocumentFragment.Listener?
) : PdfDocumentFragment<PdfiumSettings>() {
initialSettings: PdfiumSettings,
private val listener: Listener?,
private val inputListener: InputListener
) : PdfDocumentFragment<PdfiumDocumentFragment.Listener, PdfiumSettings>() {

public interface Listener : PdfDocumentFragment.Listener {
/**
* Called when a PDF resource failed to be loaded, for example because of an
* [OutOfMemoryError].
*/
public fun onResourceLoadFailed(href: Url, error: Resource.Exception) {}

public interface Listener {
/** Called when configuring [PDFView]. */
public fun onConfigurePdfView(configurator: PDFView.Configurator) {}
}

override var settings: PdfiumSettings = settings
set(value) {
if (field == value) return

val page = pageIndex
field = value
reloadDocumentAtPage(page)
}

private lateinit var pdfView: PDFView

private var isReloading: Boolean = false
private var hasToReload: Int? = null

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
Expand All @@ -65,86 +65,70 @@ public class PdfiumDocumentFragment internal constructor(

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
reloadDocumentAtPage(pageIndex)
}

private fun reloadDocumentAtPage(pageIndex: Int) {
if (isReloading) {
hasToReload = pageIndex
return
}
resetJob = SingleJob(viewLifecycleOwner.lifecycleScope)
reset(pageIndex = initialPageIndex)
}

isReloading = true
private lateinit var resetJob: SingleJob

private fun reset(pageIndex: Int = _pageIndex.value) {
if (view == null) return
val context = context?.applicationContext ?: return

viewLifecycleOwner.lifecycleScope.launch {
try {
val document = PdfiumDocumentFactory(context)
// PDFium crashes when reusing the same PdfDocument, so we must not cache it.
resetJob.launch {
val document = PdfiumDocumentFactory(context)
// PDFium crashes when reusing the same PdfDocument, so we must not cache it.
// .cachedIn(publication)
.open(publication.get(link), null)

pageCount = document.pageCount
val page = convertPageIndexToView(pageIndex)

pdfView.recycle()
pdfView
.fromSource { _, _, _ -> document.document }
.apply {
if (isPagesOrderReversed) {
// AndroidPdfViewer doesn't support RTL. A workaround is to provide
// the explicit page list in the right order.
pages(*((pageCount - 1) downTo 0).toList().toIntArray())
}
}
.swipeHorizontal(settings.scrollAxis == Axis.HORIZONTAL)
.spacing(settings.pageSpacing.roundToInt())
// Customization of [PDFView] is done before setting the listeners,
// to avoid overriding them in reading apps, which would break the
// navigator.
.apply { appListener?.onConfigurePdfView(this) }
.defaultPage(page)
.onRender { _, _, _ ->
if (settings.fit == Fit.WIDTH) {
pdfView.fitToWidth()
// Using `fitToWidth` often breaks the use of `defaultPage`, so we
// need to jump manually to the target page.
pdfView.jumpTo(page, false)
}
}
.onLoad {
val hasToReloadNow = hasToReload
if (hasToReloadNow != null) {
reloadDocumentAtPage(pageIndex)
} else {
isReloading = false
}
}
.onPageChange { index, _ ->
navigatorListener?.onPageChanged(convertPageIndexFromView(index))
.open(publication.get(href), null)
.getOrElse { error ->
Timber.e(error)
listener?.onResourceLoadFailed(href, error)
return@launch
}

pageCount = document.pageCount
val page = convertPageIndexToView(pageIndex)

pdfView.recycle()
pdfView
.fromSource { _, _, _ -> document.document }
.apply {
if (isPagesOrderReversed) {
// AndroidPdfViewer doesn't support RTL. A workaround is to provide
// the explicit page list in the right order.
pages(*((pageCount - 1) downTo 0).toList().toIntArray())
}
.onTap { event ->
navigatorListener?.onTap(PointF(event.x, event.y))
?: false
}
.swipeHorizontal(settings.scrollAxis == Axis.HORIZONTAL)
.spacing(settings.pageSpacing.roundToInt())
// Customization of [PDFView] is done before setting the listeners,
// to avoid overriding them in reading apps, which would break the
// navigator.
.apply { listener?.onConfigurePdfView(this) }
.defaultPage(page)
.onRender { _, _, _ ->
if (settings.fit == Fit.WIDTH) {
pdfView.fitToWidth()
// Using `fitToWidth` often breaks the use of `defaultPage`, so we
// need to jump manually to the target page.
pdfView.jumpTo(page, false)
}
.load()
} catch (e: Exception) {
val error = Resource.Exception.wrap(e)
Timber.e(error)
navigatorListener?.onResourceLoadFailed(link, error)
}
}
.onPageChange { index, _ ->
_pageIndex.value = convertPageIndexFromView(index)
}
.onTap { event ->
inputListener.onTap(TapEvent(PointF(event.x, event.y)))
}
.load()
}
}

override val pageIndex: Int get() = viewPageIndex ?: initialPageIndex
private var pageCount = 0

private val viewPageIndex: Int? get() =
if (pdfView.isRecycled) {
null
} else {
convertPageIndexFromView(pdfView.currentPage)
}
private val _pageIndex = MutableStateFlow(initialPageIndex)
override val pageIndex: StateFlow<Int> = _pageIndex.asStateFlow()

override fun goToPageIndex(index: Int, animated: Boolean): Boolean {
if (!isValidPageIndex(index)) {
Expand All @@ -154,8 +138,6 @@ public class PdfiumDocumentFragment internal constructor(
return true
}

private var pageCount = 0

private fun isValidPageIndex(pageIndex: Int): Boolean {
val validRange = 0 until pageCount
return validRange.contains(pageIndex)
Expand All @@ -182,6 +164,16 @@ public class PdfiumDocumentFragment internal constructor(
* right-to-left reading progressions.
*/
private val isPagesOrderReversed: Boolean get() =
settings.scrollAxis == Axis.HORIZONTAL &&
settings.readingProgression == ReadingProgression.RTL
settings.scrollAxis == Axis.HORIZONTAL && settings.readingProgression == ReadingProgression.RTL

private var settings: PdfiumSettings = initialSettings

override fun applySettings(settings: PdfiumSettings) {
if (this.settings == settings) {
return
}

this.settings = settings
reset()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import org.readium.r2.navigator.SimplePresentation
import org.readium.r2.navigator.VisualNavigator
import org.readium.r2.navigator.pdf.PdfDocumentFragmentInput
import org.readium.r2.navigator.pdf.PdfEngineProvider
import org.readium.r2.navigator.util.SingleFragmentFactory
import org.readium.r2.navigator.util.createFragmentFactory
import org.readium.r2.shared.ExperimentalReadiumApi
import org.readium.r2.shared.publication.Metadata
import org.readium.r2.shared.publication.Publication
Expand All @@ -22,19 +24,22 @@ import org.readium.r2.shared.publication.Publication
*/
@ExperimentalReadiumApi
public class PdfiumEngineProvider(
private val listener: PdfiumDocumentFragment.Listener? = null,
private val defaults: PdfiumDefaults = PdfiumDefaults()
) : PdfEngineProvider<PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor> {

override suspend fun createDocumentFragment(input: PdfDocumentFragmentInput<PdfiumSettings>): PdfiumDocumentFragment =
PdfiumDocumentFragment(
publication = input.publication,
link = input.link,
initialPageIndex = input.initialPageIndex,
settings = input.settings,
appListener = listener,
navigatorListener = input.listener
)
) : PdfEngineProvider<PdfiumDocumentFragment, PdfiumDocumentFragment.Listener, PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor> {

override fun createDocumentFragmentFactory(
input: PdfDocumentFragmentInput<PdfiumDocumentFragment.Listener, PdfiumSettings>
): SingleFragmentFactory<PdfiumDocumentFragment> =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We return a SingleFragmentFactory which is a custom implementation of FragmentFactory able to instantiate a new instance without knowing the class of the fragment.

createFragmentFactory {
PdfiumDocumentFragment(
publication = input.publication,
href = input.href,
initialPageIndex = input.pageIndex,
initialSettings = input.settings,
listener = input.listener,
inputListener = input.inputListener
)
}

override fun computeSettings(metadata: Metadata, preferences: PdfiumPreferences): PdfiumSettings {
val settingsPolicy = PdfiumSettingsResolver(metadata, defaults)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2023 Readium Foundation. All rights reserved.
* Use of this source code is governed by the BSD-style license
* available in the top-level LICENSE file of the project.
*/

package org.readium.adapters.pdfium.navigator

import org.readium.r2.navigator.pdf.PdfNavigatorFactory
import org.readium.r2.navigator.pdf.PdfNavigatorFragment
import org.readium.r2.shared.ExperimentalReadiumApi

@ExperimentalReadiumApi
public typealias PdfiumNavigatorFragment = PdfNavigatorFragment<PdfiumDocumentFragment, PdfiumDocumentFragment.Listener, PdfiumSettings, PdfiumPreferences>

@ExperimentalReadiumApi
public typealias PdfiumNavigatorFactory = PdfNavigatorFactory<PdfiumDocumentFragment, PdfiumDocumentFragment.Listener, PdfiumSettings, PdfiumPreferences, PdfiumPreferencesEditor>
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import com.pspdfkit.document.PdfDocument as _PsPdfKitDocument
import com.pspdfkit.document.PdfDocumentLoader
import java.io.File
import kotlin.reflect.KClass
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.readium.r2.shared.publication.ReadingProgression
import org.readium.r2.shared.resource.Resource
import org.readium.r2.shared.resource.ResourceTry
import org.readium.r2.shared.util.Try
import org.readium.r2.shared.util.pdf.PdfDocument
import org.readium.r2.shared.util.pdf.PdfDocumentFactory
import timber.log.Timber
Expand All @@ -30,15 +33,23 @@ public class PsPdfKitDocumentFactory(context: Context) : PdfDocumentFactory<PsPd

override val documentType: KClass<PsPdfKitDocument> = PsPdfKitDocument::class

override suspend fun open(file: File, password: String?): PsPdfKitDocument =
override suspend fun open(file: File, password: String?): ResourceTry<PsPdfKitDocument> =
open(context, DocumentSource(file.toUri(), password))

override suspend fun open(resource: Resource, password: String?): PsPdfKitDocument =
override suspend fun open(resource: Resource, password: String?): ResourceTry<PsPdfKitDocument> =
open(context, DocumentSource(ResourceDataProvider(resource), password))

private suspend fun open(context: Context, documentSource: DocumentSource): PsPdfKitDocument =
private suspend fun open(context: Context, documentSource: DocumentSource): ResourceTry<PsPdfKitDocument> =
withContext(Dispatchers.IO) {
PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource))
try {
Try.success(
PsPdfKitDocument(PdfDocumentLoader.openDocument(context, documentSource))
)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Try.failure(Resource.Exception.wrap(e))
}
}
}

Expand Down
Loading