)”和“章节标签(*class* 属性包含 *makeepub-chapter* 的标签)”两种。章节标签主要在需要对章节标题进行修饰时使用,如在章节标题前插入横幅图片等。
8 |
9 | The special tags are call "split point", including two kinds of tags: "header tags(\
, \
, ... \
)" and "chapter tags (tag's *class* attribute contains *makeepub-chapter* )". Chapter tags is useful if chapter title decoration is required, for example: add a banner image before the title.
10 |
11 | 每个拆分点有“级别”和“标题”两个属性,分别对应目录中的级别和标题。其中级别是一个0到6之间的整数,但0级拆分点只用于文件拆分,不会出现在目录中。
12 |
13 | Every split point has two properties: 'level' and 'title', they are mapped to the level and title properties of a TOC item. 'Level' is an integer betwwen 0 and 6, but level 0 split point is only for file split, will not be used for generate TOC.
14 |
15 | 此工具支持批处理模式,可一次性转换生成多个epub文件。它还支持打包、解包epub文件,合并html文件或文本文件;还可以作为一个web服务器,转换上传的zip文件为epub文件。
16 |
17 | The tool support batch mode which can generate multiple epub books in one execution. It also support pack/extract an epub book, merge html/text files. And can be used as a web server to convert an uploaded zip file to an epub book.
18 |
19 | ## 1. 命令行(Command Line)
20 |
21 | 转换(Create) : makeepub [OutputFolder] [-epub2] [-noduokan]
22 | 批处理(Batch) : makeepub -b [OutputFolder] [-epub2] [-noduokan]
23 | makeepub -b [OutputFolder] [-epub2] [-noduokan]
24 | 打包(Pack) : makeepub -p
25 | 解包(Extract) : makeepub -e
26 | 合并(Merge) HTML : makeepub -mh
27 | 合并(Merge) Text : makeepub -mt
28 | Web服务器(Server) : makeepub -s [Port]
29 |
30 | 各参数含义如下:
31 |
32 | The meaning of the arguments are as below:
33 |
34 | + **VirtualFolder** : 一个文件夹(如example文件夹下的book文件夹)或zip文件(如example文件夹下的book.zip),里面包含要处理的文件。(An OS folder (for example: folder *book* in folder *example*) or a zip file(for example: *book.zip* in folder *example*) which contains the input files.)
35 | + **OutputFolder** 一个文件夹,用于保存输出文件。(An OS folder to store the output file(s).)
36 | + **InputFolder** : 一个文件夹,里面有输入文件或文件夹。(An OS folder which contains the input folder(s)/file(s).)
37 | + **-epub2** : 默认生成EPUB3格式的文件,使用此参数将生成EPUB2格式的文件。(By default, the output file is EPUB3 format, use this argument if EPUB2 format is required.)
38 | + **-noduokan** : 禁用 [多看](http://www.duokan.com/) 扩展。(Disable [DuoKan](http://www.duokan.com/) externsion.)
39 | + **BatchFile** : 一个文本文件,里面列出了所有要处理的VirtualFolder,每行一个。(A text which lists the path of 'VirtualFolders' to be processed, one line for one 'VirtualFolder'.)
40 | + **OutputFile** : 输出文件的路径。(The path of the output file.)
41 | + **EpubFile** : 一个epub文件的路径。(The path of an EPUB file.)
42 | + **Port** : Web服务器的监听端口,默认80。(The TCP port for the web server to listen to, default value is 80.)
43 |
44 | ## 2. 转换(Create)
45 |
46 | makeepub [OutputFolder]
47 |
48 | 处理 *VirtualFolder* 中的文件,生成epub,并保存到 *OutputFolder* 中。在VirtualFolder中,必须有以下三个文件:
49 |
50 | Process files in *VirtualFolder*, generate epub file and save it to *OutputFolder* . The 3 files below 3 are mandatory and must exist in VirtualFolder:
51 |
52 | + **book.ini** 配置文件,用于指定书名、作者等信息(configuration file to specify book name, author and etc.)
53 | + **book.html** 书的正文(The content of the book)
54 | + **cover.png** or **cover.jpg** or **cover.gif** 封面图片文件(The cover image of the book)
55 |
56 | 请 **务必** 使用 *UTF-8* 编码保存前两个文件,否则程序可能不能正确处理。
57 |
58 | The first 2 files **MUST** stored in *UTF-8* encoding, otherwise, the tool may not able to process them correctly.
59 |
60 | 除以上文件外,其它书籍需要的文件,如层叠样式表(css),图片等也应保存到此文件夹中。如果文件内容是文本,建议也使用 *UTF-8* 编码保存。
61 |
62 | Besides the 3 files above, other files required by the book, like sytle sheet (css) and images, are
63 | also required to be put into the folder. And if the content of a file is text, it is also recommend to store it in *UTF-8* encoding.
64 |
65 | ### 2.1 文件格式
66 |
67 | 下面是对主要文件的格式的简单介绍,更多信息请参考example文件夹中的示例。
68 |
69 | Below is a brief introduction of the format of the mandatory files, more information please refer to the examples in the *example* folder.
70 |
71 | #### book.ini
72 |
73 | 此文件基于通用的ini文件格式,以'='开始的行将被合并到上一行,以'#'开始的行将被视为注释,并被忽略。
74 |
75 | This file is based on the common *INI* file format, line start with '=' will be joint to previous line, and line start with '#' will be regard as comment and ignored.
76 |
77 | 这个文件包含三个节, *book* 、 *split* 和 *output*,book节指定书籍信息,split节指定如何进行章节拆分,output节指定输出文件信息。下面的列表将介绍其中每一个选项的作用。
78 |
79 | This file contains three sections: *book*, *split* and *output*. section *book* is for the book information, *split* determines how chapters are split, and section *output* is for the output file. The below list explains the usage of each option.
80 |
81 | + Book节(Section Book)
82 | - **name**: 书名,如果没有提供会导致程序输出一个警告信息(Name of the book, if not specified, the tool will generate a warning)
83 | - **author**: 作者,如果没有提供会导致程序输出一个警告信息(Author of the book, if not specified, the tool will generate a warning)
84 | - **id**: 书的唯一标识,在正规出版的书中,它应该是ISBN编号,如果您没有指定,程序将随机生成一个(The unique identifier, it is the ISBN for a published book. If not specified, the tool will generate a random string for it.)
85 | - **publisher**: 出版社(The publisher of the book.)
86 | - **description**: 书籍简介(A brief introduction of the book.)
87 | - **language**: 语言,默认 *zh-CN* ,即简体中文(Language of the book, *zh-CN* by default, that's Chinese Simplified.)
88 | - **toc**: 一个 *1* 到 *6* 之间的整数,用于指定目录的粒度,默认为 *2*,即只生成1、2两级拆分点对应的目录(An integer between *1* and *6*, specifis how to TOC is generated. Default value is *2*, which means the TOC is based on level 1 and level 2 split points)
89 |
90 | + Split节(section Split)
91 | - **AtLevel**: 一个 *0* 到 *6* 之间的整数,用于指定章节拆分的粒度,默认为 *1*,即只根据1级拆分点拆分章节(An integer between *0* and *6*, specifis how to split the html file into chapters. Default value is *1*, which means the split is based on the level 1 split points)
92 | - **ByHeader**: 一个 *1* 到 *7* 之间的整数。如果一个“标题标签”拆分点的级别小于此选项的值,那么这个拆分点将被忽略。默认值是1,即不忽略任何“标题标签”拆分点。(An integer between *1* and *7*. A "header" split point will be ignored if its level property is smaller than this value. Default is *1* which means no "header" split point will be ignored.)
93 |
94 | + Output节(Section Output)
95 | - **path**: 输出epub文件的路径。如果没有指定,程序会产生一个警告且不会生成任何文件(The output path of the target epub file. If the path is not specified, the tool will generate a warning and no file will be created)
96 |
97 | 下面是book.ini的一个例子。
98 |
99 | Below is an example for book.ini.
100 |
101 | [book]
102 | name=My First eBook
103 | author=Super Man
104 | id=ISBN XXXXXXXXXXXX
105 | publisher=My Own Press
106 | description= 这是本书的简介,它占用了多行。 This is the description
107 | = of the book, and it has more than one line.
108 | language=zh-CN
109 | toc=2
110 |
111 | [split]
112 | AtLevel=1
113 | ByHeader=1
114 |
115 | [output]
116 | path=d:\MyBook.epub
117 |
118 |
119 | #### book.html
120 |
121 | 它是一个标准的html文件,根据 *split* 节的设置,程序会将此文件拆分成章节文件,根据 *toc* 设置生成书籍目录。\标签之前的内容会被复制到每个章节文件的开头。
122 |
123 | This is a standard html file. The tool will split this file into chapter files based on *split* setting, and generate TOC based on the *toc* setting. Content before \ tag will be copied to the beginning of each chapter file.
124 |
125 | 如果其中的某个 *img* 标签符合以下情况,它将会全屏显示 (An image is displayed as full screen if its *img* tag meet all below conditions):
126 | + 打开了多看扩展 (DuoKan externsion is enabled)
127 | + *img* 标签的父级是 *body* 标签 (The parent of *img* tag is *body* tag)
128 | + *img* 的 *class* 属性包含 *duokan-fullscreen* (The value of the *class* property of the *img* tag contains *duokan-fullscreen* )
129 |
130 | #### cover.png/jpg/gif
131 |
132 | 一个图片文件,它将被用来生成封面。这个文件可以是cover.png、cover.jpg和cover.gif中的任意一个,如果存在多个,如同时有cover.png和cover.jpg,那么程序会随机使用其中一个生成封面。
133 |
134 | An image file which will be used to create the book cover. It can be 'cover.png', 'cover.jpg' or 'cover.gif', if more than one file exists (for example: both 'cover.png' and 'cover.jpg'), the tool will select one randomly.
135 |
136 | 封面文件的名字是cover.html,所以请勿使用这个文件名,否则程序的行为将是未知的。
137 |
138 | The file name of the cover page is 'cover.html', please don't use this name for any other purpose, otherwise the behavior of this tool is not defined.
139 |
140 | ### 2.2 拆分点(Split Point)
141 |
142 | 拆分点的使用遵循以下规则,具体使用方法请参考 *example* 文件夹中的例子。
143 |
144 | Below are the rules of split point, please refer to the *example* folder for examples.
145 |
146 | 0. 所有拆分点都必须是 *body* 标签的直接子标签。(All split point MUST be the direct child of the *body* tag.)
147 | 1. 默认情况下,“标题标签”都是拆分点,其“级别”是这个标签的级别,“标题”是这个标签的内容。 (By default, all "header tags" are split points, their "level" are the level of the tags and "title" are the content of these tags.)
148 | 2. “标题标签”的“标题”也可以通过 *data-chapter-title* 属性指定,这种情况下,目录中的标题和正文中的标题将不一样。("Title" can also be specified by *data-chapter-title* attribute, in this case, the chapter will have different title in TOC and content.)
149 | 3. 如果一个标题标签的 *class* 属性包含 *makeepub-not-chapter* ,那么它不是拆分点。(A header tag is not split point when its *class* attribute contains *makeepub-not-chapter* .)
150 | 4. 任何标签,如果它的 *class* 属性包含 *makeepub-chapter* ,那么它是一个“章节标签”拆分点。(A tag is a "chapter tag" split point if its *class* attribute contains *makeepub-chapter* .)
151 | 5. “章节标签”拆分点的“级别”和“标题”可以由 *data-chapter-level* 和 *data-chapter-title* 属性指定。(The "level" and "title" of a "chapter tag" can be specified by the "data-chapter-level" and "data-chapter-title" attributes.)
152 | 6. 如果一个“章节标签”没有 *data-chapter-level* 属性,那么它的“级别”和“标题”由后续的(包括此标签)第一个“标题标签”决定,同时这个“标题标签”失效。但在找到所需的“标题标签”之前,如果出现了其他“章节标签”,则此“章节标签”失效。(If a "chapter tag" does not have *data-chapter-level* attribute, its "level" and "title" will be determined by the first "header tag" after it (or itself, if it is a "header tag" also), and the "header tag" will be ignored. But, if another "chapter tag" is found before the required "header tag", this "chapter tag" will be ignored.)
153 | 7. “章节标签”的优先级高于“标题标签”,即如果一个标签既是“章节标签”又是“标题标签”,它将被作为“章节标签”处理。(The priority of "chapter tag" is higer than "header tag", so if a tag is both "chapter tag" and "header tag", it is regarded as "chapter tag".)
154 | 8. 0级拆分点只用于文件拆分,不生成目录。(Level 0 split point is only for file split, will not be used for generate TOC.)
155 | 9. 级别小于 *ByHeader* 的“标题标签”拆分点会全部被忽略。("Header tag" split points whose level are smaller than *ByHeader* will be ignored.)
156 | 10. 级别大于 *toc* 的拆分点不会生成目录。(Split points whose level are larger than *toc* will not appear in TOC.)
157 | 11. 级别大于 *AtLevel* 的拆分点不会造成文件拆分。(File split will not happen on split points whose level are larger than *AtLevel* .)
158 | 12. 为尽量避免拆分出来的文件只包含章节标题,即使某个拆分点按照 *AtLevel* 选项应该被拆分,如果它和它的上级拆分点之间没有任何正文,它也不会被拆分。(To avoid a chapter file only has a chapter title, file split will not happen on a split point if there's no text between the split point and its parent split point, no matter what the value of option *AtLevel* is.)
159 |
160 | ### 2.3 输出文件的路径(path of the output file)
161 |
162 | 输出文件的路径取决于 *book.ini* 中 *output* 节的 *path* 选项,命令行中的 *OutputFolder* 参数,以及程序的当前工作文件夹。
163 |
164 | The path of the out file is determined by the *path* option of section *output* in *book.ini*, argument *OutputFolder* in command line, and the current working folder of the tool.
165 |
166 | 如果缺少path选项,不会生成任何文件。
167 |
168 | No file will be create if there's no option *path*.
169 |
170 | 如果没有OutputFolder参数,且path是相对路径,输出文件路径是相对于当前工作文件夹的path。
171 |
172 | If argument *OutputFolder* is not specified, and *path* is relative, the output file path will be *path* relative to the current working folder.
173 |
174 | 如果没有OutputFolder参数,且path是绝对路径,输出文件路径是path。
175 |
176 | If argument *OutputFolder* is not specified, and *path* is absolute, the output file path is *path*.
177 |
178 | 如果指定了OutputFolder,文件会被保存在OutputFolder,文件名是path中的文件名部分。
179 |
180 | If *OutputFolder* is specified, output file will be save at *OutputFolder*, and file name is the 'file name' in *path*.
181 |
182 | ## 3. 批处理(Batch)
183 |
184 | makeepub -b [OutputFolder] [-epub2] [-noduokan]
185 | makeepub -b [OutputFolder] [-epub2] [-noduokan]
186 |
187 | 批处理模式,相当于对InputFolder中的(或BatchFile列出的)每个VirtualFolder **folder**,调用:
188 |
189 | Batch mode, is equal to: for each *VirtualFolder* **folder** in *InputFolder* (or listed in *BatchFile), call:
190 |
191 | makeepub folder [OutputFolder] [-epub2] [-noduokan]
192 |
193 |
194 | ## 4. 打包(Pack)
195 |
196 | makeepub -p
197 |
198 | 将VirtualFolder中的文件打包成一个EPUB文件,保存为OutputFile。
199 |
200 | Pack the files in *VirtualFolder* into an EPUB and save it as *OutputFile*.
201 |
202 | ## 5. 解包(Extract)
203 |
204 | makeepub -e
205 |
206 | 将EpubFile解包到OutputFolder中。
207 |
208 | Extract *EpubFile* to folder *OutputFolder*.
209 |
210 | ## 6. 合并(Merge)
211 |
212 | makeepub -mh
213 | makeepub -mt
214 |
215 | 按文件名升序合并VirtualFolder中的文件,并将合并结果保存为OutputFile。合并模式可以使html模式(-mh)或文本(-mt)。
216 |
217 | Sort files in *VirtualFolder* in ascend order by file name, merge them, and save the merge result as *OutputFile*. The merge mode can be *html*(-mh) or *text*(-mt).
218 |
219 | 文本模式是简单的将文件内容连接在一起,Html模式会分析文件,只保留一份文件头(<body>之前的部分)和文件尾(</body>之后的部分)。
220 |
221 | *text* mode is simply merge file content one by one. *html* mode will analysis the file to keep only one copy of file header (content before <body>) and file footer (content after </body>).
222 |
223 |
224 | ## 7. Web服务器(Web Server)
225 |
226 | makeepub -s [Port]
227 |
228 | 以Web服务器形式运行,处理用户上传的zip文件,生成EPUB文件,供用户下载。
229 |
230 | Run as a web server, process the user uploaded zip file, and generate the EPUB file for user to download.
231 |
232 | 如果不需要此功能,可将其删除以减小可执行文件的体积。
233 |
234 | If you don't need this feature, it can be removed to reduce the size of the executable file.
235 |
236 | ## 8. 授权及其他(License & Others)
237 |
238 | MakeEpub是自由软件,基于[MIT授权](http://opensource.org/licenses/mit-license.html)发布
239 |
240 | MakeEpub is free software distributed under the terms of the [MIT license](http://opensource.org/licenses/mit-license.html).
241 |
242 | 此程序是根据我自己制作epub书籍的需要编写,同时也通过编写过程熟悉了[Go语言](http://golang.org/)(可能需翻墙)。今后,将仅修正bug,而不再增加新的功能。
243 |
244 | This tool is developed for my own need when creating epub book, I also learned the [Go program language](http://golang.org/). From now on, I will only fix bugs and won't add new feature any more.
--------------------------------------------------------------------------------
/batch.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "strings"
9 | )
10 |
11 | type taskResult struct {
12 | input string
13 | e error
14 | }
15 |
16 | var (
17 | chTaskResult chan *taskResult
18 | )
19 |
20 | func runTask(input string, outdir string) {
21 | var (
22 | maker = NewEpubMaker(logger)
23 | folder VirtualFolder
24 | tr = &taskResult{input: input}
25 | duokan = !getFlagBool("noduokan")
26 | ver = EPUB_VERSION_300
27 | )
28 | if getFlagBool("epub2") {
29 | ver = EPUB_VERSION_200
30 | }
31 | if folder, tr.e = OpenVirtualFolder(input); tr.e != nil {
32 | logger.Printf("%s: failed to open source folder/file.\n", input)
33 | } else if tr.e = maker.Process(folder, duokan); tr.e == nil {
34 | tr.e = maker.SaveTo(outdir, ver)
35 | }
36 |
37 | chTaskResult <- tr
38 | }
39 |
40 | func processBatchFile(f *os.File, outdir string) (count int, e error) {
41 | scanner := bufio.NewScanner(f)
42 | for scanner.Scan() {
43 | name := strings.TrimSpace(scanner.Text())
44 | if len(name) > 0 {
45 | go runTask(name, outdir)
46 | count++
47 | }
48 | }
49 | if e = scanner.Err(); e != nil {
50 | logger.Println("error reading batch file.")
51 | }
52 |
53 | return
54 | }
55 |
56 | func processBatchFolder(f *os.File, outdir string) (count int, e error) {
57 | names, e := f.Readdirnames(-1)
58 | if e != nil {
59 | logger.Println("error reading source folder.")
60 | return 0, e
61 | }
62 |
63 | for _, name := range names {
64 | name = filepath.Join(f.Name(), name)
65 | go runTask(name, outdir)
66 | count++
67 | }
68 |
69 | return count, nil
70 | }
71 |
72 | func RunBatch() {
73 | var input *os.File = nil
74 | if inpath := getArg(0, ""); len(inpath) == 0 {
75 | onCommandLineError()
76 | } else if f, e := os.Open(inpath); e != nil {
77 | logger.Fatalf("failed to open '%s'.\n", inpath)
78 | } else {
79 | input = f
80 | }
81 | defer input.Close()
82 |
83 | outpath := getArg(1, "")
84 |
85 | runtime.GOMAXPROCS(runtime.NumCPU() + 1)
86 | chTaskResult = make(chan *taskResult)
87 | defer close(chTaskResult)
88 |
89 | var count int
90 | var e error
91 | if fi, _ := input.Stat(); fi.IsDir() {
92 | count, e = processBatchFolder(input, outpath)
93 | } else {
94 | count, e = processBatchFile(input, outpath)
95 | }
96 |
97 | if e != nil && count == 0 {
98 | return
99 | }
100 |
101 | failed := 0
102 | for i := 0; i < count; i++ {
103 | if (<-chTaskResult).e != nil {
104 | failed++
105 | }
106 | }
107 |
108 | logger.Printf("total: %d succeeded: %d failed: %d\n", count, count-failed, failed)
109 | }
110 |
111 | func init() {
112 | AddCommandHandler("b", RunBatch)
113 | }
114 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "io"
7 | "os"
8 | "strconv"
9 | "strings"
10 | )
11 |
12 | type Config struct {
13 | data map[string]string
14 | }
15 |
16 | func ParseIni(reader io.Reader) (*Config, error) {
17 | section, lastKey, cfg := "/", "", make(map[string]string)
18 | firstLine, scanner := true, bufio.NewScanner(reader)
19 |
20 | for scanner.Scan() {
21 | s := scanner.Bytes()
22 | if firstLine {
23 | s = removeUtf8Bom(s)
24 | firstLine = false
25 | }
26 |
27 | s = bytes.TrimSpace(s)
28 | if len(s) == 0 || s[0] == '#' { // empty or comment
29 | continue
30 | }
31 |
32 | if s[0] == '[' && s[len(s)-1] == ']' { // section
33 | s = bytes.TrimSpace(s[1 : len(s)-1])
34 | if len(s) >= 0 {
35 | section = "/" + string(bytes.ToLower(s))
36 | }
37 | continue
38 | }
39 |
40 | k, v := "", ""
41 | if i := bytes.IndexByte(s, '='); i != -1 {
42 | k = string(bytes.ToLower(bytes.TrimSpace(s[:i])))
43 | v = string(bytes.TrimSpace(s[i+1:]))
44 | }
45 |
46 | if len(k) > 0 {
47 | lastKey = section + "/" + k
48 | cfg[lastKey] = v
49 | continue
50 | } else if len(lastKey) == 0 {
51 | continue
52 | }
53 |
54 | c, lv := byte(128), cfg[lastKey]
55 | if len(lv) > 0 {
56 | c = lv[len(lv)-1]
57 | }
58 |
59 | if len(v) == 0 { // empty value means a new line
60 | cfg[lastKey] = lv + "\n"
61 | } else if c < 128 && c != '-' && v[0] < 128 { // need a white space?
62 | // not good enough, but should be ok in most cases
63 | cfg[lastKey] = lv + " " + v
64 | } else {
65 | cfg[lastKey] = lv + v
66 | }
67 | }
68 |
69 | if e := scanner.Err(); e != nil {
70 | return nil, e
71 | }
72 |
73 | return &Config{data: cfg}, nil
74 | }
75 |
76 | func OpenIniFile(path string) (*Config, error) {
77 | f, e := os.Open(path)
78 | if e != nil {
79 | return nil, e
80 | }
81 | defer f.Close()
82 |
83 | return ParseIni(f)
84 | }
85 |
86 | func (cfg *Config) GetInt(path string, dflt int) int {
87 | path = strings.ToLower(path)
88 | if v, ok := cfg.data[path]; ok {
89 | if i, e := strconv.Atoi(v); e == nil {
90 | return i
91 | }
92 | }
93 | return dflt
94 | }
95 |
96 | func (cfg *Config) GetString(path string, dflt string) string {
97 | path = strings.ToLower(path)
98 | if v, ok := cfg.data[path]; ok {
99 | return v
100 | }
101 | return dflt
102 | }
103 |
104 | func (cfg *Config) GetBool(path string, dflt bool) bool {
105 | path = strings.ToLower(path)
106 | if v, ok := cfg.data[path]; ok {
107 | if b, e := strconv.ParseBool(v); e == nil {
108 | return b
109 | }
110 | }
111 | return dflt
112 | }
113 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // makeepub project doc.go
2 |
3 | /*
4 | makeepub document
5 | */
6 | package main
7 |
--------------------------------------------------------------------------------
/epub.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "archive/zip"
5 | "bytes"
6 | "fmt"
7 | "html"
8 | "os"
9 | "path/filepath"
10 | "strings"
11 | "time"
12 | )
13 |
14 | const (
15 | path_of_mimetype = "mimetype"
16 | path_of_toc_ncx = "toc.ncx"
17 | path_of_nav_xhtml = "nav.xhtml"
18 | path_of_content_opf = "content.opf"
19 | path_of_container_xml = "META-INF/container.xml"
20 | path_of_cover_page = "cover.html"
21 |
22 | EPUB_VERSION_NONE = iota // no version, pack all raw files into a zip package
23 | EPUB_VERSION_200 // epub version 2.0
24 | EPUB_VERSION_300 // epub version 3.0
25 |
26 | epub_NORMAL_FILE = 1 << iota // nomal files
27 | epub_CONTENT_FILE // content files: the chapters
28 | epub_FULL_SCREEN_PAGE // full screen pages in content
29 | epub_INTERNAL_FILE // internal file, generated automatically in most case
30 | )
31 |
32 | var (
33 | media_types = map[string]string{
34 | ".html": "application/xhtml+xml",
35 | ".htm": "application/xhtml+xml",
36 | ".css": "text/css",
37 | ".txt": "text/plain",
38 | ".xml": "text/xml",
39 | ".xhtml": "application/xhtml+xml",
40 | ".ncx": "application/x-dtbncx+xml",
41 | ".jpg": "image/jpeg",
42 | ".jpeg": "image/jpeg",
43 | ".gif": "image/gif",
44 | ".png": "image/png",
45 | ".bmp": "image/bmp",
46 | ".otf": "application/x-font-opentype",
47 | ".ttf": "application/x-font-ttf",
48 | }
49 | )
50 |
51 | func getMediaType(path string) string {
52 | ext := strings.ToLower(filepath.Ext(path))
53 | if mt, ok := media_types[ext]; ok {
54 | return mt
55 | }
56 | return "application/octet-stream"
57 | }
58 |
59 | ////////////////////////////////////////////////////////////////////////////////
60 | // helper class, epub compressor
61 |
62 | type epubCompressor struct {
63 | zip *zip.Writer
64 | buf *bytes.Buffer
65 | }
66 |
67 | func (this *epubCompressor) init() error {
68 | this.buf = new(bytes.Buffer)
69 | this.zip = zip.NewWriter(this.buf)
70 |
71 | header := &zip.FileHeader{
72 | Name: path_of_mimetype,
73 | Method: zip.Store,
74 | }
75 | w, e := this.zip.CreateHeader(header)
76 | if e == nil {
77 | _, e = w.Write([]byte("application/epub+zip"))
78 | }
79 | return e
80 | }
81 |
82 | func (this *epubCompressor) addFile(path string, data []byte) error {
83 | w, e := this.zip.Create(path)
84 | if e == nil {
85 | _, e = w.Write(data)
86 | }
87 | return e
88 | }
89 |
90 | func (this *epubCompressor) close() error {
91 | return this.zip.Close()
92 | }
93 |
94 | func (this *epubCompressor) result() []byte {
95 | return this.buf.Bytes()
96 | }
97 |
98 | ////////////////////////////////////////////////////////////////////////////////
99 |
100 | type Chapter struct {
101 | Level int
102 | Title string
103 | Link string
104 | }
105 |
106 | type File struct {
107 | Path string
108 | Data []byte
109 | Attr int
110 | Chapters []Chapter
111 | }
112 |
113 | type Epub struct {
114 | id string
115 | name string
116 | author string
117 | publisher string
118 | description string
119 | language string
120 | cover string // path of the cover image
121 | duokan bool // if duokan externsion is enabled
122 | files []*File
123 | }
124 |
125 | func NewEpub(duokan bool) *Epub {
126 | this := new(Epub)
127 | this.files = make([]*File, 0, 256)
128 | this.duokan = duokan
129 | return this
130 | }
131 |
132 | func (this *Epub) Id() string {
133 | if len(this.id) == 0 {
134 | this.SetId("")
135 | }
136 | return this.id
137 | }
138 |
139 | func (this *Epub) SetId(id string) {
140 | if len(id) == 0 {
141 | h, _ := os.Hostname()
142 | t := uint32(time.Now().Unix())
143 | id = fmt.Sprintf("%s-book-%08x", h, t)
144 | }
145 | this.id = id
146 | }
147 |
148 | func (this *Epub) Name() string {
149 | return this.name
150 | }
151 |
152 | func (this *Epub) SetName(name string) {
153 | this.name = name
154 | }
155 |
156 | func (this *Epub) Author() string {
157 | return this.author
158 | }
159 |
160 | func (this *Epub) SetAuthor(author string) {
161 | this.author = author
162 | }
163 |
164 | func (this *Epub) Publisher() string {
165 | return this.publisher
166 | }
167 |
168 | func (this *Epub) SetPublisher(publisher string) {
169 | this.publisher = publisher
170 | }
171 |
172 | func (this *Epub) Description() string {
173 | return this.description
174 | }
175 |
176 | func (this *Epub) SetDescription(desc string) {
177 | this.description = desc
178 | }
179 |
180 | func (this *Epub) Language() string {
181 | return this.language
182 | }
183 |
184 | func (this *Epub) SetLanguage(lang string) {
185 | this.language = lang
186 | }
187 |
188 | func (this *Epub) Duokan() bool {
189 | return this.duokan
190 | }
191 |
192 | func (this *Epub) SetCoverImage(path string) {
193 | this.cover = filepath.ToSlash(path)
194 | }
195 |
196 | func (this *Epub) AddFile(path string, data []byte) {
197 | path = filepath.ToSlash(path)
198 | if strings.ToLower(path) == path_of_mimetype {
199 | return
200 | }
201 | f := &File{
202 | Path: path,
203 | Data: data,
204 | }
205 | if path == path_of_cover_page ||
206 | path == path_of_content_opf ||
207 | path == path_of_toc_ncx ||
208 | path == path_of_nav_xhtml ||
209 | path == strings.ToLower(path_of_container_xml) {
210 | f.Attr = epub_INTERNAL_FILE
211 | }
212 | this.files = append(this.files, f)
213 | }
214 |
215 | func generateImagePage(path, alt string) []byte {
216 | path = filepath.ToSlash(path)
217 | s := fmt.Sprintf(""+
218 | "\n"+
219 | ""+
220 | "\n"+
221 | "\n"+
222 | " \n"+
223 | "\n"+
224 | "\n"+
225 | "