Remove or move java.awt dependencies
[nikiroo-utils.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
10 import be.nikiroo.fanfix.Instance;
11 import be.nikiroo.fanfix.bundles.Config;
12 import be.nikiroo.fanfix.bundles.StringId;
13 import be.nikiroo.fanfix.data.Chapter;
14 import be.nikiroo.fanfix.data.MetaData;
15 import be.nikiroo.fanfix.data.Paragraph;
16 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
17 import be.nikiroo.fanfix.data.Story;
18 import be.nikiroo.utils.IOUtils;
19 import be.nikiroo.utils.StringUtils;
20
21 class Epub extends BasicOutput {
22 private File tmpDir;
23 private BufferedWriter writer;
24 private boolean inDialogue = false;
25 private boolean inNormal = false;
26 private File images;
27 private boolean nextParaIsCover = true;
28
29 @Override
30 public File process(Story story, File targetDir, String targetName)
31 throws IOException {
32 String targetNameOrig = targetName;
33 targetName += getDefaultExtension(false);
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 BasicOutput.getOutput(OutputType.TEXT, isWriteInfo(), isWriteCover())
47 .process(story, data, targetNameOrig);
48 InfoCover.writeInfo(data, targetNameOrig, story.getMeta());
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 Instance.getCache().saveAsImage(story.getMeta().getCover(), file,
107 true);
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(para.getContentImage(), file,
213 nextParaIsCover);
214 writer.write(" <img class='page-image' src='images/"
215 + getCurrentImageBestName(false) + "'/>");
216 break;
217 }
218
219 nextParaIsCover = false;
220 }
221
222 @Override
223 protected void writeParagraphFooter(Paragraph para) throws IOException {
224 switch (para.getType()) {
225 case NORMAL:
226 writer.write("</span>\n");
227 break;
228 case QUOTE:
229 writer.write("</div>\n");
230 break;
231 default:
232 writer.write("\n");
233 break;
234 }
235 }
236
237 @Override
238 protected void writeTextLine(ParagraphType type, String line)
239 throws IOException {
240 switch (type) {
241 case QUOTE:
242 case NORMAL:
243 writer.write(decorateText(StringUtils.xmlEscape(line)));
244 break;
245 default:
246 break;
247 }
248 }
249
250 @Override
251 protected String enbold(String word) {
252 return "<strong>" + word + "</strong>";
253 }
254
255 @Override
256 protected String italize(String word) {
257 return "<emph>" + word + "</emph>";
258 }
259
260 private String generateNcx(Story story) {
261 StringBuilder builder = new StringBuilder();
262
263 String title = "";
264 String uuid = "";
265 String author = "";
266 if (story.getMeta() != null) {
267 MetaData meta = story.getMeta();
268 uuid = meta.getUuid();
269 author = meta.getAuthor();
270 title = meta.getTitle();
271 }
272
273 builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
274 builder.append("\n<!DOCTYPE ncx");
275 builder.append("\nPUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">");
276 builder.append("\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">");
277 builder.append("\n <head>");
278 builder.append("\n <!--The following four metadata items are required for all");
279 builder.append("\n NCX documents, including those conforming to the relaxed");
280 builder.append("\n constraints of OPS 2.0-->");
281 builder.append("\n <meta name=\"dtb:uid\" content=\""
282 + StringUtils.xmlEscapeQuote(uuid) + "\"/>");
283 builder.append("\n <meta name=\"dtb:depth\" content=\"1\"/>");
284 builder.append("\n <meta name=\"dtb:totalPageCount\" content=\"0\"/>");
285 builder.append("\n <meta name=\"dtb:maxPageNumber\" content=\"0\"/>");
286 builder.append("\n <meta name=\"epub-creator\" content=\""
287 + StringUtils.xmlEscapeQuote(EPUB_CREATOR) + "\"/>");
288 builder.append("\n </head>");
289 builder.append("\n <docTitle>");
290 builder.append("\n <text>" + StringUtils.xmlEscape(title) + "</text>");
291 builder.append("\n </docTitle>");
292 builder.append("\n <docAuthor>");
293
294 builder.append("\n <text>" + StringUtils.xmlEscape(author) + "</text>");
295 builder.append("\n </docAuthor>");
296 builder.append("\n <navMap>");
297 builder.append("\n <navPoint id=\"navpoint-1\" playOrder=\"1\">");
298 builder.append("\n <navLabel>");
299 builder.append("\n <text>Title Page</text>");
300 builder.append("\n </navLabel>");
301 builder.append("\n <content src=\"title.xml\"/>");
302 builder.append("\n </navPoint>");
303
304 int navPoint = 2; // 1 is above
305
306 if (story.getMeta() != null & story.getMeta().getResume() != null) {
307 Chapter chap = story.getMeta().getResume();
308 generateNcx(chap, builder, navPoint++);
309 }
310
311 for (Chapter chap : story) {
312 generateNcx(chap, builder, navPoint++);
313 }
314
315 builder.append("\n </navMap>");
316 builder.append("\n</ncx>\n");
317
318 return builder.toString();
319 }
320
321 private void generateNcx(Chapter chap, StringBuilder builder, int navPoint) {
322 String name;
323 if (chap.getName() != null && !chap.getName().isEmpty()) {
324 name = Instance.getTrans().getString(StringId.CHAPTER_NAMED,
325 chap.getNumber(), chap.getName());
326 } else {
327 name = Instance.getTrans().getString(StringId.CHAPTER_UNNAMED,
328 chap.getNumber());
329 }
330
331 String nnn = String.format("%03d", (navPoint - 2));
332
333 builder.append("\n <navPoint id=\"navpoint-" + navPoint
334 + "\" playOrder=\"" + navPoint + "\">");
335 builder.append("\n <navLabel>");
336 builder.append("\n <text>" + name + "</text>");
337 builder.append("\n </navLabel>");
338 builder.append("\n <content src=\"chapter-" + nnn + ".xml\"/>");
339 builder.append("\n </navPoint>\n");
340 }
341
342 private String generateOpf(Story story) {
343 StringBuilder builder = new StringBuilder();
344
345 String title = "";
346 String uuid = "";
347 String author = "";
348 String date = "";
349 String publisher = "";
350 String subject = "";
351 String source = "";
352 String lang = "";
353 if (story.getMeta() != null) {
354 MetaData meta = story.getMeta();
355 title = meta.getTitle();
356 uuid = meta.getUuid();
357 author = meta.getAuthor();
358 date = meta.getDate();
359 publisher = meta.getPublisher();
360 subject = meta.getSubject();
361 source = meta.getSource();
362 lang = meta.getLang();
363 }
364
365 builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
366 builder.append("\n<package xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\""
367 + uuid + "\" version=\"2.0\">");
368 builder.append("\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\"");
369 builder.append("\n xmlns:dc=\"http://purl.org/dc/elements/1.1/\">");
370 builder.append("\n <dc:title>" + StringUtils.xmlEscape(title)
371 + "</dc:title>");
372 builder.append("\n <dc:creator opf:role=\"aut\" opf:file-as=\""
373 + StringUtils.xmlEscapeQuote(author) + "\">"
374 + StringUtils.xmlEscape(author) + "</dc:creator>");
375 builder.append("\n <dc:date opf:event=\"original-publication\">"
376 + StringUtils.xmlEscape(date) + "</dc:date>");
377 builder.append("\n <dc:publisher>"
378 + StringUtils.xmlEscape(publisher) + "</dc:publisher>");
379 builder.append("\n <dc:date opf:event=\"epub-publication\"></dc:date>");
380 builder.append("\n <dc:subject>" + StringUtils.xmlEscape(subject)
381 + "</dc:subject>");
382 builder.append("\n <dc:source>" + StringUtils.xmlEscape(source)
383 + "</dc:source>");
384 builder.append("\n <dc:rights>Not for commercial use.</dc:rights>");
385 builder.append("\n <dc:identifier id=\"id\" opf:scheme=\"URI\">"
386 + StringUtils.xmlEscape(uuid) + "</dc:identifier>");
387 builder.append("\n <dc:language>" + StringUtils.xmlEscape(lang)
388 + "</dc:language>");
389 builder.append("\n </metadata>");
390 builder.append("\n <manifest>");
391 builder.append("\n <!-- Content Documents -->");
392 builder.append("\n <item id=\"titlepage\" href=\"title.xml\" media-type=\"application/xhtml+xml\"/>");
393 for (int i = 0; i <= story.getChapters().size(); i++) {
394 String name = String.format("%s%03d", "chapter-", i);
395 builder.append("\n <item id=\""
396 + StringUtils.xmlEscapeQuote(name) + "\" href=\""
397 + StringUtils.xmlEscapeQuote(name)
398 + ".xml\" media-type=\"application/xhtml+xml\"/>");
399 }
400
401 builder.append("\n <!-- CSS Style Sheets -->");
402 builder.append("\n <item id=\"style-css\" href=\"css/style.css\" media-type=\"text/css\"/>");
403
404 builder.append("\n <!-- Images -->");
405
406 if (story.getMeta() != null && story.getMeta().getCover() != null) {
407 String format = Instance.getConfig()
408 .getString(Config.IMAGE_FORMAT_COVER).toLowerCase();
409 builder.append("\n <item id=\"cover\" href=\"images/cover."
410 + format + "\" media-type=\"image/png\"/>");
411 }
412
413 builder.append("\n <!-- NCX -->");
414 builder.append("\n <item id=\"ncx\" href=\"epb.ncx\" media-type=\"application/x-dtbncx+xml\"/>");
415 builder.append("\n </manifest>");
416 builder.append("\n <spine toc=\"ncx\">");
417 builder.append("\n <itemref idref=\"titlepage\" linear=\"yes\"/>");
418 for (int i = 0; i <= story.getChapters().size(); i++) {
419 String name = String.format("%s%03d", "chapter-", i);
420 builder.append("\n <itemref idref=\""
421 + StringUtils.xmlEscapeQuote(name) + "\" linear=\"yes\"/>");
422 }
423 builder.append("\n </spine>");
424 builder.append("\n</package>\n");
425
426 return builder.toString();
427 }
428
429 private String generateTitleXml(Story story) {
430 StringBuilder builder = new StringBuilder();
431
432 String title = "";
433 String tags = "";
434 String author = "";
435 if (story.getMeta() != null) {
436 MetaData meta = story.getMeta();
437 title = meta.getTitle();
438 if (meta.getTags() != null) {
439 for (String tag : meta.getTags()) {
440 if (!tags.isEmpty()) {
441 tags += ", ";
442 }
443 tags += tag;
444 }
445
446 if (!tags.isEmpty()) {
447 tags = "(" + tags + ")";
448 }
449 }
450 author = meta.getAuthor();
451 }
452
453 String format = Instance.getConfig()
454 .getString(Config.IMAGE_FORMAT_COVER).toLowerCase();
455
456 builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
457 builder.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd \">");
458 builder.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">");
459 builder.append("\n<head>");
460 builder.append("\n <title>" + StringUtils.xmlEscape(title) + "</title>");
461 builder.append("\n <link rel=\"stylesheet\" href=\"css/style.css\" type=\"text/css\"/>");
462 builder.append("\n</head>");
463 builder.append("\n<body>");
464 builder.append("\n <div class=\"titlepage\">");
465 builder.append("\n <h1>" + StringUtils.xmlEscape(title) + "</h1>");
466 builder.append("\n <div class=\"type\">"
467 + StringUtils.xmlEscape(tags) + "</div>");
468 builder.append("\n <div class=\"cover\">");
469 builder.append("\n <img src=\"images/cover." + format + "\"></img>");
470 builder.append("\n </div>");
471 builder.append("\n <div class=\"author\">"
472 + StringUtils.xmlEscape(author) + "</div>");
473 builder.append("\n </div>");
474 builder.append("\n</body>");
475 builder.append("\n</html>\n");
476
477 return builder.toString();
478 }
479 }