不知道大家平時有沒有閱讀電子書的習慣,這裡指的并不是
.txt
的文本文檔,而是通常帶有精美封面、便捷目錄、圖文并茂的
.epub
電子書。它是怎樣實作這些效果的呢?我們能不能把自己平時用 Markdown 寫的技術筆記、部落格文章做成一本屬于自己的電子書呢?
背景
EPUB 格式是什麼
其實做 Web 開發的同學,如果把
.epub
檔案通過 zip 打開後就會發現,其實它并不神秘,反而相當開放直覺和熟悉──其内在就是一堆 xhtml 頁面、css 樣式、圖檔,以及描述這些資源關系的 xml 配置資訊,把它們一起打個 zip 包就是
.epub
電子書了。
在我們解壓出來的檔案,往往會有一個
.opf
檔案,内容開頭一般是:
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://www.idpf.org/2007/opf" ...>
<!-- ... -->
</package>
複制
我們隻要通路命名空間屬性中的這個 http://www.idpf.org/2007/opf 連結,就可以查詢到關于這個
OPF
電子書的所有規範描述了。
簡單來說,這就是一個由國際數字出版論壇和 W3C 組織一起完成的開放電子書标準,在 2007 年 9 月取代了之前的 Open eBook,被國際數字出版論壇選為新的正式标準。
既然 epub 内部就是 html 頁面,我們的 Markdown 文章也能編譯成 html,那我們寫個工具将以往的文檔處理成符合 epub 标準的檔案包,不就可以做一本自己的電子書了?
開始動手:手動篇
1. 建立電子書
1-1. 基本結構
我們先建立一個 example 目錄,其中包含 META-INF 和 EPUB 兩個子目錄。然後在現有目錄結構中建立 mimetype, META-INF/container.xml 和 EPUB/package.opf 檔案:
example
├── EPUB
│ └── package.opf
├── META-INF
│ └── container.xml
└── mimetype
複制
檔案 mimetype 内容:
application/epub+zip
複制
檔案 META-INF/container.xml 内容:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<container xmlns="urn:oasis:names:tc:opendocument:xmlns:container" version="1.0">
<rootfiles>
<rootfile full-path="EPUB/package.opf" media-type="application/oebps-package+xml"/>
</rootfiles>
</container>
複制
檔案 EPUB/package.opf 内容:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<package xmlns="http://www.idpf.org/2007/opf"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/"
version="3.0"
xml:lang="en"
unique-identifier="pub-identifier">
<metadata>
<dc:identifier id="pub-identifier">kepub:20211120:000000001</dc:identifier>
<dc:title id="pub-title">Example Book</dc:title>
<dc:language id="pub-language">en</dc:language>
<dc:date>2021-11-20</dc:date>
<meta property="dcterms:modified">2021-11-20T14:50:00Z</meta>
</metadata>
<manifest>
<!-- TODO -->
</manifest>
<spine>
<!-- TODO -->
</spine>
</package>
複制
以上就是我們的 .epub 檔案中最基礎的三個檔案。
其中 package.opf 中,我們在
package > metadata
内定義了一些 .epub 必備的元資訊。以後我們向電子書添加内容時,還需要根據實際情況繼續更新其中
package > manifest
資源清單 和
package > spine
書脊 的相關資訊。
1-2. 添加頁面
接下來就是向其中添加内容了。
在之前的基礎上,我們再建立一個 EPUB/book 目錄,在其中添加一個 EPUB/book/page-1.xhtml 檔案:
example
├── EPUB
│ ├── book
│ │ └── page-1.xhtml
│ └── package.opf
├── META-INF
│ └── container.xml
└── mimetype
複制
檔案 EPUB/book/page-1.xhtml 内容:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops"
xml:lang="en"
lang="en">
<head>
<title>Page 1</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
複制
然後修改 package.opf 中的資源清單和書脊:
<manifest>
<item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
<itemref idref="page-1"/>
</spine>
複制
對于剛才的頁面,我們建立了
package > manifest > item
條目,标記了它的
[media-type]
類型,并且設定了一個
[id="page-1"]
屬性,将其以
itemref[idref="page-1"]
的形式在書脊内進行了引用。
此時,如果将 example 目錄的内容進行 zip 打包,生成檔案名稱改為 example.epub,就已經可以在一些 epub 閱讀器中正常打開進行閱讀了。但部分基于導航目錄進行内容索引的閱讀器(比如 微信讀書)還無法正常浏覽,需要再做一點小小的改動。
1-3. 導航目錄
我們再建立一個 EPUB/toc.xhtml,内容:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops"
xml:lang="en"
lang="en">
<head>
<title>TOC</title>
</head>
<body>
<nav epub:type="toc" id="toc">
<h1>Table of Contents</h1>
<ol>
<li>
<a href="book-page1.xhtml">Page 1</a>
</li>
</ol>
</nav>
</body>
</html>
複制
注意其中的,這是 epub3 與 epub2 的差別之一,可以将目錄頁面的部分作為書籍的導航目錄,不再需要單獨提供
nav[epub:type="toc"]
檔案。
.ncx
同樣的,繼續修改 package.opf 的資源清單和書脊:
<manifest>
<item id="htmltoc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
<item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
<itemref idref="htmltoc"/>
<itemref idref="page-1"/>
</spine>
複制
其中
item#htmldoc
添加了
[properties="nav"]
表示這個頁面是導航目錄。
這次,再将 example 目錄内容打包為 example.epub 後,就能在大部分閱讀器内都正常打開了。
1-4. 封面頁
我們還可以給自己的電子書添加一個好看的封面,比如:
将其儲存為 EPUB/images/cover.jpg,然後建立 EPUB/cover.xhtml,内容為:
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:epub="http://www.idpf.org/2007/ops"
xml:lang="en"
lang="en">
<head>
<title>Cover</title>
<style type="text/css">
img {
max-width: 100%;
}
</style>
</head>
<body>
<figure id="cover-image">
<img src="images/cover.jpg"
alt="Book Cover" />
</figure>
</body>
</html>
複制
老規矩,繼續更新 package.opf 的資源清單和書脊:
<manifest>
<item id="htmltoc" href="toc.xhtml" media-type="application/xhtml+xml" properties="nav"/>
<item id="cover" href="cover.xhtml" media-type="application/xhtml+xml"/>
<item id="cover-image" href="images/cover.jpg" media-type="image/jpeg" properties="cover-image"/>
<item id="page-1" href="book/page-1.xhtml" media-type="application/xhtml+xml"/>
</manifest>
<spine>
<itemref idref="cover" linear="no"/>
<itemref idref="htmltoc"/>
<itemref idref="page-1"/>
</spine>
複制
這次,增加的圖檔檔案也需要登記在
package > manifest
資源清單内,并且添加了
properties="cover-image"
屬性,将其标記為書籍的封面圖檔。
而建立的
cover.xhtml
檔案,是為了讓我們在打開書籍後,也能在内容内看到封面的效果。
1-5. 其它
如果我們需要在電子書内,添加更多頁面、引用更多圖檔、添加裝飾樣式、改用自定義字型,也是相同的操作:
- 将資源添加到項目内。
- 更新 package.opf 内的
資源清單。manifest
- 如果是頁面,再添加對應
引用到[id]
書脊内,并且更新 toc.xhtml 内的spine
記錄。nav
- toc.xhtml 内的
導航目錄,支援nav
嵌套實作多級目錄。ol > li > ol > li ...
- 部分閱讀器不支援
導航目錄使用nav
無須清單。ul
- 如果需要為導航索引型閱讀器提供頁面引用,又不想讓對應記錄出現在目錄内;或者重複引用于不同位置的相同頁面記錄,可以為對應
/ol
設定li
屬性,進行隐藏處理。[hidden=""]
- toc.xhtml 内的
2. 自動流程準備
基于上面的原理,我們已經能夠開始手動編寫我們的電子書了。
不過這個過程中還有很多手動操作并不便捷的步驟,比如 每篇文章進行 Markdown to Html 轉化、文章中所有圖檔添加到資源清單、更新文章目錄結構 ,如果文章頁面、引用資源稍微多一些,就基本沒法手動處理過來了。
是以我們需要更高效的自動處理方案。
2-1. 自動的與手動的
剛才提到的資源清單内容,大緻可以分為兩類:
- 文章的頁面檔案。
- 文章内引用的圖檔資源
其中,後者可以直接在 Markdown 文檔渲染成 html 檔案後,進行 html 解析再對所有
img
标簽進行彙總即可得出配置清單。
前者可以基于
.md
檔案本身的目錄結構進行資源清單的整合,但是 對于頁面在書脊和導航目錄内的順序 無法進行很好的控制。
如果基于檔案名進行排序,相當于引入了一套不可控的潛規則,對于書籍遷移、頁面删減維護都不太友善。而且如果需要處理導航目錄内隐藏、重新引用的場景,還要引入更複雜的潛規則。
不如增加一個簡化的
.json
配置檔案,統一管理頁面在導航目錄内的順序和層級關系。
2-2. 新的電子書結構
我們重新建立一個 new-book 目錄,并在其中建立一些子目錄和檔案:
new-book
├── chapter-1
│ ├── index.md
│ └── ep-1.md
└── book.json
複制
檔案 book.json 内容:
{
"meta": {
"id": "kepub:20211120:000000001",
"title": "Example Book",
"lang": "en",
"date": "2021-11-20",
"modified": "2021-11-20T14:50:00Z"
},
"pages": [
{
"title": "Chapter.01",
"file": "chapter-1/index.md",
"children": [
{
"file": "chapter-1/ep-1.md",
"hidden": true
}
]
}
]
}
複制
檔案 chapter-1/index.md 内容:
# Chapter.01
Hello world!
複制
檔案 chapter-1/ep-1.md 内容:
# Episode.01
This is episode 1.
複制
這樣,我們日常建立一本電子書時,真正需要自己定制的内容基本就已收錄其中。而且能友善地在其中定義和調整頁面的順序和層級關系,控制對應條目是否在導航内隐藏了。
接下來,我們隻需要再編寫一些腳本,将上面的結構自動轉化成電子書需要的
mimetype
,
container.xml
,
package.opf
和各種頁面檔案,并且彙總對應的資源清單、書脊和導航目錄。