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