Readium-2,基于WebView的開源電子書項目介紹

Readium-2(簡稱R2)是一個由Readium基金會開發(fā)的,適用于Android與IOS平臺的閱讀器項目。與最同類的FBReader相比,最大的區(qū)別就是將電子書的解析與展示都交給了WebView來實現(xiàn),并通過css與js來實現(xiàn)電子書的閱讀效果。

支持特性

  • 支持EPUB 2.x 與 3.x
  • 支持Readium LCP
  • 支持CBZ格式
  • 自定義樣式
  • 夜間(深色)模式
  • 支持翻頁模式與滾動模式
  • 電子書目錄
  • 支持OPDS 1.x 與 2.0
  • 支持FXL格式
  • 支持RTL模式

先貼上項目的地址:https://github.com/readium/r2-testapp-kotlin

首先,來大概介紹一下這個項目的優(yōu)缺點與適用場景。

優(yōu)點

  • 將電子書的解析與展示都交給了瀏覽器完成,無需手動處理。
  • 由于項目開發(fā)時間較新,而且原生部分使用kotlin進行開發(fā),不會有FBReader等項目難以編譯的問題。
  • 項目中沒有使用Native層的代碼。

缺點

  • 性能相較基于基于原生的項目執(zhí)行效率上會差一些。
  • 由于需要同時處理原生,JS,CSS的代碼,可能會給開發(fā)和調(diào)試帶來一定的麻煩。
  • 由于加載機制的限制,部分全局功能(如獲取全書總頁數(shù))難以實現(xiàn)。
  • 在7.0或以下項目中展示效果會有問題。(這一點可以通過css的適配來解決)

注:該項目依然在持續(xù)進行更新,可能會在未來解決文中提到的部分問題,詳情還是推薦關(guān)注該項目的Github主頁。

適用場景

如果開發(fā)時間較為緊張,而且對于閱讀模塊的功能方面要求較為簡單,對樣式支持上的要求較高,又能夠完成比較簡單的js與css上的問題的話,Readium-2是一個較為不錯的選擇。

模塊結(jié)構(gòu)

代碼分析

對于一個閱讀器來說,最主要無非兩個功能:對文件的解析與文本內(nèi)容的展示。R2引入了NanoHttpd來直接在本地架設(shè)了一個輕量級的WebServer,然后將JS文件,CSS文件,字體文件與電子書文件等等都加載到這個WebServer中,再由WebServer將這些文件打包為一個完整的Web然后交由WebView展示出來。下面就以Epub格式的電子書為例,分別從這兩個角度來看看R2在這兩方面具體是如何處理的。

對文件的解析

首先,在onCreate方法中調(diào)用startServer方法啟動本地服務(wù)器并加載部分基礎(chǔ)js文件,之后由EpubParser類來解析container.xml文件與核心OPF文件

EpubParse.parse

    override fun parse(fileAtPath: String, title: String): PubBox? {
        //獲取container.xml的輸出流
        val container = try {
            generateContainerFrom(fileAtPath)
        } catch (e: Exception) {
            Timber.e(e, "Could not generate container")
            return null
        }
        val data = try {
            container.data(containerDotXmlPath)
        } catch (e: Exception) {
            Timber.e(e, "Missing File : META-INF/container.xml")
            return null
        }

        //標記電子書格式為EPUB
        container.rootFile.mimetype = mimetypeEpub
        //通過解析container.xml文件獲取核心OPF文件的路徑
        container.rootFile.rootFilePath = getRootFilePath(data)

        val xmlParser = XmlParser()

        val documentData = try {
            container.data(container.rootFile.rootFilePath)
        } catch (e: Exception) {
            Timber.e(e, "Missing File : ${container.rootFile.rootFilePath}")
            return null
        }

        //將核心OPF文件解析為XmlParser對象,即將所有的節(jié)點提取出來以便于之后的處理(OPF文件的結(jié)構(gòu)與xml文件幾乎一致)
        xmlParser.parseXml(documentData.inputStream())

        val epubVersion = xmlParser.root().attributes["version"]!!.toDouble()
        //最后將核心OPF文件解析為Publication對象
        val publication = opfParser.parseOpf(xmlParser, container.rootFile.rootFilePath, epubVersion)
                ?: return null

        val drm = container.scanForDrm()

        parseEncryption(container, publication, drm)

        parseNavigationDocument(container, publication)
        parseNcxDocument(container, publication)


        /*
         * This might need to be moved as it's not really about parsing the Epub
         * but it sets values needed (in UserSettings & ContentFilter)
         */
        setLayoutStyle(publication)

        container.drm = drm
        return PubBox(publication, container)
    }

在上面的代碼中,解析的邏輯上還是比較常規(guī)的,其中最關(guān)鍵的部分就是生成了Publication對象,其中包含了整本書的metadata與目錄(即每一個目錄節(jié)點與對應(yīng)文件的映射關(guān)系)。

之后就是R2的重頭戲,WebServer的初始化與啟動。先來看看Server類的構(gòu)造函數(shù):

class Server(port: Int) : AbstractServer(port)
abstract class AbstractServer(private var port: Int) : RouterNanoHTTPD("127.0.0.1", port)

所以Server其實就是一個擴展過的RouterNanoHTTPD,限于篇幅,就不向RouterNanoHTTPD的源碼進行深究了。在Server創(chuàng)建完成后,要將電子書的基本信息載入Server中:

Server.addEpub

    fun addEpub(publication: Publication, container: Container, fileName: String, userPropertiesPath: String?) {
        val fetcher = Fetcher(publication, container, userPropertiesPath, customResources)

        //處理link中的額外字段
        addLinks(publication, fileName)

        publication.addSelfLink(fileName, URL("$BASE_URL:$port"))

        //通過對應(yīng)Handler將相應(yīng)文件添加進本地服務(wù)器中
        if (containsMediaOverlay) {
            addRoute(fileName + MEDIA_OVERLAY_HANDLE, MediaOverlayHandler::class.java, fetcher)
        }
        addRoute(fileName + JSON_MANIFEST_HANDLE, ManifestHandler::class.java, fetcher)
        addRoute(fileName + MANIFEST_HANDLE, ManifestHandler::class.java, fetcher)
        addRoute(fileName + MANIFEST_ITEM_HANDLE, ResourceHandler::class.java, fetcher)
        addRoute(JS_HANDLE, JSHandler::class.java, resources)
        addRoute(CSS_HANDLE, CSSHandler::class.java, resources)
        addRoute(FONT_HANDLE, FontHandler::class.java, fonts)
    }

    private fun addLinks(publication: Publication, filePath: String) {
        containsMediaOverlay = false
        //判斷電子書是否支持多媒體內(nèi)容(如音頻,視頻等)
        for (link in publication.otherLinks) {
            if (link.rel.contains("media-overlay")) {
                containsMediaOverlay = true
                link.href = link.href?.replace("port", "127.0.0.1:$listeningPort$filePath")
            }
        }
    }

在上述代碼中,值得關(guān)注的有addRoute(String url, Class<?> handler, Object... initParameter)方法與Fetcher對象的創(chuàng)建。addRoute方法將url與一個RouterNanoHTTPD.DefaultHandler的子類加入服務(wù)器中。之后在瀏覽器使用url訪問本地服務(wù)器時,會調(diào)用Handler方法返回相應(yīng)的數(shù)據(jù)。下面來看看Fetcher類的部分代碼:

class Fetcher(var publication: Publication, var container: Container, private val userPropertiesPath: String?, customResources: Resources? = null) {
    // …………

    private fun getContentFilters(mimeType: String?, customResources: Resources? = null): ContentFilters {
        return when (mimeType) {
            //對epub文件內(nèi)容進行預(yù)處理后
            "application/epub+zip", "application/oebps-package+xml" -> ContentFiltersEpub(userPropertiesPath, customResources)
            "application/vnd.comicbook+zip", "application/x-cbr" -> ContentFiltersCbz()
            else -> throw Exception("Missing container or MIMEtype")
        }
    }

    //ResourceHandler類中的get方法會通過調(diào)用該方法獲取進行過預(yù)處理后的書籍內(nèi)容的InputStream
    fun dataStream(path: String): InputStream {
        var inputStream = container.dataInputStream(path)
        inputStream = contentFilters?.apply(inputStream, publication, container, path) ?: inputStream
        return inputStream
    }

在dataStream方法中會調(diào)用contentFilters對象中的apply方法對內(nèi)容部分進行預(yù)處理,如添加css樣式,引入js文件,引入字體文件等等。

到這里,對于epub文件與本地服務(wù)器的預(yù)處理就基本完成了,之后將會跳轉(zhuǎn)到EpubActivity頁面進行電子書的展示。

電子書的展示

在電子書閱讀的部分,我相信直接來看EpubActivity的布局部分就能有一個很直觀的了解了:

<!-- activity_r2_viewpager.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <org.readium.r2.navigator.pager.R2ViewPager />
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- viewpager_fragment_epub.xml -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <org.readium.r2.navigator.R2WebView/>
</androidx.constraintlayout.widget.ConstraintLayout>

通過布局可以看出,R2的閱讀部分很簡單,就是由多個WebView所組成的ViewPager,每一個WebView負責加載一個章節(jié)的內(nèi)容,因為epub格式中每個章節(jié)的內(nèi)容很類似于html,所以進行一些預(yù)處理就可以直接在WebView中展示了。而章節(jié)內(nèi)的翻頁與內(nèi)容跳轉(zhuǎn)的邏輯上的操作則交由WebView中的css與js部分來進行處理,而章節(jié)間的切換的部分則是交給了ViewPager。

對于WebView中操作的具體實現(xiàn)原理感興趣的朋友可以翻閱項目中的css與js文件,這里就不再展開了。

那么以上就是對于Readium-2這個電子書項目的簡單介紹了,希望能有更多人了解到這個項目,也給有類似需求的開發(fā)者帶來一些幫助。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容