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 }