1 package be
.nikiroo
.fanfix
.output
;
3 import java
.io
.BufferedWriter
;
5 import java
.io
.FileOutputStream
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.io
.OutputStreamWriter
;
11 import javax
.imageio
.ImageIO
;
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
;
24 class Epub
extends BasicOutput
{
26 private BufferedWriter writer
;
27 private boolean inDialogue
= false;
28 private boolean inNormal
= false;
32 public File
process(Story story
, File targetDir
, String targetName
)
34 String targetNameOrig
= targetName
;
35 targetName
+= getDefaultExtension(false);
37 tmpDir
= File
.createTempFile("fanfic-reader-epub_", ".wip");
40 if (!tmpDir
.mkdir()) {
41 throw new IOException(
42 "Cannot create a temporary directory: no space left on device?");
46 File data
= new File(tmpDir
, "DATA");
48 new InfoText().process(story
, data
, targetNameOrig
);
49 InfoCover
.writeInfo(data
, targetNameOrig
, story
.getMeta());
50 IOUtils
.writeSmallFile(data
, "version", "3.0");
52 super.process(story
, targetDir
, targetNameOrig
);
55 File epub
= new File(targetDir
, targetName
);
56 IOUtils
.zip(tmpDir
, epub
, true);
57 IOUtils
.deltree(tmpDir
);
64 public String
getDefaultExtension(boolean readerTarget
) {
69 protected void writeStoryHeader(Story story
) throws IOException
{
70 File ops
= new File(tmpDir
, "OPS");
72 File css
= new File(ops
, "css");
74 images
= new File(ops
, "images");
76 File metaInf
= new File(tmpDir
, "META-INF");
80 IOUtils
.writeSmallFile(tmpDir
, "mimetype", "application/epub+zip");
83 String containerContent
= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
84 + "<container xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\" version=\"1.0\">\n"
86 + "\t\t<rootfile full-path=\"OPS/epb.opf\" media-type=\"application/oebps-package+xml\"/>\n"
87 + "\t</rootfiles>\n" + "</container>\n";
89 IOUtils
.writeSmallFile(metaInf
, "container.xml", containerContent
);
92 InputStream inStyle
= getClass().getResourceAsStream("epub.style.css");
93 if (inStyle
== null) {
94 throw new IOException("Cannot find style.css resource");
97 IOUtils
.write(inStyle
, new File(css
, "style.css"));
103 if (story
.getMeta() != null && story
.getMeta().getCover() != null) {
104 String format
= Instance
.getConfig()
105 .getString(Config
.IMAGE_FORMAT_COVER
).toLowerCase();
106 File file
= new File(images
, "cover." + format
);
107 ImageIO
.write(story
.getMeta().getCover(), format
, file
);
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
));
116 if (story
.getMeta() != null && story
.getMeta().getResume() != null) {
117 writeChapter(story
.getMeta().getResume());
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"));
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();
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
)
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>");
154 writer
.write("\n <div class='chapter_content'>\n");
155 } catch (Exception e
) {
157 throw new IOException(e
);
162 protected void writeChapterFooter(Chapter chap
) throws IOException
{
165 writer
.write(" </div>\n");
169 writer
.write(" </div>\n");
172 writer
.write(" </div>\n</body>\n</html>\n");
180 protected void writeParagraphHeader(Paragraph para
) throws IOException
{
181 if (para
.getType() == ParagraphType
.QUOTE
&& !inDialogue
) {
182 writer
.write(" <div class='dialogues'>\n");
184 } else if (para
.getType() != ParagraphType
.QUOTE
&& inDialogue
) {
185 writer
.write(" </div>\n");
189 if (para
.getType() == ParagraphType
.NORMAL
&& !inNormal
) {
190 writer
.write(" <div class='normals'>\n");
192 } else if (para
.getType() != ParagraphType
.NORMAL
&& inNormal
) {
193 writer
.write(" </div>\n");
197 switch (para
.getType()) {
199 writer
.write(" <div class='blank'></div>");
202 writer
.write(" <hr class='break'/>");
205 writer
.write(" <span class='normal'>");
208 writer
.write(" <div class='dialogue'>— ");
211 File file
= new File(images
, getCurrentImageBestName(false));
212 Instance
.getCache().saveAsImage(new URL(para
.getContent()), file
);
213 writer
.write(" <img class='page-image' src='images/"
214 + getCurrentImageBestName(false) + "'/>");
220 protected void writeParagraphFooter(Paragraph para
) throws IOException
{
221 switch (para
.getType()) {
223 writer
.write("</span>\n");
226 writer
.write("</div>\n");
235 protected void writeTextLine(ParagraphType type
, String line
)
240 writer
.write(decorateText(StringUtils
.xmlEscape(line
)));
248 protected String
enbold(String word
) {
249 return "<strong>" + word
+ "</strong>";
253 protected String
italize(String word
) {
254 return "<emph>" + word
+ "</emph>";
257 private String
generateNcx(Story story
) {
258 StringBuilder builder
= new StringBuilder();
263 if (story
.getMeta() != null) {
264 MetaData meta
= story
.getMeta();
265 uuid
= meta
.getUuid();
266 author
= meta
.getAuthor();
267 title
= meta
.getTitle();
270 builder
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
271 builder
.append("\n<!DOCTYPE ncx");
272 builder
.append("\nPUBLIC \"-//NISO//DTD ncx 2005-1//EN\" \"http://www.daisy.org/z3986/2005/ncx-2005-1.dtd\">");
273 builder
.append("\n<ncx xmlns=\"http://www.daisy.org/z3986/2005/ncx/\" version=\"2005-1\">");
274 builder
.append("\n <head>");
275 builder
.append("\n <!--The following four metadata items are required for all");
276 builder
.append("\n NCX documents, including those conforming to the relaxed");
277 builder
.append("\n constraints of OPS 2.0-->");
278 builder
.append("\n <meta name=\"dtb:uid\" content=\""
279 + StringUtils
.xmlEscapeQuote(uuid
) + "\"/>");
280 builder
.append("\n <meta name=\"dtb:depth\" content=\"1\"/>");
281 builder
.append("\n <meta name=\"dtb:totalPageCount\" content=\"0\"/>");
282 builder
.append("\n <meta name=\"dtb:maxPageNumber\" content=\"0\"/>");
283 builder
.append("\n <meta name=\"epub-creator\" content=\""
284 + StringUtils
.xmlEscapeQuote(EPUB_CREATOR
) + "\"/>");
285 builder
.append("\n </head>");
286 builder
.append("\n <docTitle>");
287 builder
.append("\n <text>" + StringUtils
.xmlEscape(title
) + "</text>");
288 builder
.append("\n </docTitle>");
289 builder
.append("\n <docAuthor>");
291 builder
.append("\n <text>" + StringUtils
.xmlEscape(author
) + "</text>");
292 builder
.append("\n </docAuthor>");
293 builder
.append("\n <navMap>");
294 builder
.append("\n <navPoint id=\"navpoint-1\" playOrder=\"1\">");
295 builder
.append("\n <navLabel>");
296 builder
.append("\n <text>Title Page</text>");
297 builder
.append("\n </navLabel>");
298 builder
.append("\n <content src=\"title.xml\"/>");
299 builder
.append("\n </navPoint>");
301 int navPoint
= 2; // 1 is above
303 if (story
.getMeta() != null & story
.getMeta().getResume() != null) {
304 Chapter chap
= story
.getMeta().getResume();
305 generateNcx(chap
, builder
, navPoint
++);
308 for (Chapter chap
: story
) {
309 generateNcx(chap
, builder
, navPoint
++);
312 builder
.append("\n </navMap>");
313 builder
.append("\n</ncx>\n");
315 return builder
.toString();
318 private void generateNcx(Chapter chap
, StringBuilder builder
, int navPoint
) {
320 if (chap
.getName() != null && !chap
.getName().isEmpty()) {
321 name
= Instance
.getTrans().getString(StringId
.CHAPTER_NAMED
,
322 chap
.getNumber(), chap
.getName());
324 name
= Instance
.getTrans().getString(StringId
.CHAPTER_UNNAMED
,
328 String nnn
= String
.format("%03d", (navPoint
- 2));
330 builder
.append("\n <navPoint id=\"navpoint-" + navPoint
331 + "\" playOrder=\"" + navPoint
+ "\">");
332 builder
.append("\n <navLabel>");
333 builder
.append("\n <text>" + name
+ "</text>");
334 builder
.append("\n </navLabel>");
335 builder
.append("\n <content src=\"chapter-" + nnn
+ ".xml\"/>");
336 builder
.append("\n </navPoint>\n");
339 private String
generateOpf(Story story
) {
340 StringBuilder builder
= new StringBuilder();
346 String publisher
= "";
350 if (story
.getMeta() != null) {
351 MetaData meta
= story
.getMeta();
352 title
= meta
.getTitle();
353 uuid
= meta
.getUuid();
354 author
= meta
.getAuthor();
355 date
= meta
.getDate();
356 publisher
= meta
.getPublisher();
357 subject
= meta
.getSubject();
358 source
= meta
.getSource();
359 lang
= meta
.getLang();
362 builder
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
363 builder
.append("\n<package xmlns=\"http://www.idpf.org/2007/opf\" unique-identifier=\""
364 + uuid
+ "\" version=\"2.0\">");
365 builder
.append("\n <metadata xmlns:opf=\"http://www.idpf.org/2007/opf\"");
366 builder
.append("\n xmlns:dc=\"http://purl.org/dc/elements/1.1/\">");
367 builder
.append("\n <dc:title>" + StringUtils
.xmlEscape(title
)
369 builder
.append("\n <dc:creator opf:role=\"aut\" opf:file-as=\""
370 + StringUtils
.xmlEscapeQuote(author
) + "\">"
371 + StringUtils
.xmlEscape(author
) + "</dc:creator>");
372 builder
.append("\n <dc:date opf:event=\"original-publication\">"
373 + StringUtils
.xmlEscape(date
) + "</dc:date>");
374 builder
.append("\n <dc:publisher>"
375 + StringUtils
.xmlEscape(publisher
) + "</dc:publisher>");
376 builder
.append("\n <dc:date opf:event=\"epub-publication\"></dc:date>");
377 builder
.append("\n <dc:subject>" + StringUtils
.xmlEscape(subject
)
379 builder
.append("\n <dc:source>" + StringUtils
.xmlEscape(source
)
381 builder
.append("\n <dc:rights>Not for commercial use.</dc:rights>");
382 builder
.append("\n <dc:identifier id=\"id\" opf:scheme=\"URI\">"
383 + StringUtils
.xmlEscape(uuid
) + "</dc:identifier>");
384 builder
.append("\n <dc:language>" + StringUtils
.xmlEscape(lang
)
386 builder
.append("\n </metadata>");
387 builder
.append("\n <manifest>");
388 builder
.append("\n <!-- Content Documents -->");
389 builder
.append("\n <item id=\"titlepage\" href=\"title.xml\" media-type=\"application/xhtml+xml\"/>");
390 for (int i
= 0; i
<= story
.getChapters().size(); i
++) {
391 String name
= String
.format("%s%03d", "chapter-", i
);
392 builder
.append("\n <item id=\""
393 + StringUtils
.xmlEscapeQuote(name
) + "\" href=\""
394 + StringUtils
.xmlEscapeQuote(name
)
395 + ".xml\" media-type=\"application/xhtml+xml\"/>");
398 builder
.append("\n <!-- CSS Style Sheets -->");
399 builder
.append("\n <item id=\"style-css\" href=\"css/style.css\" media-type=\"text/css\"/>");
401 builder
.append("\n <!-- Images -->");
403 if (story
.getMeta() != null && story
.getMeta().getCover() != null) {
404 String format
= Instance
.getConfig()
405 .getString(Config
.IMAGE_FORMAT_COVER
).toLowerCase();
406 builder
.append("\n <item id=\"cover\" href=\"images/cover."
407 + format
+ "\" media-type=\"image/png\"/>");
410 builder
.append("\n <!-- NCX -->");
411 builder
.append("\n <item id=\"ncx\" href=\"epb.ncx\" media-type=\"application/x-dtbncx+xml\"/>");
412 builder
.append("\n </manifest>");
413 builder
.append("\n <spine toc=\"ncx\">");
414 builder
.append("\n <itemref idref=\"titlepage\" linear=\"yes\"/>");
415 for (int i
= 0; i
<= story
.getChapters().size(); i
++) {
416 String name
= String
.format("%s%03d", "chapter-", i
);
417 builder
.append("\n <itemref idref=\""
418 + StringUtils
.xmlEscapeQuote(name
) + "\" linear=\"yes\"/>");
420 builder
.append("\n </spine>");
421 builder
.append("\n</package>\n");
423 return builder
.toString();
426 private String
generateTitleXml(Story story
) {
427 StringBuilder builder
= new StringBuilder();
432 if (story
.getMeta() != null) {
433 MetaData meta
= story
.getMeta();
434 title
= meta
.getTitle();
435 if (meta
.getTags() != null) {
436 for (String tag
: meta
.getTags()) {
437 if (!tags
.isEmpty()) {
443 if (!tags
.isEmpty()) {
444 tags
= "(" + tags
+ ")";
447 author
= meta
.getAuthor();
450 String format
= Instance
.getConfig()
451 .getString(Config
.IMAGE_FORMAT_COVER
).toLowerCase();
453 builder
.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
454 builder
.append("\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd \">");
455 builder
.append("\n<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">");
456 builder
.append("\n<head>");
457 builder
.append("\n <title>" + StringUtils
.xmlEscape(title
) + "</title>");
458 builder
.append("\n <link rel=\"stylesheet\" href=\"css/style.css\" type=\"text/css\"/>");
459 builder
.append("\n</head>");
460 builder
.append("\n<body>");
461 builder
.append("\n <div class=\"titlepage\">");
462 builder
.append("\n <h1>" + StringUtils
.xmlEscape(title
) + "</h1>");
463 builder
.append("\n <div class=\"type\">"
464 + StringUtils
.xmlEscape(tags
) + "</div>");
465 builder
.append("\n <div class=\"cover\">");
466 builder
.append("\n <img src=\"images/cover." + format
+ "\"></img>");
467 builder
.append("\n </div>");
468 builder
.append("\n <div class=\"author\">"
469 + StringUtils
.xmlEscape(author
) + "</div>");
470 builder
.append("\n </div>");
471 builder
.append("\n</body>");
472 builder
.append("\n</html>\n");
474 return builder
.toString();