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