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