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