Skip to content

Commit

Permalink
Merge branch 'v3' of github.com:readium/kotlin-toolkit into fix/lcp-a…
Browse files Browse the repository at this point in the history
…uthenticating
  • Loading branch information
qnga committed Oct 3, 2023
2 parents ee90866 + e4f1cc4 commit 4a0a838
Show file tree
Hide file tree
Showing 44 changed files with 871 additions and 482 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ All notable changes to this project will be documented in this file. Take a look
### Changed

* Readium resources are now prefixed with `readium_`. Take care of updating any overridden resource by following [the migration guide](docs/migration-guide.md#300).
* `Link` and `Locator`'s `href` do not start with a `/` for packaged publications anymore.
* To ensure backward-compatibility, `href` starting with a `/` are still supported. But you may want to update the locators persisted in your database to drop the `/` prefix for packaged publications.
* `Link` and `Locator`'s `href` are normalized as valid URLs to improve interoperability with the Readium Web toolkits.
* **You MUST migrate your database if you were persisting HREFs and Locators**. Take a look at [the migration guide](docs/migration-guide.md) for guidance.

#### Shared

Expand Down
49 changes: 49 additions & 0 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,55 @@ dependencies {
}
```

### Migration of HREFs and Locators (bookmarks, annotations, etc.)

:warning: This requires a database migration in your application, if you were persisting `Locator` objects.

In Readium v2.x, a `Link` or `Locator`'s `href` could be either:

* a valid absolute URL for a streamed publication, e.g. `https://domain.com/isbn/dir/my%20chapter.html`,
* a percent-decoded path for a local archive such as an EPUB, e.g. `/dir/my chapter.html`.
* Note that it was relative to the root of the archive (`/`).

To improve the interoperability with other Readium toolkits (in particular the Readium Web Toolkits, which only work in a streaming context) **Readium v3 now generates and expects valid URLs** for `Locator` and `Link`'s `href`.

* `https://domain.com/isbn/dir/my%20chapter.html` is left unchanged, as it was already a valid URL.
* `/dir/my chapter.html` becomes the relative URL path `dir/my%20chapter.html`
* We dropped the `/` prefix to avoid issues when resolving to a base URL.
* Special characters are percent-encoded.

**You must migrate the HREFs or Locators stored in your database** when upgrading to Readium 3. To assist you, two helpers are provided: `Url.fromLegacyHref()` and `Locator.fromLegacyJSON()`.

Here's an example of a Jetpack Room migration that can serve as inspiration:

```kotlin
val MIGRATION_HREF = object : Migration(1, 2) {

override fun migrate(db: SupportSQLiteDatabase) {
val normalizedHrefs: Map<Long, String> = buildMap {
db.query("SELECT id, href FROM bookmarks").use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(0)
val href = cursor.getString(1)

val normalizedHref = Url.fromLegacyHref(href)?.toString()
if (normalizedHref != null) {
put(id, normalizedHref)
}
}
}
}

val stmt = db.compileStatement("UPDATE bookmarks SET href = ? WHERE id = ?")
for ((id, href) in normalizedHrefs) {
stmt.bindString(1, href)
stmt.bindLong(2, id)
stmt.executeUpdateDelete()
}
}
}
```

### LcpDialogAuthentication now supports configuration changes.

You no longer need to pass an activity, fragment or view as `sender` to `retrievePassphrase`.
Expand Down
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,25 @@ 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(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) }
source?.toFile()?.let { file ->
withContext(Dispatchers.IO) {
Try.success(core.fromFile(file, 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,39 @@ 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.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?
initialSettings: PdfiumSettings,
private val listener: Listener?
) : PdfDocumentFragment<PdfiumSettings>() {

public interface Listener {
/** Called when configuring [PDFView]. */
public fun onConfigurePdfView(configurator: PDFView.Configurator) {}
internal interface Listener {
fun onResourceLoadFailed(href: Url, error: Resource.Exception)
fun onConfigurePdfView(configurator: PDFView.Configurator)
fun onTap(point: PointF): Boolean
}

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 +57,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 ->
listener?.onTap(PointF(event.x, event.y)) ?: false
}
.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 +130,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 +156,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()
}
}
Loading

0 comments on commit 4a0a838

Please sign in to comment.