Commit | Line | Data |
---|---|---|
08fe2e33 NR |
1 | package be.nikiroo.fanfix.output; |
2 | ||
3 | import java.io.File; | |
4 | import java.io.IOException; | |
bee7dffe NR |
5 | import java.util.ArrayList; |
6 | import java.util.List; | |
08fe2e33 NR |
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; | |
08fe2e33 | 12 | import be.nikiroo.fanfix.data.Paragraph.ParagraphType; |
bee7dffe | 13 | import be.nikiroo.fanfix.data.Story; |
3b2b638f | 14 | import be.nikiroo.utils.Progress; |
6fd6ebe6 | 15 | import be.nikiroo.utils.Version; |
08fe2e33 NR |
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 */ | |
2206ef66 NR |
43 | LATEX, |
44 | /** HTML files in a dedicated directory */ | |
45 | HTML, | |
46 | ||
47 | ; | |
48 | ||
211f7ddb | 49 | @Override |
08fe2e33 NR |
50 | public String toString() { |
51 | return super.toString().toLowerCase(); | |
52 | } | |
53 | ||
54 | /** | |
55 | * A description of this output type. | |
56 | * | |
4d205683 NR |
57 | * @param longDesc |
58 | * TRUE for the long description, FALSE for the short one | |
59 | * | |
08fe2e33 NR |
60 | * @return the description |
61 | */ | |
4d205683 NR |
62 | public String getDesc(boolean longDesc) { |
63 | StringId id = longDesc ? StringId.OUTPUT_DESC | |
64 | : StringId.OUTPUT_DESC_SHORT; | |
65 | ||
d66deb8d | 66 | String desc = Instance.getInstance().getTrans().getStringX(id, this.name()); |
08fe2e33 NR |
67 | |
68 | if (desc == null) { | |
d66deb8d | 69 | desc = Instance.getInstance().getTrans().getString(id, this.toString()); |
08fe2e33 NR |
70 | } |
71 | ||
fee80815 | 72 | if (desc == null || desc.isEmpty()) { |
b2612f9d NR |
73 | desc = this.toString(); |
74 | } | |
75 | ||
08fe2e33 NR |
76 | return desc; |
77 | } | |
78 | ||
2206ef66 NR |
79 | /** |
80 | * The default extension to add to the output files. | |
81 | * | |
10d558d2 | 82 | * @param readerTarget |
f7460e4c NR |
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 | |
10d558d2 | 87 | * |
2206ef66 NR |
88 | * @return the extension |
89 | */ | |
10d558d2 | 90 | public String getDefaultExtension(boolean readerTarget) { |
925298fd | 91 | BasicOutput output = BasicOutput.getOutput(this, false, false); |
2206ef66 | 92 | if (output != null) { |
10d558d2 | 93 | return output.getDefaultExtension(readerTarget); |
2206ef66 NR |
94 | } |
95 | ||
96 | return null; | |
97 | } | |
98 | ||
08fe2e33 | 99 | /** |
0efd25e3 NR |
100 | * Call {@link OutputType#valueOf(String)} after conversion to upper |
101 | * case. | |
08fe2e33 NR |
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 | /** | |
0efd25e3 | 114 | * Call {@link OutputType#valueOf(String)} after conversion to upper |
e604986c | 115 | * case but return def for NULL and empty instead of raising an |
0efd25e3 | 116 | * exception. |
08fe2e33 NR |
117 | * |
118 | * @param typeName | |
119 | * the possible type name | |
e604986c NR |
120 | * @param def |
121 | * the default value | |
08fe2e33 NR |
122 | * |
123 | * @return NULL or the type | |
124 | */ | |
e604986c | 125 | public static OutputType valueOfNullOkUC(String typeName, OutputType def) { |
a6395bef | 126 | if (typeName == null || typeName.isEmpty()) { |
e604986c | 127 | return def; |
08fe2e33 NR |
128 | } |
129 | ||
130 | return OutputType.valueOfUC(typeName); | |
131 | } | |
132 | ||
133 | /** | |
0efd25e3 | 134 | * Call {@link OutputType#valueOf(String)} after conversion to upper |
e604986c | 135 | * case but return def in case of error instead of raising an exception. |
08fe2e33 NR |
136 | * |
137 | * @param typeName | |
138 | * the possible type name | |
e604986c NR |
139 | * @param def |
140 | * the default value | |
08fe2e33 NR |
141 | * |
142 | * @return NULL or the type | |
143 | */ | |
e604986c | 144 | public static OutputType valueOfAllOkUC(String typeName, OutputType def) { |
08fe2e33 NR |
145 | try { |
146 | return OutputType.valueOfUC(typeName); | |
147 | } catch (Exception e) { | |
e604986c | 148 | return def; |
08fe2e33 NR |
149 | } |
150 | } | |
151 | } | |
152 | ||
153 | /** The creator name (this program, by me!) */ | |
3f85553e NR |
154 | static protected final String EPUB_CREATOR = "Fanfix " |
155 | + Version.getCurrentVersion() + " (by Niki)"; | |
08fe2e33 NR |
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; | |
bee7dffe NR |
164 | private Progress storyPg; |
165 | private Progress chapPg; | |
08fe2e33 NR |
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) | |
bee7dffe NR |
175 | * @param pg |
176 | * the optional progress reporter | |
08fe2e33 NR |
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 | */ | |
bee7dffe NR |
184 | public File process(Story story, String target, Progress pg) |
185 | throws IOException { | |
186 | storyPg = pg; | |
187 | ||
d5a7153c NR |
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 | } | |
08fe2e33 NR |
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 | * | |
bee7dffe | 220 | * |
08fe2e33 NR |
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 | ||
5083ef84 NR |
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 | ||
08fe2e33 NR |
265 | /** |
266 | * The output type. | |
267 | * | |
268 | * @param type | |
269 | * the new type | |
0efd25e3 NR |
270 | * @param writeCover |
271 | * TRUE to enable the creation of a cover if possible | |
925298fd NR |
272 | * @param writeInfo |
273 | * TRUE to enable the creation of a .info file | |
08fe2e33 NR |
274 | * |
275 | * @return this | |
276 | */ | |
925298fd NR |
277 | protected BasicOutput setType(OutputType type, boolean writeInfo, |
278 | boolean writeCover) { | |
08fe2e33 | 279 | this.type = type; |
08fe2e33 | 280 | this.writeInfo = writeInfo; |
925298fd | 281 | this.writeCover = writeCover; |
08fe2e33 NR |
282 | |
283 | return this; | |
284 | } | |
285 | ||
286 | /** | |
287 | * The default extension to add to the output files. | |
08fe2e33 | 288 | * |
10d558d2 | 289 | * @param readerTarget |
f7460e4c NR |
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 | |
10d558d2 | 294 | * |
08fe2e33 NR |
295 | * @return the extension |
296 | */ | |
211f7ddb NR |
297 | public String getDefaultExtension( |
298 | @SuppressWarnings("unused") boolean readerTarget) { | |
08fe2e33 NR |
299 | return ""; |
300 | } | |
301 | ||
211f7ddb | 302 | @SuppressWarnings("unused") |
08fe2e33 NR |
303 | protected void writeStoryHeader(Story story) throws IOException { |
304 | } | |
305 | ||
211f7ddb | 306 | @SuppressWarnings("unused") |
08fe2e33 NR |
307 | protected void writeChapterHeader(Chapter chap) throws IOException { |
308 | } | |
309 | ||
211f7ddb | 310 | @SuppressWarnings("unused") |
08fe2e33 NR |
311 | protected void writeParagraphHeader(Paragraph para) throws IOException { |
312 | } | |
313 | ||
211f7ddb | 314 | @SuppressWarnings("unused") |
08fe2e33 NR |
315 | protected void writeStoryFooter(Story story) throws IOException { |
316 | } | |
317 | ||
211f7ddb | 318 | @SuppressWarnings("unused") |
08fe2e33 NR |
319 | protected void writeChapterFooter(Chapter chap) throws IOException { |
320 | } | |
321 | ||
211f7ddb | 322 | @SuppressWarnings("unused") |
08fe2e33 NR |
323 | protected void writeParagraphFooter(Paragraph para) throws IOException { |
324 | } | |
325 | ||
326 | protected void writeStory(Story story) throws IOException { | |
bee7dffe NR |
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 | ||
08fe2e33 NR |
333 | String chapterNameNum = String.format("%03d", 0); |
334 | String paragraphNumber = String.format("%04d", 0); | |
ecfb936e | 335 | imageName = paragraphNumber + "_" + chapterNameNum; |
08fe2e33 | 336 | |
fe999aa4 | 337 | if (story.getMeta() != null) { |
68686a37 | 338 | story.getMeta().setType("" + getType()); |
fe999aa4 | 339 | } |
68686a37 | 340 | |
276f95c6 | 341 | if (isWriteCover()) { |
08fe2e33 NR |
342 | InfoCover.writeCover(targetDir, targetName, story.getMeta()); |
343 | } | |
276f95c6 | 344 | if (isWriteInfo()) { |
08fe2e33 NR |
345 | InfoCover.writeInfo(targetDir, targetName, story.getMeta()); |
346 | } | |
347 | ||
bee7dffe NR |
348 | storyPg.setProgress(1); |
349 | ||
350 | List<Progress> chapPgs = new ArrayList<Progress>(story.getChapters() | |
351 | .size()); | |
08fe2e33 | 352 | for (Chapter chap : story) { |
bee7dffe NR |
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; | |
08fe2e33 NR |
365 | } |
366 | writeStoryFooter(story); | |
bee7dffe NR |
367 | |
368 | storyPg.setProgress(storyPg.getMax()); | |
369 | storyPg = null; | |
08fe2e33 NR |
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++); | |
16a81ef7 | 383 | imageName = chapterNameNum + "_" + paragraphNumber; |
08fe2e33 NR |
384 | |
385 | writeChapterHeader(chap); | |
bee7dffe | 386 | int i = 1; |
08fe2e33 NR |
387 | for (Paragraph para : chap) { |
388 | paragraphNumber = String.format("%04d", num++); | |
16a81ef7 | 389 | imageName = chapterNameNum + "_" + paragraphNumber; |
08fe2e33 | 390 | writeParagraph(para); |
bee7dffe NR |
391 | if (chapPg != null) { |
392 | chapPg.setProgress(i++); | |
393 | } | |
08fe2e33 NR |
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 | ||
211f7ddb | 404 | @SuppressWarnings("unused") |
08fe2e33 NR |
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 | |
925298fd NR |
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 | |
08fe2e33 NR |
526 | * to the main target file |
527 | * | |
528 | * @return the {@link BasicOutput} | |
529 | */ | |
925298fd NR |
530 | public static BasicOutput getOutput(OutputType type, boolean writeInfo, |
531 | boolean writeCover) { | |
08fe2e33 NR |
532 | if (type != null) { |
533 | switch (type) { | |
534 | case EPUB: | |
925298fd | 535 | return new Epub().setType(type, writeInfo, writeCover); |
08fe2e33 | 536 | case TEXT: |
925298fd | 537 | return new Text().setType(type, writeInfo, true); |
08fe2e33 NR |
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: | |
925298fd | 543 | return new Cbz().setType(type, writeInfo, writeCover); |
08fe2e33 | 544 | case LATEX: |
925298fd | 545 | return new LaTeX().setType(type, writeInfo, writeCover); |
2206ef66 | 546 | case HTML: |
925298fd | 547 | return new Html().setType(type, writeInfo, writeCover); |
08fe2e33 NR |
548 | } |
549 | } | |
550 | ||
551 | return null; | |
552 | } | |
553 | } |