纯 Swift 实现 MOBI/AZW3 二进制解析与电子书格式转换——从零构建零依赖引擎
一、问题背景
电子书格式转换是刚需,但现有方案重度依赖 Calibre。Calibre 功能强大,可对于一个 macOS 桌面工具来说,引入一个 300MB 的外部依赖来完成"TXT 转 EPUB"这类基础操作,太重了。
核心挑战在于两个专有格式:
-
AZW3(KF8):亚马逊闭源格式,基于 PalmDB 容器 + MOBI 头部 + PalmDOC 压缩
-
KEPUB:Kobo 增强 EPUB,结构上是 EPUB + 额外
<span>标注,但文件名必须为.kepub.epub双重后缀
本文介绍如何用纯 Swift 从零构建解析引擎,彻底移除 Calibre 依赖。
二、MOBI/AZW3 二进制结构
AZW3 文件底层是 PalmDB 格式,结构如下:
┌──────────────────────┐ │ PDB Header (78 B) │ ← 记录数、偏移量等元信息 ├──────────────────────┤ │ Record List │ ← 每条记录 (4B offset + 4B flags) × N ├──────────────────────┤ │ Record 0 │ ← MOBI Header + EXTH Header ├──────────────────────┤ │ Record 1..M │ ← HTML 正文(可能 PalmDOC 压缩) ├──────────────────────┤ │ Record M+1..N │ ← 图片资源(封面、插图) └──────────────────────┘
2.1 PDB Header 解析
struct PDBHeader {
let name: String // 32B, 书名
let numRecords: UInt16 // offset 76
}
static func parsePDBHeader(_ data: Data) throws -> PDBHeader {
guard data.count >= 78 else { throw ParseError.tooSmall }
let name = String(data: data[0..<32], encoding: .utf8)?
.trimmingCharacters(in: .nulls) ?? "Unknown"
let numRecords = data.bigUInt16(at: 76)
return PDBHeader(name: name, numRecords: Int(numRecords))
}
Record List 从 byte 78 开始,每 8 字节一条记录(offset + attributes),共 numRecords 条。
2.2 MOBI Header 与 EXTH
Record 0 的前 16 字节是 PalmDOC 记录头,之后是 MOBI Header:
-
byte 16-19:
MOBI标识 -
byte 20-23:header 长度
-
byte 92-95:EXTH flags(bit 6 = 0x40 表示 EXTH Header 存在)
EXTH Header 紧跟在 MOBI Header 之后,由若干 record 组成,每条 record 结构:
type (4B, big-endian) + length (4B) + data (length-8)B
关键 record type:
-
113(0x71):ASIN(Amazon Standard Identification Number)
-
504(0x1F8):新版 ASIN(KF8+)
func findASIN(in data: Data, record0Start: Int) -> String? {
let asinTypes: [UInt32] = [113, 504]
let exthStart = record0Start + 16 + mobiHeaderLength
var pos = exthStart
while pos + 8 < data.count {
let type = data.bigUInt32(at: pos)
let len = Int(data.bigUInt32(at: pos + 4))
if len < 8 || pos + len > data.count { break }
if asinTypes.contains(type) {
return String(data: data[(pos+8)..<(pos+len)], encoding: .utf8)?
.trimmingCharacters(in: .nulls)
}
pos += len
}
return nil
}
三、PalmDOC 解压算法
MOBI 文件的 HTML 正文通常用 PalmDOC 压缩(compression type = 2)。这是一个 LZ77 变体,字节级编码:
| 字节范围 | 含义 |
|---|---|
| 0x00 | 输出字面量 0x00 |
| 0x01-0x08 | 字面量复制 N 字节(ESC 序列) |
| 0x09-0x7F | 字面量直接输出 |
| 0x80-0xBF | LZ77 回引:与下一字节组成 16-bit code,distance = (code>>3)&0x7FF,length = (code&7)+3 |
| 0xC0-0xFF | 空格 + (byte ^ 0x80) |
Swift 实现核心循环:
func palmDocDecompress(_ data: Data, textLength: Int) -> Data {
var out = Data(); out.reserveCapacity(textLength)
var i = 0
while i < data.count && out.count < textLength {
let c = data[i]; i += 1
if c == 0 {
out.append(0)
} else if c <= 8 {
guard i + c <= data.count else { break }
out.append(data[i..<(i+Int(c))]); i += Int(c)
} else if c < 0x80 {
out.append(c)
} else if c < 0xC0 {
guard i < data.count else { break }
let n = (UInt16(c) << 8) | UInt16(data[i]); i += 1
let distance = Int((n >> 3) & 0x07FF)
let length = Int(n & 0x07) + 3
let start = out.count - distance - 1
guard start >= 0, start + length <= out.count else { break }
out.append(out[start..<(start+length)])
} else {
out.append(0x20)
out.append(c ^ 0x80)
}
}
return out.prefix(textLength)
}
四、封面提取
Kindle 封面不显示的根本原因是 EXTH 中缺少有效的 ASIN。解决方案:从源文件直接提取封面图片并写入 Kindle 的缩略图缓存。
提取封面不需要解析 HTML 结构——扫描所有 Image Record 的魔术字节即可:
let imageMagics: [[UInt8]] = [
[0xFF, 0xD8, 0xFF], // JPEG
[0x89, 0x50, 0x4E, 0x47], // PNG
[0x47, 0x49, 0x46, 0x38], // GIF
]
for recOffset in recordOffsets[1...] { // skip Record 0
let magic = data[recOffset..<(min(recOffset+4, data.count))]
if imageMagics.contains(where: { magic.starts(with: $0) }) {
return data[recOffset..<recOffset+recordLength]
}
}
五、EPUB 构建与格式转换管线
整个转换管线分三层:
源文件 → EpubBuilder(中间 EPUB)→ kepubify(KEPUB) → Calibre(AZW3,如有安装)
EpubBuilder 是纯 Swift 实现的 EPUB 生成器,负责:
-
TXT → EPUB:按段分行,包装 HTML boilerplate,生成标准 EPUB 容器(mimetype + META-INF + OEBPS)
-
HTML/FB2/CBZ:直接或转换后嵌入 EPUB 结构
-
MOBI/AZW3:调用 MOBIParser 提取 HTML + 图片,重组为 EPUB
-
RTF/DOCX:通过 macOS 内置
textutil转 HTML 再打包(零额外依赖)
生成的 EPUB 通过内嵌的 kepubify 二进制转换为 KEPUB。整个流程对用户透明。

六、架构与测试
Sources/MoZhuan/ ├── App.swift # SwiftUI 入口 ├── ContentView.swift # UI 层(拖拽、进度、状态) ├── ConvertViewModel.swift # 状态管理(@Observable) ├── KepubifyService.swift # 转换调度 + Calibre 检测 ├── EpubBuilder.swift # 纯 Swift EPUB 生成 ├── MOBIParser.swift # PDB/MOBI 二进制解析 + PalmDOC 解压 ├── KindleCoverFixer.swift # 封面修复引擎(MOBIParser → Kindle 缓存) ├── CalibreInstaller.swift # Calibre 下载安装器 ├── DesignConstants.swift # 统一间距系统 ├── KindleDetector.swift # Kindle 设备发现 └── KoboDetector.swift # Kobo 设备发现 Tests/ └── MoZhuanTests.swift # 6 个核心用例,覆盖解析+检测+常量
swift test 全部通过,零编译警告。最低 macOS 14(@Observable 宏要求)。

七、小结
核心经验:
-
二进制格式解析用纯 Data 操作 + big-endian 读取,比依赖第三方库更灵活
-
PalmDOC 解压算法 100 行 Swift 即可实现,无需引入 C 库
-
EXTH ASIN 提取是解决 Kindle 封面问题的关键
-
EPUB 生成用标准 ZIP + XML 即可,
ditto -c -k是 macOS 下最轻量的打包方式 -
合理分层(解析 / 构建 / 调度)让每个模块可独立测试
GitHub:GitHub - wordwu/mozhuan: macOS 电子书格式转换 %26 Kindle 封面修复 · GitHub(MIT 开源)
环境:macOS 14+ / Swift 6.3 / Xcode 16+
更多推荐
所有评论(0)