Commit | Line | Data |
---|---|---|
08fe2e33 NR |
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 | |
2206ef66 | 61 | public String getDefaultExtension() { |
08fe2e33 NR |
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'>— "); | |
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 | } |