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