美文网首页
Readium-2,基于WebView的开源电子书项目介绍

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

作者: jh352160 | 来源:发表于2020-01-15 16:22 被阅读0次

    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的布局部分就能有一个很直观的了解了:

    <!-- 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负责加载一个章节的内容,因为epub格式中每个章节的内容很类似于html,所以进行一些预处理就可以直接在WebView中展示了。而章节内的翻页与内容跳转的逻辑上的操作则交由WebView中的css与js部分来进行处理,而章节间的切换的部分则是交给了ViewPager。

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

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

    相关文章

      网友评论

          本文标题:Readium-2,基于WebView的开源电子书项目介绍

          本文链接:https://www.haomeiwen.com/subject/gnmgwctx.html