Merge branch 'master' into subtree
[nikiroo-utils.git] / output / Epub.java
CommitLineData
08fe2e33
NR
1package be.nikiroo.fanfix.output;
2
3d247bc3 3import java.io.BufferedWriter;
08fe2e33 4import java.io.File;
ecfb936e 5import java.io.FileInputStream;
3d247bc3 6import java.io.FileOutputStream;
08fe2e33
NR
7import java.io.IOException;
8import java.io.InputStream;
ecfb936e 9import java.io.OutputStream;
3d247bc3 10import java.io.OutputStreamWriter;
ecfb936e
NR
11import java.util.zip.ZipEntry;
12import java.util.zip.ZipOutputStream;
08fe2e33 13
08fe2e33
NR
14import be.nikiroo.fanfix.Instance;
15import be.nikiroo.fanfix.bundles.Config;
16import be.nikiroo.fanfix.bundles.StringId;
17import be.nikiroo.fanfix.data.Chapter;
18import be.nikiroo.fanfix.data.MetaData;
19import be.nikiroo.fanfix.data.Paragraph;
08fe2e33 20import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
3d247bc3 21import be.nikiroo.fanfix.data.Story;
08fe2e33
NR
22import be.nikiroo.utils.IOUtils;
23import be.nikiroo.utils.StringUtils;
24
25class Epub extends BasicOutput {
26 private File tmpDir;
3d247bc3 27 private BufferedWriter writer;
08fe2e33
NR
28 private boolean inDialogue = false;
29 private boolean inNormal = false;
30 private File images;
16a81ef7 31 private boolean nextParaIsCover = true;
08fe2e33
NR
32
33 @Override
34 public File process(Story story, File targetDir, String targetName)
35 throws IOException {
36 String targetNameOrig = targetName;
10d558d2 37 targetName += getDefaultExtension(false);
08fe2e33 38
d66deb8d 39 tmpDir = Instance.getInstance().getTempFiles().createTempDir("fanfic-reader-epub");
08fe2e33
NR
40 tmpDir.delete();
41
42 if (!tmpDir.mkdir()) {
43 throw new IOException(
44 "Cannot create a temporary directory: no space left on device?");
45 }
46
276f95c6
NR
47 super.process(story, targetDir, targetNameOrig);
48
2aac79c7 49 File epub = null;
ecfb936e 50 try {
2aac79c7
NR
51 // "Originals"
52 File data = new File(tmpDir, "DATA");
53 data.mkdir();
54 BasicOutput.getOutput(OutputType.TEXT, isWriteInfo(),
55 isWriteCover()).process(story, data, targetNameOrig);
56 InfoCover.writeInfo(data, targetNameOrig, story.getMeta());
57 IOUtils.writeSmallFile(data, "version", "3.0");
58
59 // zip/epub
60 epub = new File(targetDir, targetName);
61 IOUtils.zip(tmpDir, epub, true);
62
63 OutputStream out = new FileOutputStream(epub);
ecfb936e 64 try {
2aac79c7 65 ZipOutputStream zip = new ZipOutputStream(out);
ecfb936e 66 try {
2aac79c7
NR
67 // "mimetype" MUST be the first element and not compressed
68 zip.setLevel(ZipOutputStream.STORED);
69 File mimetype = new File(tmpDir, "mimetype");
70 IOUtils.writeSmallFile(tmpDir, "mimetype",
71 "application/epub+zip");
72 ZipEntry entry = new ZipEntry("mimetype");
73 entry.setExtra(new byte[] {});
74 zip.putNextEntry(entry);
75 FileInputStream in = new FileInputStream(mimetype);
76 try {
77 IOUtils.write(in, zip);
78 } finally {
79 in.close();
80 }
81 IOUtils.deltree(mimetype);
82 zip.setLevel(ZipOutputStream.DEFLATED);
83 //
84
85 IOUtils.zip(zip, "", tmpDir, true);
ecfb936e 86 } finally {
2aac79c7 87 zip.close();
ecfb936e 88 }
ecfb936e 89 } finally {
2aac79c7 90 out.close();
ecfb936e
NR
91 }
92 } finally {
2aac79c7
NR
93 IOUtils.deltree(tmpDir);
94 tmpDir = null;
ecfb936e
NR
95 }
96
08fe2e33
NR
97 return epub;
98 }
99
100 @Override
10d558d2 101 public String getDefaultExtension(boolean readerTarget) {
08fe2e33
NR
102 return ".epub";
103 }
104
105 @Override
106 protected void writeStoryHeader(Story story) throws IOException {
107 File ops = new File(tmpDir, "OPS");
108 ops.mkdirs();
109 File css = new File(ops, "css");
110 css.mkdirs();
111 images = new File(ops, "images");
112 images.mkdirs();
113 File metaInf = new File(tmpDir, "META-INF");
114 metaInf.mkdirs();
115
08fe2e33 116 // META-INF
ecfb936e 117 String containerContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
08fe2e33
NR
118 + "<container xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\" version=\"1.0\">\n"
119 + "\t<rootfiles>\n"
120 + "\t\t<rootfile full-path=\"OPS/epb.opf\" media-type=\"application/oebps-package+xml\"/>\n"
121 + "\t</rootfiles>\n" + "</container>\n";
122
123 IOUtils.writeSmallFile(metaInf, "container.xml", containerContent);
124
125 // OPS/css
126 InputStream inStyle = getClass().getResourceAsStream("epub.style.css");
127 if (inStyle == null) {
128 throw new IOException("Cannot find style.css resource");
129 }
130 try {
131 IOUtils.write(inStyle, new File(css, "style.css"));
132 } finally {
133 inStyle.close();
134 }
135
136 // OPS/images
137 if (story.getMeta() != null && story.getMeta().getCover() != null) {
ecfb936e 138 File file = new File(images, "cover");
43c75fb7 139 try {
d66deb8d 140 Instance.getInstance().getCache().saveAsImage(story.getMeta().getCover(), file, true);
43c75fb7 141 } catch (Exception e) {
d66deb8d 142 Instance.getInstance().getTraceHandler().error(e);
43c75fb7 143 }
08fe2e33
NR
144 }
145
146 // OPS/* except chapters
147 IOUtils.writeSmallFile(ops, "epb.ncx", generateNcx(story));
148 IOUtils.writeSmallFile(ops, "epb.opf", generateOpf(story));
ecfb936e 149 IOUtils.writeSmallFile(ops, "title.xhtml", generateTitleXml(story));
08fe2e33
NR
150
151 // Resume
152 if (story.getMeta() != null && story.getMeta().getResume() != null) {
153 writeChapter(story.getMeta().getResume());
154 }
155 }
156
157 @Override
158 protected void writeChapterHeader(Chapter chap) throws IOException {
159 String filename = String.format("%s%03d%s", "chapter-",
ecfb936e 160 chap.getNumber(), ".xhtml");
3d247bc3
NR
161 writer = new BufferedWriter(new OutputStreamWriter(
162 new FileOutputStream(new File(tmpDir + File.separator + "OPS",
163 filename)), "UTF-8"));
08fe2e33
NR
164 inDialogue = false;
165 inNormal = false;
166 try {
167 String title = "Chapter " + chap.getNumber();
168 String nameOrNum = Integer.toString(chap.getNumber());
169 if (chap.getName() != null && !chap.getName().isEmpty()) {
170 title += ": " + chap.getName();
171 nameOrNum = chap.getName();
172 }
173
ecfb936e
NR
174 writer.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
175 writer.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
176 writer.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\">");
08fe2e33
NR
177 writer.write("\n<head>");
178 writer.write("\n <title>" + StringUtils.xmlEscape(title)
179 + "</title>");
180 writer.write("\n <link rel='stylesheet' href='css/style.css' type='text/css'/>");
181 writer.write("\n</head>");
182 writer.write("\n<body>");
183 writer.write("\n <h2>");
184 writer.write("\n <span class='chap'>Chapter <span class='chapnumber'>"
185 + chap.getNumber() + "</span>:</span> ");
186 writer.write("\n <span class='chaptitle'>"
187 + StringUtils.xmlEscape(nameOrNum) + "</span>");
188 writer.write("\n </h2>");
189 writer.write("\n ");
190 writer.write("\n <div class='chapter_content'>\n");
191 } catch (Exception e) {
192 writer.close();
193 throw new IOException(e);
194 }
195 }
196
197 @Override
198 protected void writeChapterFooter(Chapter chap) throws IOException {
199 try {
200 if (inDialogue) {
201 writer.write(" </div>\n");
202 inDialogue = false;
203 }
204 if (inNormal) {
205 writer.write(" </div>\n");
206 inNormal = false;
207 }
208 writer.write(" </div>\n</body>\n</html>\n");
209 } finally {
210 writer.close();
211 writer = null;
212 }
213 }
214
215 @Override
216 protected void writeParagraphHeader(Paragraph para) throws IOException {
217 if (para.getType() == ParagraphType.QUOTE && !inDialogue) {
218 writer.write(" <div class='dialogues'>\n");
219 inDialogue = true;
220 } else if (para.getType() != ParagraphType.QUOTE && inDialogue) {
221 writer.write(" </div>\n");
222 inDialogue = false;
223 }
224
225 if (para.getType() == ParagraphType.NORMAL && !inNormal) {
226 writer.write(" <div class='normals'>\n");
227 inNormal = true;
228 } else if (para.getType() != ParagraphType.NORMAL && inNormal) {
229 writer.write(" </div>\n");
230 inNormal = false;
231 }
232
233 switch (para.getType()) {
234 case BLANK:
235 writer.write(" <div class='blank'></div>");
236 break;
237 case BREAK:
f977d05b 238 writer.write(" <hr class='break'/>");
08fe2e33
NR
239 break;
240 case NORMAL:
241 writer.write(" <span class='normal'>");
242 break;
243 case QUOTE:
244 writer.write(" <div class='dialogue'>&mdash; ");
245 break;
246 case IMAGE:
247 File file = new File(images, getCurrentImageBestName(false));
d66deb8d 248 Instance.getInstance().getCache().saveAsImage(para.getContentImage(), file, nextParaIsCover);
ecfb936e 249 writer.write(" <img alt='page image' class='page-image' src='images/"
08fe2e33
NR
250 + getCurrentImageBestName(false) + "'/>");
251 break;
252 }
16a81ef7
NR
253
254 nextParaIsCover = false;
08fe2e33
NR
255 }
256
257 @Override
258 protected void writeParagraphFooter(Paragraph para) throws IOException {
259 switch (para.getType()) {
260 case NORMAL:
261 writer.write("</span>\n");
262 break;
263 case QUOTE:
264 writer.write("</div>\n");
265 break;
266 default:
267 writer.write("\n");
268 break;
269 }
270 }
271
272 @Override
273 protected void writeTextLine(ParagraphType type, String line)
274 throws IOException {
275 switch (type) {
276 case QUOTE:
277 case NORMAL:
278 writer.write(decorateText(StringUtils.xmlEscape(line)));
279 break;
280 default:
281 break;
282 }
283 }
284
285 @Override
286 protected String enbold(String word) {
287 return "<strong>" + word + "</strong>";
288 }
289
290 @Override
291 protected String italize(String word) {
292 return "<emph>" + word + "</emph>";
293 }
294
295 private String generateNcx(Story story) {
296 StringBuilder builder = new StringBuilder();
297
298 String title = "";
299 String uuid = "";
300 String author = "";
301 if (story.getMeta() != null) {
302 MetaData meta = story.getMeta();
303 uuid = meta.getUuid();
304 author = meta.getAuthor();
305 title = meta.getTitle();
306 }
307
ecfb936e 308 builder.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
08fe2e33
NR
309 builder.append("\n<!DOCTYPE ncx");
310 builder.append("\nPUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">");
311 builder.append("\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">");
312 builder.append("\n <head>");
313 builder.append("\n <!--The following four metadata items are required for all");
314 builder.append("\n NCX documents, including those conforming to the relaxed");
315 builder.append("\n constraints of OPS 2.0-->");
316 builder.append("\n <meta name=\"dtb:uid\" content=\""
317 + StringUtils.xmlEscapeQuote(uuid) + "\"/>");
318 builder.append("\n <meta name=\"dtb:depth\" content=\"1\"/>");
319 builder.append("\n <meta name=\"dtb:totalPageCount\" content=\"0\"/>");
320 builder.append("\n <meta name=\"dtb:maxPageNumber\" content=\"0\"/>");
321 builder.append("\n <meta name=\"epub-creator\" content=\""
322 + StringUtils.xmlEscapeQuote(EPUB_CREATOR) + "\"/>");
323 builder.append("\n </head>");
324 builder.append("\n <docTitle>");
325 builder.append("\n <text>" + StringUtils.xmlEscape(title) + "</text>");
326 builder.append("\n </docTitle>");
327 builder.append("\n <docAuthor>");
328
329 builder.append("\n <text>" + StringUtils.xmlEscape(author) + "</text>");
330 builder.append("\n </docAuthor>");
331 builder.append("\n <navMap>");
332 builder.append("\n <navPoint id=\"navpoint-1\" playOrder=\"1\">");
333 builder.append("\n <navLabel>");
334 builder.append("\n <text>Title Page</text>");
335 builder.append("\n </navLabel>");
ecfb936e 336 builder.append("\n <content src=\"title.xhtml\"/>");
08fe2e33
NR
337 builder.append("\n </navPoint>");
338
339 int navPoint = 2; // 1 is above
340
341 if (story.getMeta() != null & story.getMeta().getResume() != null) {
342 Chapter chap = story.getMeta().getResume();
343 generateNcx(chap, builder, navPoint++);
344 }
345
346 for (Chapter chap : story) {
347 generateNcx(chap, builder, navPoint++);
348 }
349
350 builder.append("\n </navMap>");
351 builder.append("\n</ncx>\n");
352
353 return builder.toString();
354 }
355
356 private void generateNcx(Chapter chap, StringBuilder builder, int navPoint) {
357 String name;
358 if (chap.getName() != null && !chap.getName().isEmpty()) {
d66deb8d
NR
359 name = Instance.getInstance().getTrans().getString(StringId.CHAPTER_NAMED, chap.getNumber(),
360 chap.getName());
08fe2e33 361 } else {
d66deb8d 362 name = Instance.getInstance().getTrans().getString(StringId.CHAPTER_UNNAMED, chap.getNumber());
08fe2e33
NR
363 }
364
365 String nnn = String.format("%03d", (navPoint - 2));
366
367 builder.append("\n <navPoint id=\"navpoint-" + navPoint
368 + "\" playOrder=\"" + navPoint + "\">");
369 builder.append("\n <navLabel>");
370 builder.append("\n <text>" + name + "</text>");
371 builder.append("\n </navLabel>");
ecfb936e 372 builder.append("\n <content src=\"chapter-" + nnn + ".xhtml\"/>");
08fe2e33
NR
373 builder.append("\n </navPoint>\n");
374 }
375
376 private String generateOpf(Story story) {
377 StringBuilder builder = new StringBuilder();
378
379 String title = "";
380 String uuid = "";
381 String author = "";
382 String date = "";
383 String publisher = "";
384 String subject = "";
385 String source = "";
386 String lang = "";
387 if (story.getMeta() != null) {
388 MetaData meta = story.getMeta();
389 title = meta.getTitle();
390 uuid = meta.getUuid();
391 author = meta.getAuthor();
392 date = meta.getDate();
393 publisher = meta.getPublisher();
394 subject = meta.getSubject();
395 source = meta.getSource();
396 lang = meta.getLang();
397 }
398
ecfb936e
NR
399 builder.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
400 builder.append("\n<package xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"BookId\" version=\"2.0\">");
08fe2e33
NR
401 builder.append("\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\"");
402 builder.append("\n xmlns:dc=\"http://purl.org/dc/elements/1.1/\">");
403 builder.append("\n <dc:title>" + StringUtils.xmlEscape(title)
404 + "</dc:title>");
405 builder.append("\n <dc:creator opf:role=\"aut\" opf:file-as=\""
406 + StringUtils.xmlEscapeQuote(author) + "\">"
407 + StringUtils.xmlEscape(author) + "</dc:creator>");
408 builder.append("\n <dc:date opf:event=\"original-publication\">"
409 + StringUtils.xmlEscape(date) + "</dc:date>");
410 builder.append("\n <dc:publisher>"
411 + StringUtils.xmlEscape(publisher) + "</dc:publisher>");
412 builder.append("\n <dc:date opf:event=\"epub-publication\"></dc:date>");
413 builder.append("\n <dc:subject>" + StringUtils.xmlEscape(subject)
414 + "</dc:subject>");
415 builder.append("\n <dc:source>" + StringUtils.xmlEscape(source)
416 + "</dc:source>");
417 builder.append("\n <dc:rights>Not for commercial use.</dc:rights>");
ecfb936e 418 builder.append("\n <dc:identifier id=\"BookId\" opf:scheme=\"URI\">"
08fe2e33
NR
419 + StringUtils.xmlEscape(uuid) + "</dc:identifier>");
420 builder.append("\n <dc:language>" + StringUtils.xmlEscape(lang)
421 + "</dc:language>");
422 builder.append("\n </metadata>");
423 builder.append("\n <manifest>");
424 builder.append("\n <!-- Content Documents -->");
ecfb936e 425 builder.append("\n <item id=\"titlepage\" href=\"title.xhtml\" media-type=\"application/xhtml+xml\"/>");
08fe2e33
NR
426 for (int i = 0; i <= story.getChapters().size(); i++) {
427 String name = String.format("%s%03d", "chapter-", i);
428 builder.append("\n <item id=\""
429 + StringUtils.xmlEscapeQuote(name) + "\" href=\""
430 + StringUtils.xmlEscapeQuote(name)
ecfb936e 431 + ".xhtml\" media-type=\"application/xhtml+xml\"/>");
08fe2e33
NR
432 }
433
434 builder.append("\n <!-- CSS Style Sheets -->");
435 builder.append("\n <item id=\"style-css\" href=\"css/style.css\" media-type=\"text/css\"/>");
436
437 builder.append("\n <!-- Images -->");
438
439 if (story.getMeta() != null && story.getMeta().getCover() != null) {
d66deb8d 440 String format = Instance.getInstance().getConfig()
43c75fb7
NR
441 .getString(Config.FILE_FORMAT_IMAGE_FORMAT_COVER)
442 .toLowerCase();
08fe2e33
NR
443 builder.append("\n <item id=\"cover\" href=\"images/cover."
444 + format + "\" media-type=\"image/png\"/>");
445 }
446
447 builder.append("\n <!-- NCX -->");
448 builder.append("\n <item id=\"ncx\" href=\"epb.ncx\" media-type=\"application/x-dtbncx+xml\"/>");
449 builder.append("\n </manifest>");
450 builder.append("\n <spine toc=\"ncx\">");
451 builder.append("\n <itemref idref=\"titlepage\" linear=\"yes\"/>");
452 for (int i = 0; i <= story.getChapters().size(); i++) {
453 String name = String.format("%s%03d", "chapter-", i);
454 builder.append("\n <itemref idref=\""
455 + StringUtils.xmlEscapeQuote(name) + "\" linear=\"yes\"/>");
456 }
457 builder.append("\n </spine>");
458 builder.append("\n</package>\n");
459
460 return builder.toString();
461 }
462
463 private String generateTitleXml(Story story) {
464 StringBuilder builder = new StringBuilder();
465
466 String title = "";
467 String tags = "";
468 String author = "";
469 if (story.getMeta() != null) {
470 MetaData meta = story.getMeta();
471 title = meta.getTitle();
472 if (meta.getTags() != null) {
473 for (String tag : meta.getTags()) {
474 if (!tags.isEmpty()) {
475 tags += ", ";
476 }
477 tags += tag;
478 }
479
480 if (!tags.isEmpty()) {
481 tags = "(" + tags + ")";
482 }
483 }
484 author = meta.getAuthor();
485 }
486
d66deb8d 487 String format = Instance.getInstance().getConfig()
13fdb89a 488 .getString(Config.FILE_FORMAT_IMAGE_FORMAT_COVER).toLowerCase();
08fe2e33 489
ecfb936e
NR
490 builder.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
491 builder.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
492 builder.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\">");
08fe2e33
NR
493 builder.append("\n<head>");
494 builder.append("\n <title>" + StringUtils.xmlEscape(title) + "</title>");
495 builder.append("\n <link rel=\"stylesheet\" href=\"css/style.css\" type=\"text/css\"/>");
496 builder.append("\n</head>");
497 builder.append("\n<body>");
498 builder.append("\n <div class=\"titlepage\">");
499 builder.append("\n <h1>" + StringUtils.xmlEscape(title) + "</h1>");
500 builder.append("\n <div class=\"type\">"
501 + StringUtils.xmlEscape(tags) + "</div>");
502 builder.append("\n <div class=\"cover\">");
ecfb936e
NR
503 builder.append("\n <img alt=\"cover image\" src=\"images/cover."
504 + format + "\"></img>");
08fe2e33
NR
505 builder.append("\n </div>");
506 builder.append("\n <div class=\"author\">"
507 + StringUtils.xmlEscape(author) + "</div>");
508 builder.append("\n </div>");
509 builder.append("\n</body>");
510 builder.append("\n</html>\n");
511
512 return builder.toString();
513 }
514}