1 package be
.nikiroo
.fanfix
.output
;
3 import java
.io
.BufferedWriter
;
5 import java
.io
.FileInputStream
;
6 import java
.io
.FileOutputStream
;
7 import java
.io
.IOException
;
8 import java
.io
.InputStream
;
9 import java
.io
.OutputStream
;
10 import java
.io
.OutputStreamWriter
;
11 import java
.util
.zip
.ZipEntry
;
12 import java
.util
.zip
.ZipOutputStream
;
14 import be
.nikiroo
.fanfix
.Instance
;
15 import be
.nikiroo
.fanfix
.bundles
.Config
;
16 import be
.nikiroo
.fanfix
.bundles
.StringId
;
17 import be
.nikiroo
.fanfix
.data
.Chapter
;
18 import be
.nikiroo
.fanfix
.data
.MetaData
;
19 import be
.nikiroo
.fanfix
.data
.Paragraph
;
20 import be
.nikiroo
.fanfix
.data
.Paragraph
.ParagraphType
;
21 import be
.nikiroo
.fanfix
.data
.Story
;
22 import be
.nikiroo
.utils
.IOUtils
;
23 import be
.nikiroo
.utils
.StringUtils
;
25 class Epub
extends BasicOutput
{
27 private BufferedWriter writer
;
28 private boolean inDialogue
= false;
29 private boolean inNormal
= false;
31 private boolean nextParaIsCover
= true;
34 public File
process(Story story
, File targetDir
, String targetName
)
36 String targetNameOrig
= targetName
;
37 targetName
+= getDefaultExtension(false);
39 tmpDir
= Instance
.getInstance().getTempFiles().createTempDir("fanfic-reader-epub");
42 if (!tmpDir
.mkdir()) {
43 throw new IOException(
44 "Cannot create a temporary directory: no space left on device?");
47 super.process(story
, targetDir
, targetNameOrig
);
52 File data
= new File(tmpDir
, "DATA");
54 BasicOutput
.getOutput(OutputType
.TEXT
, isWriteInfo(),
55 isWriteCover()).process(story
, data
, targetNameOrig
);
56 InfoCover
.writeInfo(data
, targetNameOrig
, story
.getMeta());
57 IOUtils
.writeSmallFile(data
, "version", "3.0");
60 epub
= new File(targetDir
, targetName
);
61 IOUtils
.zip(tmpDir
, epub
, true);
63 OutputStream out
= new FileOutputStream(epub
);
65 ZipOutputStream zip
= new ZipOutputStream(out
);
67 // "mimetype" MUST be the first element and not compressed
68 zip
.setLevel(ZipOutputStream
.STORED
);
69 File mimetype
= new File(tmpDir
, "mimetype");
70 IOUtils
.writeSmallFile(tmpDir
, "mimetype",
71 "application/epub+zip");
72 ZipEntry entry
= new ZipEntry("mimetype");
73 entry
.setExtra(new byte[] {});
74 zip
.putNextEntry(entry
);
75 FileInputStream in
= new FileInputStream(mimetype
);
77 IOUtils
.write(in
, zip
);
81 IOUtils
.deltree(mimetype
);
82 zip
.setLevel(ZipOutputStream
.DEFLATED
);
85 IOUtils
.zip(zip
, "", tmpDir
, true);
93 IOUtils
.deltree(tmpDir
);
101 public String
getDefaultExtension(boolean readerTarget
) {
106 protected void writeStoryHeader(Story story
) throws IOException
{
107 File ops
= new File(tmpDir
, "OPS");
109 File css
= new File(ops
, "css");
111 images
= new File(ops
, "images");
113 File metaInf
= new File(tmpDir
, "META-INF");
117 String containerContent
= "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
118 + "<container xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\" version=\"1.0\">\n"
120 + "\t\t<rootfile full-path=\"OPS/epb.opf\" media-type=\"application/oebps-package+xml\"/>\n"
121 + "\t</rootfiles>\n" + "</container>\n";
123 IOUtils
.writeSmallFile(metaInf
, "container.xml", containerContent
);
126 InputStream inStyle
= getClass().getResourceAsStream("epub.style.css");
127 if (inStyle
== null) {
128 throw new IOException("Cannot find style.css resource");
131 IOUtils
.write(inStyle
, new File(css
, "style.css"));
137 if (story
.getMeta() != null && story
.getMeta().getCover() != null) {
138 File file
= new File(images
, "cover");
140 Instance
.getInstance().getCache().saveAsImage(story
.getMeta().getCover(), file
, true);
141 } catch (Exception e
) {
142 Instance
.getInstance().getTraceHandler().error(e
);
146 // OPS/* except chapters
147 IOUtils
.writeSmallFile(ops
, "epb.ncx", generateNcx(story
));
148 IOUtils
.writeSmallFile(ops
, "epb.opf", generateOpf(story
));
149 IOUtils
.writeSmallFile(ops
, "title.xhtml", generateTitleXml(story
));
152 if (story
.getMeta() != null && story
.getMeta().getResume() != null) {
153 writeChapter(story
.getMeta().getResume());
158 protected void writeChapterHeader(Chapter chap
) throws IOException
{
159 String filename
= String
.format("%s%03d%s", "chapter-",
160 chap
.getNumber(), ".xhtml");
161 writer
= new BufferedWriter(new OutputStreamWriter(
162 new FileOutputStream(new File(tmpDir
+ File
.separator
+ "OPS",
163 filename
)), "UTF-8"));
167 String title
= "Chapter " + chap
.getNumber();
168 String nameOrNum
= Integer
.toString(chap
.getNumber());
169 if (chap
.getName() != null && !chap
.getName().isEmpty()) {
170 title
+= ": " + chap
.getName();
171 nameOrNum
= chap
.getName();
174 writer
.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
175 writer
.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
176 writer
.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\">");
177 writer
.write("\n<head>");
178 writer
.write("\n <title>" + StringUtils
.xmlEscape(title
)
180 writer
.write("\n <link rel='stylesheet' href='css/style.css' type='text/css'/>");
181 writer
.write("\n</head>");
182 writer
.write("\n<body>");
183 writer
.write("\n <h2>");
184 writer
.write("\n <span class='chap'>Chapter <span class='chapnumber'>"
185 + chap
.getNumber() + "</span>:</span> ");
186 writer
.write("\n <span class='chaptitle'>"
187 + StringUtils
.xmlEscape(nameOrNum
) + "</span>");
188 writer
.write("\n </h2>");
190 writer
.write("\n <div class='chapter_content'>\n");
191 } catch (Exception e
) {
193 throw new IOException(e
);
198 protected void writeChapterFooter(Chapter chap
) throws IOException
{
201 writer
.write(" </div>\n");
205 writer
.write(" </div>\n");
208 writer
.write(" </div>\n</body>\n</html>\n");
216 protected void writeParagraphHeader(Paragraph para
) throws IOException
{
217 if (para
.getType() == ParagraphType
.QUOTE
&& !inDialogue
) {
218 writer
.write(" <div class='dialogues'>\n");
220 } else if (para
.getType() != ParagraphType
.QUOTE
&& inDialogue
) {
221 writer
.write(" </div>\n");
225 if (para
.getType() == ParagraphType
.NORMAL
&& !inNormal
) {
226 writer
.write(" <div class='normals'>\n");
228 } else if (para
.getType() != ParagraphType
.NORMAL
&& inNormal
) {
229 writer
.write(" </div>\n");
233 switch (para
.getType()) {
235 writer
.write(" <div class='blank'></div>");
238 writer
.write(" <hr class='break'/>");
241 writer
.write(" <span class='normal'>");
244 writer
.write(" <div class='dialogue'>— ");
247 File file
= new File(images
, getCurrentImageBestName(false));
248 Instance
.getInstance().getCache().saveAsImage(para
.getContentImage(), file
, nextParaIsCover
);
249 writer
.write(" <img alt='page image' class='page-image' src='images/"
250 + getCurrentImageBestName(false) + "'/>");
254 nextParaIsCover
= false;
258 protected void writeParagraphFooter(Paragraph para
) throws IOException
{
259 switch (para
.getType()) {
261 writer
.write("</span>\n");
264 writer
.write("</div>\n");
273 protected void writeTextLine(ParagraphType type
, String line
)
278 writer
.write(decorateText(StringUtils
.xmlEscape(line
)));
286 protected String
enbold(String word
) {
287 return "<strong>" + word
+ "</strong>";
291 protected String
italize(String word
) {
292 return "<emph>" + word
+ "</emph>";
295 private String
generateNcx(Story story
) {
296 StringBuilder builder
= new StringBuilder();
301 if (story
.getMeta() != null) {
302 MetaData meta
= story
.getMeta();
303 uuid
= meta
.getUuid();
304 author
= meta
.getAuthor();
305 title
= meta
.getTitle();
308 builder
.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
309 builder
.append("\n<!DOCTYPE ncx");
310 builder
.append("\nPUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">");
311 builder
.append("\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">");
312 builder
.append("\n <head>");
313 builder
.append("\n <!--The following four metadata items are required for all");
314 builder
.append("\n NCX documents, including those conforming to the relaxed");
315 builder
.append("\n constraints of OPS 2.0-->");
316 builder
.append("\n <meta name=\"dtb:uid\" content=\""
317 + StringUtils
.xmlEscapeQuote(uuid
) + "\"/>");
318 builder
.append("\n <meta name=\"dtb:depth\" content=\"1\"/>");
319 builder
.append("\n <meta name=\"dtb:totalPageCount\" content=\"0\"/>");
320 builder
.append("\n <meta name=\"dtb:maxPageNumber\" content=\"0\"/>");
321 builder
.append("\n <meta name=\"epub-creator\" content=\""
322 + StringUtils
.xmlEscapeQuote(EPUB_CREATOR
) + "\"/>");
323 builder
.append("\n </head>");
324 builder
.append("\n <docTitle>");
325 builder
.append("\n <text>" + StringUtils
.xmlEscape(title
) + "</text>");
326 builder
.append("\n </docTitle>");
327 builder
.append("\n <docAuthor>");
329 builder
.append("\n <text>" + StringUtils
.xmlEscape(author
) + "</text>");
330 builder
.append("\n </docAuthor>");
331 builder
.append("\n <navMap>");
332 builder
.append("\n <navPoint id=\"navpoint-1\" playOrder=\"1\">");
333 builder
.append("\n <navLabel>");
334 builder
.append("\n <text>Title Page</text>");
335 builder
.append("\n </navLabel>");
336 builder
.append("\n <content src=\"title.xhtml\"/>");
337 builder
.append("\n </navPoint>");
339 int navPoint
= 2; // 1 is above
341 if (story
.getMeta() != null & story
.getMeta().getResume() != null) {
342 Chapter chap
= story
.getMeta().getResume();
343 generateNcx(chap
, builder
, navPoint
++);
346 for (Chapter chap
: story
) {
347 generateNcx(chap
, builder
, navPoint
++);
350 builder
.append("\n </navMap>");
351 builder
.append("\n</ncx>\n");
353 return builder
.toString();
356 private void generateNcx(Chapter chap
, StringBuilder builder
, int navPoint
) {
358 if (chap
.getName() != null && !chap
.getName().isEmpty()) {
359 name
= Instance
.getInstance().getTrans().getString(StringId
.CHAPTER_NAMED
, chap
.getNumber(),
362 name
= Instance
.getInstance().getTrans().getString(StringId
.CHAPTER_UNNAMED
, chap
.getNumber());
365 String nnn
= String
.format("%03d", (navPoint
- 2));
367 builder
.append("\n <navPoint id=\"navpoint-" + navPoint
368 + "\" playOrder=\"" + navPoint
+ "\">");
369 builder
.append("\n <navLabel>");
370 builder
.append("\n <text>" + name
+ "</text>");
371 builder
.append("\n </navLabel>");
372 builder
.append("\n <content src=\"chapter-" + nnn
+ ".xhtml\"/>");
373 builder
.append("\n </navPoint>\n");
376 private String
generateOpf(Story story
) {
377 StringBuilder builder
= new StringBuilder();
383 String publisher
= "";
387 if (story
.getMeta() != null) {
388 MetaData meta
= story
.getMeta();
389 title
= meta
.getTitle();
390 uuid
= meta
.getUuid();
391 author
= meta
.getAuthor();
392 date
= meta
.getDate();
393 publisher
= meta
.getPublisher();
394 subject
= meta
.getSubject();
395 source
= meta
.getSource();
396 lang
= meta
.getLang();
399 builder
.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
400 builder
.append("\n<package xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\"BookId\" version=\"2.0\">");
401 builder
.append("\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\"");
402 builder
.append("\n xmlns:dc=\"http://purl.org/dc/elements/1.1/\">");
403 builder
.append("\n <dc:title>" + StringUtils
.xmlEscape(title
)
405 builder
.append("\n <dc:creator opf:role=\"aut\" opf:file-as=\""
406 + StringUtils
.xmlEscapeQuote(author
) + "\">"
407 + StringUtils
.xmlEscape(author
) + "</dc:creator>");
408 builder
.append("\n <dc:date opf:event=\"original-publication\">"
409 + StringUtils
.xmlEscape(date
) + "</dc:date>");
410 builder
.append("\n <dc:publisher>"
411 + StringUtils
.xmlEscape(publisher
) + "</dc:publisher>");
412 builder
.append("\n <dc:date opf:event=\"epub-publication\"></dc:date>");
413 builder
.append("\n <dc:subject>" + StringUtils
.xmlEscape(subject
)
415 builder
.append("\n <dc:source>" + StringUtils
.xmlEscape(source
)
417 builder
.append("\n <dc:rights>Not for commercial use.</dc:rights>");
418 builder
.append("\n <dc:identifier id=\"BookId\" opf:scheme=\"URI\">"
419 + StringUtils
.xmlEscape(uuid
) + "</dc:identifier>");
420 builder
.append("\n <dc:language>" + StringUtils
.xmlEscape(lang
)
422 builder
.append("\n </metadata>");
423 builder
.append("\n <manifest>");
424 builder
.append("\n <!-- Content Documents -->");
425 builder
.append("\n <item id=\"titlepage\" href=\"title.xhtml\" media-type=\"application/xhtml+xml\"/>");
426 for (int i
= 0; i
<= story
.getChapters().size(); i
++) {
427 String name
= String
.format("%s%03d", "chapter-", i
);
428 builder
.append("\n <item id=\""
429 + StringUtils
.xmlEscapeQuote(name
) + "\" href=\""
430 + StringUtils
.xmlEscapeQuote(name
)
431 + ".xhtml\" media-type=\"application/xhtml+xml\"/>");
434 builder
.append("\n <!-- CSS Style Sheets -->");
435 builder
.append("\n <item id=\"style-css\" href=\"css/style.css\" media-type=\"text/css\"/>");
437 builder
.append("\n <!-- Images -->");
439 if (story
.getMeta() != null && story
.getMeta().getCover() != null) {
440 String format
= Instance
.getInstance().getConfig()
441 .getString(Config
.FILE_FORMAT_IMAGE_FORMAT_COVER
)
443 builder
.append("\n <item id=\"cover\" href=\"images/cover."
444 + format
+ "\" media-type=\"image/png\"/>");
447 builder
.append("\n <!-- NCX -->");
448 builder
.append("\n <item id=\"ncx\" href=\"epb.ncx\" media-type=\"application/x-dtbncx+xml\"/>");
449 builder
.append("\n </manifest>");
450 builder
.append("\n <spine toc=\"ncx\">");
451 builder
.append("\n <itemref idref=\"titlepage\" linear=\"yes\"/>");
452 for (int i
= 0; i
<= story
.getChapters().size(); i
++) {
453 String name
= String
.format("%s%03d", "chapter-", i
);
454 builder
.append("\n <itemref idref=\""
455 + StringUtils
.xmlEscapeQuote(name
) + "\" linear=\"yes\"/>");
457 builder
.append("\n </spine>");
458 builder
.append("\n</package>\n");
460 return builder
.toString();
463 private String
generateTitleXml(Story story
) {
464 StringBuilder builder
= new StringBuilder();
469 if (story
.getMeta() != null) {
470 MetaData meta
= story
.getMeta();
471 title
= meta
.getTitle();
472 if (meta
.getTags() != null) {
473 for (String tag
: meta
.getTags()) {
474 if (!tags
.isEmpty()) {
480 if (!tags
.isEmpty()) {
481 tags
= "(" + tags
+ ")";
484 author
= meta
.getAuthor();
487 String format
= Instance
.getInstance().getConfig()
488 .getString(Config
.FILE_FORMAT_IMAGE_FORMAT_COVER
).toLowerCase();
490 builder
.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
491 builder
.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\" \"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">");
492 builder
.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\">");
493 builder
.append("\n<head>");
494 builder
.append("\n <title>" + StringUtils
.xmlEscape(title
) + "</title>");
495 builder
.append("\n <link rel=\"stylesheet\" href=\"css/style.css\" type=\"text/css\"/>");
496 builder
.append("\n</head>");
497 builder
.append("\n<body>");
498 builder
.append("\n <div class=\"titlepage\">");
499 builder
.append("\n <h1>" + StringUtils
.xmlEscape(title
) + "</h1>");
500 builder
.append("\n <div class=\"type\">"
501 + StringUtils
.xmlEscape(tags
) + "</div>");
502 builder
.append("\n <div class=\"cover\">");
503 builder
.append("\n <img alt=\"cover image\" src=\"images/cover."
504 + format
+ "\"></img>");
505 builder
.append("\n </div>");
506 builder
.append("\n <div class=\"author\">"
507 + StringUtils
.xmlEscape(author
) + "</div>");
508 builder
.append("\n </div>");
509 builder
.append("\n</body>");
510 builder
.append("\n</html>\n");
512 return builder
.toString();