/*
* #%L
* BSD implementations of Bio-Formats readers and writers
* %%
* Copyright (C) 2005 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
package loci.formats.gui;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.io.File;
import java.io.IOException;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import loci.formats.DimensionSwapper;
import loci.formats.FilePattern;
import loci.formats.FileStitcher;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.ImageWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A utility for reorganizing and converting QuickTime movies,
* TIFF series and other 4D datasets.
*/
public class DataConverter extends JFrame implements
ActionListener, ChangeListener, Runnable
{
// -- Constants --
private static final Logger LOGGER =
LoggerFactory.getLogger(DataConverter.class);
private static final String TITLE = "Data Converter";
private static final int COLUMNS = 24;
// -- Fields --
// TODO - Use byte[] instead of BufferedImage for conversion pipeline.
private FileStitcher reader = new FileStitcher(true);
private DimensionSwapper swap = new DimensionSwapper(reader);
private BufferedImageReader biReader = new BufferedImageReader(swap);
private ImageWriter writer = new ImageWriter();
private BufferedImageWriter biWriter = new BufferedImageWriter(writer);
private JFileChooser rc, wc;
private boolean shutdown, force = true;
private JTextField input, output;
private JCheckBox qtJava, forceType, includeZ, includeT, includeC;
private JLabel zLabel, tLabel, cLabel;
private JComboBox zChoice, tChoice, cChoice, codec;
private JSpinner fps, series;
private JPanel seriesRow;
private JProgressBar progress;
private JButton convert;
// -- Constructor --
public DataConverter() {
super(TITLE);
// file choosers
rc = GUITools.buildFileChooser(swap);
wc = GUITools.buildFileChooser(writer);
JPanel pane = new JPanel();
pane.setBorder(new EmptyBorder(5, 5, 5, 5));
pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
setContentPane(pane);
// -- Row 1 --
JPanel row1 = new RowPanel();
row1.setLayout(new BoxLayout(row1, BoxLayout.X_AXIS));
pane.add(row1);
pane.add(Box.createVerticalStrut(4));
JLabel inputLabel = new JLabel("Input");
row1.add(inputLabel);
row1.add(Box.createHorizontalStrut(4));
input = new JTextField(COLUMNS);
input.setEditable(false);
row1.add(input);
limitHeight(input);
row1.add(Box.createHorizontalStrut(4));
JButton chooseInput = new JButton("Choose");
row1.add(chooseInput);
chooseInput.setActionCommand("input");
chooseInput.addActionListener(this);
row1.add(Box.createHorizontalStrut(4));
// -- Row 2 --
JPanel row2 = new RowPanel();
row2.setLayout(new BoxLayout(row2, BoxLayout.X_AXIS));
pane.add(row2);
pane.add(Box.createVerticalStrut(9));
JLabel outputLabel = new JLabel("Output");
row2.add(outputLabel);
inputLabel.setPreferredSize(outputLabel.getPreferredSize());
row2.add(Box.createHorizontalStrut(4));
output = new JTextField(COLUMNS);
output.setEditable(false);
row2.add(output);
limitHeight(output);
row2.add(Box.createHorizontalStrut(4));
JButton chooseOutput = new JButton("Choose");
row2.add(chooseOutput);
chooseOutput.setActionCommand("output");
chooseOutput.addActionListener(this);
row2.add(Box.createHorizontalStrut(4));
// save a blank row here
seriesRow = new RowPanel();
seriesRow.setLayout(new BoxLayout(seriesRow, BoxLayout.X_AXIS));
pane.add(seriesRow);
// -- Row 3 --
JPanel row3 = new RowPanel();
row3.setLayout(new BoxLayout(row3, BoxLayout.X_AXIS));
pane.add(row3);
pane.add(Box.createVerticalStrut(9));
String[] axisNames = new String[] {"Time", "Slice", "Channel"};
zLabel = new JLabel(" 1) Slice <0-0>");
tLabel = new JLabel(" 2) Time <0-0>");
cLabel = new JLabel(" 3) Channel <0-0>");
zChoice = new JComboBox(axisNames);
zChoice.setSelectedIndex(1);
zChoice.setPreferredSize(new Dimension(5, 9));
zChoice.setActionCommand("zChoice");
zChoice.addActionListener(this);
tChoice = new JComboBox(axisNames);
tChoice.setSelectedIndex(0);
tChoice.setPreferredSize(new Dimension(5, 9));
tChoice.setActionCommand("tChoice");
tChoice.addActionListener(this);
cChoice = new JComboBox(axisNames);
cChoice.setSelectedIndex(2);
cChoice.setPreferredSize(new Dimension(5, 9));
cChoice.setActionCommand("cChoice");
cChoice.addActionListener(this);
includeZ = new JCheckBox("Include in file");
includeZ.setEnabled(false);
includeT = new JCheckBox("Include in file");
includeT.setEnabled(false);
includeC = new JCheckBox("Include in file");
includeC.setEnabled(false);
row3.add(zLabel);
row3.add(zChoice);
row3.add(includeZ);
row3.add(Box.createHorizontalStrut(4));
row3 = new RowPanel();
row3.setLayout(new BoxLayout(row3, BoxLayout.X_AXIS));
pane.add(row3);
pane.add(Box.createVerticalStrut(9));
row3.add(tLabel);
row3.add(tChoice);
row3.add(includeT);
row3.add(Box.createHorizontalStrut(4));
row3 = new RowPanel();
row3.setLayout(new BoxLayout(row3, BoxLayout.X_AXIS));
pane.add(row3);
pane.add(Box.createVerticalStrut(9));
row3.add(cLabel);
row3.add(cChoice);
row3.add(includeC);
row3.add(Box.createHorizontalStrut(4));
// -- Row 4 --
JPanel row4 = new RowPanel();
row4.setLayout(new BoxLayout(row4, BoxLayout.X_AXIS));
pane.add(row4);
pane.add(Box.createVerticalStrut(9));
JLabel fpsLabel = new JLabel("Frames per second: ");
row4.add(fpsLabel);
fps = new JSpinner(new SpinnerNumberModel(10, 1, 100, 1));
row4.add(fps);
row4.add(Box.createHorizontalStrut(3));
JLabel codecLabel = new JLabel("Output compression type: ");
row4.add(codecLabel);
codec = new JComboBox(new String[0]);
row4.add(codec);
// -- Row 5 --
JPanel row5 = new RowPanel();
row5.setLayout(new BoxLayout(row5, BoxLayout.X_AXIS));
pane.add(row5);
pane.add(Box.createVerticalStrut(9));
boolean canDoQT = new LegacyQTTools().canDoQT();
qtJava = new JCheckBox("Use QTJava", canDoQT);
qtJava.setEnabled(canDoQT);
row5.add(qtJava);
row5.add(Box.createHorizontalStrut(3));
forceType = new JCheckBox("Force", true);
forceType.setActionCommand("force");
forceType.addActionListener(this);
row5.add(forceType);
row5.add(Box.createHorizontalStrut(3));
row5.add(Box.createHorizontalGlue());
convert = new JButton("Convert");
row5.add(convert);
convert.setActionCommand("convert");
convert.addActionListener(this);
row5.add(Box.createHorizontalStrut(4));
JButton quit = new JButton("Quit");
row5.add(quit);
quit.setActionCommand("quit");
quit.addActionListener(this);
// -- Row 6 --
JPanel row6 = new RowPanel();
row6.setLayout(new BoxLayout(row6, BoxLayout.X_AXIS));
pane.add(row6);
progress = new JProgressBar();
progress.setString("");
progress.setStringPainted(true);
row6.add(progress);
row6.add(Box.createHorizontalStrut(8));
JLabel version = new JLabel("Built on " + FormatTools.DATE);
version.setFont(version.getFont().deriveFont(Font.ITALIC));
row6.add(version);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setLocation(100, 100);
pack();
}
// -- ActionListener methods --
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if ("input".equals(cmd)) {
int rval = rc.showOpenDialog(this);
if (rval != JFileChooser.APPROVE_OPTION) return;
File file = rc.getSelectedFile();
if (file != null) wc.setCurrentDirectory(file);
String pattern = FilePattern.findPattern(file);
input.setText(pattern);
try {
swap.setId(pattern);
if (swap.getSeriesCount() > 1 && series == null) {
JLabel seriesLabel = new JLabel("Series: ");
series = new JSpinner(new SpinnerNumberModel(1, 1,
swap.getSeriesCount(), 1));
series.addChangeListener(this);
seriesRow.add(seriesLabel);
seriesRow.add(series);
pack();
}
else if (series != null) {
((SpinnerNumberModel) series.getModel()).setMaximum(
new Integer(swap.getSeriesCount()));
pack();
}
else if (swap.getSeriesCount() == 1 && series != null) {
seriesRow.remove(series);
series = null;
pack();
}
}
catch (FormatException exc) { LOGGER.info("", exc); }
catch (IOException exc) { LOGGER.info("", exc); }
updateLabels(pattern);
}
else if ("output".equals(cmd)) {
int rval = wc.showSaveDialog(this);
if (rval != JFileChooser.APPROVE_OPTION) return;
File file = wc.getSelectedFile();
if (file != null) rc.setCurrentDirectory(file);
String s = file.getPath();
output.setText(s);
try {
writer.setId(s);
if (!writer.canDoStacks()) {
includeZ.setEnabled(false);
includeT.setEnabled(false);
includeC.setEnabled(false);
}
codec.removeAllItems();
String[] codecs = writer.getWriter().getCompressionTypes();
if (codecs == null) codecs = new String[] {"Uncompressed"};
for (int i=0; i<codecs.length; i++) {
codec.addItem(codecs[i]);
}
}
catch (FormatException exc) { LOGGER.info("", exc); }
catch (IOException exc) { LOGGER.info("", exc); }
}
else if ("zChoice".equals(cmd)) {
String newName = (String) zChoice.getSelectedItem();
String label = zLabel.getText();
String oldName = label.substring(4, label.indexOf(" ", 4));
label = label.replaceAll(oldName, newName);
zLabel.setText(label);
}
else if ("tChoice".equals(cmd)) {
String newName = (String) tChoice.getSelectedItem();
String label = tLabel.getText();
String oldName = label.substring(4, label.indexOf(" ", 4));
label = label.replaceAll(oldName, newName);
tLabel.setText(label);
}
else if ("cChoice".equals(cmd)) {
String newName = (String) cChoice.getSelectedItem();
String label = cLabel.getText();
String oldName = label.substring(4, label.indexOf(" ", 4));
label = label.replaceAll(oldName, newName);
cLabel.setText(label);
}
else if ("convert".equals(cmd)) new Thread(this, "Converter").start();
else if ("force".equals(cmd)) {
force = forceType.isSelected();
}
else if ("quit".equals(cmd)) {
shutdown = true;
new Thread("Quitter") {
@Override
public void run() { dispose(); }
}.start();
}
}
// -- ChangeListener methods --
@Override
public void stateChanged(ChangeEvent e) {
if (e.getSource() == series) {
swap.setSeries(((Integer) series.getValue()).intValue() - 1);
updateLabels(input.getText());
}
}
// -- Runnable methods --
@Override
public void run() {
convert.setEnabled(false);
includeZ.setEnabled(false);
includeT.setEnabled(false);
includeC.setEnabled(false);
zChoice.setEnabled(false);
tChoice.setEnabled(false);
cChoice.setEnabled(false);
input.setEditable(false);
fps.setEnabled(false);
if (series != null) series.setEnabled(false);
forceType.setEnabled(false);
codec.setEnabled(false);
progress.setString("Getting ready");
try {
String in = input.getText();
String out = output.getText();
if (in.trim().equals("")) {
msg("Please specify input files.");
convert.setEnabled(true);
progress.setString("");
return;
}
if (out.trim().equals("")) {
File f = new File(in);
String name = new FilePattern(in).getPrefix();
String stitchDir = name + "-stitched";
new File(f.getParent() + File.separator + stitchDir).mkdir();
out = f.getParent() + File.separator + stitchDir +
File.separator + name + ".tif";
out = out.replaceAll(File.separator + File.separator, File.separator);
output.setText(out);
}
output.setEditable(false);
swap.setId(in);
if (series != null) {
swap.setSeries(((Integer) series.getValue()).intValue() - 1);
}
writer.setFramesPerSecond(((Integer) fps.getValue()).intValue());
try {
writer.getWriter(out).setCompression((String) codec.getSelectedItem());
}
catch (NullPointerException npe) { }
//boolean isQT = swap.getFormat().equals("QuickTime");
//boolean useQTJ = isQT && qtJava.isSelected();
//((QTReader) reader.getReader(QTReader.class)).setLegacy(useQTJ);
// swap dimensions based on user input
String order = swap.getDimensionOrder();
if (zLabel.getText().indexOf("Time") != -1) {
order = order.replace('Z', 'T');
}
else if (zLabel.getText().indexOf("Channel") != -1) {
order = order.replace('Z', 'C');
}
if (tLabel.getText().indexOf("Slice") != -1) {
order = order.replace('T', 'Z');
}
else if (tLabel.getText().indexOf("Channel") != -1) {
order = order.replace('T', 'C');
}
if (cLabel.getText().indexOf("Time") != -1) {
order = order.replace('C', 'T');
}
else if (cLabel.getText().indexOf("Slice") != -1) {
order = order.replace('C', 'Z');
}
swap.swapDimensions(order);
// determine internal and external dimensions for each axis
int internalZ = includeZ.isSelected() ? swap.getSizeZ() : 1;
int internalT = includeT.isSelected() ? swap.getSizeT() : 1;
int internalC = includeC.isSelected() ? swap.getEffectiveSizeC() : 1;
int externalZ = includeZ.isSelected() ? 1 : swap.getSizeZ();
int externalT = includeT.isSelected() ? 1 : swap.getSizeT();
int externalC = includeC.isSelected() ? 1 : swap.getEffectiveSizeC();
int zDigits = ("" + externalZ).length();
int tDigits = ("" + externalT).length();
int cDigits = ("" + externalC).length();
progress.setMaximum(2 * swap.getImageCount());
int star = out.lastIndexOf(".");
if (star < 0) star = out.length();
String pre = out.substring(0, star);
String post = out.substring(star);
// determine appropriate pixel type
int type = swap.getPixelType();
writer.setId(out);
if (force && !writer.isSupportedType(type)) {
int[] types = writer.getPixelTypes();
for (int i=0; i<types.length; i++) {
if (types[i] > type) {
if (i == 0) {
type = types[i];
break;
}
else {
type = types[i - 1];
break;
}
}
if (i == types.length - 1) type = types[i];
}
}
else if (!force && !writer.isSupportedType(type)) {
throw new FormatException("Unsupported pixel type: " +
FormatTools.getPixelTypeString(type) +
"\nTo write to this format, the \"force\" box must be checked.\n" +
"This may result in a loss of precision; for best results, " +
"convert to TIFF instead.");
}
long start = System.currentTimeMillis();
int plane = 0;
for (int i=0; i<externalZ; i++) {
for (int j=0; j<externalT; j++) {
for (int k=0; k<externalC; k++) {
// construct the numeric blocks
String zBlock = "";
String tBlock = "";
String cBlock = "";
if (externalZ > 1) {
String num = "" + i;
while (num.length() < zDigits) num = "0" + num;
zBlock = "Z" + num + "_";
}
if (externalT > 1) {
String num = "" + j;
while (num.length() < tDigits) num = "0" + num;
tBlock = "T" + num + "_";
}
if (externalC > 1) {
String num = "" + k;
while (num.length() < cDigits) num = "0" + num;
cBlock = "C" + num;
}
String outFile = pre;
if (zBlock.length() > 1) outFile += zBlock;
if (tBlock.length() > 1) outFile += tBlock;
if (cBlock.length() > 1) outFile += cBlock;
if (outFile.endsWith("_")) {
outFile = outFile.substring(0, outFile.length() - 1);
}
outFile += post;
String outName = new File(outFile).getName();
int planesPerFile = internalZ * internalT * internalC;
int filePlane = 0;
for (int z=0; z<internalZ; z++) {
for (int t=0; t<internalT; t++) {
for (int c=0; c<internalC; c++) {
int zPos = z*externalZ + i;
int tPos = t*externalT + j;
int cPos = c*externalC + k;
progress.setString(outName + " " + (filePlane + 1) +
"/" + planesPerFile);
filePlane++;
progress.setValue(2 * (plane + 1));
plane++;
int ndx = swap.getIndex(zPos, cPos, tPos);
BufferedImage img = biReader.openImage(ndx);
writer.setId(out);
if (force && !writer.isSupportedType(swap.getPixelType())) {
int pixelType = 0;
switch (type) {
case FormatTools.INT8:
case FormatTools.UINT8:
pixelType = DataBuffer.TYPE_BYTE;
break;
case FormatTools.INT16:
pixelType = DataBuffer.TYPE_USHORT;
break;
case FormatTools.UINT16:
pixelType = DataBuffer.TYPE_SHORT;
break;
case FormatTools.INT32:
case FormatTools.UINT32:
pixelType = DataBuffer.TYPE_INT;
break;
case FormatTools.FLOAT:
pixelType = DataBuffer.TYPE_FLOAT;
break;
case FormatTools.DOUBLE:
pixelType = DataBuffer.TYPE_DOUBLE;
break;
}
// TODO - come up with another way to do this...
//img = ImageTools.makeType(img, pixelType);
}
writer.setId(outFile);
biWriter.savePlane(filePlane, img);
if (shutdown) break;
}
}
}
}
}
}
progress.setValue(2 * swap.getImageCount());
progress.setString("Finishing");
if (writer != null) writer.close();
long end = System.currentTimeMillis();
double time = (end - start) / 1000.0;
long avg = (end - start) / (swap.getImageCount());
progress.setString(time + " s elapsed (" + avg + " ms/plane)");
progress.setValue(0);
if (swap != null) swap.close();
}
catch (FormatException exc) {
LOGGER.info("", exc);
String err = exc.getMessage();
if (err == null) err = exc.getClass().getName();
msg("Sorry, an error occurred: " + err);
progress.setString("");
progress.setValue(0);
}
catch (IOException exc) {
LOGGER.info("", exc);
String err = exc.getMessage();
if (err == null) err = exc.getClass().getName();
msg("Sorry, an error occurred: " + err);
progress.setString("");
progress.setValue(0);
}
convert.setEnabled(true);
includeZ.setEnabled(true);
includeT.setEnabled(true);
includeC.setEnabled(true);
zChoice.setEnabled(true);
tChoice.setEnabled(true);
cChoice.setEnabled(true);
input.setEditable(true);
output.setEditable(true);
fps.setEnabled(true);
if (series != null) series.setEnabled(true);
forceType.setEnabled(true);
codec.setEnabled(true);
}
// -- Helper methods --
private void limitHeight(JComponent jc) {
int w = jc.getMaximumSize().width;
int h = jc.getPreferredSize().height;
jc.setMaximumSize(new Dimension(w, h));
}
private void msg(String msg) {
JOptionPane.showMessageDialog(this, msg, TITLE, JOptionPane.ERROR_MESSAGE);
}
private void updateLabels(String pattern) {
try {
swap.setId(pattern);
String z = zLabel.getText();
z = z.substring(0, z.indexOf("<"));
z += "<1-" + swap.getSizeZ() + ">";
zLabel.setText(z);
String t = tLabel.getText();
t = t.substring(0, t.indexOf("<"));
t += "<1-" + swap.getSizeT() + ">";
tLabel.setText(t);
String c = cLabel.getText();
c = c.substring(0, c.indexOf("<"));
c += "<1-" + swap.getEffectiveSizeC() + ">";
cLabel.setText(c);
includeZ.setEnabled(true);
includeT.setEnabled(true);
includeC.setEnabled(true);
}
catch (FormatException exc) { LOGGER.info("", exc); }
catch (IOException exc) { LOGGER.info("", exc); }
}
// -- Helper classes --
private class RowPanel extends JPanel {
@Override
public Dimension getMaximumSize() {
int w = super.getMaximumSize().width;
int h = getPreferredSize().height;
return new Dimension(w, h);
}
}
// -- Main method --
public static void main(String[] args) {
new DataConverter().setVisible(true);
}
}