1 module epub.output;
2 
3 @safe:
4 
5 import epub.books;
6 
7 import std.algorithm;
8 import std.array;
9 import std.conv;
10 import std.file;
11 import std.string;
12 import std.uuid;
13 import std.zip;
14 
15 /**
16  * Convert the given book to epub and save it to the given path.
17  */
18 void toEpub(Book book, string path) @trusted
19 {
20     auto zf = new ZipArchive();
21     toEpub(book, zf);
22     // @safe function toEpub can't call @system function ZipArchive.build
23     path.write(zf.build());
24 }
25 
26 /**
27  * Convert the given book to epub. Store it in the given zip archive.
28  */
29 void toEpub(Book book, ZipArchive zf)
30 {
31     if (book.id == null)
32     {
33         book.id = randomUUID().to!string;
34     }
35     foreach (i, ref c; book.chapters)
36     {
37         c.index = cast(int)i + 1;
38     }
39     foreach (ref attachment; book.attachments)
40     {
41         if (attachment.fileid == null)
42         {
43             attachment.fileid = randomUUID().to!string;
44         }
45     }
46 
47     // mimetype should be the first entry in the zip.
48     // Unfortunately, this doesn't seem to happen...
49     // On the other hand, most readers seem okay with mimetype not being
50     // in its proper place.
51     save(zf, "mimetype", "application/epub+zip");
52     save(zf, "META-INF/container.xml", container_xml);
53     writeZip!contentOpf(zf, "content.opf", book);
54     writeZip!tocNcx(zf, "toc.ncx", book);
55     foreach (chapter; book.chapters)
56     {
57         save(zf, chapter.filename, chapter.content);
58     }
59     foreach (attachment; book.attachments)
60     {
61         save(zf, attachment.filename, attachment.content);
62     }
63 }
64 
65 private:
66 
67 enum container_xml = import("container.xml");
68 
69 void save(ZipArchive zf, string name, const char[] content)
70 {
71     save(zf, name, cast(const(ubyte[]))content);
72 }
73 
74 void save(ZipArchive zf, string name, const(ubyte[]) content) @trusted
75 {
76     auto member = new ArchiveMember();
77     member.name = name;
78     // std.zip isn't const-friendly
79     member.expandedData = cast(ubyte[])content;
80     zf.addMember(member);
81 }
82 
83 void writeZip(alias method)(ZipArchive zf, string name, Book book)
84 {
85     save(zf, name, method(book));
86 }
87 
88 string contentOpf(Book book)
89 {
90     Appender!string s;
91     s.reserve(2000);
92     s ~= `<?xml version='1.0' encoding='utf-8'?>
93         <package xmlns="http://www.idpf.org/2007/opf" unique-identifier="uuid_id" version="2.0">
94         <metadata xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dc="http://purl.org/dc/elements/1.1/">
95         <dc:language>en</dc:language>
96         <dc:creator>Unknown</dc:creator>
97         <dc:title>`;
98     s ~= book.title;
99     s ~= `</dc:title>
100         <meta name="cover" content="cover"/>
101         <dc:identifier id="uuid_id" opf:scheme="uuid">`;
102     s ~= book.id;
103     s ~= `</dc:identifier>
104         </metadata>
105         <manifest>`;
106     foreach (chapter; book.chapters)
107     {
108         s ~= `
109             <item href="`;
110         s ~= chapter.filename;
111         s ~= `" id="`;
112         s ~= chapter.fileid;
113         s ~= `" media-type="application/xhtml+xml"/>`;
114     }
115     s ~= `
116         <item href="toc.ncx" id="ncx" media-type="application/x-dtbncx+xml"/>
117         <item href="titlepage.xhtml" id="titlepage" media-type="application/xhtml+xml"/>
118     </manifest>
119     <spine toc="ncx">
120         <itemref idref="titlepage"/>`;
121     foreach (chapter; book.chapters)
122     {
123         s ~= `
124             <itemref idref="`;
125         s ~= chapter.fileid;
126         s ~= `"/>`;
127     }
128     s ~= `
129     </spine>
130     <guide>`;
131     if (book.coverid)
132     {
133         auto coverRange = book.attachments.find!(x => x.fileid == book.coverid);
134         if (!coverRange.empty)
135         {
136             auto cover = coverRange.front;
137             s ~= `
138         <reference href="`;
139             s ~=
140         `" title="Title Page" type="cover"/>`;
141         }
142     }
143     s ~= `
144     </guide>
145 </package>
146         `;
147     return s.data;
148 }
149 
150 string tocNcx(Book book)
151 {
152     Appender!string s;
153     s.reserve(1000);
154     s ~= `<?xml version='1.0' encoding='utf-8'?>
155         <ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1" xml:lang="en">
156         <head>
157         <meta content="`;
158     s ~= book.id;
159     s ~= `" name="dtb:uid"/>
160         <meta content="2" name="dtb:depth"/>
161         <meta content="bookmaker" name="dtb:generator"/>
162         <meta content="0" name="dtb:totalPageCount"/>
163         <meta content="0" name="dtb:maxPageNumber"/>
164         </head>
165         <docTitle>
166         <text>`;
167     s ~= book.title;
168     s ~= `</text>
169         </docTitle>
170         <navMap>`;
171     foreach (i, chapter; book.chapters)
172     {
173         s ~= `
174             <navPoint id="ch`;
175         s ~= chapter.id.replace("-", "");
176         s ~= `" playOrder="`;
177         s ~= (i + 1).to!string;
178         s ~= `">
179             <navLabel>
180             <text>`;
181         s ~= chapter.title;
182         s ~= `</text>
183             </navLabel>
184             <content src="`;
185         s ~= chapter.filename;
186         s ~= `"/>
187             </navPoint>`;
188     }
189     s ~= `
190         </navMap>
191         </ncx>`;
192     return s.data;
193 }
194 
195 void htmlPrelude(OutRange)(const Book book, ref OutRange sink, bool includeStylesheets, void delegate(ref OutRange) bdy)
196 {
197     sink.put(`<?xml version='1.0' encoding='utf-8'?>
198 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
199     <head>
200         <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
201             `);
202     if (includeStylesheets && "stylesheet" in book.info)
203     {
204         foreach (stylesheet; book.info["stylesheet"])
205         {
206             sink.put(`<link rel="stylesheet" href="`);
207             sink.put(stylesheet);
208             sink.put(`" type="text"/>
209                     `);
210         }
211     }
212     sink.put(`
213             <title>`);
214     sink.put(book.info["title"][0]);
215     sink.put(`</title>
216     </head>
217     <body>
218             `);
219     bdy(sink);
220     sink.put(`
221     </body>
222 </html>`);
223 }