Readium-2罐农,基于WebView的開源電子書項(xiàng)目介紹

Readium-2(簡(jiǎn)稱R2)是一個(gè)由Readium基金會(huì)開發(fā)的条霜,適用于Android與IOS平臺(tái)的閱讀器項(xiàng)目。與最同類的FBReader相比涵亏,最大的區(qū)別就是將電子書的解析與展示都交給了WebView來(lái)實(shí)現(xiàn)宰睡,并通過(guò)css與js來(lái)實(shí)現(xiàn)電子書的閱讀效果。

支持特性

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

先貼上項(xiàng)目的地址:https://github.com/readium/r2-testapp-kotlin

首先气筋,來(lái)大概介紹一下這個(gè)項(xiàng)目的優(yōu)缺點(diǎn)與適用場(chǎng)景拆内。

優(yōu)點(diǎn)

  • 將電子書的解析與展示都交給了瀏覽器完成,無(wú)需手動(dòng)處理宠默。
  • 由于項(xiàng)目開發(fā)時(shí)間較新麸恍,而且原生部分使用kotlin進(jìn)行開發(fā),不會(huì)有FBReader等項(xiàng)目難以編譯的問(wèn)題搀矫。
  • 項(xiàng)目中沒(méi)有使用Native層的代碼抹沪。

缺點(diǎn)

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

注:該項(xiàng)目依然在持續(xù)進(jìn)行更新逝薪,可能會(huì)在未來(lái)解決文中提到的部分問(wèn)題,詳情還是推薦關(guān)注該項(xiàng)目的Github主頁(yè)蝴罪。

適用場(chǎng)景

如果開發(fā)時(shí)間較為緊張董济,而且對(duì)于閱讀模塊的功能方面要求較為簡(jiǎn)單,對(duì)樣式支持上的要求較高要门,又能夠完成比較簡(jiǎn)單的js與css上的問(wèn)題的話虏肾,Readium-2是一個(gè)較為不錯(cuò)的選擇。

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

代碼分析

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

對(duì)文件的解析

首先,在onCreate方法中調(diào)用startServer方法啟動(dòng)本地服務(wù)器并加載部分基礎(chǔ)js文件刷袍,之后由EpubParser類來(lái)解析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
        }

        //標(biāo)記電子書格式為EPUB
        container.rootFile.mimetype = mimetypeEpub
        //通過(guò)解析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對(duì)象翩隧,即將所有的節(jié)點(diǎn)提取出來(lái)以便于之后的處理(OPF文件的結(jié)構(gòu)與xml文件幾乎一致)
        xmlParser.parseXml(documentData.inputStream())

        val epubVersion = xmlParser.root().attributes["version"]!!.toDouble()
        //最后將核心OPF文件解析為Publication對(duì)象
        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對(duì)象堆生,其中包含了整本書的metadata與目錄(即每一個(gè)目錄節(jié)點(diǎn)與對(duì)應(yīng)文件的映射關(guān)系)。

之后就是R2的重頭戲雷酪,WebServer的初始化與啟動(dòng)淑仆。先來(lái)看看Server類的構(gòu)造函數(shù):

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

所以Server其實(shí)就是一個(gè)擴(kuò)展過(guò)的RouterNanoHTTPD,限于篇幅哥力,就不向RouterNanoHTTPD的源碼進(jìn)行深究了糯景。在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"))

        //通過(guò)對(duì)應(yīng)Handler將相應(yīng)文件添加進(jìn)本地服務(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對(duì)象的創(chuàng)建。addRoute方法將url與一個(gè)RouterNanoHTTPD.DefaultHandler的子類加入服務(wù)器中钞澳。之后在瀏覽器使用url訪問(wèn)本地服務(wù)器時(shí)怠惶,會(huì)調(diào)用Handler方法返回相應(yīng)的數(shù)據(jù)。下面來(lái)看看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) {
            //對(duì)epub文件內(nèi)容進(jìn)行預(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方法會(huì)通過(guò)調(diào)用該方法獲取進(jìn)行過(guò)預(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方法中會(huì)調(diào)用contentFilters對(duì)象中的apply方法對(duì)內(nèi)容部分進(jìn)行預(yù)處理轧粟,如添加css樣式策治,引入js文件,引入字體文件等等兰吟。

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

電子書的展示

在電子書閱讀的部分履腋,我相信直接來(lái)看EpubActivity的布局部分就能有一個(gè)很直觀的了解了:

<!-- 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>

通過(guò)布局可以看出,R2的閱讀部分很簡(jiǎn)單惭嚣,就是由多個(gè)WebView所組成的ViewPager遵湖,每一個(gè)WebView負(fù)責(zé)加載一個(gè)章節(jié)的內(nèi)容,因?yàn)閑pub格式中每個(gè)章節(jié)的內(nèi)容很類似于html晚吞,所以進(jìn)行一些預(yù)處理就可以直接在WebView中展示了延旧。而章節(jié)內(nèi)的翻頁(yè)與內(nèi)容跳轉(zhuǎn)的邏輯上的操作則交由WebView中的css與js部分來(lái)進(jìn)行處理,而章節(jié)間的切換的部分則是交給了ViewPager槽地。

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

那么以上就是對(duì)于Readium-2這個(gè)電子書項(xiàng)目的簡(jiǎn)單介紹了,希望能有更多人了解到這個(gè)項(xiàng)目集畅,也給有類似需求的開發(fā)者帶來(lái)一些幫助近弟。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市牡整,隨后出現(xiàn)的幾起案子藐吮,更是在濱河造成了極大的恐慌溺拱,老刑警劉巖逃贝,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迫摔,居然都是意外死亡沐扳,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門句占,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)沪摄,“玉大人,你說(shuō)我怎么就攤上這事纱烘⊙罟眨” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵擂啥,是天一觀的道長(zhǎng)哄陶。 經(jīng)常有香客問(wèn)我,道長(zhǎng)哺壶,這世上最難降的妖魔是什么屋吨? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮山宾,結(jié)果婚禮上至扰,老公的妹妹穿的比我還像新娘。我一直安慰自己资锰,他們只是感情好敢课,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绷杜,像睡著了一般翎猛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上接剩,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天切厘,我揣著相機(jī)與錄音,去河邊找鬼懊缺。 笑死疫稿,一個(gè)胖子當(dāng)著我的面吹牛培他,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播遗座,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼舀凛,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了途蒋?” 一聲冷哼從身側(cè)響起猛遍,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎号坡,沒(méi)想到半個(gè)月后懊烤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宽堆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年腌紧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片畜隶。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡壁肋,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出籽慢,到底是詐尸還是另有隱情浸遗,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布箱亿,位于F島的核電站跛锌,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏极景。R本人自食惡果不足惜察净,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望盼樟。 院中可真熱鬧氢卡,春花似錦、人聲如沸晨缴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)击碗。三九已至筑悴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間稍途,已是汗流浹背阁吝。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留械拍,地道東北人突勇。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓装盯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親甲馋。 傳聞我的和親對(duì)象是個(gè)殘疾皇子埂奈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容