Readium-2,基于WebView的开源电子书项目介绍

Readium-2(简称R2)是一个由Readium基金会开发的,适用于Android与IOS平台的阅读器项目。与最同类的FBReader相比,最大的区别就是将电子书的解析与展示都交给了WebView来实现,并通过css与js来实现电子书的阅读效果。

支持特性

  • 支持EPUB 2.x 与 3.x
  • 支持Readium LCP
  • 支持CBZ格式
  • 自定义样式
  • 夜间(深色)模式
  • 支持翻页模式与滚动模式
  • 电子书目录
  • 支持OPDS 1.x 与 2.0
  • 支持FXL格式
  • 支持RTL模式

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

首先,来大概介绍一下这个项目的优缺点与适用场景。

优点

  • 将电子书的解析与展示都交给了浏览器完成,无需手动处理。
  • 由于项目开发时间较新,而且原生部分使用kotlin进行开发,不会有FBReader等项目难以编译的问题。
  • 项目中没有使用Native层的代码。

缺点

  • 性能相较基于基于原生的项目执行效率上会差一些。
  • 由于需要同时处理原生,JS,CSS的代码,可能会给开发和调试带来一定的麻烦。
  • 由于加载机制的限制,部分全局功能(如获取全书总页数)难以实现。
  • 在7.0或以下项目中展示效果会有问题。(这一点可以通过css的适配来解决)

注:该项目依然在持续进行更新,可能会在未来解决文中提到的部分问题,详情还是推荐关注该项目的Github主页。

适用场景

如果开发时间较为紧张,而且对于阅读模块的功能方面要求较为简单,对样式支持上的要求较高,又能够完成比较简单的js与css上的问题的话,Readium-2是一个较为不错的选择。

模块结构

代码分析

对于一个阅读器来说,最主要无非两个功能:对文件的解析与文本内容的展示。R2引入了NanoHttpd来直接在本地架设了一个轻量级的WebServer,然后将JS文件,CSS文件,字体文件与电子书文件等等都加载到这个WebServer中,再由WebServer将这些文件打包为一个完整的Web然后交由WebView展示出来。下面就以Epub格式的电子书为例,分别从这两个角度来看看R2在这两方面具体是如何处理的。

对文件的解析

首先,在onCreate方法中调用startServer方法启动本地服务器并加载部分基础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对象,即将所有的节点提取出来以便于之后的处理(OPF文件的结构与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)
    }

在上面的代码中,解析的逻辑上还是比较常规的,其中最关键的部分就是生成了Publication对象,其中包含了整本书的metadata与目录(即每一个目录节点与对应文件的映射关系)。

之后就是R2的重头戏,WebServer的初始化与启动。先来看看Server类的构造函数:

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

所以Server其实就是一个扩展过的RouterNanoHTTPD,限于篇幅,就不向RouterNanoHTTPD的源码进行深究了。在Server创建完成后,要将电子书的基本信息载入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"))

        //通过对应Handler将相应文件添加进本地服务器中
        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
        //判断电子书是否支持多媒体内容(如音频,视频等)
        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")
            }
        }
    }

在上述代码中,值得关注的有addRoute(String url, Class handler, Object... initParameter)方法与Fetcher对象的创建。addRoute方法将url与一个RouterNanoHTTPD.DefaultHandler的子类加入服务器中。之后在浏览器使用url访问本地服务器时,会调用Handler方法返回相应的数据。下面来看看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文件内容进行预处理后
            "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方法会通过调用该方法获取进行过预处理后的书籍内容的InputStream
    fun dataStream(path: String): InputStream {
        var inputStream = container.dataInputStream(path)
        inputStream = contentFilters?.apply(inputStream, publication, container, path) ?: inputStream
        return inputStream
    }

在dataStream方法中会调用contentFilters对象中的apply方法对内容部分进行预处理,如添加css样式,引入js文件,引入字体文件等等。

到这里,对于epub文件与本地服务器的预处理就基本完成了,之后将会跳转到EpubActivity页面进行电子书的展示。

电子书的展示

在电子书阅读的部分,我相信直接来看EpubActivity的布局部分就能有一个很直观的了解了:



    




    

通过布局可以看出,R2的阅读部分很简单,就是由多个WebView所组成的ViewPager,每一个WebView负责加载一个章节的内容,因为epub格式中每个章节的内容很类似于html,所以进行一些预处理就可以直接在WebView中展示了。而章节内的翻页与内容跳转的逻辑上的操作则交由WebView中的css与js部分来进行处理,而章节间的切换的部分则是交给了ViewPager。

对于WebView中操作的具体实现原理感兴趣的朋友可以翻阅项目中的css与js文件,这里就不再展开了。

那么以上就是对于Readium-2这个电子书项目的简单介绍了,希望能有更多人了解到这个项目,也给有类似需求的开发者带来一些帮助。

你可能感兴趣的:(Readium-2,基于WebView的开源电子书项目介绍)