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