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 }