EPUB
阅读 EPUB 电子书的三板斧
解包器
,解析器
,阅读器
iOS EPUB 电子书 通过RPS(Reader Parser Server)实现在线阅读,并通过RSA,AES 双重加密保证数据的安全性
阅读器采用复用页面机制(Page Reuse Mechanism)用使程序开销降到最低并保证阅读体验
使用 WebKit 框架 作为swift代码与电子书html页面交互的桥梁,并编写辅助js脚本实现翻页模式,字号,主题的切换
sequenceDiagram
box 离线阅读
participant A as 阅读器
participant B as 解析器
participant C as 解包器
end
A->>C: 让解包器解压缩 EPUB 文件
C-->>B: 得到解压缩后的 EPUB 文件夹
B-->>A: 得到解析后的 EPUB 数据模型
box 在线阅读
participant A1 as Reader
participant B1 as Parser
participant C1 as Server
end
rect rgb(238, 180, 185)
par
A1->>C1: 请求 EPUB 基础数据
Note over A1,C1: 基础数据包括 Content, Toc, CSS, 密钥 等文件
C1-->>B1: 解析网络数据
B1-->>A1: 配置基础数据
A1->>C1: 请求最后一次阅读页面数据
C1-->>A1: 获得要渲染页面标识
end
end
rect rgb(243, 178, 128)
par 加载与渲染页面数据
A1->>C1: 请求页面数据
create actor D as Decryptor
C1->>D: 解密页面内容
D-->>A1: 得到页面的原始数据
loop
A1->>C1: 请求页面图片资源
C1-->>A1: 将图片资源配置到页面数据中
end
A1->>C1: 请求页面的书签与笔记数据
C1-->>A1: 将书签笔记配置到页面数据中进行最终的页面渲染
end
end
Epub 解包器
EPUB 文件其实是一个压缩包,使用压缩工具可将其解压,得到一个 EPUB 文件夹。
在 iOS 平台使用开源的解压缩工具库 SSZipArchive ,
1// Unpacker.swift
2import SSZipArchive
3import Foundation
4class Unpacker: NSObject {
5 /// Epub 文件解包
6 /// - Parameters:
7 /// - epubFileURL: 原文件路径
8 /// - unPackageURL: 解包文件路径
9 func unPackage(epubFileURL: URL, unPackageURL: URL) -> Bool {
10 guard FileManager.default.fileExists(atPath: epubFileURL.path) else {
11 debugPrint("未找到 epub 文件", epubFileURL.path)
12 return false
13 }
14 if FileManager.default.fileExists(atPath: unPackageURL.path) {
15 debugPrint("解包路径已存在:",unPackageURL.path)
16 return true
17 }
18 return SSZipArchive.unzipFile(atPath: epubFileURL.path, toDestination: unPackageURL.path, delegate: self)
19 }
20}
21
22/// 解压文件代理方法
23extension Unpacker: SSZipArchiveDelegate {
24 /// 将要解包
25 func zipArchiveWillUnzipArchive(atPath path: String, zipInfo: unz_global_info) { }
26
27 /// 是否解包
28 func zipArchiveShouldUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) -> Bool { return true }
29
30 /// 将要生成解包后的文件路径
31 func zipArchiveWillUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) { }
32
33 /// 解包完成生成解压后的文件夹
34 func zipArchiveDidUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, fileInfo: unz_file_info) { }
35
36 /// 解包进度
37 func zipArchiveProgressEvent(_ loaded: UInt64, total: UInt64) { }
38
39 /// 解包完成
40 func zipArchiveDidUnzipArchive(atPath path: String, zipInfo: unz_global_info, unzippedPath: String) { }
41
42 /// 解包完成
43 func zipArchiveDidUnzipFile(at fileIndex: Int, totalFiles: Int, archivePath: String, unzippedFilePath: String) { }
44}
1/-
2 |-META-INF
3 |-container.xml (该文件EPUB阅读器读取EPUB内容(content.opf)文件路径)
/META-INF/container.xml 该文件始终存在,否则就是非法的 EPUB 文件,解析器会通过该文件逐步将 EPUB 文件内容解析出来。
Epub 解析器
根据 EPUB 文件夹结构来确定解析流程
需要解析的 EPUB 文件类型都是 XML 类型, 虽然有些文件的后缀不是 xml,但里面的内容都是由 xml 标签组成。
这里使用到的 XML 解析工具是 SwiftSoup。
解析器委托代理事件
1// Parser.swift
2protocol ParserDelegate {
3
4 /// 开始解析 epub
5 func beginParserEpub(url: URL)
6
7 /// 已解析 container
8 func didParserContainer()
9
10 /// 已解析 Content
11 func didParserContent()
12
13 /// 已解析 TOC
14 func didParserToc()
15
16 /// 解析 epub 完成
17 func endedParserEpub()
18
19 /// 解析 Epub 出错
20 func errorParserEpub(error: ParserError)
21}
枚举解析失败错误
1enum ParserError: Error {
2 /// 无效的文件
3 case FileInvalid(message: String, fileUrl: URL?)
4
5 /// 转 String 失败
6 case ToStrFailed(message: String, data: Data?)
7
8 /// 转 XML 失败
9 case ToXMLFailed(message: String, xmlStr: String)
10
11 /// content 内容缺失
12 case ContentLack(message: String)
13}
解析器类实现
1// Parser.swift
2class Parser {
3 /// 声明解析 EPUB 数据模型
4 var parserData = ParserData()
5 /// 声明代理
6 var delegate: ParserDelegate?
7
8 /// 开始解析 Epub解压后的 文件内容
9 private func beginParserEpub(url: URL) throws { ··· }
10
11 /// 解析 container 文件
12 private func parseContainer() throws { ··· }
13
14 /// 解析 content 文件
15 private func parseContent() throws { ··· }
16
17 /// 解析 TOC 目录
18 private func parseToc() throws { ··· }
19
20
21 /// 衍生方法
22
23 /// 解析外部的 container 文件
24 public func parseContainer(data: Data) throws -> Container { ··· }
25 /// 解析外部的 Content 文件
26 public func parseContent(data: Data) throws -> Content { ··· }
27 /// 解析外部的 TOC 目录文件
28 public func parseToc(data: Data) throws -> Toc { ··· }
29
30}