Merge branch 'master' into subtree
[nikiroo-utils.git] / output / BasicOutput.java
1 package be.nikiroo.fanfix.output;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.List;
7
8 import be.nikiroo.fanfix.Instance;
9 import be.nikiroo.fanfix.bundles.StringId;
10 import be.nikiroo.fanfix.data.Chapter;
11 import be.nikiroo.fanfix.data.Paragraph;
12 import be.nikiroo.fanfix.data.Paragraph.ParagraphType;
13 import be.nikiroo.fanfix.data.Story;
14 import be.nikiroo.utils.Progress;
15 import be.nikiroo.utils.Version;
16
17 /**
18 * This class is the base class used by the other output classes. It can be used
19 * outside of this package, and have static method that you can use to get
20 * access to the correct support class.
21 *
22 * @author niki
23 */
24 public abstract class BasicOutput {
25 /**
26 * The supported output types for which we can get a {@link BasicOutput}
27 * object.
28 *
29 * @author niki
30 */
31 public enum OutputType {
32 /** EPUB files created with this program */
33 EPUB,
34 /** Pure text file with some rules */
35 TEXT,
36 /** TEXT but with associated .info file */
37 INFO_TEXT,
38 /** DEBUG output to console */
39 SYSOUT,
40 /** ZIP with (PNG) images */
41 CBZ,
42 /** LaTeX file with "book" template */
43 LATEX,
44 /** HTML files in a dedicated directory */
45 HTML,
46
47 ;
48
49 @Override
50 public String toString() {
51 return super.toString().toLowerCase();
52 }
53
54 /**
55 * A description of this output type.
56 *
57 * @param longDesc
58 * TRUE for the long description, FALSE for the short one
59 *
60 * @return the description
61 */
62 public String getDesc(boolean longDesc) {
63 StringId id = longDesc ? StringId.OUTPUT_DESC
64 : StringId.OUTPUT_DESC_SHORT;
65
66 String desc = Instance.getInstance().getTrans().getStringX(id, this.name());
67
68 if (desc == null) {
69 desc = Instance.getInstance().getTrans().getString(id, this.toString());
70 }
71
72 if (desc == null || desc.isEmpty()) {
73 desc = this.toString();
74 }
75
76 return desc;
77 }
78
79 /**
80 * The default extension to add to the output files.
81 *
82 * @param readerTarget
83 * TRUE to point to the main {@link Story} entry point for a
84 * reader (for instance, the main entry point if this
85 * {@link Story} is in a directory bundle), FALSE to point to
86 * the main file even if it is a directory for instance
87 *
88 * @return the extension
89 */
90 public String getDefaultExtension(boolean readerTarget) {
91 BasicOutput output = BasicOutput.getOutput(this, false, false);
92 if (output != null) {
93 return output.getDefaultExtension(readerTarget);
94 }
95
96 return null;
97 }
98
99 /**
100 * Call {@link OutputType#valueOf(String)} after conversion to upper
101 * case.
102 *
103 * @param typeName
104 * the possible type name
105 *
106 * @return NULL or the type
107 */
108 public static OutputType valueOfUC(String typeName) {
109 return OutputType.valueOf(typeName == null ? null : typeName
110 .toUpperCase());
111 }
112
113 /**
114 * Call {@link OutputType#valueOf(String)} after conversion to upper
115 * case but return def for NULL and empty instead of raising an
116 * exception.
117 *
118 * @param typeName
119 * the possible type name
120 * @param def
121 * the default value
122 *
123 * @return NULL or the type
124 */
125 public static OutputType valueOfNullOkUC(String typeName, OutputType def) {
126 if (typeName == null || typeName.isEmpty()) {
127 return def;
128 }
129
130 return OutputType.valueOfUC(typeName);
131 }
132
133 /**
134 * Call {@link OutputType#valueOf(String)} after conversion to upper
135 * case but return def in case of error instead of raising an exception.
136 *
137 * @param typeName
138 * the possible type name
139 * @param def
140 * the default value
141 *
142 * @return NULL or the type
143 */
144 public static OutputType valueOfAllOkUC(String typeName, OutputType def) {
145 try {
146 return OutputType.valueOfUC(typeName);
147 } catch (Exception e) {
148 return def;
149 }
150 }
151 }
152
153 /** The creator name (this program, by me!) */
154 static protected final String EPUB_CREATOR = "Fanfix "
155 + Version.getCurrentVersion() + " (by Niki)";
156
157 /** The current best name for an image */
158 private String imageName;
159 private File targetDir;
160 private String targetName;
161 private OutputType type;
162 private boolean writeCover;
163 private boolean writeInfo;
164 private Progress storyPg;
165 private Progress chapPg;
166
167 /**
168 * Process the {@link Story} into the given target.
169 *
170 * @param story
171 * the {@link Story} to export
172 * @param target
173 * the target where to save to (will not necessary be taken as is
174 * by the processor, for instance an extension can be added)
175 * @param pg
176 * the optional progress reporter
177 *
178 * @return the actual main target saved, which can be slightly different
179 * that the input one
180 *
181 * @throws IOException
182 * in case of I/O error
183 */
184 public File process(Story story, String target, Progress pg)
185 throws IOException {
186 storyPg = pg;
187
188 File targetDir = null;
189 String targetName = null;
190 if (target != null) {
191 target = new File(target).getAbsolutePath();
192 targetDir = new File(target).getParentFile();
193 targetName = new File(target).getName();
194
195 String ext = getDefaultExtension(false);
196 if (ext != null && !ext.isEmpty()) {
197 if (targetName.toLowerCase().endsWith(ext)) {
198 targetName = targetName.substring(0, targetName.length()
199 - ext.length());
200 }
201 }
202 }
203
204 return process(story, targetDir, targetName);
205 }
206
207 /**
208 * Process the {@link Story} into the given target.
209 * <p>
210 * This method is expected to be overridden in most cases.
211 *
212 * @param story
213 * the {@link Story} to export
214 * @param targetDir
215 * the target dir where to save to
216 * @param targetName
217 * the target filename (will not necessary be taken as is by the
218 * processor, for instance an extension can be added)
219 *
220 *
221 * @return the actual main target saved, which can be slightly different
222 * that the input one
223 *
224 * @throws IOException
225 * in case of I/O error
226 */
227 protected File process(Story story, File targetDir, String targetName)
228 throws IOException {
229 this.targetDir = targetDir;
230 this.targetName = targetName;
231
232 writeStory(story);
233
234 return null;
235 }
236
237 /**
238 * The output type.
239 *
240 * @return the type
241 */
242 public OutputType getType() {
243 return type;
244 }
245
246 /**
247 * Enable the creation of a .info file next to the resulting processed file.
248 *
249 * @return TRUE to enable it
250 */
251 protected boolean isWriteInfo() {
252 return writeInfo;
253 }
254
255 /**
256 * Enable the creation of a cover file next to the resulting processed file
257 * if possible.
258 *
259 * @return TRUE to enable it
260 */
261 protected boolean isWriteCover() {
262 return writeCover;
263 }
264
265 /**
266 * The output type.
267 *
268 * @param type
269 * the new type
270 * @param writeCover
271 * TRUE to enable the creation of a cover if possible
272 * @param writeInfo
273 * TRUE to enable the creation of a .info file
274 *
275 * @return this
276 */
277 protected BasicOutput setType(OutputType type, boolean writeInfo,
278 boolean writeCover) {
279 this.type = type;
280 this.writeInfo = writeInfo;
281 this.writeCover = writeCover;
282
283 return this;
284 }
285
286 /**
287 * The default extension to add to the output files.
288 *
289 * @param readerTarget
290 * TRUE to point to the main {@link Story} entry point for a
291 * reader (for instance, the main entry point if this
292 * {@link Story} is in a directory bundle), FALSE to point to the
293 * main file even if it is a directory for instance
294 *
295 * @return the extension
296 */
297 public String getDefaultExtension(
298 @SuppressWarnings("unused") boolean readerTarget) {
299 return "";
300 }
301
302 @SuppressWarnings("unused")
303 protected void writeStoryHeader(Story story) throws IOException {
304 }
305
306 @SuppressWarnings("unused")
307 protected void writeChapterHeader(Chapter chap) throws IOException {
308 }
309
310 @SuppressWarnings("unused")
311 protected void writeParagraphHeader(Paragraph para) throws IOException {
312 }
313
314 @SuppressWarnings("unused")
315 protected void writeStoryFooter(Story story) throws IOException {
316 }
317
318 @SuppressWarnings("unused")
319 protected void writeChapterFooter(Chapter chap) throws IOException {
320 }
321
322 @SuppressWarnings("unused")
323 protected void writeParagraphFooter(Paragraph para) throws IOException {
324 }
325
326 protected void writeStory(Story story) throws IOException {
327 if (storyPg == null) {
328 storyPg = new Progress(0, story.getChapters().size() + 2);
329 } else {
330 storyPg.setMinMax(0, story.getChapters().size() + 2);
331 }
332
333 String chapterNameNum = String.format("%03d", 0);
334 String paragraphNumber = String.format("%04d", 0);
335 imageName = paragraphNumber + "_" + chapterNameNum;
336
337 if (story.getMeta() != null) {
338 story.getMeta().setType("" + getType());
339 }
340
341 if (isWriteCover()) {
342 InfoCover.writeCover(targetDir, targetName, story.getMeta());
343 }
344 if (isWriteInfo()) {
345 InfoCover.writeInfo(targetDir, targetName, story.getMeta());
346 }
347
348 storyPg.setProgress(1);
349
350 List<Progress> chapPgs = new ArrayList<Progress>(story.getChapters()
351 .size());
352 for (Chapter chap : story) {
353 chapPg = new Progress(0, chap.getParagraphs().size());
354 storyPg.addProgress(chapPg, 1);
355 chapPgs.add(chapPg);
356 chapPg = null;
357 }
358
359 writeStoryHeader(story);
360 for (int i = 0; i < story.getChapters().size(); i++) {
361 chapPg = chapPgs.get(i);
362 writeChapter(story.getChapters().get(i));
363 chapPg.setProgress(chapPg.getMax());
364 chapPg = null;
365 }
366 writeStoryFooter(story);
367
368 storyPg.setProgress(storyPg.getMax());
369 storyPg = null;
370 }
371
372 protected void writeChapter(Chapter chap) throws IOException {
373 String chapterNameNum;
374 if (chap.getName() == null || chap.getName().isEmpty()) {
375 chapterNameNum = String.format("%03d", chap.getNumber());
376 } else {
377 chapterNameNum = String.format("%03d", chap.getNumber()) + "_"
378 + chap.getName().replace(" ", "_");
379 }
380
381 int num = 0;
382 String paragraphNumber = String.format("%04d", num++);
383 imageName = chapterNameNum + "_" + paragraphNumber;
384
385 writeChapterHeader(chap);
386 int i = 1;
387 for (Paragraph para : chap) {
388 paragraphNumber = String.format("%04d", num++);
389 imageName = chapterNameNum + "_" + paragraphNumber;
390 writeParagraph(para);
391 if (chapPg != null) {
392 chapPg.setProgress(i++);
393 }
394 }
395 writeChapterFooter(chap);
396 }
397
398 protected void writeParagraph(Paragraph para) throws IOException {
399 writeParagraphHeader(para);
400 writeTextLine(para.getType(), para.getContent());
401 writeParagraphFooter(para);
402 }
403
404 @SuppressWarnings("unused")
405 protected void writeTextLine(ParagraphType type, String line)
406 throws IOException {
407 }
408
409 /**
410 * Return the current best guess for an image name, based upon the current
411 * {@link Chapter} and {@link Paragraph}.
412 *
413 * @param prefix
414 * add the original target name as a prefix
415 *
416 * @return the guessed name
417 */
418 protected String getCurrentImageBestName(boolean prefix) {
419 if (prefix) {
420 return targetName + "_" + imageName;
421 }
422
423 return imageName;
424 }
425
426 /**
427 * Return the given word or sentence as <b>bold</b>.
428 *
429 * @param word
430 * the input
431 *
432 * @return the bold output
433 */
434 protected String enbold(String word) {
435 return word;
436 }
437
438 /**
439 * Return the given word or sentence as <i>italic</i>.
440 *
441 * @param word
442 * the input
443 *
444 * @return the italic output
445 */
446 protected String italize(String word) {
447 return word;
448 }
449
450 /**
451 * Decorate the given text with <b>bold</b> and <i>italic</i> words,
452 * according to {@link BasicOutput#enbold(String)} and
453 * {@link BasicOutput#italize(String)}.
454 *
455 * @param text
456 * the input
457 *
458 * @return the decorated output
459 */
460 protected String decorateText(String text) {
461 StringBuilder builder = new StringBuilder();
462
463 int bold = -1;
464 int italic = -1;
465 char prev = '\0';
466 for (char car : text.toCharArray()) {
467 switch (car) {
468 case '*':
469 if (bold >= 0 && prev != ' ') {
470 String data = builder.substring(bold);
471 builder.setLength(bold);
472 builder.append(enbold(data));
473 bold = -1;
474 } else if (bold < 0
475 && (prev == ' ' || prev == '\0' || prev == '\n')) {
476 bold = builder.length();
477 } else {
478 builder.append(car);
479 }
480
481 break;
482 case '_':
483 if (italic >= 0 && prev != ' ') {
484 String data = builder.substring(italic);
485 builder.setLength(italic);
486 builder.append(enbold(data));
487 italic = -1;
488 } else if (italic < 0
489 && (prev == ' ' || prev == '\0' || prev == '\n')) {
490 italic = builder.length();
491 } else {
492 builder.append(car);
493 }
494
495 break;
496 default:
497 builder.append(car);
498 break;
499 }
500
501 prev = car;
502 }
503
504 if (bold >= 0) {
505 builder.insert(bold, '*');
506 }
507
508 if (italic >= 0) {
509 builder.insert(italic, '_');
510 }
511
512 return builder.toString();
513 }
514
515 /**
516 * Return a {@link BasicOutput} object compatible with the given
517 * {@link OutputType}.
518 *
519 * @param type
520 * the type
521 * @param writeCover
522 * TRUE to enable the creation of a cover if possible to be saved
523 * next to the main target file
524 * @param writeInfo
525 * TRUE to enable the creation of a .info file to be saved next
526 * to the main target file
527 *
528 * @return the {@link BasicOutput}
529 */
530 public static BasicOutput getOutput(OutputType type, boolean writeInfo,
531 boolean writeCover) {
532 if (type != null) {
533 switch (type) {
534 case EPUB:
535 return new Epub().setType(type, writeInfo, writeCover);
536 case TEXT:
537 return new Text().setType(type, writeInfo, true);
538 case INFO_TEXT:
539 return new InfoText().setType(type, true, true);
540 case SYSOUT:
541 return new Sysout().setType(type, false, false);
542 case CBZ:
543 return new Cbz().setType(type, writeInfo, writeCover);
544 case LATEX:
545 return new LaTeX().setType(type, writeInfo, writeCover);
546 case HTML:
547 return new Html().setType(type, writeInfo, writeCover);
548 }
549 }
550
551 return null;
552 }
553 }