/* * Copyright 2010-2013 Institut Pasteur. * * This file is part of Icy. * * Icy is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Icy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.file; import icy.gui.frame.progress.FailedAnnounceFrame; import icy.gui.frame.progress.FileFrame; import icy.gui.menu.ApplicationMenu; import icy.image.IcyBufferedImage; import icy.image.colormodel.IcyColorModel; import icy.main.Icy; import icy.preferences.GeneralPreferences; import icy.sequence.MetaDataUtil; import icy.sequence.Sequence; import icy.system.IcyExceptionHandler; import icy.type.DataType; import icy.util.OMEUtil; import icy.util.StringUtil; import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import loci.common.services.ServiceException; import loci.formats.FormatException; import loci.formats.IFormatWriter; import loci.formats.UnknownFormatException; import loci.formats.ome.OMEXMLMetadata; import loci.formats.out.APNGWriter; import loci.formats.out.AVIWriter; import loci.formats.out.JPEG2000Writer; import loci.formats.out.JPEGWriter; import loci.formats.out.OMETiffWriter; import loci.formats.out.TiffWriter; /** * Sequence / Image saver class.<br> * <br> * Supported save format are the following : TIFF (preferred), PNG, JPG and AVI. * When sequence is saved as multiple file the following naming convention is used :<br> * <code>filename-tttt-zzzz</code> * * @author Stephane & Fab */ public class Saver { /** * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} * instead */ @Deprecated public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT, DataType dataType) throws ServiceException { return OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT, dataType, false); } /** * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, int, int, DataType, boolean)} * instead */ @Deprecated public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int sizeZ, int sizeT, int dataType, boolean signedDataType) throws ServiceException { return OMEUtil.generateMetaData(sizeX, sizeY, sizeC, sizeZ, sizeT, DataType.getDataType(dataType, signedDataType), false); } /** * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead */ @Deprecated public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, DataType dataType) throws ServiceException { return OMEUtil.generateMetaData(sizeX, sizeY, sizeC, 1, 1, dataType, false); } /** * @deprecated use {@link OMEUtil#generateMetaData(int, int, int, DataType, boolean)} instead */ @Deprecated public static OMEXMLMetadata generateMetaData(int sizeX, int sizeY, int sizeC, int dataType, boolean signedDataType) throws ServiceException { return OMEUtil.generateMetaData(sizeX, sizeY, sizeC, DataType.getDataType(dataType, signedDataType), false); } /** * Returns the {@link ImageFileFormat} corresponding to specified {@link IFormatWriter}.<br> * <code>defaultValue</code> is returned if no matching format is found. */ public static ImageFileFormat getImageFileFormat(IFormatWriter writer, ImageFileFormat defaultValue) { if (writer instanceof TiffWriter) return ImageFileFormat.TIFF; if (writer instanceof APNGWriter) return ImageFileFormat.PNG; if (writer instanceof JPEGWriter) return ImageFileFormat.JPG; if (writer instanceof JPEG2000Writer) return ImageFileFormat.JPG; if (writer instanceof AVIWriter) return ImageFileFormat.AVI; return defaultValue; } /** * @deprecated Use {@link #getImageFileFormat(IFormatWriter, ImageFileFormat)} instead. */ @Deprecated public static FileFormat getFileFormat(IFormatWriter writer, FileFormat defaultValue) { return getImageFileFormat(writer, ImageFileFormat.getFormat(defaultValue)).toFileFormat(); } /** * Return the writer to use for the specified ImageFileFormat.<br> * <br> * The following writer are currently supported :<br> * <code>OMETiffWriter</code> : TIFF image file (default)<br> * <code>APNGWriter</code> : PNG image file<br> * <code>JPEGWriter</code> : JPG image file<br> * <code>AVIWriter</code> : AVI video file<br> * * @param format * {@link ImageFileFormat} we want to retrieve the saver.<br> * Accepted values:<br> * {@link ImageFileFormat#TIFF}<br> * {@link ImageFileFormat#PNG}<br> * {@link ImageFileFormat#JPG}<br> * {@link ImageFileFormat#AVI}<br> * null */ public static IFormatWriter getWriter(ImageFileFormat format) { final IFormatWriter result; switch (format) { case PNG: result = new APNGWriter(); break; case JPG: result = new JPEGWriter(); break; case AVI: result = new AVIWriter(); break; default: result = new OMETiffWriter(); // this way we are sure the TIF saver is always compressing try { result.setCompression("LZW"); } catch (FormatException e) { // no compression } break; } return result; } /** * @deprecated Use {@link #getWriter(ImageFileFormat)} instead. */ @Deprecated public static IFormatWriter getWriter(FileFormat fileFormat) { return getWriter(ImageFileFormat.getFormat(fileFormat)); } /** * Return the writer to use for the specified filename extension.<br> * <br> * The following writer are currently supported :<br> * <code>OMETiffWriter</code> : TIFF image file (default)<br> * <code>APNGWriter</code> : PNG image file<br> * <code>JPEGWriter</code> : JPG image file<br> * <code>AVIWriter</code> : AVI video file<br> * * @param ext * Extension we want to retrieve the corresponding image writer. * @param defaultFormat * default {@link ImageFileFormat} to use if <code>ext</code> is not recognized.<br> * Accepted values:<br> * {@link ImageFileFormat#TIFF}<br> * {@link ImageFileFormat#PNG}<br> * {@link ImageFileFormat#JPG}<br> * {@link ImageFileFormat#AVI}<br> * null */ public static IFormatWriter getWriter(String ext, ImageFileFormat defaultFormat) { return getWriter(ImageFileFormat.getWriteFormat(ext, defaultFormat)); } /** * @deprecated Use {@link #getWriter(String, ImageFileFormat)} instead. */ @Deprecated public static IFormatWriter getWriter(String ext, FileFormat defaultFormat) { return getWriter(ext, ImageFileFormat.getFormat(defaultFormat)); } /** * @deprecated Use {@link #getWriter(String, FileFormat)} instead. */ @Deprecated public static IFormatWriter getWriter(String ext) { return getWriter(ext, ImageFileFormat.TIFF); } /** * Return the writer to use for the specified file.<br> * <br> * The following writer are currently supported :<br> * <code>OMETiffWriter</code> : TIFF image file (default)<br> * <code>APNGWriter</code> : PNG image file<br> * <code>JPEGWriter</code> : JPG image file<br> * <code>AVIWriter</code> : AVI video file<br> * * @param file * File we want to retrieve the corresponding image writer. * @param defaultFormat * default {@link ImageFileFormat} to use if <code>file</code> is not recognized.<br> * Accepted values:<br> * {@link ImageFileFormat#TIFF}<br> * {@link ImageFileFormat#PNG}<br> * {@link ImageFileFormat#JPG}<br> * {@link ImageFileFormat#AVI}<br> * null */ public static IFormatWriter getWriter(File file, ImageFileFormat defaultFormat) { return getWriter(FileUtil.getFileExtension(file.getName(), false), defaultFormat); } /** * @deprecated Use {@link #getWriter(File, ImageFileFormat)} instead. */ @Deprecated public static IFormatWriter getWriter(File file, FileFormat defaultFormat) { return getWriter(file, ImageFileFormat.getFormat(defaultFormat)); } /** * @deprecated Use {@link #getWriter(File, FileFormat)} instead. */ @Deprecated public static IFormatWriter getWriter(File file) { return getWriter(file, ImageFileFormat.TIFF); } /** * Return the closest compatible {@link IcyColorModel} supported by writer * from the specified image description.<br> * That means the writer is able to save the data described by the returned * {@link IcyColorModel} without any loss or conversion.<br> * * @param writer * IFormatWriter we want to test compatibility * @param numChannel * number of channel of the image * @param dataType * image data type */ public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, int numChannel, DataType dataType) { final DataType outDataType; final int outNumChannel; if (writer instanceof OMETiffWriter) { // TIFF supports all formats outDataType = dataType; outNumChannel = numChannel; } else if (writer instanceof APNGWriter) { // PNG only supports byte and short data type if (dataType.getSize() > 2) outDataType = DataType.USHORT; else outDataType = dataType; // PNG supports a maximum of 4 channels outNumChannel = Math.min(numChannel, 4); } else { // JPG, AVI, default only supports byte data type if (dataType.getSize() > 1) outDataType = DataType.UBYTE; else outDataType = dataType; // 3 channels at max if (numChannel > 3) outNumChannel = 3; else { // special case of 2 channels if (numChannel == 2) // convert to RGB outNumChannel = 3; else outNumChannel = numChannel; } } return IcyColorModel.createInstance(outNumChannel, outDataType); } /** * Return the closest compatible {@link IcyColorModel} supported by writer * from the specified {@link IcyColorModel}.<br> * That means the writer is able to save the data described by the returned * {@link IcyColorModel} without any loss or conversion.<br> * * @param writer * IFormatWriter we want to test compatibility * @param colorModel * the colorModel describing data / image format */ public static IcyColorModel getCompatibleColorModel(IFormatWriter writer, IcyColorModel colorModel) { return getCompatibleColorModel(writer, colorModel.getNumComponents(), colorModel.getDataType_()); } /** * Return true if the specified writer is compatible with the image description.<br> * That means the writer is able to save the data without any loss or conversion.<br> * * @param numChannel * number of channel of the image * @param alpha * true if the image has an alpha channel * @param dataType * image data type */ public static boolean isCompatible(IFormatWriter writer, int numChannel, boolean alpha, DataType dataType) { return isCompatible(writer, IcyColorModel.createInstance(numChannel, dataType)); } /** * Return true if the specified writer is compatible with the specified {@link IcyColorModel}.<br> * That means the writer is able to save the data described by the colorModel without any loss * or conversion.<br> * The color map data are never preserved, they are always restored to their default.<br> */ public static boolean isCompatible(IFormatWriter writer, IcyColorModel colorModel) { return colorModel.isCompatible(getCompatibleColorModel(writer, colorModel)); } /** * Return the separate channel flag from specified writer and color space */ private static boolean getSeparateChannelFlag(IFormatWriter writer, int numChannel, DataType dataType) { if (writer instanceof OMETiffWriter) return (numChannel == 2) || (numChannel > 4) || (dataType.getSize() > 1); return false; } /** * Return the separate channel flag from specified writer and color space */ private static boolean getSeparateChannelFlag(IFormatWriter writer, IcyColorModel colorModel) { return getSeparateChannelFlag(writer, colorModel.getNumComponents(), colorModel.getDataType_()); } /** * Save the specified sequence in the specified file.<br> * If sequence contains severals images then file is used as a directory<br> * to store all single images. * * @param sequence * sequence to save * @param file * file where we want to save sequence */ public static void save(Sequence sequence, File file) { save(sequence, file, 0, sequence.getSizeZ() - 1, 0, sequence.getSizeT() - 1, 15, (sequence.getSizeZ() * sequence.getSizeT()) > 1, true); } /** * @deprecated Use {@link #save(Sequence, File, boolean, boolean)} instead. */ @Deprecated public static void save(Sequence sequence, File file, boolean multipleFiles) { save(sequence, file, 0, sequence.getSizeZ() - 1, 0, sequence.getSizeT() - 1, 15, multipleFiles, true); } /** * Save the specified sequence in the specified file.<br> * When the sequence contains severals image the multiFile flag is used to indicate<br> * if images are saved in severals files (file then specify a directory) or in a single file. * * @param sequence * sequence to save * @param file * file where we want to save sequence * @param multipleFiles * flag to indicate if images are saved in separate file * @param showProgress * show progress bar */ public static void save(Sequence sequence, File file, boolean multipleFiles, boolean showProgress) { save(sequence, file, 0, sequence.getSizeZ() - 1, 0, sequence.getSizeT() - 1, 15, multipleFiles, showProgress); } /** * @deprecated Use {@link #save(Sequence, File, int, int, int, int, int, boolean, boolean)} * instead. */ @Deprecated public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps, boolean multipleFiles) { save(sequence, file, zMin, zMax, tMin, tMax, fps, multipleFiles, true); } /** * Save the specified sequence in the specified file.<br> * When the sequence contains severals image the multipleFile flag is used to indicate<br> * if images are saved as separate files (file then specify a directory) or not.<br> * zMin - zMax and tMin - tMax define the Z and T images range to save.<br> * * @param sequence * sequence to save * @param file * file where we want to save sequence * @param zMin * start Z position to save * @param zMax * end Z position to save * @param tMin * start T position to save * @param tMax * end T position to save * @param fps * frame rate for AVI sequence save * @param multipleFile * flag to indicate if images are saved in separate file * @param showProgress * show progress bar */ public static void save(Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps, boolean multipleFile, boolean showProgress) { save(null, sequence, file, zMin, zMax, tMin, tMax, fps, multipleFile, showProgress, true); } /** * @deprecated Use * {@link #save(IFormatWriter, Sequence, File, int, int, int, int, int, boolean, boolean, boolean)} * instead. */ @Deprecated public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps, boolean multipleFile, boolean showProgress) { save(formatWriter, sequence, file, zMin, zMax, tMin, tMax, fps, multipleFile, showProgress, true); } /** * Save the specified sequence in the specified file.<br> * When the sequence contains severals image the multipleFile flag is used to indicate * if images are saved as separate files (file then specify a directory) or not.<br> * <code>zMin</code> - <code>zMax</code> and <code>tMin</code> - <code>tMax</code> define the Z * and T images range to save.<br> * * @param formatWriter * writer used to save sequence (define the image format).<br> * If set to <code>null</code> then writer is determined from the file extension.<br> * If destination file does not have a valid extension (for folder for instance) then you * have to specify a valid Writer to write the image file (see * {@link #getWriter(ImageFileFormat)}) * @param sequence * sequence to save * @param file * file where we want to save sequence.<br> * Depending the <code>formatWriter</code> the file extension may be modified.<br> * That is preferred as saving an image with a wrong extension may result in error on * future read (wrong reader detection). * @param zMin * start Z position to save * @param zMax * end Z position to save * @param tMin * start T position to save * @param tMax * end T position to save * @param fps * frame rate for AVI sequence save * @param multipleFile * flag to indicate if images are saved in separate file * @param showProgress * show progress bar * @param addToRecent * add the saved sequence to recent opened sequence list */ public static void save(IFormatWriter formatWriter, Sequence sequence, File file, int zMin, int zMax, int tMin, int tMax, int fps, boolean multipleFile, boolean showProgress, boolean addToRecent) { final String filePath = FileUtil.getGenericPath(file.getAbsolutePath()); final int sizeT = (tMax - tMin) + 1; final int sizeZ = (zMax - zMin) + 1; final int numImages = sizeT * sizeZ; final FileFrame saveFrame; final ApplicationMenu mainMenu; if (addToRecent) mainMenu = Icy.getMainInterface().getApplicationMenu(); else mainMenu = null; if (showProgress && !Icy.getMainInterface().isHeadLess()) saveFrame = new FileFrame("Saving", filePath); else saveFrame = null; try { if (saveFrame != null) { saveFrame.setLength(numImages); saveFrame.setPosition(0); } // need multiple files ? if ((numImages > 1) && multipleFile) { final IFormatWriter writer; // so we won't create it for each image if (formatWriter == null) writer = getWriter(file, ImageFileFormat.TIFF); else writer = formatWriter; if (writer == null) throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file); // save as severals images final DecimalFormat decimalFormat = new DecimalFormat("0000"); final String fileName = FileUtil.getFileName(filePath, false); String fileExt = FileUtil.getFileExtension(filePath, true); String fileBaseDirectory = FileUtil.getDirectory(filePath); if (fileBaseDirectory.endsWith("/")) fileBaseDirectory = fileBaseDirectory.substring(0, fileBaseDirectory.length() - 1); // no extension (directory) ? if (StringUtil.isEmpty(fileExt)) { // filename is part of directory fileBaseDirectory += FileUtil.separator + fileName; // use the default file extension for the specified writer fileExt = "." + getImageFileFormat(writer, ImageFileFormat.TIFF).getExtensions()[0]; } final String filePathWithoutExt = fileBaseDirectory + FileUtil.separator + fileName; // create output directory FileUtil.createDir(fileBaseDirectory); // default name used --> use filename if (sequence.isDefaultName()) sequence.setName(fileName); sequence.setFilename(fileBaseDirectory); for (int t = tMin; t <= tMax; t++) { for (int z = zMin; z <= zMax; z++) { String filename = filePathWithoutExt; if ((tMax - tMin) > 0) filename += "_t" + decimalFormat.format(t); if ((zMax - zMin) > 0) filename += "_z" + decimalFormat.format(z); filename += fileExt; // save as single image file save(writer, sequence, filename, z, z, t, t, fps, saveFrame); } } // add as one item to recent file list if (mainMenu != null) mainMenu.addRecentFile(fileBaseDirectory); } else { final String fileExt = FileUtil.getFileExtension(filePath, false); final ImageFileFormat iff; if (formatWriter != null) iff = getImageFileFormat(formatWriter, ImageFileFormat.TIFF); else iff = ImageFileFormat.getWriteFormat(fileExt, ImageFileFormat.TIFF); // force to set correct file extension final String fixedFilePath; if (iff.matches(fileExt)) fixedFilePath = filePath; else fixedFilePath = filePath + "." + iff.getExtensions()[0]; // default name used --> use filename if (sequence.isDefaultName()) sequence.setName(FileUtil.getFileName(fixedFilePath, false)); sequence.setFilename(fixedFilePath); // save into a single file save(formatWriter, sequence, fixedFilePath, zMin, zMax, tMin, tMax, fps, saveFrame); // add as one item to recent file list if (mainMenu != null) mainMenu.addRecentFile(fixedFilePath); } // Sequence persistence enabled --> save XML if (GeneralPreferences.getSequencePersistence()) sequence.saveXMLData(); } catch (Exception e) { IcyExceptionHandler.showErrorMessage(e, true); if (showProgress && !Icy.getMainInterface().isHeadLess()) new FailedAnnounceFrame("Failed to save image(s) (see output console for details)", 15); return; } finally { if (saveFrame != null) saveFrame.close(); } } /** * Save a single image from bytes buffer to the specified file. */ private static void saveImage(IFormatWriter formatWriter, byte[] data, int width, int height, int numChannel, DataType dataType, File file, boolean force) throws FormatException, IOException { if (file.exists()) { // forced ? first delete the file else LOCI won't save it if (force) file.delete(); else throw new IOException("File already exists"); } // ensure parent directory exist FileUtil.ensureParentDirExist(file); final IFormatWriter writer; if (formatWriter == null) { // get the writer writer = getWriter(file, ImageFileFormat.TIFF); // prepare the metadata try { writer.setMetadataRetrieve(MetaDataUtil.generateMetaData(width, height, numChannel, dataType, getSeparateChannelFlag(writer, numChannel, dataType))); } catch (ServiceException e) { System.err.println("Saver.saveImage(...) error :"); IcyExceptionHandler.showErrorMessage(e, true); } } else // ready to use writer (metadata already prepared) writer = formatWriter; // we always save in interleaved as some image viewer need it to correctly read image // (ex: win XP system viewer) writer.setInterleaved(true); writer.setId(file.getAbsolutePath()); writer.setSeries(0); try { writer.saveBytes(0, data); } catch (Exception e) { System.err.println("Saver.saveImage(...) error :"); IcyExceptionHandler.showErrorMessage(e, true); } writer.close(); } /** * Save a single image from bytes buffer to the specified file. */ public static void saveImage(byte[] data, int width, int height, int numChannel, DataType dataType, File file, boolean force) throws FormatException, IOException { saveImage(null, data, width, height, numChannel, dataType, file, force); } /** * @deprecated Use {@link #saveImage(byte[], int, int, int, DataType, File, boolean)} instead */ @Deprecated public static void saveImage(byte[] data, int width, int height, int numChannel, int dataType, boolean signedDataType, File file, boolean force) throws FormatException, IOException { saveImage(data, width, height, numChannel, DataType.getDataType(dataType, signedDataType), file, force); } /** * Save a single image to the specified file * * @param image * @throws IOException * @throws FormatException */ public static void saveImage(IcyBufferedImage image, File file, boolean force) throws FormatException, IOException { final IFormatWriter writer = getWriter(file, ImageFileFormat.TIFF); if (writer == null) throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + file); try { writer.setMetadataRetrieve(MetaDataUtil.generateMetaData(image, getSeparateChannelFlag(writer, image.getIcyColorModel()))); } catch (ServiceException e) { System.err.println("Saver.saveImage(...) error :"); IcyExceptionHandler.showErrorMessage(e, true); } // get byte order final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue(); // then save the image (always use interleaved data to save) saveImage(writer, image.getRawData(littleEndian, true), image.getSizeX(), image.getSizeY(), image.getSizeC(), image.getDataType_(), file, force); } /** * Save the specified sequence in the specified file.<br> * When the sequence contains severals image the multipleFile flag is used to indicate<br> * if images are saved as separate files (file then specify a directory) or not.<br> * zMin - zMax and tMin - tMax define the Z and T images range to save.<br> * * @param formatWriter * writer used to save sequence (define the image format) * @param sequence * sequence to save * @param filePath * file name where we want to save sequence * @param zMin * start Z position to save * @param zMax * end Z position to save * @param tMin * start T position to save * @param tMax * end T position to save * @param fps * frame rate for AVI sequence save * @param saveFrame * progress frame for save operation (can be null) * @throws ServiceException * @throws IOException * @throws FormatException */ private static void save(IFormatWriter formatWriter, Sequence sequence, String filePath, int zMin, int zMax, int tMin, int tMax, int fps, FileFrame saveFrame) throws ServiceException, FormatException, IOException { final File file = new File(filePath); final IFormatWriter writer; if (formatWriter == null) writer = getWriter(file, ImageFileFormat.TIFF); else writer = formatWriter; // TODO: temporary fix for the "incorrect close operation" bug in Bio-Formats // with OME TIF writer, remove it when fixed. // { // try // { // writer = formatWriter.getClass().newInstance(); // } // catch (Exception e) // { // throw new ServiceException("Can't create new writer instance: " + e); // } // } if (writer == null) throw new UnknownFormatException("Can't find a valid image writer for the specified file: " + filePath); // first delete the file else LOCI won't save it correctly if (file.exists()) file.delete(); // ensure parent directory exist FileUtil.ensureParentDirExist(file); final int sizeC = sequence.getSizeC(); // Some image viewer needs interleaved channel data to correctly read image. // win XP system viewer for instance final boolean interleaved = true; final boolean separateChannel = getSeparateChannelFlag(writer, sequence.getColorModel()); // set settings writer.setFramesPerSecond(fps); // generate metadata writer.setMetadataRetrieve(MetaDataUtil.generateMetaData(sequence, (zMax - zMin) + 1, (tMax - tMin) + 1, separateChannel)); // interleaved flag writer.setInterleaved(interleaved); // set id writer.setId(filePath); // init writer.setSeries(0); // usually give better save performance writer.setWriteSequentially(true); // get endianess final boolean littleEndian = !writer.getMetadataRetrieve().getPixelsBinDataBigEndian(0, 0).booleanValue(); byte[] data = null; try { int imageIndex = 0; // XYCZT order is important here (see metadata) for (int t = tMin; t <= tMax; t++) { for (int z = zMin; z <= zMax; z++) { if ((saveFrame != null) && saveFrame.isCancelRequested()) return; final IcyBufferedImage image = sequence.getImage(t, z); // separated channel data if (separateChannel) { for (int c = 0; c < sizeC; c++) { if (image != null) { // avoid multiple allocation data = image.getRawData(c, data, 0, littleEndian); writer.saveBytes(imageIndex, data); } imageIndex++; } } else { if (image != null) { // avoid multiple allocation data = image.getRawData(data, 0, littleEndian, interleaved); writer.saveBytes(imageIndex, data); } imageIndex++; } if (saveFrame != null) saveFrame.incPosition(); } } } finally { // always close writer after a file has been saved writer.close(); } } }