/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.app.batch;
import com.lightcrafts.app.Application;
import com.lightcrafts.app.ComboFrame;
import com.lightcrafts.app.DocumentWriter;
import static com.lightcrafts.app.batch.Locale.LOCALE;
import com.lightcrafts.image.BadImageFileException;
import com.lightcrafts.image.ColorProfileException;
import com.lightcrafts.image.UnknownImageTypeException;
import com.lightcrafts.image.UnsupportedColorProfileException;
import com.lightcrafts.image.export.ImageFileExportOptions;
import com.lightcrafts.image.metadata.ImageMetadata;
import com.lightcrafts.image.types.ImageType;
import com.lightcrafts.image.types.JPEGImageType;
import com.lightcrafts.image.types.LZNImageType;
import com.lightcrafts.image.types.TIFFImageType;
import com.lightcrafts.model.Engine;
import com.lightcrafts.ui.editor.Document;
import com.lightcrafts.ui.editor.assoc.DocumentDatabase;
import com.lightcrafts.ui.export.ExportNameUtility;
import com.lightcrafts.ui.export.SaveOptions;
import com.lightcrafts.ui.LightZoneSkin;
import com.lightcrafts.utils.xml.XMLException;
import com.lightcrafts.utils.xml.XmlDocument;
import com.lightcrafts.utils.xml.XmlNode;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.io.File;
import java.io.IOException;
/**
* This encapsulates the procedures applied when LightZone processes multiple
* images: browser export, browser template application, paste tools, and
* the send action.
*/
public class BatchProcessor {
private static JDialog Dialog; // Blocks UI during batch processing
private static BatchText Text; // Where log messages stream to
private static Thread Thread; // Where the work is done
private static BatchProgressBar Progress; // Image export progress
private static BatchImageComponent Image; // The current Engine image
private static JLabel Label; // File counts and time estimates
private static JButton Button; // Either "Cancel" or "Done"
private static long Start; // Time started, for time estimates
private static boolean Interrupted; // Flag to halt background work
private static boolean Finished; // Flag to indicate work is halted
private static boolean Canceled; // Flag to indicate work is canceled by the user
private static RuntimeException Error; // Propagate unchecked exceptions
public static void process(
final ComboFrame frame,
final File[] files,
final XmlDocument template,
final BatchConfig conf
) {
Thread = new Thread(
new Runnable() {
public void run() {
frame.pause();
try {
processTemplate(files, template, conf);
}
catch (RuntimeException e) {
System.err.println(
"Unchecked batch processing exception:"
);
e.printStackTrace();
Error = e;
Dialog.setVisible(false);
}
finally {
frame.resume();
}
}
},
"Template Applicator"
);
Text = new BatchText();
JScrollPane textScroll = new JScrollPane(Text);
textScroll.setPreferredSize(new Dimension(400, 300));
textScroll.setBorder(BorderFactory.createLineBorder(Color.gray));
Label = new JLabel();
Label.setAlignmentX(.5f);
initLabel(files.length);
Button = new JButton(LOCALE.get("BatchCancelButton"));
Button.setAlignmentX(.5f);
Image = new BatchImageComponent();
Progress = new BatchProgressBar();
// Don't let the progress bar get wider than the image above it:
Dimension progSize = Progress.getComponent().getPreferredSize();
progSize.width = Image.getPreferredSize().width;
Progress.getComponent().setMaximumSize(progSize);
Box imageBox = Box.createVerticalBox();
imageBox.add(Box.createVerticalGlue());
imageBox.add(Image);
imageBox.add(Box.createVerticalStrut(4));
imageBox.add(Progress.getComponent());
imageBox.add(Box.createVerticalGlue());
Box textBox = Box.createVerticalBox();
textBox.add(textScroll);
textBox.add(Box.createVerticalStrut(8));
textBox.add(Label);
textBox.add(Box.createVerticalStrut(8));
textBox.add(Button);
Box content = Box.createHorizontalBox();
content.add(imageBox);
content.add(Box.createHorizontalStrut(8));
content.add(textBox);
content.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
// A root JPanel is needed to control the dialog's background color.
JPanel background = new JPanel(new BorderLayout());
background.setBackground(LightZoneSkin.Colors.FrameBackground);
background.setOpaque(true);
background.add(content);
Dialog = new JDialog(frame);
ActionListener disposeAction = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (!Finished) {
Canceled = true;
Button.setText("Canceling...");
} else
Dialog.setVisible(false);
}
};
Button.addActionListener(disposeAction);
Dialog.setContentPane(background);
Dialog.setModal(true);
Dialog.setTitle(LOCALE.get("BatchDialogTitle"));
Dialog.getRootPane().setDefaultButton(Button);
Dialog.pack();
Dialog.setLocationRelativeTo(frame);
Dialog.addComponentListener(
new ComponentAdapter() {
public void componentHidden(ComponentEvent e) {
Interrupted = true;
}
}
);
Interrupted = false;
Finished = false;
Canceled = false;
Thread.start();
Dialog.setVisible(true);
// If they clicked "Cancel", and the thread is live, then shut it down:
synchronized(Thread) {
if ((!Finished) && (Error == null)) {
Interrupted = true;
try {
Thread.wait();
}
catch (InterruptedException e) {
// Just continue.
}
}
Interrupted = false;
Finished = false;
Canceled = false;
}
if (Error != null) {
RuntimeException e = Error;
Error = null;
throw e;
}
}
// Conduct the export and template processes, in the background under the dialog.
static void processTemplate(
File[] files, XmlDocument template, BatchConfig conf
) {
ImageFileExportOptions export = conf.export;
boolean ignoreResize =
export.resizeWidth.getValue() == 0 &&
export.resizeHeight.getValue() == 0;
// Remember the requested output width and height, because they may get
// mutated each time a file is processed. See these methods:
// createTemplateSaveOptions()
// conformExportOptions()
// Engine.write()
int exportWidth = export.resizeWidth.getValue();
int exportHeight = export.resizeHeight.getValue();
for (int n=0; n<files.length; n++) {
if (Canceled)
break;
if (Interrupted) {
synchronized(Thread) {
Thread.notifyAll();
return;
}
}
try {
File file = files[n];
Image.setCachedFile(file);
logStart(file);
Document doc = Application.createDocumentHeadless(file);
File outFile;
String outName;
// Enforce the original requested output dimensions, since
// the ImageExportOptions may have been mutated on a previous
// iteration.
export.resizeWidth.setValue(exportWidth);
export.resizeHeight.setValue(exportHeight);
if (template != null) {
XmlNode root = template.getRoot();
doc.applyTemplate(root);
SaveOptions save = doc.getSaveOptions();
if (save == null) {
save = createTemplateSaveOptions(doc, export, ignoreResize);
}
doc.setSaveOptions(save);
ComboFrame frame = (ComboFrame) Dialog.getOwner();
DocumentWriter.save(doc, frame, false, Progress);
outFile = save.getFile();
outName = outFile.getName();
DocumentDatabase.addDocumentFile(outFile);
} else {
conformExportOptions(doc, conf, ignoreResize);
Engine engine = doc.getEngine();
DocumentWriter.export(engine, export, Progress);
outFile = export.getExportFile();
outName = outFile.getName();
}
doc.dispose();
logEnd(LOCALE.get("BatchLogSavedMessage", outName));
Image.setFile(outFile);
Progress.reset();
}
catch (XMLException e) {
logError(LOCALE.get("BatchLogXmlError"), e);
}
catch (BadImageFileException e) {
logError(LOCALE.get("BatchLogBadImageError"), e);
}
catch (IOException e) {
logError(LOCALE.get("BatchLogIOError"), e);
}
catch (OutOfMemoryError e) {
logError(LOCALE.get("BatchLogMemoryError"), e);
}
catch (UnknownImageTypeException e) {
logError(LOCALE.get("BatchLogImageTypeError"), e);
}
catch (UnsupportedColorProfileException e) {
logError(LOCALE.get("BatchLogCameraError"), e);
}
catch (ColorProfileException e) {
logError(LOCALE.get("BatchLogColorError"), e);
}
catch (Throwable e) {
logError(LOCALE.get("BatchLogUnknownError"), e);
e.printStackTrace();
}
updateLabel(n + 1, files.length);
}
synchronized(Thread) {
Finished = true;
Thread.notifyAll();
Button.setText(LOCALE.get("BatchDoneButton"));
}
}
// Construct SaveOptions for processed images that have never been saved.
// Save back to the same directory as the original image,
// with a unique file name, with the given export options, except
// the resize dimensions, which are set to the document's "natural"
// dimensions.
private static SaveOptions createTemplateSaveOptions(
Document doc, ImageFileExportOptions export, boolean ignoreResize
) {
ImageMetadata meta = doc.getMetadata();
File file = meta.getFile();
ImageType type = export.getImageType();
String ext = type.getExtensions()[0];
file = ExportNameUtility.setFileExtension(file, ext);
file = ExportNameUtility.ensureNotExists(file);
if (type == LZNImageType.INSTANCE) {
SaveOptions options = SaveOptions.createLzn(file);
return options;
}
SaveOptions options;
Engine engine = doc.getEngine();
Dimension size = engine.getNaturalSize();
if (type instanceof TIFFImageType) {
options = SaveOptions.createSidecarTiff(export);
}
else if (type instanceof JPEGImageType) {
options = SaveOptions.createSidecarJpeg(export);
}
else {
throw new IllegalArgumentException(
"Can't save to image type \"" + type.getName() + "\""
);
}
if (ignoreResize) {
export.resizeWidth.setValue(size.width);
export.resizeHeight.setValue(size.height);
}
options.setFile(file);
return options;
}
// Ensure that the given ImageExportOptions agrees with the given
// TemplateBatchConfigurator about the output folder, the batch name,
// and the output file type extension, and agrees with the given Document
// and the configurator about the output image size.
private static void conformExportOptions(
Document doc, BatchConfig conf, boolean ignoreResize
) {
ImageMetadata meta = doc.getMetadata();
File file = meta.getFile();
String name = file.getName();
File directory = conf.directory;
File outFile = new File(directory, name);
// Mutate the default file into a conformant name:
String outLabel = conf.name;
String outName = ExportNameUtility.trimFileExtension(
outFile.getName()
);
ImageFileExportOptions export = conf.export;
String outSuffix = export.getImageType().getExtensions()[0];
if (outLabel.length() > 0) {
outFile = new File(
directory,
outName + outLabel + "." + outSuffix
);
}
else {
outFile = new File(
directory, outName + "." + outSuffix
);
}
outFile = ExportNameUtility.ensureNotExists(outFile);
export.setExportFile(outFile);
if (ignoreResize) {
Engine engine = doc.getEngine();
Dimension size = engine.getNaturalSize();
export.resizeWidth.setValue(size.width);
export.resizeHeight.setValue(size.height);
}
}
private static void logStart(File file) {
String path = file.getName();
Text.appendStart(path);
}
private static void logEnd(String message) {
Text.appendEnd(message);
}
private static void logError(String message, Throwable e) {
StringBuffer buffer = new StringBuffer();
if (message != null) {
buffer.append("--");
buffer.append(message);
}
if (e != null) {
buffer.append(": ");
buffer.append(e.getClass().getName());
buffer.append(" ");
buffer.append(e.getMessage());
}
buffer.append("\n");
Text.appendError(buffer.toString());
}
private static void initLabel(int max) {
String text;
if (max == 1) {
text = LOCALE.get("BatchInitEstimateMessageSingular");
}
else {
text = LOCALE.get(
"BatchInitEstimateMessagePlural", Integer.toString(max)
);
}
Label.setText(text);
Start = System.currentTimeMillis();
}
private static void updateLabel(final int count, final int max) {
long now = System.currentTimeMillis();
long end = Start + max * (now - Start) / count;
final long remaining = end - now;
EventQueue.invokeLater(
new Runnable() {
public void run() {
int remainingSeconds = (int) (remaining / 1000);
int remainingMinutes = remainingSeconds / 60;
remainingSeconds -= remainingMinutes * 60;
String message = "" + count + " of " + max + " files processed, ";
message += (remainingMinutes > 0 ? remainingMinutes + " minutes and " : "") + remainingSeconds + " seconds remaining.";
Label.setText(message);
// Label.setText(
// LOCALE.get(
// "BatchEstimateMessage",
// Long.toString(count),
// Long.toString(max),
// Long.toString(remaining / 1000)
// )
// );
}
}
);
}
}